首页 > 代码库 > C++内存管理学习笔记(5)

C++内存管理学习笔记(5)

/****************************************************************/

/*            学习是合作和分享式的!

/* Author:Atlas                    Email:wdzxl198@163.com 

/*  转载请注明本文出处:

*   http://blog.csdn.net/wdzxl198/article/details/9112123

/****************************************************************/

上期内容回顾:

C++内存管理学习笔记(4)

     2.1-2.2 RAII规则(引入)  2.3 smart pointer   2.4 auto_ptr类  


2.5 资源传递

       资源传递(Resource Transfer)主要是讲述在不同的作用域间安全的传递资源。这一问题在当你处理容器的时候会变得十分明显。你可以动态的创建一串对象,将它们存放至一个容器中,然后将它们取出,并且在最终安排它们。为了能够让这安全的工作——没有泄露——对象需要改变其所有者。

       这个问题的一个非常显而易见的解决方法是使用Smart Pointer,无论是在加入容器前还是还找到它们以后。这是他如何运作的,你加入Release方法到Smart Pointer中:

   1: template <class T>
   2: T * SmartPointer<T>::Release ()
   3: {
   4:     T * pTmp = _p;
   5:     _p = 0;
   6:     return pTmp;
   7: }

 

       注意在Release调用以后,Smart Pointer就不再是对象的所有者了——它内部的指针指向空。现在,调用了Release都必须是一个负责的人并且迅速隐藏返回的指针到新的所有者对象中。在我们的例子中,容器调用了Release,比如这个Stack的例子:

   1: void Stack::Push (SmartPointer <Item> & item) throw (char *)
   2: {
   3:     if (_top == maxStack)
   4:     throw "Stack overflow";
   5:     _arr [_top++] = item.Release ();
   6: };

      同样的,你也可以再你的代码中用加强Release的可靠性。

      这部分内容可以参考学习《C++内存管理学习笔记(3)》中的auto_ptr智能指针,auto_ptr对象通过赋值、复制和reset操作改变对象的所有者。

2.6 共享所有权

        为每一个程序中的资源都找出或者指定一个所有者,对于共享所有权来说是最好的的选择方式。

共享的责任分配给被共享的对象和它的客户(client)。一个共享资源必须为它的所有者保持一个引用计数。另一方面,所有者再释放资源的时候必须通报共享对象。最后一个释放资源的需要在最后负责free的工作。

例子:最简单的共享的实现是共享对象继承引用计数的类RefCounted:

   1: class RefCounted
   2: {
   3: public:
   4:     RefCounted () : _count (1) {}
   5:     int GetRefCount () const { return _count; }
   6:     void IncRefCount () { _count++; }
   7:     int DecRefCount () { return --_count; }
   8: private:
   9:     int _count;
  10: };

       按照资源管理,一个引用计数是一种资源。如果你遵守它,你需要释放它。当你意识到这一事实的时候,剩下的就变得简单了。简单的遵循规则--再构造函数中获得引用计数,在析构函数中释放。

      在上一个学习笔记(3)中提到过,智能指针有两种方式,分别为设置拥有权的转移和使用引用计数的方式。针对这个两个解决方案,出现了两种风格的智能指针,STL中的auto_ptr属于拥有权转移指针,boost中的shared_ptr属于引用计数型(boost里面的智能指针有6个,scoped_ptr、scoped_array、shared_array、intrusive_ptr、weak_ptr)。

小问:STL和boost? 
1.STL 
      标准库中提供了C++程序的基本设施。虽然C++标准库随着C++标准折腾了许多年,直到标准的出台才正式定型,但是在标准库的实现上却很令人欣慰得看到多种实现,并且已被实践证明为有工业级别强度的佳作。
    STL的最主要的两个特点:数据结构和算法的分离,非面向对象本质。访问对象是通过象指针一样的迭代器实现的;容器是象链表,矢量之类的数据结构,并按模板方式提供;算法是函数模板,用于操作容器中的数据。由于STL以模板为基础,所以能用于任何数据类型和结构.
       (1) STL是数据结构和算法的分离。尽管这是个简单的概念,但这种分离确实使得STL变得非常通用。例如,由于STL的sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包括链表,容器和数组。
       (2) STL它不是面向对象的。为了具有足够通用性,STL主要依赖于模板而不是封装,继承和虚函数(多态性)——OOP的三个要素。你在STL中找不到任何明显的类继承关系。这好像是一种倒退,但这正好是使得STL的组件具有广泛通用性的底层特征。另外,由于STL是基于模板,内联函数的使用使得生成的代码短小高效。
2.boost 
      Boost库是一个经过千锤百炼、可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的发动机之一。 Boost库由C++标准委员会库工作组成员发起,在C++社区中影响甚大,其成员已近2000人。 Boost库为我们带来了最新、最酷、最实用的技术,是不折不扣的“准”标准库。

Boost中比较有名气的有这么几个库:

Regex:正则表达式库;

Spirit LL parser framework,用C++代码直接表达EBNF

Graph:图组件和算法;

Lambda:在调用的地方定义短小匿名的函数对象,很实用的functional功能

concept check:检查泛型编程中的concept

Mpl:用模板实现的元编程框架

Thread:可移植的C++多线程库

Python:把C++类和函数映射到Python之中

Pool:内存池管理

smart_ptr

      Boost总体来说是实用价值很高,质量很高的库。并且由于其对跨平台的强调,对标准C++的强调,是编写平台无关,现代C++的开发者必备的工具。但是Boost中也有很多是实验性质的东西,在实际的开发中实用需要谨慎。并且很多Boost中的库功能堪称对语言功能的扩展,其构造用尽精巧的手法,不要贸然的花费时间研读。Boost另外一面,比如Graph这样的库则是具有工业强度,结构良好,非常值得研读的精品代码,并且也可以放心的在产品代码中多多利用。

区别:

    boost是一个准标准库,相当于STL的延续和扩充,它的设计理念和STL比较接近,都是利用泛型让复用达到最大化。不过对比STL,boost更加实用。 STL集中在算法部分,而boost包含了不少工具类,可以完成比较具体的工作。

     接下来对share_ptr进行讲解,share_ptr是可以共享所有权的智能指针。

2.7 share_ptr

(1)boost中的智能指针

Boost提供了下面几种智能指针(Smart Pointers to boost your code):

     将原文部分放上来,防止笔者翻译水平有限,影响大家阅读,请对照内容:

share_ptr<T>使用一个引用计数器来判断此指针是不是需要被释放。是boost中最常用的智能指针了。
scope_ptr<T>当这个指针的作用域消失之后自动释放,性能与内置的指针差不多
intrusive_ptr<T>也维护一个引用计数器,比shared_ptr有更好的性能。但是要求T自己提供这个引用计数机制。
weak_ptr<T>弱指针,要和shared_ptr 结合使用避免循环引用
share_array<T>和shared_ptr相似,但是访问的是数组
scope_array<T>和scoped_ptr相似,但是访问的是数组

(2)share_ptr引入

     首先,我们通过例子来了解这个智能指针,

   1: void Sample_Shared()
   2: {
   3:   // (A) create a new CSample instance with one reference
   4:   boost::shared_ptr<CSample> mySample(new CSample); 
   5:   printf("The Sample now has %i references\n", mySample.use_count()); // should be 1
   6:  
   7:   // (B) assign a second pointer to it:
   8:   boost::shared_ptr<CSample> mySample2 = mySample; // should be 2 refs by now
   9:   printf("The Sample now has %i references\n", mySample.use_count());
  10:  
  11:   // (C) set the first pointer to NULL
  12:   mySample.reset(); 
  13:   printf("The Sample now has %i references\n", mySample2.use_count());  // 1
  14:  
  15:   // the object allocated in (1) is deleted automatically
  16:   // when mySample2 goes out of scope
  17: }

 

     在代码块(A)中,在堆中创建一个CSample对象,通过绑定share_ptr指针到mySample,如下图示:

   (B)中我们通过另外一个mySample2指针指向这个对象,如下图示:

   之后(C),reset操作第一个指针对象(p=NULL),但是CSample对象没有被释放,因为它mySample2在引用。

    只有当最后的引用释放掉后,出了当前作用域时,CSample对象的内存被释放掉。

  下面是shared_ptr一些应用案例: 

  • use in containers
  • using the pointer-to-implementation idiom (PIMPL)
  • Resource-Acquisition-Is-Initialization (RAII) idiom
  • Separating Interface from Implementation

      1>在容器中使用;

      2>PIMPL(pointer to implementation)惯例,即“实现的指针较短”;

      3>RAII()惯例; (详细讲解见《学习笔记(4)》)

      4>类的使用接口和实现分离

 

小知识: PIMPL idiom与RAII idiom

1.RAII

   RAII是Bjarne Stroustrup教授用于解决资源分配而发明的技术,资源获取即初始化。RAII是C++的构造机制的直接使用,即利用构造函数分配资源,利用析构函数来回收资源.

2.PIMPL

    PIMPL是一种应用十分广泛的技术,它的别名也很多,如Opaque pointer, handle classes等。PIMPL是RAII的延展,籍由RAII对资源的控制,把具体的数据布局和实现从调用者视线内移开,从而简化了API接口,也使得ABI兼容变得有可能,Qt和KDE正是使用Pimpl来维护ABI的一致性,另外也为惰性初始化提供途径,以及隐式共享提供了基础。

详细介绍参考:http://c2.com/cgi/wiki?PimplIdiom或者wiki;

   PIMPL或者RAII是C++程序中众所周知的重要概念, 智能指针只是实现这两种惯用手法的一种方式.

(If you never heard of PIMPL (a.k.a. handle/body) or RAII, grab a good C++ book - they are important concepts every C++ programmer should know. Smart pointers are just one way to implement them conveniently in certain cases)

 

(3)share_ptr的特点

这里引用《Smart Pointers to boost your code》一文中对share_ptr特点的描述,

  • shared_ptr<T> works with an incomplete type:

         When declaring or using a shared_ptr<T>T may be an "incomplete type". E.g., you do only a forward declaration usingclass T;. But do not yet define howT really looks like. Only where you dereference the pointer, the compiler needs to know "everything".

  • shared_ptr<T> works with any type:

        There are virtually no requirements towards T (such as deriving from a base class).

  • shared_ptr<T> supports a custom deleter

        So you can store objects that need a different cleanup than delete p. For more information, see the boost documentation.

  • Implicit conversion:

       If a type U * can be implicitly converted to T * (e.g., becauseT is base class ofU), a shared_ptr<U> can also be converted toshared_ptr<T> implicitly.

  • shared_ptr is thread safe

        (This is a design choice rather than an advantage, however, it is a necessity in multithreaded programs, and the overhead is low.)

  • Works on many platforms, proven and peer-reviewed, the usual things.

    综合来说,shared_ptr 具有可以共享和转移所有权,可以被标准库的容器所使用 ,线程安全的,不能指向一块动态增长的内存(用share_array代替)等特点。

    (4)举例:在容器中使用share_ptr

         在许多容器类包括标准STL容器中,都需要复制操作(inserting an existing element into a list, vector, or container)。然而,当这种复制操作很复杂或者难以实现可用的时候,指针容器是一种简单有效的解决方式。例如下面的例子:

       1: std::vector<CMyLargeClass *> vec;
       2: vec.push_back( new CMyLargeClass("bigString") );

         上面这个程序将内存管理任务的交给其调用者,这里我们可以使用share_ptr来改写它,

       1: typedef boost::shared_ptr<CMyLargeClass>  CMyLargeClassPtr;
       2: std::vector<CMyLargeClassPtr> vec;
       3: vec.push_back( CMyLargeClassPtr(new CMyLargeClass("bigString")) );

        这样改写后对任务的内存管理就非常简单了,当容器被destroyed,其中的元素也随之自动的destroyed。

    但是,如果还有其他智能指针在引用它,则引用的那个元素依然存在,而不被释放掉。如下程序,

     

       1: void Sample3_Container()
       2: {
       3:   typedef boost::shared_ptr<CSample> CSamplePtr;
       4:  
       5:   // (A) create a container of CSample pointers:
       6:   std::vector<CSamplePtr> vec;
       7:  
       8:   // (B) add three elements
       9:   vec.push_back(CSamplePtr(new CSample));
      10:   vec.push_back(CSamplePtr(new CSample));
      11:   vec.push_back(CSamplePtr(new CSample));
      12:  
      13:   // (C) "keep" a pointer to the second: 
      14:   CSamplePtr anElement = vec[1];
      15:  
      16:   // (D) destroy the vector:
      17:   vec.clear();
      18:  
      19:   // (E) the second element still exists
      20:   anElement->Use();
      21:   printf("done. cleanup is automatic\n");
      22:  
      23:   // (F) anElement goes out of scope, deleting the last CSample instance
      24: }

     

    (5)使用share_ptr需要注意的地方

    1. shared_ptr多次引用同一数据,如下:

       1: {
       2:     int* pInt = new int[100];
       3:     boost::shared_ptr<int> sp1(pInt);
       4:     // 一些其它代码之后…
       5:     boost::shared_ptr<int> sp2(pInt);
       6: }

         这种情况在实际中是很容易发生的,结果也是非常致命的,它会导致两次释放同一块内存,而破坏堆。

    2. 使用shared_ptr包装this指针带来的问题,如下:

       1: class tester
       2: {
       3: public:
       4:     tester()
       5:     ~tester()
       6:     {
       7:         std::cout << "析构函数被调用!\n";
       8:     }
       9: public:
      10:     boost::shared_ptr<tester> sget()
      11:     {
      12:         return boost::shared_ptr<tester>(this);
      13:     }
      14: };
      15: int main()
      16: {
      17:     tester t;
      18:     boost::shared_ptr<tester> sp = t.sget(); // …
      19:     return 0;
      20: }

     

         也将导致两次释放t对象破坏堆栈,一次是出栈时析构,一次就是shared_ptr析构。若有这种需要,可以使用下面代码。

       1: class tester : public boost::enable_shared_from_this<tester>
       2: {
       3: public:
       4:     tester()
       5:     ~tester()
       6:     {
       7:         std::cout << "析构函数被调用!\n";
       8:     }
       9: public:
      10:     boost::shared_ptr<tester> sget()
      11:     {
      12:         return shared_from_this();
      13:     }
      14: };
      15: int main()
      16: {
      17:     boost::shared_ptr<tester> sp(new tester);
      18:     // 正确使用sp 指针。
      19:     sp->sget();
      20:     return 0;
      21: }

    3. shared_ptr循环引用导致内存泄露,代码如下:

       1: class parent;
       2: class child;
       3: typedef boost::shared_ptr<parent> parent_ptr;
       4: typedef boost::shared_ptr<child> child_ptr;
       5: class parent
       6: {
       7: public:
       8:     ~parent() {
       9:         std::cout <<"父类析构函数被调用.\n";
      10:     }
      11: public:
      12:     child_ptr children;
      13: };
      14: class child
      15: {
      16: public:
      17:     ~child() {
      18:         std::cout <<"子类析构函数被调用.\n";
      19:     }
      20: public:
      21:     parent_ptr parent;
      22: };
      23: int main()
      24: {
      25:     parent_ptr father(new parent());
      26:     child_ptr son(new child);
      27:     // 父子互相引用。
      28:     father->children = son;
      29:     son->parent = father;
      30:     return 0;
      31: }

          如上代码,将在程序退出前,father的引用计数为2,son的计数也为2,退出时,shared_ptr所作操作就是简单的将计数减1,如果为0则释放,显然,这个情况下,引用计数不为0,于是造成father和son所指向的内存得不到释放,导致内存泄露。

    4. 在多线程程序中使用shared_ptr应注意的问题。代码如下:

       1: class tester
       2: {
       3: public:
       4:     tester() {}
       5:     ~tester() {}
       6:     // 更多的函数定义…
       7: };
       8: void fun(boost::shared_ptr<tester> sp)
       9: {
      10:     // !!!在这大量使用sp指针.
      11:     boost::shared_ptr<tester> tmp = sp;
      12: }
      13: int main()
      14: {
      15:     boost::shared_ptr<tester> sp1(new tester);
      16:     // 开启两个线程,并将智能指针传入使用。
      17:     boost::thread t1(boost::bind(&fun, sp1));
      18:     boost::thread t2(boost::bind(&fun, sp1));
      19:     t1.join();
      20:     t2.join();
      21:     return 0;
      22: }

           这个代码带来的问题很显然,由于多线程同是访问智能指针,并将其赋值到其它同类智能指针时,很可能发生两个线程同时在操作引用计数(但并不一定绝对发生),而导致计数失败或无效等情况,从而导致程序崩溃,如若不知根源,就无法查找这个bug,那就只能向上帝祈祷程序能正常运行。

    可能一般情况下并不会写出上面这样的代码,但是下面这种代码与上面的代码同样,如下:

       1: class tester
       2: {
       3: public:
       4:     tester() {}
       5:     ~tester() {}
       6: public:
       7:     boost::shared_ptr<int> m_spData; // 可能其它类型。
       8: };
       9: tester gObject;
      10: void fun(void)
      11: {
      12:     // !!!在这大量使用sp指针.
      13:     boost::shared_ptr<int> tmp = gObject.m_spData;
      14: }
      15: int main()
      16: {
      17:     // 多线程。
      18:     boost::thread t1(&fun);
      19:     boost::thread t2(&fun);
      20:     t1.join();
      21:     t2.join();
      22:     return 0;
      23: }

         情况是一样的。要解决这类问题的办法也很简单,使用boost.weak_ptr就可以很方便解决这个问题。第一种情况修改代码如下:

       1: class tester
       2: {
       3: public:
       4:     tester() {}
       5:     ~tester() {}
       6:     // 更多的函数定义…
       7: };
       8: void fun(boost::weak_ptr<tester> wp)
       9: {
      10:     boost::shared_ptr<tester> sp = wp.lock;
      11:     if (sp)
      12:     {
      13:         // 在这里可以安全的使用sp指针.
      14:     }
      15:     else
      16:     {
      17:         std::cout << “指针已被释放!” << std::endl;
      18:     }
      19: }
      20: int main()
      21: {
      22:     boost::shared_ptr<tester> sp1(new tester);
      23:     boost.weak_ptr<tester> wp(sp1);
      24:     // 开启两个线程,并将智能指针传入使用。
      25:     boost::thread t1(boost::bind(&fun, wp));
      26:     boost::thread t2(boost::bind(&fun, wp));
      27:     t1.join();
      28:     t2.join();
      29:     return 0;
      30: }

          boost.weak_ptr指针功能一点都不weak,weak_ptr是一种可构造、可赋值以不增加引用计数来管理shared_ptr的指针,它可以方便的转回到shared_ptr指针,使用weak_ptr.lock函数就可以得到一个shared_ptr的指针,如果该指针已经被其它地方释放,它则返回一个空的shared_ptr,也可以使用weak_ptr.expired()来判断一个指针是否被释放。

           boost.weak_ptr不仅可以解决多线程访问带来的安全问题,而且还可以解决上面第三个问题循环引用。Children类代码修改如下,即可打破循环引用:

       1: class child
       2: {
       3: public:
       4:     ~child() {
       5:         std::cout <<"子类析构函数被调用.\n";
       6:     }
       7: public:
       8:     boost::weak_ptr<parent> parent;
       9: };

         因为boost::weak_ptr不增加引用计数,所以可以在退出函数域时,正确的析构。

     

     

     

     


    参考资料详见《c++内存管理学习纲要》

     

    Edit by Atlas

    Time:2013/6/17  14:22

C++内存管理学习笔记(5)