C++ 智能指针完全指南(一):unique_ptr 深度详解
·
引言
C++ 程序员最头疼的问题是什么?不是语法、不是模板、不是多线程——而是内存管理。
void func() {
int* p = new int(42);
// ... 复杂逻辑 ...
if (某个条件) return; // 忘记 delete p → 内存泄漏
// ... 更多逻辑 ...
delete p; // 如果中间抛异常,永远执行不到这里
}
手动 new/delete 有三大痛点:
-
忘记 delete → 内存泄漏
-
提前 return 或抛异常 → delete 被跳过
-
多次 delete → 程序崩溃
智能指针就是来彻底解决这些问题的。从 C++11 开始,标准库提供了三种智能指针:unique_ptr、shared_ptr、weak_ptr。它们本质上是用 RAII 包装原始指针,让指针在离开作用域时自动释放。
本文作为智能指针系列第一篇,聚焦于独占所有权的 unique_ptr。

第一部分:为什么需要 unique_ptr
一、原始指针的困境
#include <iostream>
using namespace std;
class Resource {
public:
Resource() { cout << "资源获取" << endl; }
~Resource() { cout << "资源释放" << endl; }
void work() { cout << "工作中..." << endl; }
};
void badExample() {
Resource* p = new Resource();
// 场景1:提前 return,忘记 delete
if (rand() % 2 == 0) {
return; // 内存泄漏!p 没有被释放
}
// 场景2:抛异常,delete 被跳过
p->work();
throw runtime_error("出错了"); // 异常抛出 → delete 不执行
delete p; // 只有正常情况才能执行到这里
}
三个场景,只有一种情况能正确释放内存。这就是为什么大型 C++ 项目普遍禁止裸 new/delete。
二、unique_ptr 的解决方案
#include <memory>
void goodExample() {
unique_ptr<Resource> p = make_unique<Resource>();
// 场景1:提前 return → p 的析构函数自动调用 → 资源释放 ✅
if (rand() % 2 == 0) return;
// 场景2:抛异常 → p 的析构函数仍然会被调用 → 资源释放 ✅
p->work();
throw runtime_error("出错了");
// 不需要 delete!离开作用域自动释放 ✅
}
第二部分:unique_ptr 的核心特性
一、独占所有权
unique_ptr 独占它指向的对象,不能被拷贝,只能被移动。
unique_ptr<int> p1 = make_unique<int>(42);
unique_ptr<int> p2 = p1; // ❌ 编译错误!不能拷贝
unique_ptr<int> p3 = std::move(p1); // ✅ 移动后 p1 变成 nullptr

二、三种创建方式
#include <memory>
using namespace std;
int main() {
// 方式1:make_unique(C++14,最推荐)
auto p1 = make_unique<int>(42);
// 方式2:用 new(C++11 兼容)
unique_ptr<int> p2(new int(42));
// 方式3:从已有 unique_ptr 移动
unique_ptr<int> p3 = std::move(p1); // p1 变成 nullptr
return 0;
// p2 和 p3 自动释放,p1 已经为空
}
优先用 make_unique,原因:
-
代码更简洁(不需要写两次类型名)
-
异常安全
-
C++14 起可用
三、访问被管理的对象
unique_ptr<string> p = make_unique<string>("hello");
// 像指针一样使用
cout << *p << endl; // hello(解引用)
cout << p->size() << endl; // 5(访问成员)
cout << p.get() << endl; // 获取原始指针(谨慎使用)
| 操作 | 含义 |
|---|---|
*p |
解引用,获取对象的引用 |
p-> |
访问对象成员 |
p.get() |
获取原始指针(不转移所有权) |
p == nullptr |
判断是否为空 |
if(p) |
判断是否为空 |
四、释放与重置
unique_ptr<int> p = make_unique<int>(42);
p.reset(); // 释放资源,p 变成 nullptr
p.reset(new int(100)); // 释放旧资源,接管新资源
int* raw = p.release(); // 放弃所有权,返回原始指针
// p 变成 nullptr,raw 需要手动 delete!
delete raw;
| 操作 | 含义 |
|---|---|
p.reset() |
释放资源,p 为空 |
p.reset(newPtr) |
释放旧资源,接管新资源 |
p.release() |
放弃所有权,返回裸指针(需要手动 delete) |
第三部分:unique_ptr 的常用场景
一、作为函数返回值(最安全)
unique_ptr<Resource> createResource() {
return make_unique<Resource>(); // 返回临时对象 → 自动移动
}
int main() {
auto p = createResource(); // 接收移动
p->work();
}
二、放入容器
vector<unique_ptr<Resource>> resources;
resources.push_back(make_unique<Resource>()); // 移动进去
resources.emplace_back(make_unique<Resource>());
// 遍历
for (const auto& r : resources) {
r->work();
}
// 容器销毁时,所有 unique_ptr 自动释放 → 不用手动删!
三、工厂模式
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};
class ProductA : public Product {
public:
void use() override { cout << "使用产品A" << endl; }
};
class ProductB : public Product {
public:
void use() override { cout << "使用产品B" << endl; }
};
// 工厂函数返回 unique_ptr<基类>
unique_ptr<Product> createProduct(char type) {
if (type == 'A') return make_unique<ProductA>();
if (type == 'B') return make_unique<ProductB>();
return nullptr;
}
int main() {
auto p = createProduct('A');
p->use(); // 使用产品A
// 多态析构!需要基类有虚析构函数
}
第四部分:自定义删除器
一、什么时候需要自定义删除器
默认的删除器是 delete,但有些资源不是用 new 分配的:
// 文件句柄
FILE* f = fopen("test.txt", "r");
// 用完后需要 fclose(f),不是 delete f
// Socket
int sockfd = socket(...);
// 用完后需要 close(sockfd),不是 delete sockfd
// 自定义内存池
void* mem = my_pool_alloc(1024);
// 用完后需要 my_pool_free(mem),不是 delete mem
二、自定义删除器示例
#include <cstdio>
// 文件句柄管理
auto fileDeleter = [](FILE* f) {
if (f) {
cout << "关闭文件" << endl;
fclose(f);
}
};
int main() {
FILE* f = fopen("test.txt", "w");
unique_ptr<FILE, decltype(fileDeleter)> filePtr(f, fileDeleter);
fprintf(filePtr.get(), "hello\n");
// 离开作用域 → 自动调用 fclose!
}
// Socket 管理
auto socketDeleter = [](int* fd) {
if (fd && *fd != -1) {
cout << "关闭 socket" << endl;
close(*fd);
}
delete fd;
};
int main() {
int* sockfd = new int(socket(AF_INET, SOCK_STREAM, 0));
unique_ptr<int, decltype(socketDeleter)> sockPtr(sockfd, socketDeleter);
// 离开作用域 → 自动关闭 socket!
}
三、unique_ptr 管理数组
// 管理动态数组(C++14 起)
auto arr = make_unique<int[]>(10); // 10 个 int
arr[0] = 1;
arr[1] = 2;
cout << arr[3] << endl;
// 离开作用域 → 自动调用 delete[]
// 注意:shared_ptr 也支持数组(C++17 起)
第五部分:unique_ptr vs 原始指针
| 对比项 | 原始指针 | unique_ptr |
|---|---|---|
| 内存释放 | 手动 delete | 自动 |
| 所有权 | 不明确 | 独占,明确 |
| 拷贝 | 浅拷贝(可能出问题) | 禁止拷贝 |
| 异常安全 | ❌ 可能泄漏 | ✅ 保证释放 |
| 性能开销 | 无 | 零开销(和原始指针一样) |
| 大小 | 8 字节 | 8 字节(无自定义删除器时) |
unique_ptr 是零开销抽象:在编译优化后,unique_ptr 的大小和性能与原始指针完全一样。
第六部分:常见错误
// ❌ 错误1:试图拷贝 unique_ptr
unique_ptr<int> p1 = make_unique<int>(42);
unique_ptr<int> p2 = p1; // 编译错误!
// ❌ 错误2:把同一个裸指针给多个 unique_ptr
int* raw = new int(42);
unique_ptr<int> p3(raw);
unique_ptr<int> p4(raw); // p3 和 p4 都会尝试 delete raw → 双重释放!
// ❌ 错误3:忘记 release 后要手动 delete
unique_ptr<int> p5 = make_unique<int>(42);
int* raw2 = p5.release();
// raw2 需要手动 delete,否则泄漏
delete raw2;
// ✅ 正确:用 get 只是临时查看,不转移所有权
unique_ptr<int> p6 = make_unique<int>(42);
int* raw3 = p6.get(); // 只是查看,所有权还是 p6 的
func(raw3); // 可以传给只读函数
总结
一、核心要点
| 特性 | 说明 |
|---|---|
| 所有权 | 独占,不可拷贝,只可移动 |
| 创建 | make_unique<T>(args) |
| 释放 | 自动(离开作用域)、reset()、release() |
| 访问 | *p、p->、p.get() |
| 自定义删除器 | 非 new 分配的资源(FILE*、socket 等) |
| 性能 | 零开销(和原始指针相同) |
| 头文件 | <memory> |
二、一句话记忆
unique_ptr 独占对象所有权,禁止拷贝只允许移动。用 make_unique 创建,离开作用域自动释放。需要管理非 new 资源时用自定义删除器。零开销,和原始指针一样快。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)