🌽智能指针
2022-5-30
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 

 
新版本的 C++ 最重要的更新之一就是提供了更为强大的智能指针(smart pointer),智能指针是模拟指针的抽象数据结构,提供了额外的功能包括内存管理(memory management)或者界限检查(bounds checking)。在保留性能的情况下,减少了因为指针滥用导致的难以查找的bug。智能指针常用于跟踪其指向的内存,亦可用于管理其它资源,比如:网络连接和文件句柄。
在智能指针中,一个对象什么情况下被析构或被删除,是由指针本身决定的,并不需要用户进行手动管理。有自动垃圾回收机制的语言不需要智能指针用于内存管理,但依然可以用于缓存管理和其它资源管理(如:文件句柄和网络)。
 
之前的程序只使用了静态(static)栈(stack)内存。静态内存用于本地静态变量、类静态成员和定义在任何函数之外的变量。栈内存则用于保存函数内定义的非静态对象。在静态和栈内存中分配的对象,其创建和销毁都由编译器自动管理。
除此之外,每个程序还有一个内存池,这种内存被称为自由内存(free store)堆(heap)。程序使用堆来分配给动态对象,即在运行时分配内存给对象。这种对象的生命周期由程序进行管理,代码中必须显式销毁不再使用的对象。除非是必要的情况下,不应该直接管理动态内存,因为,这是非常容易出错的。
 
C++中原始指针使用 new 操作来分配和初始化动态对象,并返回一个指向对象的指针。delete操作符则以此指针为操作数,销毁其指向的对象,并释放其内存。
动态内存容易出错的原因在于:很难在正确的时机释放内存,可能忘记释放并造成内存泄漏(memory leak)或者在指针依然在使用时释放其内存,这种时候指针指向的内存是无效的。
 

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
  1. 不需要显式地释放资源。
  1. 采用这种方式,对象所需的资源在其生命期内始终保持有效
 
使用RAII的思想设计一个智能指针SmartPtr
上面还实现了*->的运算符重载,这样做是为了让其对象能够像指针一样使用:
 
实现好了智能指针,程序就不会出现内存泄漏了,调整后的代码如下:
此时这个程序发生除0错误时会抛异常,跳到main函数进行捕获,但此时也会出了new资源的生命周期,自动调用托管给的智能指针的析构函数来进行释放,不会出现内存泄漏问题。
当任何一个new出来的资源抛异常时,都会跳出去进行捕获,同样是先前new出来的资源出了生命周期,自动调用托管给的智能指针的析构函数来进行释放,而后面的还没new,自然不用处理,也不会出现内存泄漏问题。
 
 
智能指针的浅拷贝问题
此时拿sp1拷贝给sp2完成拷贝构造,或者是拿sp3赋值给sp3都会存在问题,导致程序崩溃:
  • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此sp1拷贝sp2后,相当于sp1sp2管理了同一块内存空间,所以当sp1sp2析构时就会导致这块空间被释放两次,程序崩溃。
  • 类似的,把sp4赋值给sp3时,相当于sp3sp4管理的都是原来sp3管理的空间,当sp3sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。
需要注意的是,智能指针就是要模拟原生指针的行为,将一个指针赋值给另一个指针时,目的就是为了让这两个指针指向同一块内存空间,所以这里本就应该时浅拷贝(和迭代器那块有点像,也是浅拷贝),迭代器浅拷贝没问题的原因在于其不释放节点,而智能指针的资源是托管给我的,需要释放资源,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同类型的智能指针。
为了使得动态内存易于使用并且更加安全,新标准中提供了两个智能指针来管理动态对象。shared_ptr 通过引用计数来实现,允许多个指针指向同一个对象,unique_ptr 则拥有其指向的对象,因而是排外的。标准库还定义了 weak_ptr 表示对shared_ptr管理的对象的弱引用。所有这三个类都定义在memory头文件中。

shared_ptr 

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  1. 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数进行--
  1. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  1. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
 
通过引用计数的方式就能至此多个对象一起管理某个资源,也就支持了智能指针的拷贝,并且也只有当一个资源的引用计数减到0时才会释放资源,从而保证同一个资源不会释放多次。
 
shared_ptr的模拟实现:
  1. 在shared_ptr类中定义一个int*类型的成员变量_pCount在堆上,表示智能指针对象管理的资源对应的引用计数
  1. 单独写个Release释放函数,用于处理释放资源和计数的函数,将管理资源对应的引用计数--,当减到0时释放资源和计数,便于后续析构函数和拷贝赋值函数的复用
  1. 在析构函数中直接复用Release释放函数
  1. 在构造函数中获取资源,并把引用计数设为1,表示当前仅有一个资源在管理此资源
  1. 在拷贝构造函数中,一同管理传入对象的资源和计数,并且++计数,每拷贝一次,就++计数一次
  1. 在拷贝赋值函数中,先将当前对象管理的资源对应的计数--,如果减到0就要释放此资源(这个步骤可以复用Release函数),然后一同管理传入对象的资源和计数,同时对应的计数++,注意(管理同一块资源的对象之间不能进行赋值操作)
  1. 对*和->运算符进行重载,使其可以像指针一样使用
引用计数一定是int*类型的指针(在堆区)
 
 
 
智能指针也是模板类,创建智能指针需要提供指向的对象类型作为模板参数,默认初始化的智能指针表示空指针:
 
使用智能指针的方式于常规指针是一样的。解引用智能指针返回其指向的对象引用。当将智能指针用于条件语句中,效果相当于测试其是否是空指针。
 
shared_ptrunique_ptr 共有的操作:
notion image
shared_ptr特有的操作:
notion image

make_shared函数

最安全的分配和使用动态内存的方式是调用库函数 make_shared。此函数在动态内存中分配一个对象并初始化它,然后返回一个指向它的 shared_ptr 智能指针。make_shared 被定义在memory头文件中,是一个模板函数,调用时需要提供需要创建的对象类型:
make_shared 使用其参数构建一个给定类型的对象,创建类对象时传的参数必须匹配其任一构造函数的原型,创建内置类型对象则直接传递其值。如果没有传递任何参数,则对象是值初始化的。
 

shared_ptr的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他share_ptr指向相同的对象:
可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(referencecount )。无论何时拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当给shared_ptr赋予一个新值或是shared_ptr被销毁,例如一个局部的shared_ptr离开其作用域时,计数器就会递减。
一旦shared_ptr的计数变为 0 之后,shared_ptr 就会自动释放其指向的对象的内存。
到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。
 

shared_ptr自动释放其指向的对象...

当最后一个指向对象的 shared_ptr 被销毁时,其指向的对象将自动销毁。销毁对象使用的成员函数是析构函数(destructor)。类似于构造函数,每个类都有析构函数。构造函数用于控制初始化,析构函数控制当对象销毁时发生什么。
shared_ptr 的析构函数递减其引用计数,当引用计数变为 0 时,shared_ptr 的析构函数将销毁其指向的对象,并释放其内存。
 

自动释放与其相关动态对象的内存

shared_ptr 可以自动释放动态对象使得使用动态内存相当简单。
p被销毁时,其引用计数将递减。由于p是指向factory分配的动态内存的唯一指针,当p被销毁时,它会自动销毁其指向的对象,并且内存将被释放。
 
而如果有任何其它 shared_ptr 指向这个对象,那么它就不会被释放内存。
由于内存只有到了最后一个 shared_ptr 销毁后才会释放,所以重要的是确保当不再需要动态对象时,shared_ptr 对象不会一直存在。一种可能保存不必要的 shared_ptr 是在将其放在容器中,之后又调整了容器使得不再需要所有元素,应当将不需要的元素擦除。
 

类与具有动态生命周期的资源

程序在以下三种情况下会使用动态内存:
  1. 不知道需要多少对象
  1. 不知道需要的对象的精确类型
  1. 在多个对象之间共享数据
 
 

shared_ptr运用于 new

当不初始化智能指针,其将被初始化为空指针。如果将智能指针初始化为一个从 new 返回的指针,那么此智能指针将接管这块动态内存:
接受指针参数的智能指针构造函数是explicit的。因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针:
p1的初始化隐式地要求编译器用一个new返回的int*来创建一个shared_ptr。由于不能进行内置指针到智能指针间的隐式转换,因此这条初始化语句是错误的。出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
 
必须将shared_ptr显式绑定到一个想要返回的指针上:
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代 delete
 
其它的定义和改变 shared_ptr 的方式:
notion image
notion image
 
不要混合使用内置指针和智能指针
shared_ptr只能与其它拷贝自己的shared_ptr配合使用。当在创建动态对象时就将其它与一个 shared_ptr 绑定,将没有机会将这个对象与另外一个独立的shared_ptr进行绑定。如果混合使用内置指针,将导致在智能指针已经释放掉了内存,而指针并不知道这种情况,结果将导致指针变为空悬指针。一旦将shared_ptr与内置指针绑定,这个智能指针将获取内存的所有权,从而,不应该再继续使用内置指针来访问那块内存了。
使用内置指针访问智能指针所拥有的对象是很危险的,因为不知道对象将在何时被销毁。
 
不要使用shared_ptr.get()得到的指针用于初始化或者赋值给另外一个智能指针。这个函数的目的在于某些以前的函数并不接受智能指针作为参数,get返回的指针一定不能在外部被删除。尽管编译器不检查,但使用此返回的指针绑定到另外一个智能指针是一个错误。
 
 

unique_ptr

C++11引入的unique_ptr通过防拷贝的方式解决智能指针的拷贝问题,也就是简单粗暴的防止对智能指针对象进行拷贝,如果强行拷贝,那么编译就会报错,这样也能保证资源不会被多次释放:
 
unique_ptr的模拟实现:
  1. 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源
  1. *->运算符进行重载
  1. C++98的方式把拷贝构造函数和拷贝赋值函数只声明,不实现,并且声明为私有,或者用C++11的方式在这俩函数后面加=delete,从而防止外部调用。
 
 
unique_ptr具有其指向的对象的所有权。不似shared_ptr ,只有一个unique_ptr指向一个对象,其将独占对象。对象将在unique_ptr销毁时被释放。
以下是unique_ptr特有的操作:
notion image
unique_ptr 没有类似于 make_shared 的函数,相反,通常直接将 unique_ptr 直接与 new 返回的内置指针绑定。与 shared_ptr 一样,只能使用直接初始化对其进行初始化,而不能直接用内置指针对智能指针进行等号初始化。因为 unique_ptr 拥有其指向的对象,所以,unique_ptr 不支持拷贝和赋值。
调用 release 会切断unique_ptr和对象之间的关系,返回的指针通常用于初始化或赋值另外一个智能指针。这样对象所有权就从一个智能指针转移到了另外一个智能指针,然而,如果不使用另外一个智能指针来接收这个指针,将由程序员来管理这个资源。
 
 

传递和返回unique_ptr

不能拷贝unique_ptr的原则有一个例外就是可以拷贝或赋值一个即将销毁的unique_ptr,在新标准中这叫移动(move)
新的程序应该放弃使用 auto_ptr 的冲动,auto_ptr 是一个不好的设计,不能用于容器中,不能从函数中返回。
auto_ptrC++98引入的智能指针,其通过管理权转移的方式解决智能指针的拷贝问题,保证一个资源在任何时刻都只有一个对象在对其进行管理,这样同一个资源就不会被多次释放了:
 
 

向unique_ptr传递删除器

类似shared_ptrunique_ptr默认情况下用delete释放它指向的对象。与shared_ptr一样,可以重载一个unique_ptr中默认的删除器。但是,unique_ptr管理删除器的方式与shared_ptr不同。
重载一个unique_ptr 中的删除器会影响到unique_ptr 类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,必须在尖括号中unique_ptr 指向类型之后提供删除器类型。在创建或reset 一个这种unique_ptr 类型的对象时,必须提供一个指定类型的可调用对象(删除器):
作为一个更具体的例子,重写连接程序,用unique_ptr来代替shared_ptr
使用了decltype来指明函数指针类型。由于decltype(end_connection)返回一个函数类型,所以必须添加一个*来指出正在使用该类型的一个指针。
 

weak_ptr

weak_ptr 是一种不控制其指向的对象的生命周期的智能指针,相反它指向shared_ptr管理的对象。将weak_ptr绑定到shared_ptr并不会改变其引用计数。当最后一个shared_ptr被销毁时,其管理的对象依然会被释放,即使weak_ptr依然指向这个对象。因此,称之为弱指针。
 
以下是 weak_ptr 的常用操作:
notion image
使用 weak_ptr ,可以在不影响其指向的对象的生命周期的情况下,安全的访问该对象。
 
 
 

智能指针和异常

使用异常的程序需要保证,当异常发生时资源可以被回收,其中一个简单地方法就是使用智能指针。智能指针可以保证即使是在函数异常退出地情况下依然会正确释放不再使用地内存。而内置指针则不做任何事情,由于在函数外部根本无法访问这块内存,从而就造成了内存泄漏。
有一些类被设计用于CC++的胶水层时,通常需要用用户自己手动释放内存。可以使用用于管理动态内存的技术来管理这些资源。即将资源交给 shared_ptr 进行管理。首先需要定义一个函数来替代delete 操作符,可以调用这个删除器(deleter)并以存储在 shared_ptr 中的指针作为参数来进行实际的清理工作。
 
智能指针只有被恰当的使用才能发出作用,以下是一些约定:
  • 不要使用相同的内置指针去初始化超过一个智能指针;
  • 不要使用删除 get 函数返回的指针;
  • 不要用 get 函数返回指针去初始化或 reset 别的智能指针;
  • 当使用 get 函数返回的指针时,应当记住当最后一个智能指针销毁时,这个指针会变得无效;
  • 使用智能指针管理资源而不是内存时,记得传递一个删除器(deleter)过去;
  • C++
  • 动态内存管理动态数组
    目录