## **智能指针**
C++11 引入了 3 个智能指针类型:
1. `std::unique_ptr<T>`:独占资源所有权的指针。
2. `std::shared_ptr<T>`:共享资源所有权的指针。
3. `std::weak_ptr<T>`:共享资源的观察者,需要和 std::shared\_ptr 一起使用,不影响资源的生命周期。
std::auto\_ptr 已被废弃。
## **std::unique\_ptr**
简单说,当我们独占资源的所有权的时候,可以使用 std::unique\_ptr 对资源进行管理——离开 unique\_ptr 对象的作用域时,会自动释放资源。这是很基本的 RAII 思想。
std::unique\_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。
1. 使用裸指针时,要记得释放内存。
~~~text
{
int* p = new int(100);
// ...
delete p; // 要记得释放内存
}
~~~
1. 使用 std::unique\_ptr 自动管理内存。
~~~text
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
//...
// 离开 uptr 的作用域的时候自动释放内存
}
~~~
1. std::unique\_ptr 是 move-only 的。
~~~text
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
std::unique_ptr<int> uptr1 = uptr; // 编译错误,std::unique_ptr<T> 是 move-only 的
std::unique_ptr<int> uptr2 = std::move(uptr);
assert(uptr == nullptr);
}
~~~
1. std::unique\_ptr 可以指向一个数组。
~~~text
{
std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
for (int i = 0; i < 10; i++) {
uptr[i] = i * i;
}
for (int i = 0; i < 10; i++) {
std::cout << uptr[i] << std::endl;
}
}
~~~
1. 自定义 deleter。
~~~text
{
struct FileCloser {
void operator()(FILE* fp) const {
if (fp != nullptr) {
fclose(fp);
}
}
};
std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}
~~~
1. 使用 Lambda 的 deleter。
~~~text
{
std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
fclose(fp);
});
}
~~~
## **std::shared\_ptr**
std::shared\_ptr 其实就是对资源做引用计数——当引用计数为 0 的时候,自动释放资源。
~~~text
{
std::shared_ptr<int> sptr = std::make_shared<int>(200);
assert(sptr.use_count() == 1); // 此时引用计数为 1
{
std::shared_ptr<int> sptr1 = sptr;
assert(sptr.get() == sptr1.get());
assert(sptr.use_count() == 2); // sptr 和 sptr1 共享资源,引用计数为 2
}
assert(sptr.use_count() == 1); // sptr1 已经释放
}
// use_count 为 0 时自动释放内存
~~~
和 unique\_ptr 一样,shared\_ptr 也可以指向数组和自定义 deleter。
~~~text
{
// C++20 才支持 std::make_shared<int[]>
// std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
std::shared_ptr<int[]> sptr(new int[10]);
for (int i = 0; i < 10; i++) {
sptr[i] = i * i;
}
for (int i = 0; i < 10; i++) {
std::cout << sptr[i] << std::endl;
}
}
{
std::shared_ptr<FILE> sptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
std::cout << "close " << fp << std::endl;
fclose(fp);
});
}
~~~
## **std::shared\_ptr 的实现原理**
一个 shared\_ptr 对象的内存开销要比裸指针和无自定义 deleter 的 unique\_ptr 对象略大。
~~~text
std::cout << sizeof(int*) << std::endl; // 输出 8
std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 输出 8
std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>)
<< std::endl; // 输出 40
std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 输出 16
std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
std::cout << "close " << fp << std::endl;
fclose(fp);
});
std::cout << sizeof(sptr) << std::endl; // 输出 16
~~~
无自定义 deleter 的 unique\_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。
shared\_ptr 需要维护的信息有两部分:
1. 指向共享资源的指针。
2. 引用计数等共享资源的控制信息——实现上是维护一个指向控制信息的指针。
所以,shared\_ptr 对象需要保存两个指针。shared\_ptr 的 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared\_ptr 对象的大小。
当我们创建一个 shared\_ptr 时,其实现一般如下:
~~~text
std::shared_ptr<T> sptr1(new T);
~~~
![](https://pic1.zhimg.com/80/v2-b0bf2ba18de7f3a364dda717a0a51c54_1440w.jpg)
复制一个 shared\_ptr :
~~~text
std::shared_ptr<T> sptr2 = sptr1;
~~~
![](https://pic3.zhimg.com/80/v2-e48536157d6181fdb97181769a7c364a_1440w.jpg)
为什么控制信息和每个 shared\_ptr 对象都需要保存指向共享资源的指针?可不可以去掉 shared\_ptr 对象中指向共享资源的指针,以节省内存开销?
答案是:不能。 因为 shared\_ptr 对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。
来看一个例子。
~~~text
struct Fruit {
int juice;
};
struct Vegetable {
int fiber;
};
struct Tomato : public Fruit, Vegetable {
int sauce;
};
// 由于继承的存在,shared_ptr 可能指向基类对象
std::shared_ptr<Tomato> tomato = std::make_shared<Tomato>();
std::shared_ptr<Fruit> fruit = tomato;
std::shared_ptr<Vegetable> vegetable = tomato;
~~~
![](https://pic4.zhimg.com/80/v2-8dfc6105c0016d7c22e6212732faf1ef_1440w.jpg)
另外,std::shared\_ptr 支持 aliasing constructor。
~~~text
template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
~~~
Aliasing constructor,简单说就是构造出来的 shared\_ptr 对象和参数 r 指向同一个控制块(会影响 r 指向的资源的生命周期),但是指向共享资源的指针是参数 ptr。看下面这个例子。
~~~text
using Vec = std::vector<int>;
std::shared_ptr<int> GetSPtr() {
auto elts = {0, 1, 2, 3, 4};
std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);
return std::shared_ptr<int>(pvec, &(*pvec)[2]);
}
std::shared_ptr<int> sptr = GetSPtr();
for (int i = -2; i < 3; ++i) {
printf("%d\n", sptr.get()[i]);
}
~~~
![](https://pic2.zhimg.com/80/v2-ada2e2b5dc8551bf879d77a2b484e071_1440w.jpg)
看上面的例子,使用 std::shared\_ptr 时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。C++ 标准库提供了 std::make\_shared 函数来创建一个 shared\_ptr 对象,只需要一次内存分配。
![](https://pic1.zhimg.com/80/v2-49e619699b5c924097e027cd173df758_1440w.jpg)
这种情况下,不用通过控制块中的指针,我们也能知道共享资源的位置——这个指针也可以省略掉。
![](https://pic4.zhimg.com/80/v2-4d50514c2c685097588908f69c3dc027_1440w.jpg)
## **std::weak\_ptr**
std::weak\_ptr 要与 std::shared\_ptr 一起使用。 一个 std::weak\_ptr 对象看做是 std::shared\_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:
1. 如果需要使用 weak\_ptr 正在观察的资源,可以将 weak\_ptr 提升为 shared\_ptr。
2. 当 shared\_ptr 管理的资源被释放时,weak\_ptr 会自动变成 nullptr。\\
~~~text
void Observe(std::weak_ptr<int> wptr) {
if (auto sptr = wptr.lock()) {
std::cout << "value: " << *sptr << std::endl;
} else {
std::cout << "wptr lock fail" << std::endl;
}
}
std::weak_ptr<int> wptr;
{
auto sptr = std::make_shared<int>(111);
wptr = sptr;
Observe(wptr); // sptr 指向的资源没被释放,wptr 可以成功提升为 shared_ptr
}
Observe(wptr); // sptr 指向的资源已被释放,wptr 无法提升为 shared_ptr
~~~
![](https://pic1.zhimg.com/80/v2-5f40e9422551bb244753e87ef43d1e64_1440w.jpg)
当 shared\_ptr 析构并释放共享资源的时候,只要 weak\_ptr 对象还存在,控制块就会保留,weak\_ptr 可以通过控制块观察到对象是否存活。
![](https://pic4.zhimg.com/80/v2-f387c7135acf9101029fc4981ce2269b_1440w.jpg)
## **enable\_shared\_from\_this**
一个类的成员函数如何获得指向自身(this)的 shared\_ptr? 看看下面这个例子有没有问题?
~~~text
class Foo {
public:
std::shared_ptr<Foo> GetSPtr() {
return std::shared_ptr<Foo>(this);
}
};
auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);
~~~
上面的代码其实会生成两个独立的 shared\_ptr,他们的控制块是独立的,最终导致一个 Foo 对象会被 delete 两次。
![](https://pic3.zhimg.com/80/v2-a4b338b9b6a84d56fb3ceba59b168aba_1440w.jpg)
成员函数获取 this 的 shared\_ptr 的正确的做法是继承 std::enable\_shared\_from\_this。
~~~text
class Bar : public std::enable_shared_from_this<Bar> {
public:
std::shared_ptr<Bar> GetSPtr() {
return shared_from_this();
}
};
auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);
~~~
一般情况下,继承了 std::enable\_shared\_from\_this 的子类,成员变量中增加了一个指向 this 的 weak\_ptr。这个 weak\_ptr 在第一次创建 shared\_ptr 的时候会被初始化,指向 this。
![](https://pic4.zhimg.com/80/v2-fbbbc83da3c87fe48817a4cbda49ce03_1440w.jpg)
似乎继承了 std::enable\_shared\_from\_this 的类都被强制必须通过 shared\_ptr 进行管理。
~~~text
auto b = new Bar;
auto sptr = b->shared_from_this();
~~~
在我的环境下(gcc 7.5.0)上面的代码执行的时候会直接 coredump,而不是返回指向 nullptr 的 shared\_ptr:
~~~text
terminate called after throwing an instance of 'std::bad_weak_ptr'
what(): bad_weak_ptr
~~~
## **小结**
智能指针,本质上是对资源所有权和生命周期管理的抽象:
1. 当资源是被独占时,使用 std::unique\_ptr 对资源进行管理。
2. 当资源会被共享时,使用 std::shared\_ptr 对资源进行管理。
3. 使用 std::weak\_ptr 作为 std::shared\_ptr 管理对象的观察者。
4. 通过继承 std::enable\_shared\_from\_this 来获取 this 的 std::shared\_ptr 对象。
## **参考资料**
1. **[Back to Basics: Smart Pointers](https://link.zhihu.com/?target=https%3A//github.com/CppCon/CppCon2019/blob/master/Presentations/back_to_basics_smart_pointers/back_to_basics_smart_pointers__arthur_odwyer__cppcon_2019.pdf)**
2. **[unique\_ptr](https://link.zhihu.com/?target=https%3A//en.cppreference.com/w/cpp/memory/unique_ptr)**
3. **[shared\_ptr](https://link.zhihu.com/?target=https%3A//en.cppreference.com/w/cpp/memory/shared_ptr)**
4. **[weak\_ptr](https://link.zhihu.com/?target=https%3A//en.cppreference.com/w/cpp/memory/weak_ptr)**
5. **[enable\_shared\_from\_this](https://link.zhihu.com/?target=https%3A//en.cppreference.com/w/cpp/memory/enable_shared_from_this)**
- C++基础
- 什么是 POD 数据类型?
- 面向对象三大特性五大原则
- 低耦合高内聚
- C++类型转换
- c++仿函数
- C++仿函数了解一下?
- C++对象内存模型
- C++11新特性
- 智能指针
- 动手实现C++的智能指针
- C++ 智能指针 shared_ptr 详解与示例
- 现代 C++:一文读懂智能指针
- Lamda
- c++11多线程
- std::thread
- std::async
- std::promise
- std::future
- C++11 的内存模型
- 初始化列表
- std::bind
- std::tuple
- auto自动类型推导
- 可变参数模板
- 右值引用与移动语义
- 完美转发
- 基于范围的for循环
- C++11之POD类型
- std::enable_if
- C++14/17
- C++20
- 协成
- 模块
- Ranges
- Boost
- boost::circular_buffer
- 使用Boost.Asio编写通信程序
- Boost.Asio C++ 网络编程
- 模板
- 模板特化/偏特化
- C++模板、类模板、函数模板详解都在这里了
- 泛化之美--C++11可变模版参数的妙用
- 模板元编程
- 这是我见过最好的模板元编程文章!