做者 Eatonios
導語 在 C++ 中,內存管理是十分重要的問題,一不當心就會形成程序內存泄露,那麼怎麼避免呢?經過智能指針能夠優雅地管理內存,讓開發者只須要關注內存的申請,內存的釋放則會被自動管理。在文章 開源微服務框架 TARS 之 基礎組件 中已經簡要介紹過,TARS 框架組件中沒有直接使用 STL 庫中的智能指針,而是實現了本身的智能指針。本文將會分別對 STL 庫中的智能指針和 TarsCpp 組件中的智能指針進行對比分析,並詳細介紹 TARS 智能指針的實現原理。git
智能指針github
STL 庫中的智能指針segmentfault
在計算機程序中,泄露是常見的問題,包括內存泄露和資源泄露。其中資源泄露指的是系統的 socket
、文件描述符等資源在使用後,程序再也不須要它們時沒有獲得釋放;內存泄露指的是動態內存在使用後,程序再也不須要它時沒有獲得釋放。安全
內存泄露會使得程序佔用的內存愈來愈多,而很大一部分每每是程序再也不須要使用的。在 C++ 程序中,內存泄露常見於咱們使用了 new
或者 malloc
申請動態存儲區的內存,卻忘了使用 delete
或者 free
去釋放內存,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。併發
隨着計算機應用需求的日益增長,應用的設計與開發日趨複雜,開發人員在開發過程當中處理的變量也愈來愈多。如何有效進行內存分配和釋放、防止內存泄漏逐漸成爲開發者面臨的重要難題。爲了解決忘記手動釋放內存形成的內存泄露問題,智能指針誕生了。框架
常見的智能指針的使用場景,包括類中的成員變量(指針型)和普通的變量(指針型)。智能指針能夠實現指針指向對象的共享,而無需關注動態內存的釋放。通用實現技術是引用計數(Reference count),下一部分會介紹,簡單講就是將一個計數器與類指向的對象相關聯,跟蹤有多少個指針指向同一對象,新增一個指針指向該對象則計數器 +1
,減小一個則執行 -1
。socket
引用計數是智能指針的一種通用實現技術,上圖爲大體流程,基本原理以下:分佈式
1
;+1
;=
將左操做數所指對象的引用計數 -1
,將右操做數所指對象的引用計數 +1
;-1
;0
時,刪除基礎對象;STL 庫中的智能指針 shared_ptr
和 TARS 智能指針都使用了該引用計數原理,後面會進行介紹。
C++ 標準模板庫 STL 中提供了四種指針 auto_ptr
, unique_ptr
, shared_ptr
, weak_ptr
。
auto_ptr
在 C++98 中提出,但其不能共享對象、不能管理數組指針,也不能放在容器中。所以在 C++11 中被摒棄,並提出 unique_ptr
來替代,支持管理數組指針,但不能共享對象。
shared_ptr
和 weak_ptr
則是 C++11 從標準庫 Boost 中引入的兩種智能指針。shared_ptr
用於解決多個指針共享一個對象的問題,但存在循環引用的問題,引入 weak_ptr
主要用於解決循環引用的問題。
接下來將詳細介紹 shared_ptr
,關於其它智能指針的更多信息和用法請讀者自行查閱。
shared_ptr
解決了在多個指針間共享對象全部權的問題,最初實現於 Boost 庫中,後來收錄於 C++11 中,成爲了標準的一部分。shared_ptr
的用法以下
#include <memory> #include <iostream> using namespace std; class A { public: A() {}; ~A() { cout << "A is destroyed" << endl; } }; int main() { shared_ptr<A> sptrA(new A); cout << sptrA.use_count() << endl; { shared_ptr<A> cp_sptrA = sptrA; cout << sptrA.use_count() << endl; } cout << sptrA.use_count() << endl; return 0; }
上述代碼的意思是 cp_sptrA
聲明並賦值後,引用計數增長 1
,cp_sptrA
銷燬後引用計數 -1
,可是沒有觸發 A
的析構函數,在 sprtA
銷燬後,引用計數變爲 0
,才觸發析構函數,實現內存的回收。執行結果以下
1 2 1 A is destroyed
shared_ptr
主要的缺陷是遇到循環引用時,將形成資源沒法釋放,下面給出一個示例:
#include <memory> #include <iostream> using namespace std; class B; class A { public: A() : m_sptrB(nullptr) {}; ~A() { cout << " A is destroyed" << endl; } shared_ptr<B> m_sptrB; }; class B { public: B() : m_sptrA(nullptr) {}; ~B() { cout << " B is destroyed" << endl; } shared_ptr<A> m_sptrA; }; int main( ) { { shared_ptr<B> sptrB( new B );//sptrB對應的引用計數置爲1 shared_ptr<A> sptrA( new A );//sptrA對應的引用計數置爲1 sptrB->m_sptrA = sptrA;//sptrA對應的引用計數變成2,sptrB仍然是1 sptrA->m_sptrB = sptrB;//sptrB對應的引用計數變成2,sptrA是2 } //退出main函數後,sptrA和sptrB對應的引用計數都-1,變成1, //此時A和B的析構函數都不能執行(引用計數爲0才能執行),沒法釋放內存 return 0; }
在上述例子中,咱們首先定義了兩個類 A
和 B
:A
的成員變量是指向 B
的 shared_ptr
指針,B
的成員變量是指向 A
的 shared_ptr
指針。
而後咱們建立了 sptrB
和 sptrA
兩個智能指針對象,而且相互賦值。這會形成環形引用,使得 A
和 B
的析構函數都沒法執行(能夠經過 cout
觀測),從而內存沒法釋放。當咱們沒法避免循環使用時,可使用 weak_ptr
來解決,這裏再也不展開,感興趣的讀者能夠自行查閱。
TARS 誕生於 2008 年,當時 shared_ptr
尚未被收錄到 STL 標準庫中,所以本身實現了智能指針 TC_AutoPtr
。TARS 的智能指針主要是對 auto_ptr
的改進,和 share_ptr
的思想基本一致,可以實現對象的共享,也能存儲在容器中。與 shared_ptr
相比,TC_AutoPtr
更加輕量化,擁有更好的性能,本文後續會對比。
在 TARS 中,智能指針類 TC_AutoPtr
是一個模板類,支持拷貝和賦值等操做,其指向的對象必須繼承自智能指針基類 TC_HandleBase
,包含了對引用計數的加減操做。計數採用的是 C++ 標準庫 <atomic>
中的原子計數類型 std::atomic
。
計數的實現封裝在類 TC_HandleBase
中,開發者無需關注。使用時,只要將須要共享對象的類繼承 TC_HandleBase
,而後傳入模板類 TC_AutoPtr
聲明並構造對象便可,以下
#include <iostream> #include "util/tc_autoptr.h" using namespace std; // 繼承 TC_HandleBase class A : public tars::TC_HandleBase { public: A() { cout << "Hello~" << endl; } ~A() { cout << "Bye~" << endl; } }; int main() { // 聲明智能指針並構造對象 tars::TC_AutoPtr<A> autoA = new A(); // 獲取計數 1 cout << autoA->getRef() << endl; // 新增共享 tars::TC_AutoPtr<A> autoA1(autoA); // 獲取計數 2 cout << autoA->getRef() << endl; }
使用方式和 shared_ptr
類似,能夠經過函數 getRef
獲取當前計數,getRef
定義於 TC_HandleBase
類中。運行結果以下
Hello~ 1 2 Bye~
下面咱們將自底向上介紹分析原子計數器 std::atomic
、智能指針基類 TC_HandleBase
和智能指針模板類 TC_AutoPtr
,並對 TC_AutoPtr
與 shared_ptr
的性能進行簡單的對比測試。
std::atomic
在 C++11 標準庫 <atomic>
中定義。std::atomic
是模板類,一個模板類型爲 T
的原子對象中封裝了一個類型爲 T
的值。
template <class T> struct atomic;
原子類型對象的主要特色就是從不一樣線程訪問不會致使數據競爭(data race)。所以從不一樣線程訪問某個原子對象是良性 (well-defined) 行爲。而一般對於非原子類型而言,併發訪問某個對象(若是不作任何同步操做)會致使未定義 (undifined) 行爲發生。
C++11 標準庫 std::atomic
提供了針對整型(integral
)和指針類型的特化實現。下面是針對整型的特化實現的主要部分
template <> struct atomic<integral> { ... ... operator integral() const volatile; operator integral() const; atomic() = default; constexpr atomic(integral); atomic(const atomic&) = delete; atomic& operator=(const atomic&) = delete; atomic& operator=(const atomic&) volatile = delete; integral operator=(integral) volatile; integral operator=(integral); integral operator++(int) volatile; integral operator++(int); integral operator--(int) volatile; integral operator--(int); integral operator++() volatile; integral operator++(); integral operator--() volatile; integral operator--(); integral operator+=(integral) volatile; integral operator+=(integral); integral operator-=(integral) volatile; integral operator-=(integral); integral operator&=(integral) volatile; integral operator&=(integral); integral operator|=(integral) volatile; integral operator|=(integral); integral operator^=(integral) volatile; integral operator^=(integral); };
能夠看到重載了大部分整型中經常使用的運算符,包括自增運算符 ++
和自減運算符 --
,能夠直接使用自增或自減運算符直接對原子計數對象的引用值 +1
或 -1
。
TC_HandleBase
是 TARS 的智能指針基類,包含兩個成員變量 _atomic
和 _bNoDelete
,定義以下
protected: /** * 計數 */ std::atomic<int> _atomic; /** * 是否自動刪除 */ bool _bNoDelete;
TC_HandleBase
,爲 TARS 智能指針模板類 TC_AutoPtr<T>
提供引用計數的相關操做,增長計數和減小計數接口的相關代碼以下
/** * @brief 增長計數 */ void incRef() { ++_atomic; } /** * @brief 減小計數 * 當計數==0時, 且須要刪除數據時, 釋放對象 */ void decRef() { if((--_atomic) == 0 && !_bNoDelete) { _bNoDelete = true; delete this; } } /** * @brief 獲取計數. * @return int 計數值 */ int getRef() const { return _atomic; }
能夠看到,這裏經過整型的原子計數類的對象 _atomic
實現引用計數,管理智能指針指向對象的引用計數。
TC_AutoPtr
的定義及其構造函數和成員變量以下述代碼,成員變量 _ptr
是一個 T*
指針。構造函數初始化該指針並調用了 TC_HandleBase
成員函數 incRef
進行引用計數 +1
,這要求類 T
是繼承自 TC_HandleBase
的。
/** * @brief 智能指針模板類. * * 能夠放在容器中,且線程安全的智能指針. * 經過它定義智能指針,該智能指針經過引用計數實現, * 能夠放在容器中傳遞. * * template<typename T> T必須繼承於TC_HandleBase */ template<typename T> class TC_AutoPtr { public: /** * @brief 用原生指針初始化, 計數+1. * * @param p */ TC_AutoPtr(T* p = 0) { _ptr = p; if(_ptr) { _ptr->incRef(); } } ... public: T* _ptr; };
TC_AutoPtr
在使用時能夠簡單的看成 STL 的 shared_ptr
使用,須要注意的是指向的對象必須繼承自 TC_HandleBase
(固然也能夠本身實現智能指針基類,並提供與 TC_HandleBase
一致的接口),同時還要避免環形引用。下面咱們看一下 TC_AutoPtr
其餘接口的定義:
/** * @brief 用其餘智能指針r的原生指針初始化, 計數+1. * * @param Y * @param r */ template<typename Y> TC_AutoPtr(const TC_AutoPtr<Y>& r) { _ptr = r._ptr; if(_ptr) { _ptr->incRef(); } } /** * @brief 拷貝構造, 計數+1. * * @param r */ TC_AutoPtr(const TC_AutoPtr& r) { _ptr = r._ptr; if(_ptr) { _ptr->incRef(); } } /** * @brief 析構,計數-1 */ ~TC_AutoPtr() { if(_ptr) { _ptr->decRef(); } } /** * @brief 賦值, 普通指針 * @param p * @return TC_AutoPtr& */ TC_AutoPtr& operator=(T* p) { if(_ptr != p) { if(p) { p->incRef(); } T* ptr = _ptr; _ptr = p; //因爲初始化時_ptr=NULL,所以計數不會-1 if(ptr) { ptr->decRef(); } } return *this; }
能夠看到,這些接口都知足通用的引用計數規則。
+1
;+1
;+1
,左邊的 -1
;-1
;通過上述分析,能夠發現 TC_AutoPtr
和 shared_ptr
在用法和功能上很是類似,都支持多個指針共享一個對象,支持存儲在容器中,那 TC_AutoPtr
有什麼優點呢?
相比於 STL 庫中的 shared_ptr
,TC_AutoPtr
更加輕量,具備更好的性能,咱們能夠經過以下簡單的測試代碼,經過測試兩者構造和複製的耗時來衡量它們的性能
#include <iostream> #include <chrono> #include <memory> #include <vector> #include "util/tc_autoptr.h" using namespace tars; using namespace std; using namespace chrono; // 測試類 class Test : public TC_HandleBase { public: Test() {} private: int test; }; // 打印時間間隔 void printDuration(const string & info, system_clock::time_point start, system_clock::time_point end) { auto duration = duration_cast<microseconds>(end - start); cout << info << double(duration.count()) * microseconds::period::num / microseconds::period::den << " s" << endl; } int main() { int exec_times = 10000000; // 次數 // 構造耗時對比 { auto start = system_clock::now(); for (int i = 0; i < exec_times; ++i) { TC_AutoPtr<Test> a = TC_AutoPtr<Test>(new Test); } auto end = system_clock::now(); printDuration("TC_AutoPtr construct: ", start, end); } { auto start = system_clock::now(); for (int i = 0; i < exec_times; ++i) { shared_ptr<Test> a = shared_ptr<Test>(new Test); } auto end = system_clock::now(); printDuration("shared_ptr construct: ", start, end); } // 複製耗時對比 { auto start = system_clock::now(); TC_AutoPtr<Test> a = TC_AutoPtr<Test>(new Test); for (int i = 0; i < exec_times; ++i) { TC_AutoPtr<Test> b = a; } auto end = system_clock::now(); printDuration("TC_AutoPtr copy: ", start, end); } { auto start = system_clock::now(); shared_ptr<Test> a = shared_ptr<Test>(new Test); for (int i = 0; i < exec_times; ++i) { shared_ptr<Test> b = a; } auto end = system_clock::now(); printDuration("shared_ptr copy: ", start, end); } }
最後運行測試,輸出的結果以下
TC_AutoPtr construct: 0.208995 s shared_ptr construct: 0.423324 s TC_AutoPtr copy: 0.107914 s shared_ptr copy: 0.107716 s
能夠看出,兩者的複製性能相近,而構造性能上, TC_AutoPtr
要比 shared_ptr
快一倍以上。
本文主要介紹了 TARS 的智能指針組件 TC_AutoPtr
和 STL 的智能指針 shared_ptr
。TC_AutoPtr
指向繼承自智能指針基類 TC_HandleBase
的對象。TC_HandleBase
經過原子計數器 std::atomic<int>
實現引用計數,確保引用計數是線程安全的。相比於 shared_ptr
,TC_AutoPtr
擁有更好的性能;而 shared_ptr
有更加完善的功能。TarsCpp 框架已經支持 C++11,開發者可以根據業務具體需求自由選擇。
TARS能夠在考慮到易用性和高性能的同時快速構建系統並自動生成代碼,幫助開發人員和企業以微服務的方式快速構建本身穩定可靠的分佈式應用,從而令開發人員只關注業務邏輯,提升運營效率。多語言、敏捷研發、高可用和高效運營的特性使 TARS 成爲企業級產品。
TARS微服務助您數字化轉型,歡迎訪問:
TARS官網:https://TarsCloud.org
TARS源碼:https://github.com/TarsCloud
Linux基金會官方微服務免費課程:https://www.edx.org/course/bu...
獲取《TARS官方培訓電子書》:https://wj.qq.com/s2/6570357/...
或掃碼獲取: