RAII
资源获取初始化——利用对象的生命周期来管理资源
在对象构造时获取资源,在对象析构时释放资源,这样可以确保资源的正确获取和释放,避免资源泄漏的问题。
智能指针的实现,就是RAII概念的典型,关键词:
- 引用计数
- 所有权转移
- 析构函数
- 自定义删除器
RAII的其他应用场景:
- 文件句柄
- 数据库连接
- ...
现代C++智能指针
实现方式是类模板——定义在
头文件中
你就问呗
智能指针提供了自动的内存管理和资源释放机制,可以避免手动释放内存或资源的错误和遗漏。通过将原始指针传递给智能指针,我们可以确保在智能指针超出作用域时,它会负责自动释放内存或释放资源。
- std::unique_ptr
std::unique_ptr<int> uptr = std::make_unique<int>(200);
这样子定义出来的指针uptr,当程序运行离开其作用域时会自动释放内存。
std::unique_ptr 类型的对象只能通过移动语义进行转移,而不能进行复制操作。
例如:
std::unique_ptr<int> uptr = std::make_unique<int>(200);
std::unique_ptr<int> uptr1 = uptr; //false
std::unique_ptr<int> uptr2 = std::move(uptr);
std::unique_ptr是独占所有权的,也就是说同一时间内不允许其他std::unique_ptr来指向同一目标
std::shared_ptr
std::shared_ptr<int> sptr = std::make_shared<int>(200);
对资源做引用计数,例如现在的情况就是1
sptr.use_count()==1
再例如:
std::shared_ptr<int> sptr2 = sptr1;
此时:
sptr.use_count()==2
当引用计数为0时,内存被自动释放——开销比unique_ptr大
控制块
控制块用于储存引用计数等信息,与对象分开存储,但是也是动态分配的,所以和对象一样存储在堆区。所以shared_ptr的指针有两个,一个指向对象,一个指向控制块,控制块再含有指向对象的指针。
这样的结构使得实现了以下功能:
- 共享拥有
- 控制块访问——比如 deleter 是保存在控制信息(控制块)中的
虽然控制块实际上和对象是分开的,但实际理解中最好和对象联系在一起,比如说引用计数,实际上就是对象“被引用”的计数,虽然是指针创建的但其实是针对对象来说的,这样子理解可能更好一点
std::weak_ptr
shared_ptr的观察者,与其一起使用
首先试想一下这样的情况:
class ObjectB;
class ObjectA {
public:
std::shared_ptr<ObjectB> bPtr;
};
class ObjectB {
public:
std::shared_ptr<ObjectA> aPtr;
};
int main() {
std::shared_ptr<ObjectA> a = std::make_shared<ObjectA>();
std::shared_ptr<ObjectB> b = std::make_shared<ObjectB>();
a->bPtr = b;
b->aPtr = a;
return 0;
}
所示,每个对象一直持有对方类型的 std::shared_ptr
在main函数中,初始化指针a和b并指向初始化的对象A和对象B,此时分别引用计数为1,后来分别调用两者的成员指针指向对方,这个时候A和B已经被两个shared_ptr指上了,所以计数为2;return 0退出作业域,指针a和指针b的析构函数触发,(注意这个时候析构函数做的两件事——1.销毁自己2.销毁引用计数为0的对象),此时a和b销毁所有引用计数都减一,但A和B自身还相互被对象指着,所以不会被销毁。
永动机出现了?
这种情况发生时,我们只能强行手动将指针指向拨开,但是太麻烦了,所以。。。
weak_ptr
正是这么一个解决这样的情况的指针,作为弱引用,他的指向不会使得对象引用计数加一:
class ObjectA {
public:
std::weak_ptr<ObjectB> bPtr;
};
这样的化,对象B的引用计数不会增加到2,于是析构的时候正常销毁,连带着A也销毁了。
当然这只是weak_ptr的一种功能,weak_ptr的本质功能是当对象的“观察者”。
weak_ptr本身不会对对象造成什么影响,但在需要的时候会转换为“shared_ptr”来实现对对象的操控,这样也算行了作为指针的“本分”。
expired() 判断所指向的原生指针是否被释放,如果被释放了返回 true,否则返回 false;
lock() 返回 shared_ptr,如果原生指针没有被释放,则返回一个非空的 shared_ptr,否则返回一个空的 shared_ptr
智能指针工厂函数
std::make_shared
——等价于
auto son_ = new Son();
shared_ptr
为什么不要用new来创建?比如shared_ptr
因为new会动态分配一次内存,shared_ptr又会动态分配一次内存用于保存控制块等信息,分开两次影响性能,此外使用new还会不由自主地使用delete(?),这个时候两次删除同一块内存是很危险的。换句话说人家两对就是配套用的,别拆散。
类似的有std::make_unique
、std::allocate_shared
这样可以延申出,智能指针只能管理堆对象,不能管理栈对象
其他的别人的博客里说了很多,但我感觉其实只要理解智能指针会自动释放内存这一点就可以了,可以从这一点延申出很多使用的技巧。
附加:多线程与智能指针
试想一下多线程过程中子线程引用外部对象,结果外部对象销毁的时候,子对象还在使用外部对象的野指针。这个时候用shared_ptr来管理这个外部对象,用weak_ptr将对象传给子进程,这样当对象消失的时候,weak_ptr自动置空,而不是变成一个野指针指向一块未知的内存。
//todo:源码剖析——额别期待了我自己都不信我以后会补
2023/12/18更新
麻了,我说不补充源码剖析结果面试的时候真给问上了,所以现在重新补充一下,后悔莫及。。。
最常被问到的:unique_ptr底层是怎样实现自动释放内存的?
这个问题最方便的回答方式,应该是重点放在智能指针的源代码的析构函数之上
~unique_ptr() {reset();}
可以说“智能指针本身是栈上分配的对象,由模板类实例化,所以在离开作用域时会自动调用析构函数”
所示智能指针的析构函数中的reset()函数,这个函数是用来干嘛的呢?首先如果给这个函数传入某个对象,那么这个函数会直接更改本对象的指向到这个传入的对象,并释放原对象的内存。所示调用该函数的时候没有传入对象,也可以视为传入NULL,所以调用析构函数的时候会将指针指向NULL并释放原内存,这样就自动释放了内存。
对于shared_ptr,比unique_ptr多了引用计数和指向控制块的指针,由于引用计数是存在控制块当中的,所以拥有特定方法来获取控制块当中的引用计数,具体代码可见参考博客。
对于weak_ptr,增加的则是弱引用计数。
C++ 智能指针最佳实践&源码分析 - 极术社区 - 连接开发者与智能计算生态 (aijishu.com)