【c++面向对象编程】第30篇:RAII与智能指针(一):auto_ptr的缺陷与unique_ptr
目录
一、一个手动管理的痛点
cpp
void riskyFunction() {
int* p = new int[1000];
// ... 做一些操作 ...
if (something_wrong) {
return; // 忘记 delete[] → 内存泄漏!
}
delete[] p;
}
问题在于:delete 很容易被遗忘,特别是在有多个返回路径或异常抛出的情况下。
RAII 解决方案:让对象自动管理资源。
cpp
void safeFunction() {
vector<int> v(1000); // 内存由 vector 管理
// ... 无论怎么返回,v 的析构函数都会释放内存
}
这就是 RAII 的精髓:将资源的生命周期与对象的生命周期绑定。
二、RAII 核心思想
RAII(Resource Acquisition Is Initialization)的含义:
-
获取资源即初始化:在构造函数中获取资源(分配内存、打开文件、加锁等)
-
释放资源即析构:在析构函数中释放资源(释放内存、关闭文件、解锁等)
-
对象生命周期结束,资源自动释放
cpp
class FileHandle {
FILE* file;
public:
FileHandle(const char* filename) {
file = fopen(filename, "r");
}
~FileHandle() {
if (file) fclose(file); // 自动关闭
}
// 禁止拷贝(后面解释)
};
只要 FileHandle 对象存在,文件就是打开的;对象销毁时,文件自动关闭。不再需要手动调用 fclose。
三、auto_ptr:C++98 的尝试与缺陷
C++98 引入了 auto_ptr,试图实现 RAII 式的动态内存管理。
cpp
#include <memory> // auto_ptr 在这里 auto_ptr<int> p1(new int(42)); auto_ptr<int> p2 = p1; // 看起来像拷贝,但实际是转移所有权! // 此时 p1 已经变成 nullptr
auto_ptr 的核心缺陷
auto_ptr 的拷贝构造函数和赋值运算符会转移所有权(被复制的指针变成 nullptr)。这违反了正常的拷贝语义:
cpp
auto_ptr<string> p1(new string("hello"));
auto_ptr<string> p2(p1); // p1 被置空
cout << *p1; // 崩溃!p1 已经是空指针
更严重的问题:auto_ptr 不能用于 STL 容器,因为容器要求元素有正常的拷贝行为。
cpp
vector<auto_ptr<int>> vec; // 能编译,但绝对不要这样做! vec.push_back(auto_ptr<int>(new int(1))); vec.push_back(auto_ptr<int>(new int(2))); // 容器内部拷贝时,所有权转移,导致原始指针变空,非常容易出错
结论:auto_ptr 在 C++11 中被正式废弃(deprecated),C++17 中已移除。永远不要用它。
四、unique_ptr:真正的独占式智能指针
unique_ptr 是 auto_ptr 的现代替代品,核心特点:
-
独占所有权:一个资源只能被一个
unique_ptr拥有 -
禁止拷贝:拷贝构造函数和拷贝赋值被
delete -
支持移动:通过移动语义转移所有权
-
零开销:与裸指针大小相同(通常 8 字节),没有额外虚函数开销
基本用法
cpp
#include <memory> using namespace std; unique_ptr<int> p1(new int(42)); unique_ptr<int> p2 = move(p1); // 转移所有权,p1 变为空 if (!p1) cout << "p1 为空" << endl; cout << *p2 << endl; // 42 // ❌ 编译错误:不能拷贝 // unique_ptr<int> p3 = p1;
常用成员函数
| 函数 | 作用 |
|---|---|
get() |
返回裸指针(不转移所有权) |
release() |
释放所有权,返回裸指针(不 delete) |
reset() |
释放当前对象并(可选)接管新指针 |
reset(nullptr) |
释放当前对象,置空 |
operator bool() |
判断是否为空 |
cpp
unique_ptr<int> p(new int(100));
int* raw = p.release(); // p 放弃所有权,变为 nullptr
delete raw; // 需要手动释放
p.reset(new int(200)); // 释放旧对象,接管新对象
p.reset(); // 释放对象,置空
if (p) { cout << "p 非空" << endl; }
五、unique_ptr 与数组
unique_ptr 支持数组版本,使用 T[] 模板参数:
cpp
unique_ptr<int[]> arr(new int[10]); arr[0] = 42; // 可以用下标访问 // 不需要手动 delete[] // 离开作用域时自动调用 delete[]
注意:unique_ptr<T> 用 delete,unique_ptr<T[]> 用 delete[],编译器会自动选择正确的释放方式。
六、自定义删除器
有时资源不是 new 分配的(如 fopen、malloc、socket),需要自定义释放方式。
cpp
#include <memory>
#include <cstdio>
using namespace std;
// 自定义删除器:函数对象
struct FileDeleter {
void operator()(FILE* f) const {
if (f) {
fclose(f);
cout << "文件已关闭" << endl;
}
}
};
int main() {
unique_ptr<FILE, FileDeleter> file(fopen("test.txt", "r"));
// 自动调用 fclose,不需要手动关闭
return 0;
}
使用 lambda 作为删除器(更简洁):
cpp
auto deleter = [](FILE* f) { if (f) fclose(f); };
unique_ptr<FILE, decltype(deleter)> file(fopen("test.txt", "r"), deleter);
七、make_unique(C++14)
C++14 提供了 make_unique,更安全、更高效地创建 unique_ptr:
cpp
// C++11 方式 unique_ptr<int> p1(new int(42)); // C++14 方式(推荐) auto p2 = make_unique<int>(42); auto p3 = make_unique<int[]>(10); // 数组版本
优势:
-
异常安全(避免
new和unique_ptr构造之间的空隙) -
代码更简洁
-
减少重复类型名
八、完整例子:对比手动管理 vs unique_ptr
cpp
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class Resource {
int id;
public:
Resource(int i) : id(i) {
cout << "Resource " << id << " 已获取" << endl;
}
~Resource() {
cout << "Resource " << id << " 已释放" << endl;
}
void use() {
cout << "使用 Resource " << id << endl;
}
};
// 手动管理:容易出错
void manualManagement() {
cout << "=== 手动管理 ===" << endl;
Resource* r = new Resource(1);
r->use();
// 忘记 delete → 内存泄漏!
// 即使记得,如果中间抛异常也会泄漏
}
// unique_ptr 管理:自动释放
void uniquePtrManagement() {
cout << "\n=== unique_ptr 管理 ===" << endl;
auto r = make_unique<Resource>(2);
r->use();
// 函数结束,自动释放
}
// 所有权转移示例
void ownershipTransfer() {
cout << "\n=== 所有权转移 ===" << endl;
unique_ptr<Resource> p1 = make_unique<Resource>(3);
cout << "p1 拥有资源" << endl;
unique_ptr<Resource> p2 = move(p1); // 转移所有权
cout << "转移后: p1 = " << (p1 ? "非空" : "空") << endl;
cout << "p2 拥有资源" << endl;
p2->use();
// p2 释放资源
}
// 放入容器
void containerUsage() {
cout << "\n=== 放入容器 ===" << endl;
vector<unique_ptr<Resource>> vec;
vec.push_back(make_unique<Resource>(4));
vec.push_back(make_unique<Resource>(5));
vec.push_back(make_unique<Resource>(6));
for (auto& p : vec) {
p->use();
}
// 容器销毁时,所有 unique_ptr 自动释放资源
}
int main() {
// manualManagement(); // 会泄漏,不运行
uniquePtrManagement();
ownershipTransfer();
containerUsage();
return 0;
}
输出:
text
=== unique_ptr 管理 === Resource 2 已获取 使用 Resource 2 Resource 2 已释放 === 所有权转移 === Resource 3 已获取 p1 拥有资源 转移后: p1 = 空 p2 拥有资源 使用 Resource 3 Resource 3 已释放 === 放入容器 === Resource 4 已获取 Resource 5 已获取 Resource 6 已获取 使用 Resource 4 使用 Resource 5 使用 Resource 6 Resource 6 已释放 Resource 5 已释放 Resource 4 已释放
九、unique_ptr 的常见误区
误区1:试图拷贝 unique_ptr
cpp
unique_ptr<int> p1(new int(5)); unique_ptr<int> p2 = p1; // ❌ 编译错误
误区2:用 get() 获取裸指针后 delete
cpp
unique_ptr<int> p(new int(5)); int* raw = p.get(); delete raw; // ❌ 重复释放,p 析构时会再次 delete
误区3:release() 后忘记释放内存
cpp
auto p = make_unique<int>(42); int* raw = p.release(); // p 放弃所有权 // 没有 delete raw → 内存泄漏
误区4:用裸指针重新赋值 unique_ptr
cpp
unique_ptr<int> p(new int(5)); p = new int(6); // ❌ 编译错误,不支持隐式转换 p.reset(new int(6)); // ✅ 正确
十、这一篇的收获
你现在应该理解:
-
RAII:构造函数获取资源,析构函数释放资源,对象生命周期绑定资源
-
auto_ptr缺陷:拷贝转移所有权,违反直觉,不能用于容器,已被废弃 -
unique_ptr:独占所有权,禁止拷贝,支持移动,零开销 -
常用操作:
make_unique、reset、release、get、移动语义转移所有权 -
数组支持:
unique_ptr<T[]>自动调用delete[] -
自定义删除器:可管理
fopen、malloc等非new资源
💡 小作业:写一个
unique_ptr<FILE, CustomDeleter>管理打开的文件,验证函数返回或异常时文件自动关闭。然后尝试写一段代码演示move转移所有权后原指针变为空。
下一篇预告:第31篇《智能指针(二):shared_ptr与weak_ptr——循环引用破解》——多个对象需要共享同一资源时用 shared_ptr。但两个 shared_ptr 互相引用会导致资源永远无法释放,weak_ptr 正是破解循环引用的利器。下篇详解。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)