C++內存管理學習筆記(5)

/****************************************************************/c++

/*            學習是合做和分享式的!正則表達式

/* 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類  app


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

相關文章
相關標籤/搜索