unique_ptr
定义与作用
std::unique_ptr(C++11)是独占所有权的智能指针:同一时刻只有一个 unique_ptr 拥有所指向的对象。当 unique_ptr 被销毁或重置时,所拥有的对象被自动 delete。
auto p = std::make_unique<int>(42);
// p 独占这个 int,离开作用域自动释放
核心价值:零额外开销(与裸指针同等大小和性能)+ 独占所有权语义(不可拷贝,只可移动)。
核心原理
unique_ptr 的所有权转移
unique_ptr 的内部结构
完整示例
示例一:对象池管理系统
场景说明:使用 unique_ptr 管理内存中的对象池,确保无泄漏。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <map>
#include <stdexcept>
// 资源类——不可拷贝,可移动
class Texture {
public:
Texture(std::string name, size_t size_kb)
: name_(std::move(name)), size_kb_(size_kb) {
std::cout << " [加载] 纹理 " << name_
<< " (" << size_kb_ << " KB)" << std::endl;
}
~Texture() {
std::cout << " [释放] 纹理 " << name_ << std::endl;
}
const std::string& name() const { return name_; }
size_t size() const { return size_kb_; }
// 禁止拷贝
Texture(const Texture&) = delete;
Texture& operator=(const Texture&) = delete;
// 允许移动
Texture(Texture&&) noexcept = default;
Texture& operator=(Texture&&) noexcept = default;
private:
std::string name_;
size_t size_kb_;
};
// 纹理管理器——unique_ptr 管理所有权
class TextureManager {
public:
// 加载纹理——返回裸指针(不转移所有权)
Texture* load(const std::string& name, size_t size_kb) {
auto texture = std::make_unique<Texture>(name, size_kb);
Texture* raw_ptr = texture.get();
textures_[name] = std::move(texture);
return raw_ptr;
}
// 卸载纹理
bool unload(const std::string& name) {
return textures_.erase(name) > 0;
}
// 查询纹理
Texture* get(const std::string& name) {
auto it = textures_.find(name);
return it != textures_.end() ? it->second.get() : nullptr;
}
// 移动纹理所有权——调用方接管
std::unique_ptr<Texture> release(const std::string& name) {
auto it = textures_.find(name);
if (it == textures_.end()) return nullptr;
auto ptr = std::move(it->second);
textures_.erase(it);
return ptr;
}
size_t count() const { return textures_.size(); }
size_t total_size() const {
size_t total = 0;
for (const auto& [_, tex] : textures_)
total += tex->size();
return total;
}
void print() const {
std::cout << " 管理器中有 " << count() << " 个纹理 ("
<< total_size() << " KB):" << std::endl;
for (const auto& [name, tex] : textures_)
std::cout << " " << name << " (" << tex->size() << " KB)" << std::endl;
}
private:
std::map<std::string, std::unique_ptr<Texture>> textures_;
};
int main() {
std::cout << "===== unique_ptr 纹理管理器 =====" << std::endl;
TextureManager mgr;
mgr.load("player.png", 256);
mgr.load("enemy.png", 128);
mgr.load("background.jpg", 2048);
mgr.print();
std::cout << "\n--- 查询 ---" << std::endl;
Texture* t = mgr.get("player.png");
if (t) std::cout << " 找到: " << t->name() << std::endl;
std::cout << "\n--- 释放纹理 ---" << std::endl;
mgr.unload("enemy.png");
mgr.print();
std::cout << "\n--- 转移所有权 ---" << std::endl;
{
auto released = mgr.release("player.png");
if (released) {
std::cout << " 外部持有: " << released->name() << std::endl;
mgr.print();
}
// 离开作用域 → released 析构 → Texture 自动释放
}
std::cout << " [外部 unique_ptr 已析构]" << std::endl;
mgr.print();
std::cout << "\n--- 管理器析构(自动释放剩余纹理)---" << std::endl;
return 0;
}
预期输出:
===== unique_ptr 纹理管理器 =====
[加载] 纹理 player.png (256 KB)
[加载] 纹理 enemy.png (128 KB)
[加载] 纹理 background.jpg (2048 KB)
管理器中有 3 个纹理 (2432 KB):
background.jpg (2048 KB)
enemy.png (128 KB)
player.png (256 KB)
--- 查询 ---
找到: player.png
--- 释放纹理 ---
[释放] 纹理 enemy.png
管理器中有 2 个纹理 (2304 KB):
background.jpg (2048 KB)
player.png (256 KB)
--- 转移所有权 ---
外部持有: player.png
管理器中有 1 个纹理 (2048 KB):
background.jpg (2048 KB)
[释放] 纹理 player.png
[外部 unique_ptr 已析构]
管理器中有 1 个纹理 (2048 KB):
background.jpg (2048 KB)
--- 管理器析构(自动释放剩余纹理)---
[释放] 纹理 background.jpg
逐段分析:
std::make_unique<Texture>(name, size)是创建 unique_ptr 的首选方式——直接构造、无new裸露Texture* get()返回裸指针用于访问,不转移所有权release()用std::move转移所有权给调用方——本管理器不再持有- map 中存储
unique_ptr——每个纹理所有权唯一且清晰 - 管理器析构时,map 中的 unique_ptr 自动析构 → Texture 自动释放
示例二:自定义删除器与 pImpl
#include <iostream>
#include <memory>
#include <cstdio>
// ====== 场景 1:FILE* 用 unique_ptr 管理 ======
struct FileCloser {
void operator()(std::FILE* fp) const {
if (fp) {
std::fclose(fp);
std::cout << " 文件已关闭" << std::endl;
}
}
};
void read_file_with_raii() {
// unique_ptr + 自定义删除器
std::unique_ptr<std::FILE, FileCloser> file(
std::fopen("test.txt", "w"));
if (!file) {
std::cerr << "无法打开文件" << std::endl;
return;
}
std::fputs("Hello, unique_ptr!\n", file.get());
std::cout << " 写入成功" << std::endl;
// 离开作用域 → FileCloser 自动 fclose
}
// Lambda 删除器(更简洁)
void read_file_with_lambda() {
auto file_deleter = [](std::FILE* fp) {
if (fp) {
std::fclose(fp);
std::cout << " [lambda] 文件已关闭" << std::endl;
}
};
std::unique_ptr<std::FILE, decltype(file_deleter)> file(
std::fopen("test2.txt", "w"), file_deleter);
if (file) {
std::fputs("Hello, lambda deleter!\n", file.get());
}
}
// ====== 场景 2:pImpl 惯用法 ======
// 头文件中的接口(pImpl 隐藏实现细节)
class Widget {
public:
Widget();
~Widget(); // 必须在 .cpp 定义(Impl 是不完整类型)
Widget(Widget&&) noexcept; // 移动语义
Widget& operator=(Widget&&) noexcept;
Widget(const Widget&) = delete; // 简化:禁止拷贝
Widget& operator=(const Widget&) = delete;
void doWork();
int value() const;
private:
struct Impl; // 前向声明——不暴露实现细节
std::unique_ptr<Impl> pImpl_;
};
// .cpp 文件中的实现
struct Widget::Impl {
int data = 42;
std::string name = "WidgetImpl";
void work() {
std::cout << " Impl::work() → data=" << data
<< ", name=" << name << std::endl;
}
};
Widget::Widget() : pImpl_(std::make_unique<Impl>()) {
std::cout << " Widget 构造" << std::endl;
}
// 析构函数必须在 .cpp 中定义(此时 Impl 已是完整类型)
Widget::~Widget() = default;
Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;
void Widget::doWork() { pImpl_->work(); }
int Widget::value() const { return pImpl_->data; }
int main() {
std::cout << "===== 自定义删除器 =====" << std::endl;
read_file_with_raii();
read_file_with_lambda();
std::cout << "\n===== pImpl 惯用法 =====" << std::endl;
Widget w;
w.doWork();
std::cout << " value: " << w.value() << std::endl;
// 移动语义
Widget w2 = std::move(w);
w2.doWork();
return 0;
}
预期输出:
===== 自定义删除器 =====
写入成功
文件已关闭
[lambda] 文件已关闭
===== pImpl 惯用法 =====
Widget 构造
Impl::work() → data=42, name=WidgetImpl
value: 42
Impl::work() → data=42, name=WidgetImpl
逐段分析:
- 自定义删除器让 unique_ptr 管理任意资源(FILE*、socket、OS handle)
- Lambda 删除器比仿函数更简洁——用
decltype获取类型 - pImpl(Pointer to Implementation)用 unique_ptr 管理实现类——接口与实现完全分离,缩短编译时间
- 析构函数必须在 Impl 完整定义的 .cpp 中——否则 unique_ptr 无法调用 Impl 的析构函数
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 拷贝 unique_ptr | 编译错误 | 用 std::move 转移所有权 |
| 多个 unique_ptr 指向同对象 | 双重释放 | unique_ptr 保证独占 |
| 从 unique_ptr 取裸指针后释放 unique_ptr | 悬挂指针 | 理解所有权转移时机 |
| pImpl 的析构函数在头文件 | 编译错误(不完整类型) | 析构函数定义在 .cpp |
用 new 创建 unique_ptr | 异常不安全 | 优先用 make_unique |
常见面试问题
unique_ptr 和 shared_ptr 的核心区别?——unique_ptr 独占所有权(不可拷贝,只可移动),零额外开销(与裸指针同等大小)。shared_ptr 共享所有权(引用计数),有控制块开销。
为什么推荐
make_unique而非new?——(1) 异常安全:f(make_unique<T>(), make_unique<T>())不会泄漏;(2) 代码简洁:不需要重复类型名;(3) C++14 才引入,但可自行实现。unique_ptr 如何支持自定义删除器?——作为第二个模板参数:
unique_ptr<T, Deleter>。删除器是类型的一部分——函数指针删除器会比无状态删除器大。pImpl 惯用法中为什么析构函数必须在 .cpp 定义?——unique_ptr 析构时需要完整类型的定义来调用 delete。头文件中 Impl 只有前向声明(不完整类型),所以析构函数必须放在 Impl 定义之后的 .cpp 文件中。
unique_ptr 可以用于数组吗?——可以:
unique_ptr<T[]>。支持operator[],但没有operator*和operator->。通常推荐用std::vector或std::array代替。
小结
- unique_ptr:独占所有权、不可拷贝只可移动、零额外开销
make_unique<T>(args...)是创建首选——异常安全、代码简洁- 自定义删除器扩展 unique_ptr 到任意资源类型(FILE*、socket 等)
- pImpl 惯用法依赖 unique_ptr 实现编译期封装
- 所有权转移用
std::move,返回裸指针用get(),释放所有权用release()