建议34:用智能指针管理通过new创建的对象
前面的建议中我们不厌其烦的一再重复:内存泄漏是一个很大很大的问题!为了应对这个问题,已经有许多技术被研究出来,比如Garbage Collection(垃圾回收)、Smart Pointer(智能指针)等。Garbage Collection技术一直颇受注目,并且在Java中已经发展成熟,成为内存管理的一大利器,但它在C++语言中的发展却不顺利,C++为了追求运行速度,20年来态度坚决地将其排除在标准之外。真不知C++通过加大开发难度来换取执行速度的做法究竟是利还是弊。为了稍许平复因为没有Garbage Collection而引发的C++程序员的怨气,C++对Smart Pointer技术采取了不同的态度,它选择对这一技术的支持,并在STL中包含了支持Smart Pointer技术的class,赐予了C/C++程序员们一件管理内存的神器。
Smart Pointer是Stroustrup博士所推崇的RAII(Resource Acquisition In Initialization)的最好体现。该方法使用一个指针类来代表对资源的管理逻辑,并将指向资源的句柄(指针或引用)通过构造函数传递给该类。当离开当前范围(scope)时,该对象的析构函数一定会被调用,所以嵌在析构函数中的资源回收的代码也总是会被执行。这种方法的好处在于,由于将资源回收的逻辑通过特定的类从原代码中剥离出来,自动正确地销毁动态分配的对象,这会让思路变得更加清晰,同时确保内存不发生泄露。
它的一种通用实现技术是使用引用计数(Reference Count)。引用计数智能指针,是一种生命期受管的对象,其内部有一个引用计数器。当内部引用计数为零时,这些对象会自动销毁自身的智能指针类。每次创建类的新对象时,会初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,它会调用拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数;如果引用计数减至0,则删除对象,并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数,直到计数为0,释放对象空间。
Smart Pointer具有非常强大的能力,谨慎而明智的选择能给我们带来极大的便利。前面已经说到STL中包含了支持Smart Pointer技术的class,它就是智能指针:auto_ptr。要使用auto_prt,首先要包含memory头文件:
#include <memory> |
auto_ptr可以指向一个以new建立的对象,当auto_ptr的生命周期结束时,其所指向的对象之资源也会被自动释放,且不必显式地调用delete,而对象指针的操作依旧如故。例如:
|
当然,也可以建立一个未指向任何对象的auto_prt,例如:
std::auto_ptr<int> iPtr; |
它就像空指针,未指向任何对象,所以也就不能进行操作,但是可以通过get()函数来判断它是否指向对象的地址:
|
当使用sPtr1来建立sPtr2时,sPtr1不再对所指向对象的资源释放负责,而是将接力棒传递到了sPtr2的手里,sPtr1丧失了使用string类成员函数的权利,所以在判断sPtr1->empty()时程序会崩溃。
auto_ptr的资源维护动作是以inline的方式来完成的,在编译时代码会被扩展开来,所以使用它并不会牺牲效率。虽然auto_ptr指针是一个RAII对象,能够给我们带来很多便利,但是它的缺点同样不可小觑:
auto_ptr对象不可作为STL容器的元素,所以二者带来的便利不能同时拥有。这一重大缺陷让STL的忠实拥趸们愤怒不已。
auto_ptr缺少对动态配置而来的数组的支持,如果用它来管理这些数组,结果是可怕的、不可预期的。
auto_ptr在被复制的时候会发生所有权转移。
Smart Pointer作为C++垃圾回收机制的核心,必须足够强大、具有工业强度,并且保证安全性。可是STL中的auto_ptr却像是扶不起的阿斗,不堪大用。在这样的情况下,C++标准委员会自然需要考虑引入新的智能指针。其中由C++标准委员会库工作组发起的Boost组织开发的Boost系列智能指针最为著名。除此之外,还有Loki库提供的SmartPtr、ATL提供的CComPtr和CComQIPtr。一个好消息是,就在2011年的9月刚刚获得通过的C++新标准C++ 11中废弃了auto_ptr指针,取而代之的是两个新的指针类:shared_ptr和unique_ptr。shared_ptr只是单纯的引用计数指针,unique_ptr是用来取代auto_ptr的。unique_ptr提供了auto_ptr的大部分特性,唯一的例外是auto_ptr的不安全、隐性的左值搬移;而unique_ptr可以存放在C++0x提出的那些能察觉搬移动作的容器之中。
在Boost中的智能指针共有五种:scoped_ptr、scoped_array、shared_ptr、shared_array、weak_ptr,其中最有用的就是shared_ptr,它采取了引用计数,并且是线程安全的,同时支持扩展,推荐在大多数情况下使用。
boost::shared_ptr支持STL容器:
|
当vector被销毁时,其元素—智能指针对象才会被销毁,除非这个对象被其他的智能指针引用,如下面的代码片段所示:
|
Boost智能指针同样支持数组,boost::scoped_array 和boost::shared_array对象指向的是动态配置的数组。
Boost的智能指针虽然增强了安全性,处理了潜在的危险,但是我们在使用时还是应该遵守一定的规则,以确保代码更加鲁棒。
规则1:Smart_ptr<T>不同于T*
Smart_ptr<T>的真实身份其实是一个对象,一个管理动态配置对象的对象,而T*是指向T类型对象的一个指针,所以不能盲目地将一个T*和一个智能指针类型Smart_ptr<T>相互转换。
在创建一个智能指针的时候需要明确写出Smart_ptr<T> tPtr<new T>。
禁止将T*赋值给一个智能指针。
不能采用tPtr = NULL的方式将tPtr置空,应该使用智能指针类的成员函数。
规则2:不要使用临时的 share_ptr对象
如下所示:
|
调用ProcessObject函数之前,C++编译器必须完成三件事:
(1)执行"new A"。
(2)调用 boost::shared_ptr的构造函数。
(3)调用函数IsAllReady()。
因为函数参数求值顺序的不确定性,如果调用IsAllReady()发生在另外两个过程中间,而它又正好出现了异常,那么new A得到的内存返回的指针就会丢失,进而发生内存泄露,因为返回的指针没有被存入我们期望能阻止资源泄漏的boost::shared_ptr上。避免出现这种问题的方式就是不要使用临时的share_ptr对象,改用一个局部变量来实现,在一个独立的语句中将通过new 创建出来的对象存入智能指针中:
|
如果疏忽了这一点,当异常发生时,可能会引起微妙的资源泄漏。
请记住:
时刻谨记RAII原则,使用智能指针协助我们管理动态配置的内存能给我们带来极大的便利,但是需要我们谨慎而明智地做出选择。
建议35:使用内存池技术提高内存申请效率与性能
Doug Lea曾有言曰:“自1960年以来,动态内存分配就已经成为大多计算机系统的重要部分。”
动态内存管理确实是件让人头疼的事儿,然而在实际的编程实践中,又不可避免地要大量用到堆上的内存。而这些通过malloc或new进行的内存分配却有着一些天生的缺陷:一方面,利用默认的内存管理函数在堆上分配和释放内存会有一些额外的开销,需要花费很多时间;另一方面,也是更糟糕的,随着时间的流逝,内存将形成碎片,一个应用程序的运行会越来越慢。
当程序中需要对相同大小的对象频繁申请内存时,常会采用内存池(Memory Pool)技术来提高内存申请效率。经典的内存池技术,是一种用于分配大量大小相同的小对象的技术。通过该技术可以极大地加快内存分配/释放过程。内存池技术通过批量申请内存,降低了内存申请次数,从而节省了时间。对于大批量的小对象而言,使用内存池技术整体申请内存,减少了内存碎片的产生,对性能提升的帮助也是很显著的。
内存池技术的基本原理通过这个“池”字就进行了很好的自我阐释:应用程序可以通过系统的内存分配调用预先一次性申请适当大小的内存块(Block),并会将它分成较小的块(Smaller Chunks),之后每次应用程序会从先前已经分配的块(chunks)中得到相应的内存空间,对象分配和释放的操作都可以通过这个“池”来完成。只有当“池”的剩余空间太小,不能满足应用程序需要时,应用程序才会再调用系统的内存分配函数对其大小进行动态扩展。
经典的内存池实现原理如下:
|
其中MemPool涉及两个常量:m_nMemBlockSize、m_nItemSize,还有两个指针变量m_pMemBlockHeader、m_pFreeNodeHeader。指针变量m_pMemBlockHeader是用来把所有申请的内存块(MemBlock)串成一个链表。m_pFreeNodeHeader变量则是把所有自由的内存结点(FreeNode)串成一个链表。内存块在申请之初就被划分为了多个内存结点,每个结点的大小为ItemSize(对象的大小),共计MemBlockSize/ItemSize个。然后,这些内存结点会被串成链表。每次分配的时候从链表中取一个给用户,不够时继续向系统申请大块内存。在释放内存时,只须把要释放的结点添加到自由内存链表m_pFreeNodeHeader中即可。在MemPool对象析构时,可完成对内存的最终释放。
Boost库同样对该技术提供了较好的支持:
|
pool的析构函数会释放pool占用的内存。
object_pool (#include <boost/pool/object_pool.hpp>) |
object_pool和pool的区别在于:pool指定每次分配的块的大小,object_pool指定分配的对象的类型,如下所示:
boost::object_pool<A> p; |
用A * pA = p.malloc()只会分配内存而不会调用构造函数,如果要调用构造函数应该使用A * const t = p.construct();
singleton_pool (#include <boost/pool/singleton_pool.hpp>) |
singleton_pool和object_pool一样,不过它可以定义多个pool类型的object,给它们都分配同样大的内存块,另外singleton_pool提供静态方法分配内存,且不用定义对象,如下所示:
|
由此可见,Boost确实是一个值得称赞、更值得使用的库,它能为我们提供极大的便利。当需要使用内存池技术时,请考虑Boost。
请记住:
当你需要频繁地分配相同大小的对象,而又苦恼于默认的内存管理函数带来的问题时,内存池技术将是灵丹妙药,它能提高内存操作效率,以及应用程序的鲁棒性。