🌴智能指针|C++11

智能指针

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己 管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的 概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常 时内存泄露等问题等,使用智能指针能更好的管理堆内存。

C++里面的四个智能指针: auto_ptr,unique_ptr,shared_ptr, weak_ptr 其中后三个是C++11支持, 并且第一个已经被C++11弃用。

使用这些智能指针时需要引用头文件<memory>

shared_ptr共享的智能指针

共享的智能指针 std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析 构的时候,内存才会被释放。 shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个 shared_ptr对象销毁时,被管理对象自动销毁。 简单来说,shared_ptr实现包含了两部分,

  • 一个指向堆上创建的对象的裸指针:raw_ptr

  • 一个指向内部隐藏的、共享的管理对象:share_count_object

第一部分没什么好说的,第二部分是需要关注的重点. use_count:当前这个堆上对象被多少对象引用了,简单来说就是引用计数。

shared_ptr的基本用法

  1. 初始化

通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr,代码如下:

// 智能指针初始化
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));
if(p3) {
	cout << "p3 is not null";
}

我们应该优先使用make_shared来构造智能指针,因为他更高效。

当使用 std::shared_ptr 来管理动态分配的对象时,通常会涉及两个主要的动态内存分配:

  1. 对象的内存分配:这是为了分配存储对象本身的内存。在C++中,这通常是通过 new 表达式完成的,它分配了足够的内存来存储特定类型的对象,并调用该类型的构造函数来初始化这块内存。

  2. 控制块的内存分配std::shared_ptr 需要一个额外的控制块来存储与对象管理相关的信息,如引用计数和可能的自定义删除器。这个控制块也需要动态分配内存。

使用 newstd::shared_ptr 构造函数

当你手动使用 new 表达式创建一个对象,并将其传递给 std::shared_ptr 的构造函数时,首先是对象的内存分配,然后是控制块的内存分配:

在这种情况下,new MyClass(42, 3.14) 首先分配内存并构造 MyClass 对象,然后 std::shared_ptr 构造函数为其控制块分配另一块内存。

使用 std::make_shared

相比之下,std::make_shared 只进行一次动态内存分配:

在这个例子中,std::make_shared 同时为 MyClass 对象和控制块分配了一块连续的内存。这种方法更高效,因为它减少了内存分配的次数,并且由于对象和控制块在内存中是相邻的,还可以提高缓存的使用效率。

不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:

shared_ptr不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。 对于一个未初始化的智能指针,可以通过reset方法来初始化,当智能指针有值的时候调用reset会引起引用计数减1。另外智能指针通过重载的bool类型操作符来判断是否为空。

  1. 获取原始指针

当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:

谨慎使用p.get()的返回值,如果你不知道其危险性则永远不要调用get()函数。

p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生, 遵守以下几个约定:

  • 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的。保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针

  • 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

  1. 指定删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。

当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个lambda表达式,上面的写法可以改为:

当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象(后来C++17支持了),代码如下所示:

在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete<T>()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:

另外,我们还可以自己封装一个make_shared_array方法来让shared_ptr支持数组,代码如下:

😅但是std::make_shared 并不支持创建数组类型的 shared_ptr。如果你需要创建一个数组并想利用 make_shared 的性能优势(如减少内存分配次数),你可能需要考虑其他解决方案,例如使用 std::vector 或其他容器类,或者自行管理内存。

从 C++20 开始,std::make_shared 支持创建数组,可以直接使用 std::make_shared 来创建并管理数组。

示例:使用 std::make_shared 创建数组(C++20 及之后)

在这个示例中,std::make_shared<int[]>(10) 创建了一个 std::shared_ptr<int[]>,管理一个大小为 10 的数组。

  • 智能指针什么时候需要指定删除器

在需要 delete 以外的析构行为的时候用. 因为 shared_ptr 在引用计数为 0 后默认调用 delete ptr; 如果不满足需求就要提供定制的删除器.

一些场景:

  • 资源不是 new 出来的(一般也意味着不能 delete), 比如可能是 malloc 出来的

  • 资源是被第三方库管理的 (第三方提供 资源获取 和 资源释放 接口, 那么要么写一个 wrapper 类要么就提供定制的 deleter)

  • 资源不是 RAII 的, 意味着析构函数不会把资源完全释放掉,也就是单纯 delete还不够。

使用shared_ptr要注意的问题

  1. 不要用一个原始指针初始化多个shared_ptr,例如下面错误范例:

  1. 不要在函数实参中创建shared_ptr,对于下面的写法:

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也 可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还 没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针,代码如下:

  • 形参和实参的区别和联系:

    • 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。

    • 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。

    • 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。

    • 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。

请看下面的例子:

在这段代码中,函数定义处的 m、n 是形参,函数调用处的 a、b 是实参。通过 scanf() 可以读取用户输入的数据,并赋值给 a、b,在调用 sum() 函数时,这份数据会传递给形参 m、n。

从运行情况看,输入 a 值为 1,即实参 a 的值为 1,把这个值传递给函数 sum() 后,形参 m 的初始值也为 1,在函数执行过程中,形参 m 的值变为 5050。函数运行结束后,输出实参 a 的值仍为 1,可见实参的值不会随形参的变化而变化。

  1. 通过shared_from_this()返回this指针。不要将this指针作为shared_ptr返回出来,因为this指针 本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。

在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1sp2,而他们之间是没有任何关系 的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。

正确返回thisshared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的 成员函数shared_from_this()来返回thisshared_ptr,如下所示。

  1. 避免循环引用。循环引用会导致内存泄漏,比如:

循环引用导致apbp的引用计数为2,在离开作用域之后,apbp的引用计数减为1,并不回减为0,导 致两个指针都不会被析构,产生内存泄漏。解决的办法是把A和B任何一个成员变量改为weak_ptr

  • 什么是循环引用?

循环引用(circular reference)是指在编程中,两个或多个对象之间形成一个循环的引用关系,导致这些对象之间的内存无法被正确释放,从而引发内存泄漏。这种情况也被称为循环依赖或循环关联。

unique_ptr独占的智能指针

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将 一个unique_ptr赋值给另一个unique_ptr。下面的错误示例。

unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其 他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。例如

std::make_sharedc++11的一部分,std::make_unique不是,它是在c++14里加入标准库的。

使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的 一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。

除了unique_ptr的独占性, unique_ptr和shared_ptr还有一些区别,比如

  • unique_ptr可以指向一个数组,代码如下所示

std::shared_ptr<int []> ptr2(new int[10])C++17中支持了此类写法

  • unique_ptr指定删除器和shared_ptr有区别

unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:

在上面的代码中第7行,func_ptr的类型和lambda表达式的类型是一致的。在lambda表达式没有捕获任何变量的情况下是正确的,如果捕获了变量,编译时则会报错:

上面的代码中错误原因是这样的,在lambda表达式没有捕获任何外部变量时,可以直接转换为函数指针,一旦捕获了就无法转换了(仿函数),如果想要让编译器成功通过编译,那么需要使用可调用对象包装器来处理声明的函数指针:

关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。

weak_ptr弱引用的智能指针

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互 使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内 存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设 计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr 是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引 用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数, 和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得 shared_ptr。

weak_ptr没有重载操作符*->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得 资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者 来监视shared_ptr中管理的资源是否存在。weak_ptr还可以返回this指针和解决循环引用的问题。

weak_ptr的基本用法

  1. 通过use_count()方法获取当前观察资源的引用计数,如下所示:

  1. 通过expired()方法判断所观察资源是否已经释放,如下所示:

  1. 通过lock方法获取监视的shared_ptr,如下所示:

weak_ptr返回this指针

shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生 std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是 std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用 shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回,再看前面的范例

在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,sp1的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。

需要注意的是,获取自身智能指针的函数尽量在shared_ptr的构造函数被调用之后才能使用,因为 enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

weak_ptr解决循环引用问题

在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过 weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr

这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。

weak_ptr使用注意事项

  1. weak_ptr在使用前需要检查合法性。

因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。 得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。 因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后 一个weak_ptr对象被析构,这块“堆”存储块才能被回收。如果shared_ptr sp_ok和weak_ptr wp;属于同一个作用域呢?如下所示:

reference

Last updated

Was this helpful?