noiile 2005-12-1
首先,我們來分析下面一段程式:
#include <iostream>#include <string>using namespace std;class Obj{public: Obj() {cout << "Default Constructor/n";} Obj(const obj& o) {cout << "Copy Constructor/n";}
string getName() {return name;}
string name;};
void printObjName(Obj o){ cout << o.getName() << endl;}
int main(){ Obj myobj; myobj.name = "hello"; printObjName(myobj); return 0;}
以上程式實際上會構建多少個obj對象呢?
1個?錯,是兩個!
其執行出來的結果是:
Default Constructor
Copy Constructor
我們可以看到,第二個對象是調用copy constructor,但無論如何程式是建立了兩個對象,第二個對象是什麼時候建立的呢?很顯然,是將myobj傳進printObjName這個函數時建立的,因為printObjName的參數類型是obj,所以這裡編譯器調用複製建構函式,構造一個新的對象。
但是,printObjName僅僅是為了列印該對象的名字,為此而建立一個對象,對於C++程式員來說,這簡直是件不能忍受的事情,為此,需要浪費構建一個對象的記憶體和CPU時間。
實際上還不止如此,當調用obj的成員方法getName()的時候,實際上也對name這個字串對象進行了一次複製。別忘了,name也是個對象。而getName的傳回值是string。這種情況更容易讓程式員忽略,因為從代碼來看只調用getName這個方法而已,並沒有任何顯式的對象。但編譯器會為它隱式的產生一個臨時對象,因為getName()的傳回值是string。
更要命的是,這個get方法我們可能會頻繁的使用。
這種情況很容易避免,只要我們在函數的參數或傳回值的類型使用指標或引用。
例如前面那個程式,將函數的參數或傳回值改成常量引用。
將程式修改如下:
class obj{public: obj() {cout << "Default Constructor/n";} obj(const obj& o) {cout << "Copy Constructor/n";} const string& getName() const {return name;}
string name;};
void printObjName(const obj& o){ cout << o.getName() << endl;}
int main(){ obj myobj; myobj.name = "hello"; printObjName(myobj); return 0;}
如此一來,程式再不會浪費半點鐘無用的記憶體或者CPU時間,看起來完美的多了。
但是,似乎換了指標,也能行的通,確實如此。那我們是應該什麼時候用指標什麼時候用引用呢?
在弄明白這個問題之前,得先瞭解指標和引用的區別:
引用保證了對象必須存在,不可為空,也不能更換,也不能delete。
指標允許為空白,也允許重新指向別的對象,而且有被別人delete的可能。
很顯然,如果我們希望必須有對象傳進來,那麼就用引用,如果我們不打算修改傳進來的對象,那麼我們就用常量引用,告訴使用介面的人,”hey,我不會修改你的對象,你放心傳進來吧”。
如果我們允許有Null 物件傳進來,那當然用指標了。當我們不會修改指標所指的對象時,就應該用常量指標。我們設計介面,必須要清晰的告訴使用介面的人,我們會對他傳進來的對象幹什麼,會修改它嗎?還是只是使用它而已,還是會delete它?
但是,僅用指標似乎不能夠表達這一切。試想下,假如介面函數因為只有你才知道的原因,需要獲得傳進來的指標對象的生命控制權,也就是說,你的介面函數有可能delete使用者傳進來的指標。怎麼能通過介面告訴使用者呢?
或許你會說,既然常量指標,表示我們不能修改對象的內容,那當然也不能delete它了,相反,非常量指標,就表示我們要獲得對象的生命控制權啦。
void testGetObj(const Obj* obj){ cout << obj->getName() << endl;}void testGetObj2(Obj* obj){ cout << obj->getName() << endl; delete obj;}
但是,還不足夠,如果我們的介面函數既要修改對象的內容(不能使用const),但又不需要對對象的生命週期負責,那怎麼辦?如果用非常量指標,使用者如果按照之前所說的原則判斷,認為介面會delete對象,而實際上介面函數沒有delete對象,那會照成很頭痛的後果——記憶體泄露!
實際上,我們還應該盡量遵循另一個原則,對象由誰產生(new),就應該由誰銷毀(delete)。除非你能很清晰的告訴使用介面的人,你會delete這個對象。
所幸,標準庫為我們提供了一個很好的解決方案:auto_ptr。
將一個指標賦給智能指標auto_ptr,那麼智能指標將會具有指標對象的擁有權,它會在生命結束之前,自動delete指標所指的對象。
很好,到現在,我們可以解決問題了,我們定義規則如下:
如果我們要擁有對象的生命週期,那麼我們的介面應該使用auto_ptr.
void testGetObj(auto_ptr<Obj> obj);
如果我們不需要擁有對象的生命週期,也不需要修改指標所指的對象,那麼我們用常量指標。
void testGetObj(const Obj* obj);
如果我們不需要擁有對象的生命週期,但是需要修改指標所指的對象,那麼我們用非常量指標。
void testGetObj(Obj* obj);
對於介面函數的傳回值,我們同樣也能用以上原則。
對於Obj的getName函數,或許會很頻繁的被調用,為了避免無謂的對象複製,我們應該用常量引用(因為不希望使用者隨便修改名字)
const string& getName() const;
由使用者來決定,是否對該對象進行複製。如果使用者需要對該對象進行複製,那麼他可以這樣寫
const string myObjName = obj.getName();
當然,如果他要修改,則必須進行常量轉換const_cast。
更多的使用,使用者不需要對該對象進行複製,那麼他可以這樣寫:
const string& myObjName = obj.getName();
作為介面的設計者,你必須保留使用者的這種權利,而不應該強制使用者對你的返回對象進行複製。
不過,有種情況,很容易讓我們犯錯誤,看下面這個例子:
const string& getName() const{ string name = “Obj”; return name;}int main(){ cout << getName << endl; return 0;}
想想這段程式會列印什麼出來?天知道!問題出在哪裡?因為string name是一個臨時變數,它的生命週期僅限於這個函數體內,而函數的傳回值是對臨時變數的引用,一超出函數的範圍,這個對象是無效的,所以那個引用的傳回值已經是個無效的對象。
well,到這裡已經講完我要講的東西,最後來小結一下:
我們設計介面,必須要遵循一定的規則,讓使用介面的人能清楚知道該傳什麼類型的對象進去,而返回的對象又有可能是什麼.
根據指標,引用,和智能指標的特點,我們定義行為規範如下:
- 以下規則,通常情況下對基礎類型無效(因為通常情況下,基礎類型不大需要考慮值拷貝的消耗)
- 如果沒有特殊需要,我們盡量避免介面的參數,傳回值是物件類型,以省去對象的複製所帶來無謂的消耗.
- 當介面的參數一定是存在值的時候,我們應該用引用,如果我們的函數不需要修改傳進來的參數, 則應該用常量引用.
- 如果介面的參數不一定存在值,有時可以是Null 物件的時候,而且對傳進來的指標參數,函數不會控制它的生命週期,我們應該用指標,同樣,如果對該指標對象我們不會修改,則該介面的參數,應該用常量指標.
- 如果當函數需要控制參數的指標對象的生命週期的時候,我們應該用auto_ptr。
- 通常,我們類的一些get函數,我們更應該用引用。由調用該函數的人來決定是否複製該對象。當然,如果返回的是一個臨時對象(比如該對象是函數體內的局部變數),則不能用引用。