C++ 11 常用特性的使用經驗總結(二)
4、智慧指標記憶體管理
在記憶體管理方面,C++11的std::auto_ptr基礎上,移植了boost庫中的智慧指標的部分實現,如std::shared_ptr、std::weak_ptr等,當然,想boost::thread一樣,C++11也修復了boost::make_shared中構造引數的限制問題。把智慧指標新增為標準,個人覺得真的非常方便,畢竟在C++中,智慧指標在程式設計設計中使用的還是非常廣泛。
什麼是智慧指標?網上已經有很多解釋,個人覺得“智慧指標”這個名詞似乎起得過於“霸氣”,很多初學者看到這個名詞就覺得似乎很難。
簡單地說,智慧指標只是用物件去管理一個資源指標,同時用一個計數器計算當前指標引用物件的個數,當管理指標的物件增加或減少時,計數器也相應加1或減1,當最後一個指標管理物件銷燬時,計數器為1,此時在銷燬指標管理物件的同時,也把指標管理物件所管理的指標進行delete操作。
如下圖所示,簡單話了一下指標、智慧指標物件和計數器之間的關係:

下面的小章節中,我們分別介紹常用的兩個智慧指標std::shared_ptr、std::weak_ptr的用法。
4.1、std::shared_ptr
std::shared_ptr包裝了new操作符動態分別的記憶體,可以自由拷貝複製,基本上是使用最多的一個智慧指標型別。
我們通過下面例子來了解下std::shared_ptr的用法:
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html #include class Test { public: Test() { std::cout "Test()" std::endl; } ~Test() { std::cout "~Test()" std::endl; } }; int main() { std::shared_ptr p1 = std::make_shared(); std::cout "1 ref:" std::endl; { std::shared_ptr p2 = p1; std::cout "2 ref:" std::endl; } std::cout "3 ref:" std::endl; return 0; }
執行結果:

從上面程式碼的執行結果,需要讀者瞭解的是:
1、std::make_shared封裝了new方法,boost::make_shared之前的原則是既然釋放資源delete由智慧指標負責,那麼應該把new封裝起來,否則會讓人覺得自己呼叫了new,但沒有呼叫delete,似乎與誰申請,誰釋放的原則不符。C++也沿用了這一做法。
2、隨著引用物件的增加std::shared_ptr p2 = p1,指標的引用計數有1變為2,當p2退出作用域後,p1的引用計數變回1,當main函式退出後,p1離開main函式的作用域,此時p1被銷燬,當p1銷燬時,檢測到引用計數已經為1,就會在p1的解構函式中呼叫delete之前std::make_shared建立的指標。
4.2、std::weak_ptr
std::weak_ptr網上很多人說其實是為了解決std::shared_ptr在相互引用的情況下出現的問題而存在的,C++官網對這個只能指標的解釋也不多,那就先甭管那麼多了,讓我們暫時完全接受這個觀點。
std::weak_ptr有什麼特點呢?與std::shared_ptr最大的差別是在賦值是,不會引起智慧指標計數增加。
我們下面將繼續如下兩點:
1、std::shared_ptr相互引用會有什麼後果;
2、std::weak_ptr如何解決第一點的問題。
A、std::shared_ptr 相互引用的問題示例:
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html #include class TestB; class TestA { public: TestA() { std::cout "TestA()" std::endl; } void ReferTestB(std::shared_ptr test_ptr) { m_TestB_Ptr = test_ptr; } ~TestA() { std::cout "~TestA()" std::endl; } private: std::shared_ptr m_TestB_Ptr; //TestB的智慧指標 }; class TestB { public: TestB() { std::cout "TestB()" std::endl; } void ReferTestB(std::shared_ptr test_ptr) { m_TestA_Ptr = test_ptr; } ~TestB() { std::cout "~TestB()" std::endl; } std::shared_ptr m_TestA_Ptr; //TestA的智慧指標 }; int main() { std::shared_ptr ptr_a = std::make_shared(); std::shared_ptr ptr_b = std::make_shared(); ptr_a->ReferTestB(ptr_b); ptr_b->ReferTestB(ptr_a); return 0; }
執行結果:

大家可以看到,上面程式碼中,我們建立了一個TestA和一個TestB的物件,但在整個main函式都執行完後,都沒看到兩個物件被析構,這是什麼問題呢?
原來,智慧指標ptr_a中引用了ptr_b,同樣ptr_b中也引用了ptr_a,在main函式退出前,ptr_a和ptr_b的引用計數均為2,退出main函式後,引用計數均變為1,也就是相互引用。
這等效於說:
ptr_a對ptr_b說,哎,我說ptr_b,我現在的條件是,你先釋放我,我才能釋放你,這是天生的,造物者決定的,改不了。
ptr_b也對ptr_a說,我的條件也是一樣,你先釋放我,我才能釋放你,怎麼辦?
是吧,大家都沒錯,相互引用導致的問題就是釋放條件的衝突,最終也可能導致記憶體洩漏。
B、std::weak_ptr 如何解決相互引用的問題
我們在上面的程式碼基礎上使用std::weak_ptr進行修改:
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html #include <memory> class TestB; class TestA { public: TestA() { std::cout << "TestA()" << std::endl; } void ReferTestB(std::shared_ptr<TestB> test_ptr) { m_TestB_Ptr = test_ptr; } void TestWork() { std::cout << "~TestA::TestWork()" << std::endl; } ~TestA() { std::cout << "~TestA()" << std::endl; } private: std::weak_ptr<TestB> m_TestB_Ptr; }; class TestB { public: TestB() { std::cout << "TestB()" << std::endl; } void ReferTestB(std::shared_ptr<TestA> test_ptr) { m_TestA_Ptr = test_ptr; } void TestWork() { std::cout << "~TestB::TestWork()" << std::endl; } ~TestB() { std::shared_ptr<TestA> tmp = m_TestA_Ptr.lock(); tmp->TestWork(); std::cout << "2 ref a:" << tmp.use_count() << std::endl; std::cout << "~TestB()" << std::endl; } std::weak_ptr<TestA> m_TestA_Ptr; }; int main() { std::shared_ptr<TestA> ptr_a = std::make_shared<TestA>(); std::shared_ptr<TestB> ptr_b = std::make_shared<TestB>(); ptr_a->ReferTestB(ptr_b); ptr_b->ReferTestB(ptr_a); std::cout << "1 ref a:" << ptr_a.use_count() << std::endl; std::cout << "1 ref b:" << ptr_a.use_count() << std::endl; return 0; }
執行結果:

由以上程式碼執行結果我們可以看到:
1、所有的物件最後都能正常釋放,不會存在上一個例子中的記憶體沒有釋放的問題。
2、ptr_a 和ptr_b在main函式中退出前,引用計數均為1,也就是說,在TestA和TestB中對std::weak_ptr的相互引用,不會導致計數的增加。在TestB解構函式中,呼叫std::shared_ptr tmp = m_TestA_Ptr.lock(),把std::weak_ptr型別轉換成std::shared_ptr型別,然後對TestA物件進行呼叫。
5、其他
本章節介紹的內容如果按照分類來看,也屬於以上語法類別,但感覺還是單獨拿出來總結好些。
下面小節主要介紹std::function、std::bind和lamda表示式的一些特點和用法,希望對讀者能有所幫助。
5.1、std::function、std::bind 封裝可執行物件
std::bind和std::function也是從boost中移植進來的C++新標準,這兩個語法使得封裝可執行物件變得簡單而易用。此外,std::bind和std::function也可以結合我們一下所說的lamda表示式一起使用,使得可執行物件的寫法更加“花俏”。
我們下面通過例項一步步瞭解std::function和std::bind的用法:
Test.h檔案
//Test.h 示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html class Test { public: void Add() { } };
main.cpp檔案
//main.cpp 示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html #include #include #include "Test.h" int add(int a,int b) { return a + b; } int main() { Test test; test.Add(); return 0; }
解釋:
上面程式碼中,我們實現了一個add函式和一個Test類,Test類裡面有一個Test函式也有一個函式Add。
OK,我們現在來考慮一下這個問題,假如我們的需求是讓Test裡面的Add由外部實現,如main.cpp裡面的add函式,有什麼方法呢?
沒錯,我們可以用函式指標。
我們修改Test.h
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html class Test { public: typedef int(*FunType)(int, int); void Add(FunType fun,int a,int b) { int sum = fun(a, b); std::cout "sum:" std::endl; } };
修改main.cpp的呼叫
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html .... .... Test test; test.Add(add, 1, 2); ....
執行結果:

到現在為止,完美了嗎?如果你是Test.h的提供者,你覺得有什麼問題?
我們把問題升級,假如add實現是在另外一個類內部,如下程式碼:
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html class TestAdd { public: int Add(int a,int b) { return a + b; } }; int main() { Test test; //test.Add(add, 1, 2); return 0; }
假如add方法在TestAdd類內部,那你的Test類沒轍了,因為Test裡的Test函式只接受函式指標。你可能說,這個不是我的問題啊,我是介面的定義者,使用者應該遵循我的規則。但如果現在我是客戶,我們談一筆生意,就是我要購買使用你的Test類,前提是需要支援我傳入函式指標,也能傳入物件函式,你做不做這筆生意?
是的,你可以選擇不做這筆生意。我們現在再假設你已經好幾個月沒吃肉了(別跟我說你是素食主義者),身邊的蒼蠅肉、蚊子肉啊都不被你吃光了,好不容易等到有機會吃肉,那有什麼辦法呢?
這個時候std::function和std::bind就幫上忙了。
我們繼續修改程式碼:
Test.h
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html class Test { public: void Add(std::functionint(int, int)> fun, int a, int b) { int sum = fun(a, b); std::cout "sum:" std::endl; } };
解釋:
Test類中std::function表示std::function封裝的可執行物件返回值和兩個引數均為int型別。
main.cpp
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html int add(int a,int b) { std::cout "add" std::endl; return a + b; } class TestAdd { public: int Add(int a,int b) { std::cout "TestAdd::Add" std::endl; return a + b; } }; int main() { Test test; test.Add(add, 1, 2); TestAdd testAdd; test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2); return 0; }
解釋:
std::bind第一個引數為物件函式指標,表示函式相對於類的首地址的偏移量;
testAdd為物件指標;
std::placeholders::_1和std::placeholders::_2為引數佔位符,表示std::bind封裝的可執行物件可以接受兩個引數。
執行結果:

是的,得出這個結果,你就可以等著吃肉了,我們的Test函式在函式指標和類物件函式中都兩種情況下都完美執行。
5.2、lamda 表示式
在眾多的C++11新特性中,個人覺得lamda表示式不僅僅是一個語法新特性,對於沒有用過java或C#lamda表示式讀者,C++11的lamda表示式在一定程度上還衝擊著你對傳統C++程式設計的思維和想法。
我們先從一個簡單的例子來看看lamda表示式:
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html int main() { auto add= [](int a, int b)->int{ return a + b; }; int ret = add(1,2); std::cout << "ret:" << ret << std::endl; return 0; }
解釋:
第3至5行為lamda表示式的定義部分
[]:中括號用於控制main函式與內,lamda表示式之前的變數在lamda表示式中的訪問形式;
(int a,int b):為函式的形參
->int:lamda表示式函式的返回值定義
{}:大括號內為lamda表示式的函式體。
執行結果:

我使用lamda表示式修改5.1中的例子看看:
main.cpp
//示例程式碼1.0 http://www.cnblogs.com/feng-sc/p/5710724.html ..... int main() { Test test; test.Add(add, 1, 2); TestAdd testAdd; test.Add(std::bind(&TestAdd::Add, testAdd, std::placeholders::_1, std::placeholders::_2), 1, 2); test.Add([](int a, int b)->int { std::cout "lamda add fun" std::endl; return a + b; },1,2); return 0; }
另外本人從事線上教育多年,將自己的資料整合建了一個QQ群,對於有興趣一起交流學習c/c++的初學者可以加群:941636044,裡面有大神會給予解答,也會有許多的資源可以供大家學習分享,歡迎大家前來一起學習進步!