
文章目录
-
- [1. RAII 把资源释放放进对象生命周期](#1. RAII 把资源释放放进对象生命周期)
- [2. unique_ptr 表达独占所有权](#2. unique_ptr 表达独占所有权)
-
- [2.1 移动改变所有者,不搬动被管理对象](#2.1 移动改变所有者,不搬动被管理对象)
- [2.2 get、release 和 reset 的责任不同](#2.2 get、release 和 reset 的责任不同)
- [2.3 创建方式与 const](#2.3 创建方式与 const)
- [3. shared_ptr 共享生命周期,不复制被管理对象](#3. shared_ptr 共享生命周期,不复制被管理对象)
-
- [3.1 一个资源只能建立一次初始接管关系](#3.1 一个资源只能建立一次初始接管关系)
- [3.2 make_shared 的作用与取舍](#3.2 make_shared 的作用与取舍)
- [4. weak_ptr 观察存活状态,lock 临时取得所有权](#4. weak_ptr 观察存活状态,lock 临时取得所有权)
- [5. 释放方式必须与资源类型匹配](#5. 释放方式必须与资源类型匹配)
-
- [5.1 动态数组](#5.1 动态数组)
- [5.2 文件与自定义删除器](#5.2 文件与自定义删除器)
- [6. 小结](#6. 小结)
在前面实现容器时,我们已经多次遇到了"谁申请、谁释放"的情况。到了实际程序里,创建对象和使用对象往往发生在不同函数中:函数可以返回对象,容器可以保存对象,几个模块也可能共同使用对象。此时,只有一个地址远远不够,还必须约定谁承担释放责任。
这种释放责任就是本文所说的所有权。智能指针把所有权规则放进类的构造、复制、移动和析构中,让编译器参与检查,让对象生命周期负责收尾。
1. RAII 把资源释放放进对象生命周期
RAII是ResourceAcquisition Is Initialization的缩写,他是一种管理资源的类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。
普通局部指针离开作用域,只会结束指针变量本身的生命周期,不会自动 delete 它指向的对象:
cpp
// 问题片段:DoWork() 可能抛异常,delete 就可能执行不到。
Widget* p = new Widget;
DoWork();
delete p;
提前 return 也有相似问题。若有两次申请,第二次 new 失败时,第一次申请的资源还需要有人处理。但如果给每条路径补释放语句,就会让清理逻辑与业务逻辑相互纠缠在一起。
RAII(Resource Acquisition Is Initialization),通常译作"资源获取即初始化"。常见的做法是:让一个对象在初始化时取得资源,析构时释放资源 。例如,vector 替我们管理它的动态数组;智能指针则适合管理需要独立生命周期的动态对象。
下面是完整程序。throw 抛出异常后,程序寻找对应的 catch;在这里,从 Work 传播到 main 的过程中,已经构造成功的局部对象会被析构,这个过程称为栈展开。
cpp
#include <iostream>
#include <memory>
#include <stdexcept>
struct Resource
{
int id;
explicit Resource(int n)
: id(n)
{
std::cout << "open " << id << std::endl;
}
~Resource()
{
std::cout << "close " << id << std::endl;
}
};
void Work(bool fail)
{
// unique_ptr:独占式智能指针,离开作用域自动释放资源
std::unique_ptr<Resource> first(new Resource(1));
std::unique_ptr<Resource> second(new Resource(2));
// 抛出异常,函数提前退出
if (fail) throw std::runtime_error("work failed");
std::cout << "work done" <<std::endl;
}
int main()
{
try {
Work(true);
}
catch (const std::exception& e) {
// 捕获抛出的异常
std::cout << "caught: " << e.what() << std::endl;
}
}
运行示例:

两个 unique_ptr 按构造的逆序析构,分别释放资源。释放发生在进入外层 catch 之前。若第二个资源申请失败,第一个智能指针已经构造完成,也能在相应栈展开中清理第一个资源。

RAII 依赖于对象的正常析构;进程被强制结束、调用某些直接终止进程的接口等情况,不属于它能保证完成清理的范围。析构和删除器也应避免向外抛异常,尤其不能在异常传播中再抛出另一个异常。
2. unique_ptr 表达独占所有权
<memory> 中的 std::unique_ptr<T> 管理一个对象,默认用 delete 释放它。
"独占"表示的是同一份资源只有一个这样的所有者;它允许别人临时访问,但不允许随意再造一个所有者。
cpp
std::unique_ptr<Date> p(new Date(2026, 10, 22));
if (p) {
p->_day = 23;
}
// std::unique_ptr<Date> q = p; // 错误:复制被禁止。
*p、p->member 沿用普通指针的访问习惯,空指针仍然不能解引用。
2.1 移动改变所有者,不搬动被管理对象
前一组 C++11 文章已经讲过:std::move 本身只是转换表达式的值类别,实际动作由移动操作完成。下面的完整程序将同时比较移动赋值、观察指针和主动交出资源。
cpp
#include <iostream>
#include <memory>
#include <utility>
struct Item
{
int id;
// 显式构造函数,并防止隐式类型转换
explicit Item(int n) : id(n) {}
// 析构函数,打印销毁信息,观察对象生命周期
~Item() { std::cout << "destroy " << id << std::endl; }
};
int main()
{
// 输出bool值以true/false形式打印
std::cout << std::boolalpha;
// 创建两个unique_ptr,分别管理Item(1)、Item(2)堆对象
std::unique_ptr<Item> a(new Item(1));
std::unique_ptr<Item> b(new Item(2));
// observer保存a管理的原始裸指针,仅用于观察,不负责内存释放
Item* observer = a.get();
// 移动语义:把a的管理权转移给b
// 原来b持有的Item(2)会被自动销毁;a变为空智能指针
b = std::move(a);
// 判断a是否为空,打印b管理对象的id
std::cout << "a empty: " << !a << ", b: " << b->id << std::endl;
// 比较裸指针,验证observer和b现在指向同一个对象
std::cout << "same object: " << (observer == b.get()) << std::endl;
// release:放弃b对对象的管理权,返回原始裸指针
// release不会释放内存,内存释放责任交给调用者
Item* raw = b.release();
// b已经变成空指针;raw仍然指向原来的Item(1)
std::cout << "b empty: " << !b << ", released: " << raw->id << std::endl;
// 手动释放release返回的堆内存
delete raw;
// reset:让b接管新堆对象Item(3)
b.reset(new Item(3));
// 不带参数reset:释放b当前管理的对象,b置空
b.reset();
return 0;
}
运行示例:

-
destroy 2:执行b = std::move(a),b 原本持有 Item (2),unique_ptr 赋值移动前会销毁原有托管对象,打印销毁信息。 -
a empty: true, b: 1:移动后 a 失去管理权变为空,b 接管原来 a 指向的 Item (1),输出 a 为空,b 托管对象 id=1。 -
same object: true:observer 保存的是最初 a.get () 拿到的裸指针,移动后 b 接管同一堆对象,两个裸指针地址相等,输出 true。 -
b empty: true, released: 1:b.release()让 b 放弃对象管理权,b 变成空指针;返回的 raw 指针仍然指向 Item (1),打印对应信息。 -
destroy 1:手动执行delete raw,释放 release 交出的堆对象 Item (1),触发析构打印。 -
destroy 3:b.reset()无参调用,释放 b 托管的 Item (3),触发析构打印;main 结束时 b 已是空智能指针,不会再有额外销毁输出。
b原来管理对象 2,移动赋值时先处理它原有的资源,然后接管对象 1。对象 1 的地址没变,也没有调用Item的移动构造。
操作后a.get() == nullptr,a自身仍是有效的智能指针对象,可以判空、重新赋值和销毁。称其为空智能指针。

2.2 get、release 和 reset 的责任不同
| 接口 | 操作后谁负责资源 | 是否立即释放旧资源 |
|---|---|---|
p.get() |
仍由 p 负责,返回的裸指针只供观察 |
否 |
p.release() |
p 放弃责任并变空,调用方必须接好责任 |
否 |
p.reset() |
p 变空 |
释放原有资源 |
p.reset(new T(...)) |
p 接管新资源 |
释放原有资源 |
release() 容易被误解为"释放内存",实际意思是交出所有权。
不要写
delete p.get()、p.reset(p.get())之类的代码,也不要用p.get()再构造一个独立所有者。这些写法都会破坏已有管理关系,进而导致指针悬空访问或重复释放。
2.3 创建方式与 const
cpp
// make_unique 从 C++14 起提供。
auto p = std::make_unique<Date>(2026, 10, 22);
它把对象构造和独占管理写成一次调用,通常优先采用;
| 声明 | 能否改所有权关系 | 能否通过它修改 T |
|---|---|---|
unique_ptr<T> |
可以移动、reset |
可以 |
const unique_ptr<T> |
不能移动交出或 reset |
可以 |
unique_ptr<const T> |
可以移动、reset |
不可以 |
智能指针的 const 与所指对象的 const 是两回事。这与 T* const、const T* 的区别相呼应。
3. shared_ptr 共享生命周期,不复制被管理对象
有些对象确实需要多个持有者共同维持生命期:一个模块先结束使用时,另一个模块还需要对象活着。这个时候就可以用 std::shared_ptr<T>。
cpp
#include <iostream>
#include <memory>
#include <utility>
struct Date
{
int day;
explicit Date(int n) : day(n) {}
~Date() { std::cout << "destroy Date " << day << std::endl; }
};
int main()
{
std::cout << std::boolalpha;
// make_shared:在堆上创建Date对象,并用shared_ptr托管,引用计数初始 = 1
auto a = std::make_shared<Date>(10);
std::cout << "owners: " << a.use_count() << std::endl;
// 局部作用域,b、c仅在这个大括号内有效
{
// shared_ptr 拷贝:a 和 b 共享同一个堆对象,引用计数 +1
auto b = a;
// 移动语义:把b的管理权转移给c;b变为空,引用计数不变(只是转移,不新增共享者)
auto c = std::move(b);
// use_count:查看当前共享该对象的智能指针数量
std::cout << "owners: " << a.use_count() << ", b empty: " << !b << std::endl;
// c修改共享对象的成员,a、c指向同一个对象,修改互相可见
c->day = 11;
std::cout << "a sees: " << a->day << std::endl;
// a.reset():a放弃对对象的共享所有权;引用计数减1,a变成空shared_ptr
a.reset();
std::cout << "c owners: " << c.use_count() << std::endl;
}
// 离开局部作用域,c、b依次析构;c是最后一个持有者,Date对象在此处销毁
std::cout << "scope ended" << std::endl;
return 0;
}
运行示例:

-
b = a复制了一份所有权,引用计数从 1 变为 2; -
c从b移动构造,接替b的那一份,计数仍为 2。 -
b变空。修改c->day后,a也看到 11,因为它们访问同一个Date。 -
a.reset()只撤销a的份额,不会影响c继续使用。最后c离开作用域,强引用计数降到 0,才销毁Date。

引用计数不是对象成员的访问次数,也不是所有保存相同地址的变量数量。
3.1 一个资源只能建立一次初始接管关系
cpp
// 错误示例片段
int* raw = new int(7);
std::shared_ptr<int> a(raw);
std::shared_ptr<int> b(raw); // 独立建立另一组管理信息,不会自动找到 a。
两个对象保存着相同的地址,却分别认为自己要负责最终释放,结果就是重复释放的未定义行为。正确的方式应该是复制已有的 shared_ptr:auto b = a;。
"相同地址"和"相同所有权组"不是同一件事。
3.2 make_shared 的作用与取舍
std::make_shared<T>(args...) 从 C++11 起提供,将参数转发给 T 的构造函数并返回 shared_ptr<T>。如果只需要单一所有者,应优先用独占指针,而不是为了能调用 make_shared 就选择共享。

对象与控制块合并分配也有取舍:最后一个强引用消失时,
T的析构函数照常执行;若仍有弱引用,合并分配的整块存储可能要等弱引用全部消失后才能回收。
| 函数 | C++ 版本 | 返回类型 | 适用场景 |
|---|---|---|---|
make_unique<T>(args) |
C++14 | unique_ptr<T> |
独占所有权(单个所有者),首选 |
make_shared<T>(args) |
C++11 | shared_ptr<T> |
需要多个地方共享同一个对象,有引用计数开销 |
4. weak_ptr 观察存活状态,lock 临时取得所有权
裸指针不会增加共享计数,但对象销毁后,它也不能可靠地告诉我们"我现在已经失效"。std::weak_ptr<T> 关联已有的共享所有权组,能判断该组管理的对象是否仍有强所有者,却不让对象因为自己而继续存活。
它不能直接从 new T 接管资源,也没有 * 和 ->。访问前要调用 lock():对象仍存活时返回共享同一组所有权的 shared_ptr,否则返回空指针。这里的 lock 表示尝试取得强引用,不是给对象数据加互斥锁。
cpp
#include <iostream>
#include <memory>
#include <string>
int main()
{
std::cout << std::boolalpha;
// 创建shared_ptr,管理字符串"hello",引用计数初始 = 1
auto a = std::make_shared<std::string>("hello");
// 拷贝shared_ptr,引用计数+1,现在a、b共享对象,计数=2
auto b = a;
// weak_ptr 弱引用,只观察对象,不增加引用计数,不会阻止对象销毁
std::weak_ptr<std::string> watch = a;
// weak_ptr的use_count:查看当前托管对象的shared_ptr所有者数量
std::cout << "owners: " << watch.use_count() << std::endl;
// a放弃所有权,引用计数减1,此时只剩b持有,计数=1
a.reset();
{
// lock():尝试把weak_ptr提升为shared_ptr;对象存活则返回有效shared_ptr,否则返回空
auto hold = watch.lock();
if (hold) {
// b放弃所有权,引用计数减1;现在hold是唯一持有者,计数 = 1
b.reset();
// 修改字符串内容
*hold += "!";
// hold.use_count():当前shared_ptr持有者数量
std::cout << *hold << ", owners: " << hold.use_count() << std::endl;
}
// 离开局部作用域,hold析构,引用计数归零,字符串对象被销毁
}
// expired():判断weak_ptr观察的对象是否已经被销毁
std::cout << "expired: " << watch.expired() << std::endl;
// 再次lock,对象已经销毁,得到空shared_ptr
auto empty = watch.lock();
std::cout << "lock empty: " << !empty << std::endl;
}
运行示例:

-
watch不增加计数。a.reset()后,b仍持有字符串; -
hold = watch.lock()又取得一个份额。即使随后b.reset(),hold仍保证这段访问期间字符串存活。 -
hold离开作用域后,最后一个强引用消失,watch才过期。

说
weak_ptr"完全不支持 RAII"也不准确。它不负责延长被观察对象的生命,却仍通过自身的构造和析构维护对管理信息的弱引用。
5. 释放方式必须与资源类型匹配
5.1 动态数组
unique_ptr<T> 默认 delete,unique_ptr<T[]> 默认 delete[],数组形式提供下标访问,不提供单对象形式的 *、->。
cpp
#include <iostream>
#include <memory>
int main()
{
// unique_ptr管理动态数组,使用数组版本unique_ptr,会自动调用delete[]释放内存
std::unique_ptr<int[]> values(new int[3] {1, 2, 3});
// unique_ptr数组支持[]下标访问
std::cout << "array sum: " << values[0] + values[1] + values[2] << std::endl;
// C++11中shared_ptr默认使用delete释放,管理数组必须自定义删除器,调用delete[]
auto delete_array = [](int* p) {
std::cout << "delete shared array" << std::endl;
delete[] p;
};
// shared_ptr托管动态int数组,第二个参数传入自定义lambda删除器
std::shared_ptr<int> shared(new int[2] {4, 5}, delete_array);
// shared_ptr不支持[]运算符,要用.get()获取裸指针,再下标访问
std::cout << "shared array sum: " << shared.get()[0] + shared.get()[1] << std::endl;
}
运行示例:

版本区别放在使用点记住即可:
| 写法 | 支持版本与用途 |
|---|---|
unique_ptr<T[]>(new T[n]) |
C++11,独占数组 |
make_unique<T[]>(n) |
C++14,创建独占数组 |
shared_ptr<T[]>(new T[n]) |
C++17,共享数组并支持 [] |
把
new T[n]交给默认delete的单对象指针,是释放方式不匹配的未定义行为。
5.2 文件与自定义删除器
文件流关闭时调用 fclose。下面的完整程序使用临时文件,检查打开失败的情况:
cpp
#include <cstdio>
#include <iostream>
#include <memory>
struct FileCloser
{
void operator()(std::FILE* p) const noexcept
{
if (p)
fclose(p);
}
};
int main()
{
// 不依赖工作目录中已有某个文件;关闭后临时文件由系统移除。
std::unique_ptr<std::FILE, FileCloser> file(tmpfile());
if (!file)
{
std::cerr << "cannot open temporary file" << std::endl;
return 1;
}
if (fputs("42\n", file.get()) == EOF)
return 2;
rewind(file.get());
int value = 0;
if (fscanf(file.get(), "%d", &value) != 1)
return 3;
std::cout << "file value: " << value << std::endl;
}
临时文件创建与读写成功时,输出:

对象在析构时会调用 FileCloser。
| 删除器形式 | unique_ptr 的典型写法(片段) |
shared_ptr 的典型写法(片段) |
|---|---|---|
仿函数 D |
unique_ptr<T, D> p(raw, D{}); |
shared_ptr<T> p(raw, D{}); |
函数指针 Free |
unique_ptr<T, decltype(&Free)> p(raw, &Free); |
shared_ptr<T> p(raw, &Free); |
lambda 变量 del |
unique_ptr<T, decltype(del)> p(raw, del); |
shared_ptr<T> p(raw, del); |
unique_ptr 的删除器类型属于智能指针类型的一部分;shared_ptr 的智能指针类型不因删除器不同而改变,删除行为由控制块保存。
需要注意的是:
shared_ptr<T>(raw, del)即使raw是空的,也可以创建计数为 1 的所有权组,并在最终释放时调用del(nullptr)。因此用于shared_ptr<FILE>的文件删除器尤其应妥善处理空值。
6. 小结
-
std::unique_ptr:独占所有权,同一时刻只能有一个指针管理资源; -
std::shared_ptr:共享所有权,通过引用计数管理资源;多个指针可持有同一资源,计数归零时释放资源; -
std::weak_ptr:弱引用,不增加引用计数,仅观察shared_ptr管理的资源;使用lock()可提升为shared_ptr,用于解决循环引用问题。 -
自定义删除器:可定制资源释放逻辑;
unique_ptr模板参数携带删除器类型;shared_ptr支持类型擦除,即使传入空裸指针也会执行删除器。 -
数组支持:
unique_ptr<T[]>C++11 可用,支持下标访问;shared_ptr<T[]>C++17 才支持,早期版本管理数组需自定义删除器。
详细信息可进一步查阅:
