0%

C++智能指针

C++智能指针

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&lt;int&gt; 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-&gt;bPtr = b;
b-&gt;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 son(son_);

为什么不要用new来创建?比如shared_ptr son = new Son();

因为new会动态分配一次内存,shared_ptr又会动态分配一次内存用于保存控制块等信息,分开两次影响性能,此外使用new还会不由自主地使用delete(?),这个时候两次删除同一块内存是很危险的。换句话说人家两对就是配套用的,别拆散。

类似的有std::make_uniquestd::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)