| title | 智能指针 |
|---|
C++11 在 <memory> 中以类模板 (class template) 的形式提供了三种智能指针 (smart pointers):std::unique_ptr、std::shared_ptr、 std::weak_ptr。
默认初始化均接管或分享 nullptr:
std::unique_ptr<T> uptr;
std::shared_ptr<T> sptr;
std:: weak_ptr<T> wptr;只支持 std::unique_ptr<T> 和 std::shared_ptr<T>:
std::unique_ptr<T> uptr;
assert(!uptr);只支持 std::unique_ptr<T> 和 std::shared_ptr<T>:
*p; // 解引用,获得 p 所指对象的(左值)引用
p->mem; // 等价于 (*p).mem即使在离开作用域或重置前抛出了**异常 (exception)**,智能指针也会确保资源被正确释放:
void f() {
auto sptr = std::make_shared<int>(42);
// 中间代码可能抛出异常,并且没有被 f 捕获
return;
} // 离开作用域前,std::shared_ptr 负责释放资源而用原始指针则有可能因忘记释放资源或忘记捕获异常而造成内存泄漏 (memory leak):
void f() {
auto *ip = new int(42);
// 中间代码可能抛出异常,并且没有被 f 捕获
delete ip; // 手动释放资源,但有可能因 忘记捕获异常 而运行不到这一行
}交换两个同一类型的智能指针所管理的原始指针:
p.swap(q);
std::swap(p, q);返回智能指针所管理的原始指针:
auto *p = sptr.get();std::unique_ptr<T> 用于管理独占所有权的资源,具有以下优点:
- 体积小:默认情况下,与
T*大小相同。 - 速度快:大多数操作(含解引用)与
T*执行相同的指令。 - 独占所有权:不能拷贝 (copy),只能移动 (move)。
自 C++14 起,推荐使用 std::make_unique<T>() 函数来创建 std::unique_ptr<T> 对象:
auto uptr = std::make_unique<T>(args);该函数依次完成三个任务:
- 动态分配所需内存。
- 用
args初始化T类型的对象。 - 返回指向该对象的
std::unique_ptr<T>对象。
智能指针类型 std::unique_ptr<T> 其实是 std::unique_ptr<T,D> 的简写:
- 删除器类型
D是智能指针类型std::unique_ptr<T,D>的一部分 。- 每一个
std::unique_ptr<T,D>对象所使用的删除器对象 是在编译期 (compile time) 绑定的,因此无法在运行期 (run time) 更换。 - 如果没有显式指定删除器类型,那么将采用
std::default_delete<T>。
- 每一个
- 删除器对象 是
std::unique_ptr<T,D>对象的一部分。- 如果删除器是函数指针或含有数据成员的函数对象,则
sizeof(std::unique_ptr<T,D>) >= sizeof(T*) + sizeof(D)。 - 如果删除器是不含数据成员的函数对象,例如无捕获的 lambda 表达式,则
sizeof(std::unique_ptr<T,D>) == sizeof(T*)。
- 如果删除器是函数指针或含有数据成员的函数对象,则
#include <cstdlib>
#include <memory>
int main() {
auto deleter = [](void *p){ std::free(p); };
auto pa = std::unique_ptr<int, decltype(deleter)>(
(int*)std::malloc(sizeof(int)), deleter);
}delete 当前所管理的原始指针,然后接管传入的原始指针,含一次原始指针的赋值操作。
std::unique_ptr<T> 独占其所指对象的所有权,因此要确保
- 传入的
T*不被其他智能指针管理。 - 传入的
T*不会在其他地方被delete。
uptr.reset(ptr); // 接管 原始指针 ptr
uptr.reset(nullptr); // 接管 nullptr
uptr.reset(); // 同上
uptr = nullptr; // 同上(不推荐)让渡当前所管理的原始指针的所有权。
auto *p = uptr.release();该方法至少含两次原始指针赋值操作:
// 可能的实现:
pointer release() noexcept {
auto p_temp = p_; // 第一次 原始指针赋值
p_ = nullptr; // 第二次 原始指针赋值
return p_temp; // 通常由另一个智能指针接管
}典型用例:在函数中构造一个 std::unique_ptr<T> 并将其返回:
template <class... Args>
unique_ptr<T> Create(Args&&... args) {
auto uptr = make_unique<T>(std::forward<Args>(args)...);
// ...
return uptr;
}这种函数被称为工厂方法,以 std::unique_ptr<T> 作为其返回类型有如下好处:
std::unique_ptr<T>可以很容易地转为std::shared_ptr<T>。- 将原始指针赋值给
std::unique_ptr<T>的错误在编译期就能被发现。
联合使用 release() 与 reset(),可以在两个 std::unique_ptr<T> 之间传递所有权 (transfer ownership):
auto p1 = std::make_unique<int>(16); // p1 指向 16
std::unique_ptr<int> p2(p1.release()); // p1 为空,p2 指向 16
auto p3 = std::make_unique<int>(32); // p1 为空,p2 指向 16,p3 指向 32
p2.reset(p3.release()); // p1 为空,p2 指向 32,p3 为空类模板 std::unique_ptr 支持两种形式的模板实参:
std::unique_ptr<T>用于管理单个动态对象。std::unique_ptr<T[]>用于管理含一个或多个动态对象的动态数组。⚠️ 这种形式只应当用于接管 C-style API 返回的动态数组。
与 T* 类似,可以用 operator[] 访问被 std::unique_ptr<T[]> 接管的数组的成员:
#include <cstdio>
#include <cstdlib>
#include <memory>
int main() {
const int n = 10;
auto deleter = [](void *p){ std::free(p); };
auto pa = std::unique_ptr<int[], decltype(deleter)>(
(int*)std::malloc(n * sizeof(int)), deleter);
for (int i = 0; i < n; ++i) {
pa[i] = (i == 0 ? 1 : i * pa[i-1]);
std::printf("%d: %d\n", i, pa[i]);
}
}自 C++11 起,推荐使用 std::make_shared<T>() 函数来创建 std::shared_ptr<T> 对象:
auto sptr = std::make_shared<T>(args);该函数依次完成三个任务:
- 动态分配所需内存。
- 用
args初始化T类型的对象。 - 返回指向该对象的
std::shared_ptr<T>对象。
std::shared_ptr<T> 的构造函数的场景:
std::shared_ptr<T> sptr(p); // sptr 接管或分享 p 所指对象
std::shared_ptr<T> sptr(p, d); // sptr 接管或分享 p 所指对象, 并以 d 为删除器具体语义取决于 p 的类型:
p 的类型 |
语义 |
|---|---|
std::shared_ptr<T> |
sptr 分享 p 所指对象的所有权 |
std::unique_ptr<T> |
sptr 接管 p 所指对象, |
T*(必须是直接初始化) |
sptr 接管 p 所指对象 |
尽管 C++ 标准没有规定 std::shared_ptr 的实现方式,但几乎所有实现都采用了引用计数 (reference count) 方案:
- 一个
T*可以被多个std::shared_ptr<T>共享所有权,管理同一T*的std::shared_ptr<T>的个数称为它的引用计数。 - 引用计数作为控制块 (control block) 的一部分,需要存储在动态内存里,并通过
std::shared_ptr<T>中的指针成员来访问。 - 所谓共享所有权其实是通过共享控制块来实现的。
sptr.use_count(); // 获取 引用计数
sptr.unique(); // 判断 引用计数 是否为 1这一方案存在以下性能缺陷:
- 【空间开销】每个
std::shared_ptr<T>至少含有2个指针成员,分别用于存储被管理对象与控制块的地址,因此std::shared_ptr<T>的大小至少是T*的2倍。 - 【时间开销】为避免数据竞争 (data racing),增减引用计数的操作必须是原子的 (atomic)。因此,隐含读写引用计数的操作(构造、析构、赋值)会比非原子操作消耗更多时间。
与 std::unique_ptr<T> 不同,
- 删除器类型不是
std::shared_ptr<T>类型的一部分。- 每个
std::shared_ptr<T>对象 所绑定的删除器对象可以在运行期更换。 - 如果没有显式指定删除器,那么将采用
delete表达式。
- 每个
- 删除器对象不是
std::shared_ptr<T>对象的一部分。- 删除器对象存储在控制块中,因此不会影响
std::shared_ptr<T>的大小。 - 如果删除器是函数指针或含有数据成员的函数对象,则会作为控制块的一部分,存储在动态内存中。
- 如果删除器是不含有数据成员的函数对象,例如无捕获的 lambda 表达式,则不会占据控制块的空间。
- 删除器对象存储在控制块中,因此不会影响
用一个 std::shared_ptr<T> 对另一个 std::shared_ptr<T> 进行拷贝赋值 (copy-assign) 会改变二者的引用计数:
p = q; // p 的引用计数 - 1,q 的引用计数 + 1同理,用一个 std::shared_ptr<T> 拷贝构造 (copy-construct) 另一个 std::shared_ptr<T> 会增加前者的引用计数:
auto p = q; // q 的引用计数 + 1,p 的引用计数与之相同移动赋值 (move-assign) 与移动构造 (move-construct) 不改变引用计数。
如果当前引用计数为 1,则 delete 当前所管理的原始指针,然后接管传入的原始指针;
否则跳过 delete 操作。
p.reset(q, d); // 接管 *原始指针* q,并将 *删除器* 替换为 d
p.reset(q); // 接管 *原始指针* q
p.reset(); // 接管 nullptr用 this 去创建 std::shared_ptr<T>,所得结果的引用计数为 1。
考虑以下情形:
class Request {
public:
void Process();
private:
std::vector<std::shared_ptr<Request>> processed_requests_;
};如果在 Process() 的实现中,用 this 创建了新的 std::shared_ptr<Request>:
void Request::Process() {
// ...
processed_requests_.emplace_back(this);
// ...
}则有可能造成
- 一个由构造函数直接创建的非动态对象被一个
std::shared_ptr<T>管理,或者 - 一个动态对象被两个独立的
std::shared_ptr<T>管理。
为避免以上情形,应当
- 【对外】将
Request的构造函数设为private,改用工厂方法来创建std::shared_ptr<Request>对象。 - 【对内】借助于
std::enable_shared_from_this<Request>::shared_from_this()来获取std::shared_ptr<Request>对象。
#include <memory>
class Request: public std::enable_shared_from_this<Request> {
public:
void Process();
// 工厂方法:
template<typename... Args>
static std::shared_ptr<Request> Create(Args&&... args);
private:
std::vector<std::shared_ptr<Request>> processed_requests_;
// 构造函数 设为 private
Request();
// 其他成员方法 ...
};
void Request::Process() {
// ...
processed_requests_.emplace_back(shared_from_this());
// ...
}std::weak_ptr<T> 必须与 std::shared_ptr<T> 配合使用,并且不支持条件判断或解引用等常用的指针操作,因此它不是一种独立的智能指针。
指向一个 std::shared_ptr<T> 所管理的对象,但不改变其引用计数:
std::weak_ptr<T> wptr(sptr);获取引用计数的操作与 std::shared_ptr<T> 类似:
wptr.use_count(); // 返回与之共享所有权的 std::shared_ptr<T> 的引用计数
wptr.expired(); // 等价于 (wptr.use_count() == 0)如果引用计数不为零,通常希望执行解引用以获取所管理的对象。 但在判断引用计数是否为零与解引用这两步之间,所管理的对象有可能被其他线程 (thread) 析构了,因此需要将两步合并为一个原子的 (atomic) 操作:
// 如果 expired() 返回 true, 则返回一个空的 std::shared_ptr<T>
// 否则,返回一个与之共享所有权的 std::shared_ptr<T>,引用计数 += 1
wptr.lock();以上所说的引用计数均指 std::shared_ptr<T> 的个数。
除此之外,控制块中还有一个弱计数 (weak count),用于统计指向同一对象的 std::weak_ptr<T> 的数量。
因此,std::weak_ptr<T> 的构造、析构、赋值等操作都会读写弱计数。
与 std::shared_ptr<T> 的引用计数类似:为避免数据竞争,增减弱计数的操作必须是原子的。
因此,含读写弱计数的操作(构造、析构、赋值)会比非原子操作消耗更多时间。
一个 std::weak_ptr<T> 或 std::shared_ptr<T> 可以拷贝赋值给另一个 std::weak_ptr<T>,但不改变引用计数:
wptr = p; // p 可以是 std::weak_ptr<T> 或 std::shared_ptr<T>只将自己所管理的 T* 设为 nullptr,不负责析构对象或释放内存:
wptr.reset();工厂方法返回 std::shared_ptr<T> 而非 std::unique_ptr<T>:
std::shared_ptr<const Request> FastLoad(RequestId id) {
static std::unordered_map<RequestId, std::weak_ptr<const Request>> cache;
auto obj_ptr = cache[id].lock();
if (!obj_ptr) {
obj_ptr = RealLoad(id);
cache[id] = obj_ptr;
}
return obj_ptr;
}观察者模式要求:Subject 的状态发生变化时,应当通知所有的 Observer。
这一需求可以通过在 Subject 对象中维护一个存储 std::weak_ptr<Observer> 的容器来实现。
- 对于图 (graph) 这种数据结构,只用
std::shared_ptr<Node>有可能形成环 (cycle)。- 当环外不再有指向环内任一成员的
std::shared_ptr<Node>时,环内的成员就成了孤儿 (orphan),从而造成内存泄露。
- 当环外不再有指向环内任一成员的
- 对于树 (tree) 这种数据结构,
parent的生存期总是覆盖其child,因此parent指向child的指针应当选用std::unique_ptr<Node>。child指向parent的指针应当选用Node*。
- 如果树的深度过大,例如长达
10000000000ul的链表 (linked list),则有可能导致std::unique_ptr<Node>的析构函数递归爆栈。- 此时可以考虑用循环 (iteration) 代替递归 (recursion) 来实现析构。
尽量
- 用
std::make_unique<T>()创建std::unique_ptr<T>。 - 用
std::make_shared<T>()创建std::shared_ptr<T>。
对于 std::shared_ptr<T>,除了被管理的动态对象本身,控制块也需要分配动态内存。
用 std::make_shared<T>() 函数可以节省存储空间和运行时间:
std::shared_ptr<Object> sptr1(new Object); // 分配 2 次
auto sptr2 = std::make_shared<Object>(); // 分配 1 次make_ 函数有助于减少代码重复(与 auto 配合可以少写一次类型名)并确保异常安全。
在如下语句中
Process(std::unique_ptr<Request>(new Request), ComputePriority());编译器只保证参数在被传入函数之前被取值,因此实际的运行顺序可能是
new Request
ComputePriority() // 可能抛出异常
std::unique_ptr<Request>()如果第二行抛出了异常,则由 new 获得的 Request* 来不及被 std::unique_ptr<Request> 接管,从而有可能发生泄漏。
用 make_ 函数就可以避免这种情况的发生:
Process(std::make_unique<Request>(), ComputePriority());实际的运行顺序只能是
std::make_unique<Request>()
ComputePriority() // 可能抛出异常或
ComputePriority() // 可能抛出异常
std::make_unique<Request>()在不应或无法使用 make_ 函数的情况下,一定要确保:
由 new 获得的动态内存在一条语句内被智能指针接管,并且在该语句内不做任何其他的事。
make_ 函数用 std::forward<Args>(args) 进行完美转发,因此无法直接使用列表初始化构造函数。
一种解决办法是:先用 auto 创建一个 std::initializer_list<T> 对象,再将其传给 make 函数:
auto init_list = { 10, 20 };
auto sptrv = std::make_shared<std::vector<int>>(init_list);对于 std::shared_ptr<T>,不应或无法使用 make_ 函数的情形还包括:
- 需要指定内存管理方案(分配器、删除器)的类。
- 系统内存紧张,对象体积巨大,且
std::weak_ptr<T>比相应的std::shared_ptr<T>存活得更久。
该模式又被称为桥模式,它完全符合依赖倒置原则,甚至用 C 语言也可以实现。
假设在原始设计中,Algorithm 是一个含有 Implementor 型成员的类:
// algorithm.h
#include "implementor.h"
class Algorithm {
public:
Algorithm();
// 其他成员方法 ...
private:
Implementor implementor_;
};使用 Algorithm 的 user.cpp 必须 #include "algorithm.h",这样会导致
user.cpp间接地#include "implementor.h",从而会造成编译时间延长。implementor.h更新后,必须重新编译algorithm.cpp及user.cpp。
所谓 pImpl 就是用指向实现的指针 (Pointer to IMPLementation) 代替数据成员:
- 将
Algorithm对Implementor的依赖从algorithm.h移入algorithm.cpp,从而将user.cpp与implementor.h隔离。 implementor.h更新后,只需重新编译algorithm.cpp,而不必重新编译user.cpp,但可能需要重新链接。
// algorithm.h
class Algorithm {
public:
Algorithm(); // 需要 分配 资源,不是默认行为,需要显式声明
~Algorithm(); // 需要 释放 资源,不是默认行为,需要显式声明
// 其他成员方法 ...
private:
struct Implementor; // 仅声明,完整定义在 algorithm.cpp 中给出
Implementor* pImpl_;
};// algorithm.cpp
#include "algorithm.h"
#include "implementor.h" // 定义 RealImplementor
struct Algorithm::Implementor {
RealImplementor implementor;
};
// 实现 构造 和 析构 函数:
Algorithm::Algorithm()
: pImpl_(/* 分配 */new Implementor) {
}
Algorithm::~Algorithm() {
/* 释放 */delete pImpl_;
}// algorithm.h
#include <memory>
class Algorithm {
public:
Algorithm();
~Algorithm();
Algorithm(Algorithm&& rhs);
Algorithm& operator=(Algorithm&& rhs);
Algorithm(const Algorithm& rhs);
Algorithm& operator=(const Algorithm& rhs);
// 其他成员方法 ...
private:
struct Implementor; // 仅声明,完整定义在 algorithm.cpp 中给出
std::unique_ptr<Implementor> pImpl_; // 代替 Implementor*
};- 尽管希望使用默认析构函数,但还是要显式声明,因为
- 编译器在生成默认析构函数时,通常要求
std::unique_ptr<Implementor>中的Implementor是完整类型。 - 在
pImpl模式中,Implementor的定义只能在algorithm.cpp中给出,因此~Algorithm()只能在algorithm.cpp中实现。 - 在
algorithm.cpp中实现的方法,必须在algorithm.h中预先声明。
- 编译器在生成默认析构函数时,通常要求
- 尽管希望使用默认移动操作,但还是要显式声明,因为
- 显式声明析构函数,会阻止编译器生成默认移动操作。
- 默认移动操作在捕获异常时需要调用默认析构函数。
- 拷贝操作需要显式声明,因为
- 编译器无法为含有 move-only 成员的类生成默认拷贝操作。
- 默认拷贝操作是浅拷贝,通常不符合拷贝语义。
// algorithm.cpp
#include "algorithm.h"
#include "implementor.h" // 定义 RealImplementor
#include <memory>
struct Algorithm::Implementor {
RealImplementor implementor;
}; // 至此,Implementor 已经是完整类型。
// 实现 构造函数:
Algorithm::Algorithm()
: pImpl_(std::make_unique<Implementor>()) {
}
// 实现 析构函数,可采用默认版本:
Algorithm::~Algorithm() = default;
// 实现 move 操作,可采用默认版本:
Algorithm::Algorithm(Algorithm&& rhs) = default;
Algorithm& Algorithm::operator=(Algorithm&& rhs) = default;
// 实现 copy 操作,不可采用默认版本:
Algorithm& Algorithm::operator=(const Algorithm& rhs) {
// 拷贝所指对象,而非指针成员:
pImpl_ = std::make_unique<Implementor>(*rhs.pImpl_);
return *this;
}
Algorithm::Algorithm(const Algorithm& rhs)
: pImpl_(std::make_unique<Implementor>(*rhs.pImpl_)) {
}