復(fù)雜的東西寫多了,如今寫點(diǎn)簡單的好了。由于功能上的需要,
Vczh Library++3.0被我搞得很離譜。為了開發(fā)維護(hù)的遍歷、減少粗心犯下的錯(cuò)誤以及增強(qiáng)單元測試、回歸測試和測試工具,因此記錄下一些開發(fā)上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
C++實(shí)謂各種語言中的軟肋,功能強(qiáng)大,陷阱更強(qiáng)大。當(dāng)然我認(rèn)為一門語言用得不好完全是程序員的責(zé)任,不過因?yàn)镃++涉及到的概念實(shí)在是太多,想用好實(shí)在也不是一件容易的事情。C++開發(fā)的時(shí)候總是會(huì)遇到各種各樣的問題,其中最嚴(yán)重的無非是內(nèi)存相關(guān)的。C語言由于結(jié)構(gòu)簡單,內(nèi)存處理起來雖然不得力,但總的來說慣用法已經(jīng)深入人心,因此也不會(huì)造成什么很難發(fā)現(xiàn)的錯(cuò)誤。C++就不一樣了。有了虛函數(shù)、構(gòu)造函數(shù)、析構(gòu)函數(shù)、復(fù)制構(gòu)造函數(shù)和operator=重載之后,還是有很多人喜歡把一個(gè)類直接寫進(jìn)文件流,或者拿來memset,代碼一團(tuán)亂麻,不知悔改也。但是不能因此因噎廢食,就像某人因?yàn)镃++帶來的心智問題太多,自己搞不定,自己團(tuán)隊(duì)也搞不定,就說C++不好一樣。
因此第一篇文章主要針對(duì)內(nèi)存來講。我們處理內(nèi)存,第一件事就是不要有內(nèi)存泄露。內(nèi)存泄露不能等到測試的時(shí)候,通過長時(shí)間運(yùn)行程序并觀察任務(wù)管理器的方法來做,這顯然已經(jīng)晚了。幸好Visual C++給了我們一個(gè)十分好用的工具:_CrtDumpMemoryLeaks函數(shù)。這個(gè)函數(shù)會(huì)在Debug模式下往Visual Studio的output窗口打印出那個(gè)時(shí)候你new(malloc)了但是還沒delete(free)的所有內(nèi)存塊的地址、長度、前N個(gè)字節(jié)的內(nèi)容和其他信息。怎么做呢?其實(shí)很簡單:
1 #define _CRTDBG_MAP_ALLOC
2 #include <stdlib.h>
3 #include <crtdbg.h>
4 #include <windows.h>
5
6 int wmain(vint argc , wchar_t* args[])
7 {
8 // 這里運(yùn)行程序,并在下面的函數(shù)調(diào)用之前delete掉所有new的東西
9 _CrtDumpMemoryLeaks();
10 return 0;
11 }
我們只需要在注釋的地方完成我們程序的功能,然后確信自己已經(jīng)delete掉所有應(yīng)該delete的東西,最后_CrtDumpMemoryLeaks()函數(shù)調(diào)用的時(shí)候就可以打印出沒被delete的東西了。這個(gè)方法十分神奇,因?yàn)槟阒恍枰趍ain函數(shù)所在的cpp文件這么#include一下,所有的cpp文件里面的new都會(huì)受到監(jiān)視,跟平常所用的用宏把new給換掉的這種破方法截然不同。如果你使用了全局變量的話也要小心,因?yàn)槿肿兞康奈鰳?gòu)函數(shù)是在main函數(shù)結(jié)束之后才執(zhí)行的,因此如果在全局變量的析構(gòu)函數(shù)里面delete的東西仍然會(huì)被_CrtDumpMemoryLeaks函數(shù)當(dāng)成泄露掉的資源對(duì)待。當(dāng)然本人認(rèn)為全局變量可以用,但是全局變量的賦值必須在main里面做,釋放也是,除非那個(gè)全局變量的構(gòu)造函數(shù)沒有申請(qǐng)任何內(nèi)存,所以這也是一個(gè)很好的檢查方法。
不過上面也僅僅是一個(gè)告訴你有沒有內(nèi)存泄漏的方法罷了。那么如何避免內(nèi)存泄露呢?當(dāng)然在設(shè)計(jì)一些性能要求沒有比操作系統(tǒng)更加嚴(yán)格的程序的時(shí)候,可以使用以下方法:
1、如果構(gòu)造函數(shù)new了一個(gè)對(duì)象
并使用成員指針變量保存的話,那么必須在析構(gòu)函數(shù)delete它,并且不能有為了某些便利而將這個(gè)對(duì)象的所有權(quán)轉(zhuǎn)讓出去的事情發(fā)生。
2、在能使用shared_ptr的時(shí)候,盡量使用shared_ptr。shared_ptr只要你不發(fā)生循環(huán)引用,那么這個(gè)東西可以安全地互相傳遞、隨便你放在什么容器里面添加刪除、你想放哪里就放在哪里,再也不用考慮這個(gè)對(duì)象的生命周期問題了。
3、不要在有構(gòu)造函數(shù)和析構(gòu)函數(shù)的對(duì)象上使用memset(或者memcpy)。如果一個(gè)對(duì)象需要memset,那么在該對(duì)象的構(gòu)造函數(shù)里面memset自己。如果你需要memset一個(gè)對(duì)象數(shù)組,那也在該對(duì)象的構(gòu)造函數(shù)里面memset自己。
如果你需要memset一個(gè)沒有構(gòu)造函數(shù)的復(fù)雜對(duì)象,那么請(qǐng)為他添加一個(gè)構(gòu)造函數(shù),除非那是別人的API提供的東西。
4、如果一個(gè)對(duì)象是繼承了其他東西,或者某些成員被標(biāo)記了virtual的話,絕對(duì)不要memset。對(duì)象是獨(dú)立的,也就是說父類內(nèi)部結(jié)構(gòu)的演變不需要對(duì)子類負(fù)責(zé)。哪天父類里面加了一個(gè)string成員,被子類一memset,就欲哭無淚了。
5、如果需要為一個(gè)對(duì)象定義構(gòu)造函數(shù),那么連復(fù)制構(gòu)造函數(shù)、operator=重載和析構(gòu)函數(shù)都全部寫全。如果不想寫復(fù)制構(gòu)造函數(shù)和operator=的話,那么用一個(gè)空的實(shí)現(xiàn)寫在private里面,確保任何試圖調(diào)用這些函數(shù)的代碼都出現(xiàn)編譯錯(cuò)誤。
6、如果你實(shí)在很喜歡C語言的話,那麻煩換一個(gè)只支持C不支持C++的編譯器,全面杜絕因?yàn)檎`用了C++而導(dǎo)致你的C壞掉的情況出現(xiàn)。
什么是循環(huán)引用呢?如果兩個(gè)對(duì)象互相使用一個(gè)shared_ptr成員變量直接或者間接指向?qū)Ψ降脑?,就是循環(huán)引用了。在這種情況下引用計(jì)數(shù)會(huì)失效,因?yàn)榫退阃膺叺膕hared_ptr全釋放光了,引用計(jì)數(shù)也不會(huì)是0的。
今天就說到這里了,過幾天我高興的話再寫一篇續(xù)集,如果我持續(xù)高興的話呢……嗯嗯……。
復(fù)雜的東西寫多了,如今寫點(diǎn)簡單的好了。由于功能上的需要,
Vczh Library++3.0被我搞得很離譜。為了開發(fā)維護(hù)的遍歷、減少粗心犯下的錯(cuò)誤以及增強(qiáng)單元測試、回歸測試和測試工具,因此記錄下一些開發(fā)上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
上一篇文章講到了如何檢查內(nèi)存泄露。其實(shí)只要肯用C++的STL里面的高級(jí)功能的話,內(nèi)存泄露是很容易避免的。我在開發(fā)Vczh Library++ 3.0的時(shí)候,所有的測試用例都保證跑完了沒有內(nèi)存泄露。但是很可惜有些C++團(tuán)隊(duì)不能使用異常,更甚者不允許寫構(gòu)造函數(shù)析構(gòu)函數(shù)之類,前一個(gè)還好,后一個(gè)簡直就是在用C。當(dāng)然有這些變態(tài)規(guī)定的地方STL都是用不了的,所以我們更加需要扎實(shí)的基礎(chǔ)來開發(fā)C++程序。
今天這一篇主要還是講指針的問題。因?yàn)樯弦黄恼乱还P帶過,今天就來詳細(xì)講內(nèi)存泄漏或者野指針發(fā)生的各種情況。當(dāng)然我不可能一下子舉出全部的例子,只能說一些常見的。
一、錯(cuò)誤覆蓋內(nèi)存。
之前提到的不能隨便亂memset其實(shí)就是為了避免這個(gè)問題的。其實(shí)memcpy也不能亂用,我們來看一個(gè)例子,最簡單的:
1 #define MAX_STRING 20;
2
3 struct Student
4 {
5 char name[MAX_STRING];
6 char id[MAX_STRING];
7 int chinese;
8 int math;
9 int english;
10 };
大家對(duì)這種結(jié)構(gòu)肯定十分熟悉,畢竟是大學(xué)時(shí)候經(jīng)常要寫的作業(yè)題……好了,大家很容易看得出來這其實(shí)是C語言的經(jīng)典寫法。我們拿到手之后,一般會(huì)先初始化一下,然后賦值。
1 Student vczh;
2 memset(&vczh, 0, sizeof(vczh));
3 strcpy(vczh.name, "vczh");
4 strcpy(vczh.id, "VCZH'S ID");
5 vczh.chinese=70;
6 vczh.math=90;
7 vczh.english=80;
為什么要在這里使用memset呢?memset的用處是將一段內(nèi)存的每一個(gè)字節(jié)都設(shè)置成同一個(gè)數(shù)字。這里是0,因此兩個(gè)字符串成員的所有字節(jié)都會(huì)變成0。因此在memset了Student之后,我們通過正常方法來訪問name和id的時(shí)候都會(huì)得到空串。而且如果Student里面有指針的話,0指針代表的是沒有指向任何有效對(duì)象,因此這個(gè)時(shí)候?qū)χ羔樦赶虻膶?duì)象進(jìn)行讀寫就會(huì)立刻崩潰。對(duì)于其他數(shù)值,0一般作為初始值也不會(huì)有什么問題(double什么的要小心)。這就是我們寫程序的時(shí)候使用memset的原因。
好了,如今社會(huì)進(jìn)步,人民當(dāng)家做主了,死程們?cè)僖膊恍枰艿娇蓯旱腃語言剝削了,我們可以使用C++!因此我們借助STL的力量把Student改寫成下面這種帶有C++味道的形式:
1 struct Student
2 {
3 std::string name;
4 std::string id;
5 int chinese;
6 int math;
7 int english;
8 };
我們?nèi)匀恍枰獙?duì)Student進(jìn)行初始化,不然三個(gè)分?jǐn)?shù)還是隨機(jī)值。但是我們又不想每一次創(chuàng)建的時(shí)候都對(duì)他們分別進(jìn)行賦值初始化城0。這個(gè)時(shí)候你心里可能還是想著memset,
這就錯(cuò)了!在memset的時(shí)候,你會(huì)把std::string內(nèi)部的不知道什么東西也給memset掉。假如一個(gè)空的std::string里面存放的指針指向的是一個(gè)空的字符串而不是用0來代表空的時(shí)候,一下子內(nèi)部的指針就被你刷成0,等下std::string的析構(gòu)函數(shù)就沒辦法delete掉指針了,于是
內(nèi)存泄露就出現(xiàn)了。有些朋友可能不知道上面那句話說的是什么意思,我們現(xiàn)在來模擬一下不能memset的std::string要怎么實(shí)現(xiàn)。
為了讓memset一定出現(xiàn)內(nèi)存泄露,那么std::string里面的指針必須永遠(yuǎn)都指向一個(gè)有效的東西。當(dāng)然我們還需要在字符串進(jìn)行復(fù)制的時(shí)候復(fù)制指針。我們這里不考慮各種優(yōu)化技術(shù),用最簡單的方法做一個(gè)字符串出來:
1 class String
2 {
3 private:
4 char* buffer;
5
6 public:
7 String()
8 {
9 buffer=new char[1];
10 buffer[0]=0;
11 }
12
13 String(const char* s)
14 {
15 buffer=new char[strlen(s)+1];
16 strcpy(buffer, s);
17 }
18
19 String(const String& s)
20 {
21 buffer=new char[strlen(s.buffer)+1];
22 strcpy(buffer, s.buffer);
23 }
24
25 ~String()
26 {
27 delete[] buffer;
28 }
29
30 String& operator=(const String& s)
31 {
32 delete[] buffer;
33 buffer=new char[strlen(s.buffer)+1];
34 strcpy(buffer, s.buffer);
35 }
36 };
于是我們來做一下memset。首先定義一個(gè)字符串變量,其次memset掉,讓我們看看會(huì)發(fā)生什么事情:
1 string s;
2 memset(&s, 0, sizeof(s));
第一行我們構(gòu)造了一個(gè)字符串s。這個(gè)時(shí)候字符串的構(gòu)造函數(shù)就會(huì)開始運(yùn)行,因此strcmp(s.buffer, "")==0。第二行我們把那個(gè)字符串給memset掉了。這個(gè)時(shí)候s.buffer==0。于是函數(shù)結(jié)束了,字符串的析構(gòu)函數(shù)嘗試delete這個(gè)指針。我們知道delete一個(gè)0是不會(huì)有問題的,因此程序不會(huì)發(fā)生錯(cuò)誤。
我們活生生把構(gòu)造函數(shù)賦值給buffer的new char[1]給丟了!鐵定發(fā)生內(nèi)存泄露!
好了,提出問題總要解決問題,我們不使用memset的話,怎么初始化Student呢?這個(gè)十分好做,我們只需要為Student加上構(gòu)造函數(shù)即可:
1 struct Student
2 {
3
.//不重復(fù)那些聲明
4
5 Student():chinese(0),math(0),english(0)
6 {
7 }
8 }; 這樣就容易多了。每當(dāng)我們定義一個(gè)Student變量的時(shí)候,所有的成員都初始化好了。name和id因?yàn)閟tring的構(gòu)造函數(shù)也自己初始化了,因此所有的成員也都初始化了。加入Student用了一半我們想再初始化一下怎么辦呢?也很容易:
1 Student vczh;
2
.//各種使用
3 vczh=Student(); 經(jīng)過一個(gè)等號(hào)操作符的調(diào)用,舊Student的所有成員就被一個(gè)新的初始化過的Student給覆蓋了,就如同我們對(duì)一個(gè)int變量重新賦值一樣常見。當(dāng)然因?yàn)楦鞣N復(fù)制經(jīng)常會(huì)出現(xiàn),因此我們也要跟上面貼出來的string的例子一樣,實(shí)現(xiàn)好那4個(gè)函數(shù)。至此我十分不理解為什么某些團(tuán)隊(duì)不允許使用構(gòu)造函數(shù),我猜就是為了可以memset,其實(shí)是很沒道理的。
二、異常。 咋一看內(nèi)存泄露跟異常好像沒什么關(guān)系,但實(shí)際上這種情況更容易發(fā)生。我們來看一個(gè)例子:
1 char* strA=new char[MAX_PATH];
2 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
3 char* strB=new char[MAX_PATH];
4 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
5
6 DoSomething(strA, strB);
7
8 RELEASE_STRB:
9 delete[] strB;
10 RELEASE_STRA:
11 delete[] strA;
相信這肯定是大家的常用模式。我在這里也不是教唆大家使用goto,不過對(duì)于這種例子來說,用goto是最優(yōu)美的解決辦法了。但是大家可以看出來,我們用的是C++,因?yàn)檫@里有new。如果DoSomething發(fā)生了異常怎么辦呢?如果GetXXX發(fā)生了異常怎么辦呢?我們這里沒有任何的try-catch,一有異常,函數(shù)里克結(jié)束,兩行可憐的delete就不會(huì)被執(zhí)行到了,
于是內(nèi)存泄漏發(fā)生了!
那我們?nèi)绾伪苊膺@種情況下的內(nèi)存泄露呢?一些可愛的小盆友可能會(huì)想到,既然是因?yàn)闆]有catch異常才發(fā)生的內(nèi)存泄露,那我們來catch吧:
1 char* strA=new char[MAX_PATH];
2 try
3 {
4 if(GetXXX(strA, MAX_PATH)==ERROR) goto RELEASE_STRA;
5 char* strB=new char[MAX_PATH];
6 try
7 {
8 if(GetXXX(strB, MAX_PATH)==ERROR) goto RELEASE_STRB;
9 DoSomething(strA, strB);
10 }
11 catch(
)
12 {
13 delete[] strB;
14 throw;
15 }
16 }
17 catch(
)
18 {
19 delete[] strA;
20 throw;
21 }
22
23 RELEASE_STRB:
24 delete[] strB;
25 RELEASE_STRA:
26 delete[] strA; 你能接受嗎?當(dāng)然是不能的。問題出在哪里呢?因?yàn)镃++沒有try-finally。你看這些代碼到處都是雷同的東西,顯然我們需要編譯器幫我們把這些問題搞定。最好的解決方法是什么呢?顯然還是構(gòu)造函數(shù)和析構(gòu)函數(shù)。總之記住,
如果想要事情成對(duì)發(fā)生,那么使用構(gòu)造函數(shù)和析構(gòu)函數(shù)。
第一步,GetXXX顯然只能支持C模式的東西,因此我們要寫一個(gè)支持C++的:
1 bool GetXXX2(string& s)
2 {
3 char* str=new char[MAX_PATH];
4 bool result;
5 try
6 {
7 result=GetXXX(str, MAX_PATH);
8 if(result)s=str;
9 }
10 catch(
)
11 {
12 delete[] str;
13 throw;
14 }
15 delete[] str;
16 return result;
17 } 借助這個(gè)函數(shù)我們可以看到,因?yàn)橛辛薌etXXX這種C的東西,導(dǎo)致我們多了多少麻煩。不過這總是一勞永逸的,有了GetXXX2和修改之后的DoSomething2之后,我們就可以用更簡單的方法來做了:
1 string a,b;
2 if(GetXXX2(a) && GetXXX2(b))
3 {
4 DoSomething2(a, b);
5 }
多么簡單易懂。這個(gè)代碼在任何地方發(fā)生了異常,所有new的東西都會(huì)被delete。這就是析構(gòu)函數(shù)的一個(gè)好處。一個(gè)變量的析構(gòu)函數(shù)在這個(gè)變量超出了作用域的時(shí)候一定會(huì)被調(diào)用,無論代碼是怎么走出去的。
今天就說到這里了。說了這么多還是想讓大家不要小看構(gòu)造函數(shù)和析構(gòu)函數(shù)。那種微不足道的因?yàn)橐恍〔糠植皇瞧款i的性能問題而放棄構(gòu)造函數(shù)和析構(gòu)函數(shù)的做法,終究是要為了修bug而加班的。只要明白并用好了構(gòu)造函數(shù)、析構(gòu)函數(shù)和異常,那么C++的特性也可以跟C一樣清楚明白便于理解,而且寫出來的代碼更好看的。大家期待第三篇哈。
復(fù)雜的東西寫多了,如今寫點(diǎn)簡單的好了。由于功能上的需要,Vczh Library++3.0被我搞得很離譜。為了開發(fā)維護(hù)的遍歷、減少粗心犯下的錯(cuò)誤以及增強(qiáng)單元測試、回歸測試和測試工具,因此記錄下一些開發(fā)上的小技巧,以便拋磚引玉,造福他人。歡迎高手來噴,菜鳥膜拜。
今天是關(guān)于內(nèi)存的最后一篇了。上一篇文章講了為什么不能對(duì)一個(gè)東西隨便memset。里面的demo代碼出了點(diǎn)小bug,不過我不喜歡在發(fā)文章的時(shí)候里面的demo代碼也拿去編譯和運(yùn)行,所以大家有什么發(fā)現(xiàn)的問題就評(píng)論吧。這樣也便于后來的人不會(huì)受到誤導(dǎo)。這次說的仍然是構(gòu)造函數(shù)和析構(gòu)函數(shù)的事情,不過我們將通過親手開發(fā)一個(gè)智能指針的方法,知道引用計(jì)數(shù)如何幫助管理資源,以及錯(cuò)誤使用引用計(jì)數(shù)的情況。
首先先來看一下智能指針是如何幫助我們管理內(nèi)存的。現(xiàn)在智能指針的實(shí)現(xiàn)非常多,我就假設(shè)這個(gè)類型叫Ptr<T>吧。這跟Vczh Library++ 3.0所使用的實(shí)現(xiàn)一樣。
1 class Base
2 {
3 public:
4 virtual ~Base(){}
5 };
6
7 class Derived1 : public Base
8 {
9 };
10
11 class Derived2 : public Base
12 {
13 };
14
15 //---------------------------------------
16
17 List<Ptr<Base>> objects;
18 objects.Add(new Derived1);
19 objects.Add(new Derived2);
20
21 List<Ptr<Base>> objects2;
22 objects2.Add(objects[0]);
當(dāng)然這里的List也是Vczh Library++3.0實(shí)現(xiàn)的,不過這玩意兒跟vector也好跟C#的List也好都是一個(gè)概念,因此也就不需要多加解釋了。我們可以看到智能指針的一個(gè)好處,只要沒有循環(huán)引用出現(xiàn),你無論怎么復(fù)制它,最終總是可以被析構(gòu)掉的。另一個(gè)例子告訴我們智能指針如何處理類型轉(zhuǎn)換:
1 Ptr<Derived1> d1=new Derived1;
2 Ptr<Base> b=d1;
3 Ptr<Derived2> d2=b.Cast<Derived2>();
4 // d2是空,因?yàn)閎指向的是Derived1而不是Derived2。
這就如同我們Derived1*可以隱式轉(zhuǎn)換到Base*,而當(dāng)你使用dynamic_cast<Derived2*>(static_cast<Base*>(new Derived1))會(huì)得到0一樣。智能指針在幫助我們析構(gòu)對(duì)象的同時(shí),也要做好類型轉(zhuǎn)換的工作。
好了,現(xiàn)在先讓我們一步一步做出那個(gè)Ptr<T>。我們需要清楚這個(gè)智能指針?biāo)獙?shí)現(xiàn)的功能是什么,然后我們一個(gè)一個(gè)來做。首先讓我們列出一張表:
1、沒有參數(shù)構(gòu)造的時(shí)候,初始化為空
2、使用指針構(gòu)造的時(shí)候,擁有那個(gè)指針,并且在沒有任何智能指針指向那個(gè)指針的時(shí)候刪除掉該指針。
3、智能指針進(jìn)行復(fù)制的時(shí)候,兩個(gè)智能指針共同擁有該內(nèi)部指針。
4、智能指針可以使用新的智能指針或裸指針重新賦值。
5、需要支持隱式指針類型轉(zhuǎn)換,static_cast不支持而dynamic_cast支持的轉(zhuǎn)換則使用Cast<T2>()成員函數(shù)來解決。
6、如果一個(gè)裸指針直接用來創(chuàng)建兩個(gè)智能指針的話,期望的情況是當(dāng)兩個(gè)智能指針析構(gòu)掉的時(shí)候,該指針會(huì)被delete兩次從而崩潰。
7、不處理循環(huán)引用。
最后兩點(diǎn)實(shí)際上是錯(cuò)誤使用智能指針的最常見的兩種情況。我們從1到5一個(gè)一個(gè)實(shí)現(xiàn)。首先是1。智能指針可以隱式轉(zhuǎn)換成bool,可以通過operator->()拿到內(nèi)部的T*。在沒有使用參數(shù)構(gòu)造的時(shí)候,需要轉(zhuǎn)換成false,以及拿到0:
1 template<typename T>
2 class Ptr
3 {
4 private:
5 T* pointer;
6 int* counter;
7
8 void Increase()
9 {
10 if(counter)++*counter;
11 }
12
13 void Decrease()
14 {
15 if(counter && --*counter==0)
16 {
17 delete counter;
18 delete pointer;
19 counter=0;
20 pointer=0;
21 }
22 }
23
24 public:
25 Ptr():pointer(0),counter(0)
26 {
27 }
28
29 ~Ptr()
30 {
31 Decrease();
32 }
33
34 operator bool()const
35 {
36 return counter!=0;
37 }
38
39 T* operator->()const
40 {
41 return pointer;
42 }
43 };
在這里我們實(shí)現(xiàn)了構(gòu)造函數(shù)和析構(gòu)函數(shù)。構(gòu)造函數(shù)把內(nèi)部指針和引用計(jì)數(shù)的指針都初始化為空,而析構(gòu)函數(shù)則進(jìn)行引用計(jì)數(shù)的減一操作。另外兩個(gè)操作符重載很容易理解。我們主要來看看Increase函數(shù)和Decrease函數(shù)都分別做了什么。Increase函數(shù)在引用計(jì)數(shù)存在的情況下,把引用計(jì)數(shù)加一。而Decrease函數(shù)在引用計(jì)數(shù)存在的情況下,把引用計(jì)數(shù)減一,如果引用計(jì)數(shù)在減一過程中變成了0,則刪掉擁有的資源。
當(dāng)然到了這個(gè)時(shí)候智能指針還不能用,我們必須替他加上復(fù)制構(gòu)造函數(shù),operator=操作符重載以及使用指針賦值的情況。首先讓我們來看使用指針賦值的話我們應(yīng)該加上什么:
1 Ptr(T* p):pointer(0),counter(0)
2 {
3 *this=p;
4 }
5
6 Ptr<T>& operator=(T* p)
7 {
8 Decrease();
9 if(p)
10 {
11 pointer=p;
12 counter=new int(1);
13 }
14 else
15 {
16 pointer=0;
17 counter=0;
18 }
19 return *this;
20 }
這里還是偷工減料了的,構(gòu)造函數(shù)接受了指針的話,還是轉(zhuǎn)給operator=去調(diào)用了。當(dāng)一個(gè)智能指針被一個(gè)新指針賦值的時(shí)候,我們首先要減掉一個(gè)引用計(jì)數(shù),因?yàn)樵瓉淼闹羔樤僖膊槐贿@個(gè)智能指針共享了。之后就進(jìn)行判斷,如果來的是0,那么就變成空。如果不是0,就擁有該指針,引用計(jì)數(shù)初始化成1。于是我們就可以這么使用了:
1 Ptr<Base> b=new Derived1;
2 Ptr<Derived2> d2=new Derived2;
讓我們開始復(fù)制他們吧。復(fù)制的要領(lǐng)是,先把之前擁有的指針脫離掉,然后連接到一個(gè)新的智能指針上面去。我們知道非空智能指針有多少個(gè),總的引用計(jì)數(shù)的和就是多少,只是分配到各個(gè)指針上面的數(shù)字不一樣而已:
1 Ptr(const Ptr<T>& p):pointer(p.pointer),counter(p.counter)
2 {
3 Increase();
4 }
5
6 Ptr<T>& operator=(const Ptr<T>& p)
7 {
8 if(this!=&p)
9 {
10 Decrease();
11 pointer=p.pointer;
12 counter=p.counter;
13 Increase();
14 }
15 return *this;
16 }
在上一篇文章有朋友指出重載operator=的時(shí)候需要考慮是不是自己賦值給自己,其實(shí)這是很正確的。我們寫每一類的時(shí)候,特別是當(dāng)類擁有自己控制的資源的時(shí)候,需要非常注意這件事情。當(dāng)然如果只是復(fù)制幾個(gè)對(duì)象而不會(huì)new啊delete還是close什么handle,那檢查不檢查也無所謂了。在這里我們非常清楚,當(dāng)增加一個(gè)新的非空智能指針的時(shí)候,引用計(jì)數(shù)的總和會(huì)加一。當(dāng)修改一個(gè)非空智能指針的結(jié)果也是非空的時(shí)候,引用計(jì)數(shù)的和保持不變。當(dāng)然這是應(yīng)該的,因?yàn)槲覀冃枰谒蟹强罩悄苤羔樁急粴У舻臅r(shí)候,釋放受保護(hù)的所有資源。
到了這里一個(gè)智能指針基本上已經(jīng)能用了,但是還不能處理父類子類的情況。這個(gè)是比較麻煩的,一個(gè)Ptr<Derived>事實(shí)上沒有權(quán)限訪問Ptr<Base>的內(nèi)部對(duì)象。因此我們需要通過友元類來解決這個(gè)問題?,F(xiàn)在讓我們來添加兩個(gè)新的函數(shù)吧,從一個(gè)任意的Ptr<C>復(fù)制到Ptr<T>,然后保證只有當(dāng)C*可以隱式轉(zhuǎn)換成T*的時(shí)候編譯能夠通過:
1 template<X> friend class Ptr;
2
3 template<typename C>
4 Ptr(const Ptr<C>& p):pointer(p.pointer),counter(p.counter)
5 {
6 Increase();
7 }
8
9 template<typename C>
10 Ptr<T>& operator=(const Ptr<C>& p)
11 {
12 Decrease();
13 pointer=p.pointer;
14 counter=p.counter;
15 Increase();
16 return *this;
17 }
注意這里我們的operator=并不用檢查是不是自己給自己賦值,因?yàn)檫@是兩個(gè)不同的類,相同的話會(huì)調(diào)用上面那個(gè)operator=的。如果C*不能隱式轉(zhuǎn)換到T*的話,這里的pointer=p.pointer就會(huì)失敗,從而滿足了我們的要求。
現(xiàn)在我們能夠做的事情就更多了:
1 Ptr<Derived1> d1=new Derived1;
2 Ptr<Base> b=d1;
于是我們只剩下最后一個(gè)Cast函數(shù)了。這個(gè)函數(shù)內(nèi)部使用dynamic_cast來做判斷,如果轉(zhuǎn)換失敗,會(huì)返回空指針:
1 tempalte<typename C>
2 Ptr<C> Cast()const
3 {
4 C* converted=dynamic_cast<C*>(pointer);
5 Ptr<C> result;
6 if(converted)
7 {
8 result.pointer=converted;
9 result.counter=counter;
10 Increase();
11 }
12 return result;
13 }
這是一種hack的方法,平時(shí)是不鼓勵(lì)的……不過因?yàn)椴僮鞯亩际荘tr,而且特化Ptr也是使用錯(cuò)誤的一種,所以這里就不管了。我們會(huì)檢查dynamic_cast的結(jié)果,如果成功了,那么會(huì)返回一個(gè)非空的新智能指針,而且這個(gè)時(shí)候我們也要記住Increase一下。
好了,基本功能就完成了。當(dāng)然一個(gè)智能指針還要很多其他功能,譬如說比較什么的,這個(gè)就你們自己搞定哈。
指針和內(nèi)存就說到這里了,下一篇講如何利用一個(gè)好的IDE構(gòu)造輕量級(jí)單元測試系統(tǒng)。我們都說好的工具能夠提高生產(chǎn)力,因此這種方法不能脫離一個(gè)好的IDE使用。
本文轉(zhuǎn)自:
http://www.shnenglu.com/vczh/category/14099.html
posted on 2012-09-12 14:51
王海光 閱讀(501)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
C++