復(fù)雜的東西寫多了,如今寫點(diǎn)簡(jiǎn)單的好了。由于功能上的需要,Vczh Library++3.0被我搞得很離譜。為了開發(fā)維護(hù)的遍歷、減少粗心犯下的錯(cuò)誤以及增強(qiáng)單元測(cè)試、回歸測(cè)試和測(cè)試工具,因此記錄下一些開發(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è)問題。現(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í)單元測(cè)試系統(tǒng)。我們都說好的工具能夠提高生產(chǎn)力,因此這種方法不能脫離一個(gè)好的IDE使用。
posted on 2010-06-23 23:03
陳梓瀚(vczh) 閱讀(9848)
評(píng)論(15) 編輯 收藏 引用 所屬分類:
C++實(shí)用技巧