默認(rèn)情況下,C++為函數(shù)傳入和傳出對(duì)象是采用傳值方式的(這是由C語(yǔ)言繼承而來(lái)的特征)。除非你明確使用其他方法,函數(shù)的形式參數(shù)總會(huì)通過(guò)復(fù)制實(shí)在參數(shù)的副本來(lái)創(chuàng)建,并且,函數(shù)的調(diào)用者得到的也是函數(shù)返回值的一個(gè)副本。這些副本是由對(duì)象的拷貝構(gòu)造函數(shù)創(chuàng)建的。這使得“傳值”成為一項(xiàng)代價(jià)十分昂貴的操作。請(qǐng)觀察下邊的示例中類(lèi)的層次結(jié)構(gòu):
class Person {
public:
Person(); // 省略參數(shù)表以簡(jiǎn)化代碼
virtual ~Person(); // 條目7解釋了它為什么是虛函數(shù)
...
private:
std::string name;
std::string address;
};
class Student: public Person {
public:
Student(); // 再次省略參數(shù)表
virtual ~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
請(qǐng)觀察下面的代碼,這里我們調(diào)用一個(gè)名為validateStudent的函數(shù),通過(guò)為這一函數(shù)傳進(jìn)一個(gè)Student類(lèi)型的參數(shù)(傳值方式),它將返回這一學(xué)生的身份是否合法:
bool validateStudent(Student s); // 通過(guò)傳值方式接受一個(gè)Student對(duì)象
Student plato; // 柏拉圖是蘇格拉底的學(xué)生
bool platoIsOK = validateStudent(plato); // 調(diào)用這一函數(shù)
在這個(gè)函數(shù)被調(diào)用時(shí)將會(huì)發(fā)生些什么呢?
很顯然地,在這一時(shí)刻,通過(guò)調(diào)用Student的拷貝構(gòu)造函數(shù),可以將這一函數(shù)的s參數(shù)初始化為plato的值。同樣顯然的是,s在validateStudent返回的時(shí)候?qū)⒈讳N(xiāo)毀。所以這一函數(shù)中傳參的開(kāi)銷(xiāo)就是調(diào)用一次Student的拷貝構(gòu)造函數(shù)和一次Student的析構(gòu)函數(shù)。
但是上邊的分析僅僅是冰山一角。一個(gè)Student對(duì)象包含兩個(gè)string對(duì)象,所以每當(dāng)你構(gòu)造一個(gè)Student對(duì)象時(shí),你都必須構(gòu)造兩個(gè)string對(duì)象。同時(shí),由于Student類(lèi)是從Person類(lèi)繼承而來(lái),所以在每次構(gòu)造Student對(duì)象時(shí),你都必須再構(gòu)造一個(gè)Person對(duì)象。一個(gè)Person對(duì)象又包含兩個(gè)額外的string對(duì)象,所以每次對(duì)Person的構(gòu)造還要進(jìn)行額外的兩次string的構(gòu)造。最后的結(jié)果是,通過(guò)傳值方式傳遞一個(gè)Student對(duì)象會(huì)引入以下幾個(gè)操作:調(diào)用一次Student的拷貝構(gòu)造函數(shù),調(diào)用一次Person的拷貝構(gòu)造函數(shù),調(diào)用四次string的拷貝構(gòu)造函數(shù)。在Student的這一副本被銷(xiāo)毀時(shí),相應(yīng)的每次構(gòu)造函數(shù)調(diào)用都對(duì)應(yīng)著一次析構(gòu)函數(shù)的調(diào)用。因此我們看到:通過(guò)傳值方式傳遞一個(gè)Student對(duì)象總體的開(kāi)銷(xiāo)究竟有多大?竟達(dá)到了六次構(gòu)造函數(shù)和六次析構(gòu)函數(shù)的調(diào)用!
下面向你介紹正確的方法,這一方法才會(huì)使函數(shù)擁有期望的行為。畢竟你期望的是所有對(duì)象以可靠的方式進(jìn)行初始化和銷(xiāo)毀。與此同時(shí),如果可以繞過(guò)所有這些構(gòu)造和析構(gòu)操作將是件很愜意的事情。這個(gè)方法就是:通過(guò)引用常量傳遞參數(shù):
bool validateStudent(const Student& s);
這樣做效率會(huì)提高很多:由于不會(huì)創(chuàng)建新的對(duì)象,所以就不會(huì)存在構(gòu)造函數(shù)或析構(gòu)函數(shù)的調(diào)用。改進(jìn)的參數(shù)表中的const是十分重要的。由于早先版本的validateStudent通過(guò)傳值方式接收Student參數(shù),所以調(diào)用者了解:無(wú)論函數(shù)對(duì)于傳入的Student對(duì)象進(jìn)行什么樣的操作,都不會(huì)對(duì)原對(duì)象造成任何影響,validateStudent僅僅會(huì)對(duì)對(duì)象的副本進(jìn)行修改。而改進(jìn)版本中Student對(duì)象是以引用形式傳入的,有必要將其聲明為const的,因?yàn)槿绻贿@樣,調(diào)用者就需要關(guān)心傳入validateStudent的Student對(duì)象有可能會(huì)被修改。
通過(guò)引用傳參也可以避免“截?cái)鄦?wèn)題”。當(dāng)一個(gè)派生類(lèi)的對(duì)象以一個(gè)基類(lèi)對(duì)象的形式傳遞(傳值方式)時(shí),基類(lèi)的拷貝構(gòu)造函數(shù)就會(huì)被調(diào)用,此時(shí),這一對(duì)象的獨(dú)有特征——使它區(qū)別于基類(lèi)對(duì)象的特征會(huì)被“截掉”。剩下的只是一個(gè)簡(jiǎn)單的基類(lèi)對(duì)象,這并不奇怪,因?yàn)樗怯苫?lèi)構(gòu)造函數(shù)創(chuàng)建的。這肯定不是你想要的。請(qǐng)看下邊的示例,假設(shè)你正在使用一組類(lèi)來(lái)實(shí)現(xiàn)一個(gè)圖形窗口系統(tǒng):
class Window {
public:
...
std::string name() const; // 返回窗口的名字
virtual void display() const; // 繪制窗口和內(nèi)容
};
class WindowWithScrollBars: public Window {
public:
...
virtual void display() const;
};
所有的Window對(duì)象都有一個(gè)名字,可以通過(guò)name函數(shù)取得。所有的窗口都可以被顯示出來(lái),可以通過(guò)調(diào)用display實(shí)現(xiàn)。display是虛函數(shù),這一事實(shí)告訴我們,簡(jiǎn)單基類(lèi)Window的對(duì)象與派生出的WindowWithScrollBars對(duì)象的顯示方式是不一樣的。(參見(jiàn)條目34和36)
現(xiàn)在,假設(shè)你期望編寫(xiě)一個(gè)函數(shù)來(lái)打印出當(dāng)前窗口的名字然后顯示這一窗口。下面是錯(cuò)誤的實(shí)現(xiàn)方法:
void printNameAndDisplay(Window w) // 錯(cuò)誤! 參數(shù)傳遞的對(duì)象將被截?cái)啵?/span>
{
std::cout << w.name();
w.display();
}
考慮一下當(dāng)你將一個(gè)WindowWithScrollBars對(duì)象傳入這個(gè)函數(shù)時(shí)將會(huì)發(fā)生些什么:
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
參數(shù)w將被構(gòu)造為一個(gè)Window對(duì)象——還記得么?它是通過(guò)傳值方式傳入的。這里,使wwsb具體化的獨(dú)有信息將被截掉。無(wú)論傳入函數(shù)的對(duì)象的具體類(lèi)型是什么,在printNameAndDisplay的內(nèi)部,w將總保有一個(gè)Window類(lèi)的對(duì)象的身份(因?yàn)樗旧砭褪且粋€(gè)Window的對(duì)象)。特別地,在printNameAndDisplay內(nèi)部對(duì)display的調(diào)用總是Window::display,而永遠(yuǎn)不會(huì)是WindowWithScrollBars::display。
解決截?cái)鄦?wèn)題的方法是:通過(guò)引用常量傳參:
void printNameAndDisplay(const Window& w)
{ // 工作正常,參數(shù)將不會(huì)被截?cái)唷?/span>
std::cout << w.name();
w.display();
}
現(xiàn)在w的類(lèi)型就是傳入窗口對(duì)象的精確類(lèi)型。
揭開(kāi)C++編譯器的面紗,你將會(huì)發(fā)現(xiàn)引用通常情況下是以指針的形式實(shí)現(xiàn)的,所以通過(guò)引用傳遞通常意味著實(shí)際上是在傳遞一個(gè)指針。因此,如果傳遞一個(gè)內(nèi)建數(shù)據(jù)類(lèi)型的對(duì)象(比如int),傳值會(huì)被傳遞引用更為高效。那么,對(duì)于內(nèi)建數(shù)據(jù)類(lèi)型,當(dāng)你在傳值和傳遞常量引用之間徘徊時(shí),傳值方式不失為一個(gè)更好的選擇。迭代器和STL中的函數(shù)對(duì)象也是如此,這是因?yàn)樗鼈冊(cè)O(shè)計(jì)的初衷就是能夠更適于傳值,這是C++的慣例。迭代器和函數(shù)對(duì)象的設(shè)計(jì)人員有責(zé)任考慮復(fù)制時(shí)的效率問(wèn)題和截?cái)鄦?wèn)題。(這也是一個(gè)“使用哪種規(guī)則,取決于當(dāng)前使用哪一部份的C++”的例子,參見(jiàn)條目1)
內(nèi)建數(shù)據(jù)類(lèi)型體積較小,所以一些人得出這樣的結(jié)論:所有體積較小的類(lèi)型都適合使用傳值,即使它們是用戶(hù)自定義的。這是一個(gè)不可靠的推理。僅僅通過(guò)一個(gè)對(duì)象體積小并不能判定調(diào)用它的拷貝構(gòu)造函數(shù)的代價(jià)就很低。許多對(duì)象——包括大多數(shù)STL容器——其中僅僅包含一個(gè)指針和很少量的其它內(nèi)容,但是復(fù)制此類(lèi)對(duì)象的同時(shí),它所指向的所有內(nèi)容都需要復(fù)制。這將付出十分高昂的代價(jià)。
即使體積較小的對(duì)象的拷貝構(gòu)造函數(shù)不會(huì)帶來(lái)巨大的開(kāi)銷(xiāo),它也會(huì)引入性能問(wèn)題。一些編譯器對(duì)內(nèi)建數(shù)據(jù)類(lèi)型和用戶(hù)自定義數(shù)據(jù)類(lèi)型是分別對(duì)待的,即使它們的表示方式完全相同。比如說(shuō)一些編譯器很樂(lè)意將一個(gè)單純的double值放入寄存器中,這是語(yǔ)言的常規(guī);但將一個(gè)僅包含一個(gè)double值的對(duì)象放入寄存器時(shí),編譯器就會(huì)報(bào)錯(cuò)了。當(dāng)你遇到這種事情時(shí),你可以使用引用傳遞這類(lèi)對(duì)象,因?yàn)榫幾g器此時(shí)一定會(huì)將指針(引用的具體實(shí)現(xiàn))放入寄存器中。
對(duì)于“小型的用戶(hù)自定義數(shù)據(jù)類(lèi)型不適用于傳值方式”還有一個(gè)理由,那就是:作為用戶(hù)自定義類(lèi)型,它們的大小可能會(huì)改變。現(xiàn)在很小的類(lèi)型在未來(lái)的版本中可能會(huì)變得很大,這是因?yàn)樗膬?nèi)部實(shí)現(xiàn)方式可能會(huì)改變。即使是你更改了C++語(yǔ)言的具體實(shí)現(xiàn)都可能會(huì)影響到類(lèi)型的大小。比如,在我編寫(xiě)上面的示例的時(shí)候,一些對(duì)標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)中string的大小竟然達(dá)到了另一些的七倍。
總體上講,只有內(nèi)建數(shù)據(jù)類(lèi)型、STL迭代器和函數(shù)對(duì)象類(lèi)型適用于傳值方式。對(duì)于所有其它的類(lèi)型,都應(yīng)該遵循本條款中的建議:盡量使用引用常量傳參,而不是傳值。
時(shí)刻牢記
l 盡量使用引用常量傳參,而不是傳值方式。因?yàn)橐话闱闆r下傳引用更高效,而且可以避免“截?cái)鄦?wèn)題”。
l 對(duì)于內(nèi)建數(shù)據(jù)類(lèi)型、STL迭代和函數(shù)對(duì)象類(lèi)型,這一規(guī)則就不適用了,對(duì)它們來(lái)說(shuō)通常傳值方式更實(shí)用。