6. unique_ptr 详解(上):独占所有权、移动语义、make_unique、自定义删除器
unique_ptr 是 C++11 引入的智能指针中,我个人用得最多的一个。说白了,它就是「独占所有权」的代名词。你想想看,在项目里我们经常遇到这样的场景:一个资源只能被一个对象拥有,不能共享,也不能复制。这时候 unique_ptr 就是最合适的选择。
我记得刚接触 C++11 那会儿,团队里还在大量使用裸指针,手动 new/delete。有一次线上服务莫名其妙地内存泄漏,查了三天才发现是某个异常路径下忘了 delete。从那以后,我就强制团队全面改用 unique_ptr。嗯,效果立竿见影。
独占所有权:一个资源,一个主人
unique_ptr 的核心语义就是「独占」。它不允许拷贝构造,也不允许拷贝赋值。为什么呢?因为一旦拷贝了,两个 unique_ptr 指向同一块内存,那到底谁来负责释放?这就乱套了。
核心原则:unique_ptr 只能移动,不能拷贝。移动意味着所有权转移,源指针会变成空。
来看个简单的例子:
#include <memory>
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource released\n"; }
void doWork() { std::cout << "Working...\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 is now empty\n";
}
ptr2->doWork();
// 离开作用域时,ptr2 自动释放资源
return 0;
}
输出结果:
Resource acquired
ptr1 is now empty
Working...
Resource released
看到没?ptr1 把所有权交给了 ptr2,自己变成了空指针。资源只会在 ptr2 析构时释放一次。这就是独占所有权的精髓。
注意:移动后的 unique_ptr 处于「空」状态,对它解引用是未定义行为。我见过有人移动后忘了检查,直接崩溃了。所以移动后最好显式判断一下。
移动语义:所有权的高效流转
unique_ptr 的移动语义是它的灵魂。你想想看,如果我们要把一个资源从函数 A 传到函数 B,用裸指针你得小心翼翼地约定「谁负责释放」。用 unique_ptr 就简单了——移动过去,所有权自然跟着走。
我在项目中经常用 unique_ptr 作为函数返回值:
std::unique_ptr<Resource> createResource() {
return std::unique_ptr<Resource>(new Resource());
}
void useResource(std::unique_ptr<Resource> res) {
res->doWork();
// 函数结束,资源自动释放
}
int main() {
auto res = createResource(); // 从工厂函数获取所有权
useResource(std::move(res)); // 转移所有权给 useResource
// 此时 res 为空
return 0;
}
这里有个细节:createResource 返回 unique_ptr 时,编译器会优先使用移动语义,甚至可能触发 RVO(返回值优化),效率非常高。我曾经在代码审查时看到有人写 return std::move(ptr),其实没必要——直接返回局部 unique_ptr 就行,编译器会自动处理。
小技巧:函数参数接收 unique_ptr 时,建议按值传递,而不是用右值引用。按值传递语义更清晰,调用方必须显式 std::move,一眼就能看出所有权在转移。
make_unique:更安全、更简洁的创建方式
C++14 引入了 std::make_unique,我个人强烈推荐用它来创建 unique_ptr。为什么?
- 异常安全:避免裸 new 可能导致的资源泄漏
- 代码简洁:不用写
new和类型名两次 - 语义清晰:一看就知道是创建独占资源
举个例子:
// 不推荐
std::unique_ptr<Resource> ptr1(new Resource("config"));
// 推荐
auto ptr2 = std::make_unique<Resource>("config");
你可能会问:「不就是少写几个字吗?」其实不止。考虑这个场景:
void process(std::unique_ptr<Resource> a, std::unique_ptr<Resource> b);
// 危险写法
process(std::unique_ptr<Resource>(new Resource("A")),
std::unique_ptr<Resource>(new Resource("B")));
// 安全写法
process(std::make_unique<Resource>("A"),
std::make_unique<Resource>("B"));
第一种写法存在潜在的异常安全问题。C++ 对函数参数的求值顺序没有严格规定,如果先 new 了 A,再 new B 时抛出异常,A 的资源就泄漏了。用 make_unique 就不会有这个问题——资源在 unique_ptr 构造完成后才暴露。
最佳实践:除非你需要自定义删除器,否则一律使用 std::make_unique。这是 C++ 核心指南的建议,也是我团队的铁律。
自定义删除器:掌控资源释放的细节
unique_ptr 默认用 delete 释放资源。但现实世界没那么简单——你可能需要关闭文件句柄、释放网络连接、或者调用某个特定的清理函数。这时候就需要自定义删除器了。
我曾经接手过一个遗留系统,里面用 malloc 分配内存,但代码里到处是 free 遗漏的 bug。后来我用 unique_ptr 配合自定义删除器,一次性解决了:
// 自定义删除器:用 free 释放
auto malloc_deleter = [](void* p) {
std::cout << "Freeing memory\n";
std::free(p);
};
int main() {
std::unique_ptr<int, decltype(malloc_deleter)> ptr(
static_cast<int*>(std::malloc(sizeof(int))),
malloc_deleter
);
*ptr = 42;
std::cout << *ptr << std::endl;
// 离开作用域时自动调用 free
return 0;
}
注意这里删除器的类型是模板参数的一部分。如果你用 lambda,类型是匿名的,所以要用 decltype 推导。当然,你也可以用函数指针或函数对象:
// 函数指针作为删除器
void file_closer(FILE* fp) {
if (fp) {
std::cout << "Closing file\n";
std::fclose(fp);
}
}
int main() {
std::unique_ptr<FILE, decltype(&file_closer)> fp(
std::fopen("test.txt", "w"),
file_closer
);
// 使用文件...
// 离开作用域时自动关闭
return 0;
}
注意:自定义删除器会影响 unique_ptr 的大小。无状态的函数对象(如 lambda 捕获为空)可以通过空基类优化(EBO)不增加大小,但函数指针会额外占用一个指针的大小。如果你的删除器有状态(比如捕获了变量),那 unique_ptr 会更大。
知识体系总览
下面这张图总结了 unique_ptr 的核心知识点,方便你快速回顾:
避坑指南
最后,分享几个我踩过的坑:
- 不要用 unique_ptr 管理数组:虽然 unique_ptr 支持数组特化(
unique_ptr<T[]>),但更推荐用std::vector或std::array。除非你明确需要延迟初始化或与 C 接口交互。 - 小心删除器类型不匹配:如果你用 lambda 做删除器,两个 lambda 即使代码相同,类型也不同。这会导致编译错误。解决办法是用
std::function包装,或者用函数指针。 - 不要混用 unique_ptr 和裸指针:一旦把裸指针传给 unique_ptr,就别再用裸指针操作了。我曾经见过有人
ptr.get()拿到裸指针后自己 delete 了,结果 unique_ptr 析构时 double free。
个人习惯:我一般在类成员变量里用 unique_ptr 表示「这个资源归我管,但我不想暴露拷贝语义」。配合工厂函数和移动语义,代码既安全又灵活。
好了,unique_ptr 的上半部分就讲到这里。记住它的核心:独占所有权、移动语义、make_unique、自定义删除器。这四个点掌握了,你就能在大部分场景下安全地管理独占资源。
公众号:蓝海资料掘金营,微信 deep3321