C++标准库笔记-智能指针

C++中常见问题

内存泄漏

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。
随着计算机应用需求的日益增加,应用程序的设计与开发也相应的日趋复杂,开发人员在程序实现的过程中处理的变量也大量增加,如何有效进行内存分配和释放,防止内存泄漏的问题变得越来越突出。例如服务器应用软件,需要长时间的运行,不断的处理由客户端发来的请求,如果没有有效的内存管理,每处理一次请求信息就有一定的内存泄漏。这样不仅影响到服务器的性能,还可能造成整个系统的崩溃。因此,内存管理成为软件设计开发人员在设计中考虑的主要方面 。
开发人员进行程序开发的过程使用动态存储变量时,不可避免地面对内存管理的问题。程序中动态分配的存储空间,在程序执行完毕后需要进行释放。没有释放动态分配的存储空间而造成内存泄漏,是使用动态存储变量的主要问题。一般情况下,开发人员使用系统提供的内存管理基本函数,如malloc、recalloc、calloc、free等,完成动态存储变量存储空间的分配和释放。但是,当开发程序中使用动态存储变量较多和频繁使用函数调用时,就会经常发生内存管理错误,例如:
    分配一个内存块并使用其中未经初始化的内容;
    释放一个内存块,但继续引用其中的内容;
    子函数中分配的内存空间在主函数出现异常中断时、或主函数对子函数返回的信息使用结束时,没有对分配的内存进行释放;
    程序实现过程中分配的临时内存在程序结束时,没有释放临时内存。内存错误一般是不可再现的,开发人员不易在程序调试和测试阶段发现,即使花费了很多精力和时间,也无法彻底消除。
产生方式的分类
以产生的方式来分类,内存泄漏可以分为四类:
  常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行时都会导致一块内存泄漏。
  偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
  一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。
  隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

句柄泄露

  句柄泄漏是进程在调用系统文件之后,没有释放已经打开的文件句柄。
  对于句柄泄露,轻则影响某个功能模块正常运行,重则导致整个应用程序崩溃。在 Windows系统中, GDI 句柄上限是 12000 个,USER 句柄上限是 18000 个。
  与 Windows 系统的设置不同,Linux 系统对进程可以调用的文件句柄数做了限制,在默认情况下,每个进程可以调用的最大句柄数为 1024 个。超过了这个数值,进程则无法获得新的句柄。因此,句柄的泄露将会对进程的功能失效造成极大的隐患。
  理论上,我们编程时,1 个进程使用的句柄数建议不应该超过 1000。

怎么解决

  解决内存泄漏和句柄泄露,最简单的方式就是new后,使用完就delete;获取句柄后,使用完毕就释放;
  在写程序中,难免会不小心忘记delete或者释放;光凭人力不能保证万无一失;让程序自动释放内存和句柄,使用RAII或者智能指针;

RAII

  **RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。**C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
  根据RAII对资源的所有权可分为常性类型和变性类型;从所管资源的初始化位置上可分为外部初始化类型和内部初始化类型。常性类型是指获取资源的地点是构造函数,释放点是析构函数,并且在这两点之间的一段时间里,任何对该RAII类型实例的操纵都不应该从它手里夺走资源的所有权。变性类型是指可以中途被设置为接管另一个资源,或者干脆被置为不拥有任何资源。外部初始化类型是指资源在外部被创建,并被传给RAII实例的构造函数,后者进而接管了其所有权。其中,常性且内部初始化的类型是最为纯粹的RAII形式,最容易理解,最容易编码。

智能指针

  smart pointer有可能如此smart以至于能够“知道”它自己是不是“指向某物”的最后一个pointer,并运用这样的知识,在它的确是该对象的最后一个拥有者而且它被删除时,销毁它所指向的对象;
  从C++11开始,C++标准库开始提供智能指针对象;分为两类型:1)共享式拥有(shared ownership);2)独占式拥有(exclusive ownership);下面分别记录两类不同智能指针功能和接口使用;

shared_ptr

概念

  可以看作式对象指针的包装对象,shared_ptr<class\ T>内部包含了一个T类型的指针和这个指针被引用的计数;多个shared_ptr指向了同一个内存对象,该对象和其相关资源会在“最后一个reference被销毁”时被释放,即“当对象再也不被使用时就被清理”的语义;
  如果对象以new产生,默认情况下清理工作就由delete完成,也可以定义其他清理方法。可以定义自己的析构策略;如果对象是使用new[]分配得到的array,就必须定义相对于的delete[]来清理;如果其他资源申请后,也需要编写相应的删除资源的操作,比如:handle、lock等等;

接口

接口功能
shared_ptr<T> spDefault构造函数,建立一个empty shared pointer,使用default deleter(也就是调用delete)
shared_ptr<T> sp(ptr)建立一个shared pointer令其拥有*ptr,使用default deleter(也就是调用delete)
shared_ptr<T> sp(ptr, del)建立一个shared pointer令其拥有*ptr,使用del作为deleter
shared_ptr<T> sp(ptr, del, ac)建立一个shared pointer令其拥有*ptr,使用del作为deleter并以ac为allocator
shared_ptr<T> sp(nullptr)建立一个empty shared pointer,使用default deleter(也就是调用delete)
shared_ptr<T> sp(nullptr, del)建立一个empty shared pointer,使用del作为deleter
shared_ptr<T> sp(nullptr, del, ac)建立一个empty shared pointer,使用del作为deleter 并以ac为allocator
shared_ptr<T> sp(sp2)建立一个shared pointer,与sp2共享拥有权
shared_ptr<T> sp(move(sp2))建立一个shared pointer,拥有sp2先前拥有的pointer(此后sp2将为empty)
shared_ptr<T> sp(sp2, ptr)Alias构造函数,建立一个shared pointer,共享sp2的拥有权,但指向*ptr
shared_ptr<T> sp(wp)基于一个weak pointer wp创建出一个shared pointer
shared_ptr<T> sp(move(up))基于一个unique_ptr up创建出一个shared pointer
shared_ptr<T> sp(move(ap))基于一个auto_ptr ap创建出一个shared pointer
sp.~shared_ptr()析构函数,调用deleter——如果sp拥有一个对象
sp = sp2赋值(此后sp将共享sp2的拥有权,放弃其先前所拥有的对象的拥有权)
sp = move(sp2)Move assignment(sp2将拥有权移交sp)
sp = move(up)赋予一个unique_ptr up(up将拥有权移交给sp)
sp = move(ap)赋予一个auto_ptr ap(ap将拥有权移交给sp)
sp1.swap(sp2)置换sp1和sp2的pointer和deleter
swap(sp1, sp2)置换sp1和sp2的pointer和deleter
sp.reset()放弃拥有权并重新初始化shared pointer,使它像是empty
sp.reset(ptr)放弃拥有权,并重新初始化shared pointer(拥有*ptr),使用default deleter(也就是调用delete)
sp.reset(ptr, del)放弃拥有权,并重新初始化shared pointer(拥有*ptr),使用del作为deleter
sp.reset(ptr, del, ac)放弃拥有权,并重新初始化shared pointer(拥有*ptr),使用del作为deleter并以ac为allocator
make_shared(…)为一个新对象(以传入的实参为初值)建立一个shared pointer
allocate_shared(ac, …)为一个新对象(以传入的实参为初值)建立一个shared pointer,使用allocator ac
sp.get()返回存储的pointer(通常是被拥有物的地址;若不拥有对象则返回nullptr)
*sp返回拥有的对象(如果none则形成不确定行为)
sp->…为拥有物提供成员访问(如果none则形成不确定行为)
sp.use_count()返回共享对象之拥有者数量(包括sp本身),如果shared pointer是empty则返回0
sp.unique()返回“sp是否为唯一拥有者”(等价于sp.use_count()==1但可能较快)
if (sp)操作符bool();判断sp是否为empty
sp1 == sp2针对存储的pointer(有可能是nullptr)调用 ==
sp1!= sp2针对存储的pointer(有可能是nullptr)调用 !=
sp1 < sp2针对存储的pointer(有可能是nullptr)调用 <
sp1 <= sp2针对存储的pointer(有可能是nullptr)调用 <=
sp1 > sp2针对存储的pointer(有可能是nullptr)调用 >
sp1 >= sp2针对存储的pointer(有可能是nullptr)调用 >=
static_pointer_cast(sp)对sp执行static_cast<>语义
dynamic_pointer_cast(sp)对sp执行dynamic_cast<>语义
const_pointer_cast(sp)对sp执行const_cast语义
get_deleter(sp)返回deleter的地址(如果有的话),否则返回nullptr
strm << sp针对sp的raw pointer调用output操作符(相当于strm << sp.get())
sp.owner_before(sp2)提供自己与另一个shared pointer 之间的strict weak ordering
sp.owner_before(wp)提供自己与某个weak pointer之间的strict weak ordering

注意事项

  注意:一个empty shared_ptr并不能分享对象拥有权,所以use_count()返回0;
  注意:一旦拥有权被转移至一个“已拥有其他对象”的shared_ptr,deleter就会针对先前被拥有的那个对象被调用——如果那个shared_ptr是最末一个拥有者的话。如果shared pointer取得一个新值(不论是由于被赋值或由于调用reset()),相同的事情发生:如果shared pointer先去拥有某对象,而且它是最后一个拥有者,相应的deleter会为此对象而被调用。
  注意:传入的deleter不该抛出异常;
  注意:shared pointer可能使用不同的对象类型,前提是存在一个隐式的pointer转换;
  注意:每个比较操作符所比较的都是shared pointer内部持有的原始对象指针;

设置释放器deleter
auto del = [](int* p){ delete p;}
std::shared_ptr<int> p(new int,del);
decltype(del)* pd = std::get_deleter<decltype(del)>(p);
为int* buf = new int[size]使用
	template <typename T>
	std::shared_ptr<T> make_shared_array(size_t size)
	{		
		//default_delete是STL中的默认删除器  
		std::shared_ptr<T> pReturn = std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
		SetZero(pReturn.get(), size);
		return pReturn;
	}

weak_ptr

shared_ptr带来的问题

  在shared_ptr的使用中可能会出现问题,下面列举出来了两种情况:
    1.环式指向。如果两对象使用shared_ptr互相指向对方,而一旦不存在其他reference指向它们时,你想释放它们和其相应资源。这种情况下shared_ptr不会释放数据,因为每个对象的use_count()仍是1。这种情况下,要么使用原始指针,手动控制资源的释放,那么之前使用智能指针消去的资源泄露,可能会卷土重来;
    2.发生在你“明确想共享但不愿拥有”某对象的情况下。你要的语义是:reference的寿命比其所指对象的寿命更长。因此shared_ptr绝不释放对象,而寻常pointer可能不会注意到它们指向的对象已经不再有效,导致“访问已被释放的数据”的风险。

概念

  于是标准库提供了class weak_ptr,允许你“共享但不拥有”某对象。这个class会建立起一个shared pointer。一旦最末一个拥有该对象的shared pointer失去了拥有权,任何weak pointer都会自动成空(empty)。因此,在default和copy构造函数之外,class weak_ptr只提供“接受一个shared_ptr”的构造函数;
  注意:不能使用操作符*和->访问weak_ptr指向的对象,而是必须另外建立一个shared pointer;
  注意:class weak_ptr只提供小量操作,只能够用来创建、复制、赋值weak pointer,以及转换为一个shared pointer,或检查自己是否指向某对象。

接口

操作效果
weak_ptr<T> wpDefault构造函数,建立一个empty weak pointer
weak_ptr<T> wp(sp)建立一个weak pointer ,共享被sp拥有的pointer的拥有权
weak_ptr<T> wp(wp2)建立一个weak pointer ,共享被wp2拥有的pointer的拥有权
wp.~weak_ptr()析构函数,销毁weak pointer但不影响它所拥有的对象
wp = wp2赋值(此后wp将共享wp2的拥有权,放弃其先前拥有之对象的拥有权)
wp = sp赋予shared pointer sp(此后wp共享sp的拥有权,放弃其先前拥有之对象的拥有权)
wp.swap(wp2)置换wp和wp2的pointer
swap(wp1, wp2)置换wp和wp2的pointer
wp1.reset()放弃被拥有物的拥有权(如果有的话),重新初始化为一个empty weak pointer
wp1.use_count()返回拥有者(shared_ptr拥有对象)的数量。如果weak pointer是empty 则返回0
wp.expired()返回“wp是否为空”(等价于wp.use_count()==0但也许较快)
wp.lock()返回一个shared pointer,共享“被weak pointer拥有的pointer”的拥有权(如果是none则返回一个empty shared pointer)
wp.owner_before(wp2)提供自己与另一个weak pointer之间的strict weak ordering
wp.owner_before(sp)提供自己与一个shared pointer之间的 strict weak ordering

unique_ptr

概念

  unique_ptr是C++标准库自C++11起开始提供的类型。是一种在异常发生时可以帮助避免资源泄露的智能指针。unique_ptr实现了独占式拥有概念,意味着它可以确保一个对象和其相应资源同一时间只被一个pointer拥有,一旦拥有者被销毁或变成empty,或者开始拥有另外一个对象,先前拥有的对象就会被销毁,其任何相应资源也会被释放;
  unique_ptr集成自auto_ptr,以后取代auto_ptr,unique_ptr提供了一个更简明干净的接口,更不容易出错;
  unique_ptr就是为了解决函数内或者代码块中异常或中间返回造成的资源泄露,unique_ptr是“其所指向之对象”的唯一拥有者,在unique_ptr对象在其生命周期结束时,会自动销毁所指向的对象;
  unique_ptr关注的是最小量的空间开销和时间开销。

接口

操作效果
unique_ptr<…> up默认构造函数,建立一个empty unique pointer,使用default/passed deleter类型的一个实例作为deleter
unique_ptr<T> up(nullptr)建立一个empty unique pointer,使用default/passed deleter类型的一个实例作为deleter
unique_ptr<…> up(ptr)建立一个unique pointer,拥有*ptr,使用default/passed deleter类型的一个实例作为deleter
unique_ptr<…> up(ptr,del)建立一个unique pointer,拥有*ptr,使用del作为deleter
unique_ptr<T> up(move(up2))建立一个unique pointer,拥有先前被up2拥有的pointer(此后up2为空)
unique_ptr<T> up(move(ap))建立一个unique pointer,拥有先前被auto_ptr ap拥有的pointer(此后ap为空)
up.~unique_ptr()析构函数,为一个被拥有的对象调用deleter
up = move(up2)Move赋值(up2移交拥有权给up)
up = nullptr对一个被拥有物调用deleter,并令为空(等价于up.reset())
up1.swap(up2)置换up1和up2的pointer和deleter
swap(up1,up2)置换up1和up2的pointer和deleter
up.reset()对一个被拥有物调用deleter,并令为空(等价于up=nullptr)
up.reset(ptr)对一个被拥有物调用deleter,并重新初始化shared pointer使它拥有*ptr
up.release()放弃拥有权,将拥有权交给调用者(也就是说,返回被拥有物其不调用其deleter)
up.get()返回被存储的pointer(被存储物的地址,如实none则返回nullptr)
*up只作用于单对象;返回被拥有之物(如果none则行为不确定)
up->…只作用于单对象;为被拥有物提供成员访问(如果none则行为不确定)
up[idx]只作用于array对象;返回被存储之array内索引为idx的元素(如果none则行为不确定)
if(up)操作符bool();判断是否up为空
up1 == up2为被存储的pointer(有可能是nullptr)调用==
up1 != up2为被存储的pointer(有可能是nullptr)调用!=
up1 < up2为被存储的pointer(有可能是nullptr)调用<
up1 <= up2为被存储的pointer(有可能是nullptr)调用<=
up1 > up2为被存储的pointer(有可能是nullptr)调用>
up1 >= up2为被存储的pointer(有可能是nullptr)调用>=
up.get_deleter()返回一个reference 代表deleter
注意

  1.不提供pointer算术,比如:++;
  2.不允许以赋值语法将一个寻常的pointer作为初值,只能通过构造函数创建或者reset函数进行设置;
  3.unique_ptr可以不拥有对象,是empty,使用默认构造函数时;
  4.可以使用bool()用以检查unique_ptr内是否拥有对象;也可以使用unique_ptr与nullptr进行判断比较,也可以使用get()函数获取原始指针判断是否为nullptr;
  5.同一个原始指针不可以多次给多个unique_ptr进行初始化;会产生运行期错误。比如:

std::string* sp = new std::string("hello");
std::unique_ptr<std::string> up1(sp);
std::unique_ptr<std::string> up2(sp);

  6.可以使用move语义转移对象的拥有权;

std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2(up1);

std::unique_ptr<ClassA> up1(new ClassA);
std::unique_ptr<ClassA> up2;
up2 = std::move(up1);

  7.unique_ptr对数组处理,使用偏特化版本来处理array,这个版本会在遗失其所指对象的拥有权时,对该对象调用delete[]。这个偏特化版本不再提供操作符*和->,改而提供操作符[],用以访问其所指向的array中的某一个对象;偏特化的unique_ptr版本不支持多态的指针访问

std::unique_ptr<std::string[]> up(new std::string[10]);
std::cout<<up[0]<<std::endl;

总结

  1.shared_ptr用来共享拥有权;
  2.unique_ptr用来独占拥有权;
  shared_ptr和weak_ptr内部都需要额外的辅助对象,以内部pointer指向。这意味着无法进行许多优化动作(包括empty base class优化,那可以消除任何内存额外开销)。
  unique_ptr完全不需要这样的额外开销。它们的智能建立在“特殊的构造函数和析构函数”及“copy语义的消除”上。如果给予它的deleter是stateless(无状态,也就是无数据栏)或empty,它消费的内存应该和native pointer相同,而和“使用native pointer并手动delete”相比,也应该不会由运行期额外开销。然而,为了避免带来非必要开销,你应该使用function object(包括lambda)作为deleter,以便成就最佳优化,甚至理想找那个的领开销;
  shared_ptr和unique_ptr以不同的方式对付array和deleter。unique_ptr有个偏特化版本针对array,提供不同的接口。它的弹性较高而效能较低,使用时比较费工

参考资料

1.内存泄漏
2.RAII

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑山老妖的笔记本

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值