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; // 手动释放资源,但有可能因 忘记捕获异常 而运行不到这一行
}
swap()
交换两个同一类型的智能指针所管理的原始指针:
p.swap(q);
std::swap(p, q);
get()
⚠️返回智能指针所管理的原始指针:
auto *p = sptr.get();
⚠️ 该方法只应当用于向只接受原始指针且不会释放资源的函数传递参数。
std::unique_ptr
std::unique_ptr<T>
用于管理独占所有权的资源,具有以下优点:
T*
大小相同。T*
执行相同的指令。自 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)
。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);
}
reset()
delete
当前所管理的原始指针,然后接管传入的原始指针,含一次原始指针的赋值操作。
std::unique_ptr<T>
独占其所指对象的所有权,因此要确保
T*
不被其他智能指针管理。T*
不会在其他地方被 delete
。uptr.reset(ptr); // 接管 原始指针 ptr
uptr.reset(nullptr); // 接管 nullptr
uptr.reset(); // 同上
uptr = nullptr; // 同上(不推荐)
release()
让渡当前所管理的原始指针的所有权。
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]);
}
}
std::shared_ptr
自 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>
的个数称为它的引用计数。std::shared_ptr<T>
中的指针成员来访问。sptr.use_count(); // 获取 引用计数
sptr.unique(); // 判断 引用计数 是否为 1
这一方案存在以下性能缺陷:
std::shared_ptr<T>
至少含有 2
个指针成员,分别用于存储被管理对象与控制块的地址,因此 std::shared_ptr<T>
的大小至少是 T*
的 2
倍。与 std::unique_ptr<T>
不同,
std::shared_ptr<T>
类型的一部分。 std::shared_ptr<T>
对象 所绑定的删除器对象可以在运行期更换。delete
表达式。std::shared_ptr<T>
对象的一部分。 std::shared_ptr<T>
的大小。用一个 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) 不改变引用计数。
reset()
如果当前引用计数为 1
,则 delete
当前所管理的原始指针,然后接管传入的原始指针; 否则跳过 delete
操作。
p.reset(q, d); // 接管 *原始指针* q,并将 *删除器* 替换为 d
p.reset(q); // 接管 *原始指针* q
p.reset(); // 接管 nullptr
shared_from_this()
用 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
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>
reset()
只将自己所管理的 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>
的容器来实现。
std::shared_ptr<T>
成环std::shared_ptr<Node>
有可能形成环 (cycle)。 std::shared_ptr<Node>
时,环内的成员就成了孤儿 (orphan),从而造成内存泄露。parent
的生存期总是覆盖其 child
,因此 parent
指向 child
的指针应当选用 std::unique_ptr<Node>
。child
指向 parent
的指针应当选用 Node*
。10000000000ul
的链表 (linked list),则有可能导致 std::unique_ptr<Node>
的析构函数递归爆栈。 make_
函数尽量
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>
存活得更久。pImpl
模式该模式又被称为桥模式,它完全符合依赖倒置原则,甚至用 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_;
}
std::unique_ptr
实现// 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
中预先声明。// 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_)) {
}