5、unique_ptr详解:独占所有权、移动语义、自定义删除器、数组支持、工厂函数make_unique
unique_ptr,说白了就是C++11里最实在的智能指针。它解决了一个老生常谈的问题——谁拥有这块内存,谁负责释放。我刚开始用C++那会儿,new和delete满天飞,一个不小心就内存泄漏。后来项目里全面换用unique_ptr,这类问题几乎绝迹了。
独占所有权:一个对象只有一个主人
unique_ptr的核心思想很简单:同一时间,只有一个unique_ptr可以指向某个对象。你不能复制它,只能移动它。这就像一把钥匙,只能交给一个人拿着。
核心原则:unique_ptr不能被拷贝,只能被移动。所有权转移后,原指针自动置空。
#include <memory>
#include <iostream>
class Resource {
public:
Resource() { std::cout << "资源创建\n"; }
~Resource() { std::cout << "资源销毁\n"; }
void doWork() { std::cout << "工作中...\n"; }
};
int main() {
std::unique_ptr<Resource> ptr1(new Resource());
// std::unique_ptr<Resource> ptr2 = ptr1; // 编译错误!不能拷贝
std::unique_ptr<Resource> ptr2 = std::move(ptr1); // 可以移动
if (!ptr1) {
std::cout << "ptr1已经空了\n"; // 确实空了
}
ptr2->doWork();
// 离开作用域时,ptr2自动释放资源
return 0;
}
我在项目中遇到过有人试图用引用传递unique_ptr,结果编译报错一脸懵。记住:想传递所有权,就用std::move;想临时借用,传原始指针或引用。
移动语义:所有权交接的艺术
移动语义是unique_ptr的灵魂。你想想看,如果每次都要深拷贝一个对象,那开销多大。移动就是「我把东西给你,我手里不留了」。
我的习惯:函数返回unique_ptr时,直接返回局部对象就好,编译器会自动用移动语义,别画蛇添足写std::move。
std::unique_ptr<Resource> createResource() {
auto ptr = std::make_unique<Resource>();
// 做一些初始化...
return ptr; // 自动移动,效率最高
}
void useResource() {
auto res = createResource(); // 所有权从createResource转移到这里
// res用完后自动释放
}
这里有个细节:返回局部unique_ptr时,用的是C++11的「隐式移动」规则。编译器知道这个变量即将销毁,所以自动把它当成右值处理。我曾经见过有人非要写return std::move(ptr),其实多此一举,反而可能阻止某些优化。
自定义删除器:不只有delete
默认情况下,unique_ptr用delete释放内存。但现实世界哪有那么理想?我遇到过用malloc分配的内存、需要fclose的文件句柄、甚至某些C库里的特殊资源释放函数。这时候就需要自定义删除器了。
// 自定义删除器:处理文件句柄
struct FileDeleter {
void operator()(FILE* fp) const {
if (fp) {
fclose(fp);
std::cout << "文件已关闭\n";
}
}
};
// 使用自定义删除器
std::unique_ptr<FILE, FileDeleter> filePtr(fopen("test.txt", "w"));
// 或者用lambda(C++17起更简洁)
auto lambdaDeleter = [](FILE* fp) {
if (fp) fclose(fp);
};
std::unique_ptr<FILE, decltype(lambdaDeleter)> filePtr2(
fopen("test.txt", "w"), lambdaDeleter);
注意:自定义删除器会影响unique_ptr的大小。无状态的函数对象(如lambda无捕获)通常不会增加体积,但有状态的删除器(如捕获了变量的lambda)会让unique_ptr变大。
我曾经在嵌入式项目里吃过这个亏——为了记录日志,给删除器捕获了一个字符串,结果每个unique_ptr多了几十字节,内存池直接炸了。后来改用无状态删除器才解决。
数组支持:管理动态数组
unique_ptr对数组有特化版本。你写std::unique_ptr<int[]>时,它调用的是delete[]而不是delete。这个区别很重要,搞错了就是未定义行为。
// 数组版本的unique_ptr
std::unique_ptr<int[]> arrPtr(new int[100]);
// 支持下标访问
for (int i = 0; i < 100; ++i) {
arrPtr[i] = i * 2;
}
// 自动调用delete[]释放
// 注意:数组版本没有*和->操作符,只有[]
最佳实践:除非有特殊理由,否则优先用std::vector或std::array代替动态数组。unique_ptr<T[]>更多用于与C接口交互的场景。
我记得有一次接手遗留代码,里面全是裸指针管理的数组,各种越界和泄漏。我花了一周时间全部改成unique_ptr数组,配合范围for循环,代码清晰多了。不过要注意,unique_ptr数组不记录大小,你得自己记住长度。
工厂函数make_unique:安全又简洁
C++14引入了std::make_unique,我愿称之为「最实用的标准库函数之一」。它解决了两个问题:一是避免显式写new,二是异常安全。
// 传统方式
std::unique_ptr<Resource> ptr1(new Resource("config"));
// 推荐方式
auto ptr2 = std::make_unique<Resource>("config");
// 数组版本(C++17)
auto arrPtr = std::make_unique<int[]>(100);
为什么make_unique更安全?看这个例子:
void process(std::unique_ptr<Resource> r1, std::unique_ptr<Resource> r2);
// 危险写法:如果new Resource("A")成功,但new Resource("B")抛出异常,
// 第一个资源就泄漏了
process(std::unique_ptr<Resource>(new Resource("A")),
std::unique_ptr<Resource>(new Resource("B")));
// 安全写法:make_unique保证不会泄漏
process(std::make_unique<Resource>("A"),
std::make_unique<Resource>("B"));
我的建议:除非你需要自定义删除器,否则永远用std::make_unique。它更短、更安全、更符合现代C++风格。
知识体系总览
下面这张图把unique_ptr的核心知识点串起来了,你可以对照着回顾一下:
这张图把unique_ptr的五大特性以及它们各自的子要点都列出来了。你可以看到,独占所有权和移动语义是根基,自定义删除器解决特殊场景,数组支持填补了与C接口的鸿沟,而make_unique则是日常开发的首选工具。
嗯,unique_ptr的内容就这些。它不像shared_ptr那么复杂,但用好了能解决80%的内存管理问题。我个人在项目里,能用unique_ptr的地方绝不用裸指针,这已经成了团队规范。你试试看,用上一段时间就会发现——原来那些头疼的内存泄漏,其实根本不该发生。