C++ 智能指针的使用
从 C++ 11 开始引入了智能指针的概念,并使用了引用计数的想法,让程序员不再需要关心手动释放内存。使用智能指针需要包含头文件 <memory> 。
std::shared_ptr
shared_ptr 代表的是共享所有权,即多个 shared_ptr 可以共享同一块内存。它能够记录多少个 shared_ptr 共同指向一个对象,从而消除显式的调用 delete,当引用计数变为零的时候就会将对象自动删除。
复制与移动
从语义上来看,shared_ptr 是支持复制的。
1 | auto w = std::make_shared<foobar>(); |
shared_ptr 也支持移动。从语义上来看,移动指的是所有权的传递。
1 | auto w = std::make_shared<foobar>(); |
性能
- 内存占用高
shared_ptr的内存占用是裸指针的两倍。因为除了要管理一个裸指针外,还要维护一个引用计数。 - 原子操作性能低
考虑到线程安全问题,引用计数的增减必须是原子操作。而原子操作一般情况下都比非原子操作慢。 - 使用移动优化性能
shared_ptr在性能上固然是低于unique_ptr。而通常情况,我们也可以尽量避免shared_ptr复制。如果,一个shared_ptr需要将所有权共享给另外一个新的shared_ptr,而我们确定在之后的代码中都不再使用这个shared_ptr,那么这是一个非常鲜明的移动语义。对于此种场景,我们尽量使用std::move,将shared_ptr转移给新的对象。因为移动不用增加引用计数,性能比复制更好。
shared_from_this()
在类的内部获取 shared_ptr 是在所难免的。
在日常 C++ 编程中,为了更好的管理资源,我们通常借助 shared_ptr 来达到对资源的自动管理。由于其原理是通过跟踪引用计数实现的,也就是说在使用了 shared_ptr 后就不能再使用裸指针 this。比如说在类的内部直接使用 std::shared_ptr<XX>(this),就会导致智能指针失效。
shared_from_this() 是 enable_shared_from_this<T> 的成员函数,返回 shared_ptr<T> 。需要注意的是,这个函数仅在 shared_ptr<T> 的构造函数被调用之后才能使用。原因是 enable_shared_from_this::weak_ptr 并不在构造函数中设置,而是在 shared_ptr<T> 的构造函数中设置。
1 |
|
1 | gp2.use_count() = 2 |
throwing an instance of 'std::bad_weak_ptr' 错误排查
- 在构造函数中调用
shared_from_this(),此时类的实例本身尚未构造成功,weak_ptr也就尚未设置,所以程序抛出bad_weak_ptr异常。 - 类的实例是在栈上构造,或直接使用
new的裸指针。 - 存在继承情况下确保
std::enable_shared_from_this<T>声明为public。
std::unique_ptr
std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全:
1 | std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入,C++11 没有提供 `std::make_unique` |
独占性
unique_ptr 代表的是专属所有权,即由 unique_ptr 管理的内存,只能被一个对象持有,所以,unique_ptr 不支持复制和赋值 。
1 | auto w = std::make_unique<foobar>(); |
unique_ptr 只支持移动,如果想要把一个 unique_ptr 的内存交给另外一个 unique_ptr 对象管理。只能使用 std::move 转移当前对象的所有权。转移之后,当前对象不再持有此内存,新的对象将获得专属所有权。
1 | auto w = std::make_unique<foobar>(); |
释放对象
当 unique_ptr 被销毁时(如离开作用域),对象也就自动释放了,也可以通过其他方式下显示释放对象。
1 | up = nullptr; // 置为空,释放 up 指向的对象 |
release() 和 reset() 的区别在于,前者会释放控制权,返回裸指针,你还可以继续使用,而后者直接释放了指向对象。
作为参数
因为不允许被复制,所以直接把 unique_ptr 作为参数就会报错
1 |
|
可以向函数中传递普通指针,使用 get 函数就可以获取
1 |
|
或者使用引用作为参数
1 |
|
作为返回值
1 |
|
性能
因为 C++ 的 zero cost abstraction 的特点,unique_ptr 在默认情况下和裸指针的大小是一样的,所以内存上没有任何的额外消耗,性能是最优的。
与 std::shared_ptr 转换
可以把 std::unique_ptr 转换成 std::shared_ptr ,但是不能把 std::shared_ptr 转换成 std::unique_ptr 。
1 | // 1 |
std::weak_ptr
weak_ptr 是为了解决 shared_ptr 双向引用的问题。
1 | struct A; |
运行结果是 A 和 B 都不会被销毁,这是因为 a 与 b 内部的指针同时又引用了 a 与 b,这使得 a 与 b 的引用计数均变为了 2,而离开作用域时,a 与 b 智能指针被析构,却只能造成这块区域的引用计数减一,这样就导致了 a 与 b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
解决这个问题的办法就是使用弱引用指针 std::weak_ptr,std::weak_ptr 是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加。
std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作,它的唯一作用就是用于检查 std::shared_ptr 是否存在,其 expired() 方法能在资源未被释放时,会返回 false,否则返回 true。
lock()
为了解决循环引用的问题而引入 weak_ptr ,但 std::weak_ptr 没有 * 运算符和 -> 运算符,所以不能够对资源进行操作。但如果想访问 weak_ptr所引用的堆内存实体,必须调用 lock() 方法转换成一个 shared_ptr 对象。
const 与智能指针
1 | shared_ptr<T> p; ---> T * p; : nothing is const |
dynamic_pointer_cast
1 | // static_pointer_cast example |
在函数中按值返回智能指针
在函数中建议按值返回智能指针,理由如下:
- 智能指针是由移动语义提供支持:它们所持有的动态分配的资源是被移动,而不是被复制。
- 现代编译器使用返回值优化 (RVO) 技术。这能够检测到正在按值返回的对象,使用一种返回捷径来避免无用的拷贝,并且从 C++17 标准开始保证了这一点,所以即使是智能指针本身也会被优化掉。
- 通过引用返回
std::shared_ptr不会正确增加引用计数,这会在错误时间带来删除某些内容的风险。