智能指针的引入
在传统 C++ 中,总是需要使用delete
手动释放资源,很有可能就忘记了去释放资源而导致泄露。
通常的做法是对于一个对象而言,我们在构造函数的时候申请空间,而在析构函数(在离开作用域时调用)的时候释放空间,也就是我们常说的 RAII 资源获取即初始化技术。
——欧长坤《现代 C++ 教程》
C++11 引入了智能指针的概念,使用引用计数的思想,让程序员不再需要关心手动释放内存。
- 这些智能指针包括
std::shared_ptr
/std::unique_ptr
/std::weak_ptr
- 使用它们需要包含头文件
<memory>
智能指针和普通指针的区别
智能指针和普通指针的区别在于,智能指针实际上是对普通指针加了一层封装机制,它负责自动释放所指的对象,这样的一层封装机制使得智能指针可以方便的管理一个对象的生命周期。
引用计数的思想
- 引用计数是为了防止内存泄露而产生的,基本思想是对于动态分配的对象,每当增加一次对同一个对象的引用,那么引用对象的引用计数就会增加一,每删除一次引用,引用计数就会减一,当一个对象的引用计数减为零时,就自动删除指向的堆内存。
- 引用计数不是垃圾回收,引用计数能够尽快收回不再被使用的对象,同时在回收的过程中也不会造成长时间的等待,更能够清晰明确的表明资源的生命周期。
std::unique_ptr
unique_ptr
是一种独占所有权的智能指针,即同一时间只能有一个 unique_ptr
指向某个对象。当 unique_ptr
被销毁时(例如超出作用域或被重置),它所指向的对象也会被自动删除。这种特性使得 unique_ptr
非常适用于管理在堆上动态分配的单个对象。
- 它禁止其他智能指针与其共享同一个对象,从而保证代码的安全
// 独占所有权
std::unique_ptr<int> pointer = std::make_unique<int>(10);
std::unique_ptr<int> pointer2 = pointer; // 非法
- C++11中没有提供
std::make_unique
方法,C++14中才引入,不过可以自行实现
// make_unique 自行实现
template<typename T, typename ...Args>
std::unique_ptr<T> make_unique( Args&& ...args ) {
return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) );
}
- 既然是独占,就是不可复制。但是可以利用
std::move
将其转移给其他的unique_ptr
更多 unique_ptr 相关可以参考这篇文章 用C++从0到1实现一下自己的unique_ptr
std::shared_ptr
std::shared_ptr
是一种共享式智能指针,允许多个shared_ptr
共享对同一个对象的所有权,使用引用计数机制记录有多少个 shared_ptr
共同指向一个对象。
- 当最后一个指向该对象的
shared_ptr
超出作用域或被销毁时,引用计数变为零,对象将自动删除,从而避免显式的调用delete
。
void func() {
std::shared_ptr<int> sptr1(new int(42));
{
std::shared_ptr<int> sptr2 = sptr1; // sptr1 和 sptr2 指向同一个整数
} // sptr2 超出作用域,但由于 sptr1 仍然存在,所以不释放内存
} // sptr1 超出作用域,引用计数为0,自动释放内存
std::make_shared
用来消除显式的使用new
,std::make_shared
会创建传入参数中的对象并分配内存,返回这个对象类型的std::shared_ptr
指针。
void foo(std::shared_ptr<int> i) {
(*i)++;
}
int main() {
// auto pointer = new int(10); // illegal, no direct assignment
auto pointer = std::make_shared<int>(10);
foo(pointer);
std::cout << *pointer << std::endl; // 11
return 0;
}
std::shared_ptr
可以通过get()
方法来获取原始指针,通过reset()
来减少一个引用计数,并通过use_count()
来查看一个对象的引用计数。
std::weak_ptr
std::weak_ptr
是一种弱引用智能指针,它不对所指向的对象的内存进行管理,不会引起引用计数增加。weak_ptr
可以从shared_ptr
或另一个weak_ptr
创建。weak_ptr
通常与shared_ptr
一起使用,用于避免循环引用(导致内存泄漏)的问题。
std::weak_ptr
没有*
运算符和->
运算符,所以不能够对资源进行操作,但可以用于检查std::shared_ptr
是否存在;- 其
expired()
方法能在资源未被释放时,会返回false
,否则返回true
; - 它也可以用于获取指向原始对象的
std::shared_ptr
指针,其lock()
方法在原始对象未被释放时,返回一个指向原始对象的std::shared_ptr
指针,进而访问原始对象的资源,否则返回nullptr
;
// weak_ptr 创建与对象访问
std::shared_ptr<int> sptr(new int(42));
std::weak_ptr<int> wptr = sptr;
if (auto locked = wptr.lock()) { // 提升为 shared_ptr
// 使用 locked 访问对象
} else {
// 对象已被释放
}
循环引用问题及解决
循环引用问题:当两个对象同时使用一个shared_ptr
成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄露。
以下代码中,pa, pb 都不会被销毁,这是因为struct A,B 内部的指针ptrb、ptra同时又引用了 pa, pb,这使得 pa, pb 的引用计数均变为了 2,当离开作用域时,pa, pb 智能指针被析构,但只能造成这块区域的引用计数减一,这就导致 pa, pb 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露。
常用的解决方法是使用 weak_ptr
,它不会增加引用计数,但可以在需要时获取 shared_ptr
实例。通过将循环引用中的某个 shared_ptr
替换为 weak_ptr
,可以避免循环引用,从而解决内存泄漏问题。
比如将struct B 内改为weak_ptr
,则当pb->ptra = pa时,指针pa的引用计数不会加1,此时pa, pb 的引用计数分别为1,2,当离开作用域时,pa、pb的引用计数都减1,pa的引用计数变为0,所以析构,因此也不再有指针指向pb,pb的引用计数变为0,所以也可以析构。
//智能指针测试:循环引用
#include <iostream>
#include <memory>
using namespace std;
struct A;
struct B;
struct A{
shared_ptr<B> ptrb;
// weak_ptr<B> ptrb;
~A() {cout << "A has destoried!" << endl;}
};
struct B{
shared_ptr<A> ptra;
// weak_ptr<A> ptra;
~B() {cout << "B has destoried!" << endl;}
};
int main()
{
shared_ptr<A> pa(new A);
shared_ptr<B> pb(new B);
cout << "pa: "<< pa.use_count() << endl;
cout << "pb: "<< pb.use_count() << endl;
pa->ptrb = pb;
cout << "pa: "<< pa.use_count() << endl;
cout << "pb: "<< pb.use_count() << endl;
pb->ptra = pa;
cout << "pa: "<< pa.use_count() << endl;
cout << "pb: "<< pb.use_count() << endl;
}
/* 两个都是shared_ptr
pa: 1
pb: 1
pa: 1
pb: 2
pa: 2
pb: 2
*/
/* struct B 内改为weak_ptr
pa: 1
pb: 1
pa: 1
pb: 2
pa: 1
pb: 2
A has destoried!
B has destoried!
*/
/* struct A 内改为weak_ptr
pa: 1
pb: 1
pa: 1
pb: 1
pa: 2
pb: 1
B has destoried!
A has destoried!
*/
/* 两个都改为weak_ptr
pa: 1
pb: 1
pa: 1
pb: 1
pa: 1
pb: 1
B has destoried!
A has destoried!
*/
线程安全性
std::unique_ptr
通常是线程安全的,因为在任何给定时间只有一个线程可以访问资源。但是,如果在多个线程之间转移std::unique_ptr
的所有权,需要确保在转移所有权之前正确地同步访问。- 如果尝试在多个线程之间共享同一个
std::unique_ptr
,这通常是不安全的,因为没有内置的线程安全机制来确保同时访问资源的安全性。 - 由于引用计数的增减操作需要原子性,因此
std::shared_ptr
的引用计数操作通常是线程安全的。 - 多个线程同时读同一个
shared_ptr
对象是线程安全的,但是如果是多个线程对同一个shared_ptr
对象进行读和写,则需要加锁。 - 多线程读写
shared_ptr
所指向的同一个对象,不管是相同的shared_ptr
对象,还是不同的shared_ptr
对象,都需要加锁保护。
参考资料
内容参考自:
用C++从0到1实现一下自己的unique_ptr
CSView计算机招聘知识分享
现代 C++ 教程:高速上手 C++ 11/14/17/20
文中图来自:
现代 C++ 教程:高速上手 C++ 11/14/17/20 第5章5.4节