C++中有這樣一種對象:它在代碼中看不到,但是確實存在。它就是臨時對象---由編譯器定義的一個沒有命名的非堆對象(non-heap object)。爲什麼研究臨時對象?主要是爲了提高程序的性能以及效率,因爲臨時對象的構造與析構對系統性能而言絕不是微小的影響,所以我們應該去了解它們,知道它們如何造成,從而儘可能去避免它們。
臨時對象通常產生於以下4種情況:
- 類型裝換
- 按值傳遞
- 按值返回
- 對象定義
下面我們逐一看看:
1、類型轉換:它通常是爲了讓函數調用成功而產生臨時對象。發生於 “傳遞某對象給一個函數,而其類型與它即將綁定上去的參數類型不同” 的時候。
例如:
- void test(const string& str);
- char buffer[] = "buffer";
- test(buffer); // 此時發生類型轉換
此時,編譯器會幫你進行類型轉換:它產生一個類型爲string的臨時對象,該對象以buffer爲參數調用string constructor。當test函數返回時,此臨時對象會被自動銷燬。
注意:對於引用(reference)參數而言,只有當對象被傳遞給一個reference-to-const參數時,轉換才發生。如果對象傳遞給一個reference-to-non-const對象,不會發生轉換。
例如:
- void upper(string& str);
- char lower[] = "lower";
- upper(lower); // 此時不能轉換,編譯出錯
此時如果編譯器對reference-to-non-const對象進行了類型轉換,那麼將會允許臨時對象的值被修改。而這和程序員的期望是不一致的。試想,在上面的代碼中,如果編譯器允許upper運行,將lower中的值轉換爲大寫,但是這是對臨時對象而言的,char lower[]的值還是“lower”,這和你的期望一致嗎?
有時候,這種隱式類型轉換不是我們期望的,那麼我們可以通過聲明constructor爲explicit來實現。explicit告訴編譯器,我們反對將constructor用於類型轉換。
例如:
- explicit string(const char*);
2、按值傳遞:這通常也是爲了讓函數調用成功而產生臨時對象。當按值傳遞對象時,實參對形參的初始化與T formalArg = actualArg的形式等價。
例如:
- void test(T formalArg);
- T actualArg;
- test(actualArg);
此時編譯器產生的僞碼爲:
- T _temp;
- _temp.T::T(acutalArg); // 通過拷貝構造函數生成_temp
- g(_temp); // 按引用傳遞_temp
- _temp.T::~T(); // 析構_temp
因爲存在局部參數formalArg,test()的調用棧中將存在formalArg的佔位符。編譯器必須複製對象actualArg的內容到formalArg的佔位符中。所以,此時編譯器生成了臨時對象。
3、按值返回:如果函數是按值返回的,那麼編譯器很可能爲之產生臨時對象。
例如:
- class Integer {
- public:
- friend Integer operator+(const Integer& a, const Integer& b);
- Integer(int val=0): value(val) {
- }
- Integer(const Integer& rhs): value(rhs.value) {
- }
- Integer& operator=(const Integer& rhs);
- ~Integer() {
- }
- private:
- int value;
- };
- Integer operator+(const Integer& a, const Integer& b) {
- Integer retVal;
- retVal.value = a.value + b.value;
- return retVal;
- }
- Integer c1, c2, c3;
- c3 = c1 + c2;
編譯器生成的僞代碼:
- struct Integer _tempResult; // 表示佔位符,不調用構造函數
- operator+(_tempResult, c1, c2); // 所有參數按引用傳遞
- c3 = _tempResult; // operator=函數執行
- Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {
- struct Integer retVal;
- retVal.Integer::Integer(); // Integer(int val=0)執行
- retVal.value = a.value + b.value;
- _tempResult.Integer::Integer(retVal); // 拷貝構造函數Integer(const Integer& rhs)執行,生成臨時對象。
- retVal.Integer::~Integer(); // 析構函數執行
- return;
- }
- return retVal;
- }
如果對operator+進行返回值優化(RVO:Return Value Optimization),那麼臨時對象將不會產生。
例如:
- Integer operator+(const Integer& a, const Integer& b) {
- return Integer(a.value + b.value);
- }
編譯器生成的僞代碼:
- Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {
- _tempResult.Integer::Integer(); // Integer(int val=0)執行
- _tempResult.value = a.value + b.value;
- return;
- }
對照上面的版本,我們可以看出臨時對象retVal消除了。
4、對象定義:
例如:
- Integer i1(100); // 編譯器肯定不會生成臨時對象
- Integer i2 = Integer(100); // 編譯器可能生成臨時對象
- Integer i3 = 100; // 編譯器可能生成臨時對象
然而,實際上大多數的編譯器都會通過優化省去臨時對象,所以這裏的初始化形式基本上在效率上都是相同的。
備註:
臨時對象的生命期:按照C++標準的說法,臨時對象的摧毀,是對完整表達式求值過程中的最後一個步驟。該完整表達式照成了臨時對象的產生。
完整表達式通常是指包含臨時對象表達式的最外圍的那個。例如:
((objA >1024)&&(objB <1024) ) ? (objA - objB) :(objB-objA)
這個表達式中一共含有5個表達式,最外圍的表達式是?。任何一個子表達式所產生的任何一個臨時對象,都應該在完整表達式被求值完成後,纔可以銷燬。
臨時對象的生命週期規則有2個例外:
1、在表達式被用來初始化一個object時。例如:
- String progName("test");
- String progVersion("ver-1.0");
- String progNameVersion = progName + progVersion
如果progName + progVersion產生的臨時對象在表達式求值結束後就析構,那麼progNameVersion就無法產生。所以,C++標準規定:含有表達式執行結果的臨時對象,應該保留到object的初始化操作完成爲止。
小心這種情況:
- const char* progNameVersion = progName + progVersion
這個初始化操作是一定會失敗的。編譯器產生的僞碼爲:
- String _temp;
- operator+(_temp, progName, progVersion);
- progNameVersion = _temp.String::operator char*();
- _temp.String::~String();
2、當一個臨時對象被一個reference綁定時。例如:
- const String& name = "C++";
編譯器產生的僞碼爲:
- String _temp;
- temp.String::String("C++");
- const String& name = _temp;
針對這種情況,C++標準上是這樣說的:如果一個臨時對象被綁定於一個reference,對象將保留,直到被初始化的reference的生命結束,或直到臨時對象的生命範圍結束-----看哪種情況先到達而定。