閱讀944 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

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

/*            學習是合作和分享式的!

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

/*  轉載請注明本文出處:

*   https://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的一致性,另外也為惰性初始化提供途徑,以及隱式共享提供了基礎。

詳細介紹參考:https://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

最後更新:2017-04-03 18:52:12

  上一篇:go 第二章 IoC Bean的初始化與延遲加載
  下一篇:go 第二章 IoC Setter注入