小問題大思考之C++臨時對象

C++中有這樣一種對象:它在代碼中看不到,但是確實存在。它就是臨時對象---由編譯器定義的一個沒有命名的非堆對象(non-heap object)。爲什麼研究臨時對象?主要是爲了提高程序的性能以及效率,因爲臨時對象的構造與析構對系統性能而言絕不是微小的影響,所以我們應該去了解它們,知道它們如何造成,從而儘可能去避免它們。

臨時對象通常產生於以下4種情況:

  1. 類型裝換
  2. 按值傳遞
  3. 按值返回
  4. 對象定義

下面我們逐一看看:

1、類型轉換:它通常是爲了讓函數調用成功而產生臨時對象。發生於 “傳遞某對象給一個函數,而其類型與它即將綁定上去的參數類型不同” 的時候。

例如:

[cpp] view plain copy
  1. void test(const string& str);  
  2.   
  3. char buffer[] = "buffer";  
  4.   
  5. test(buffer); // 此時發生類型轉換  

此時,編譯器會幫你進行類型轉換:它產生一個類型爲string的臨時對象,該對象以buffer爲參數調用string constructor。當test函數返回時,此臨時對象會被自動銷燬。

注意:對於引用(reference)參數而言,只有當對象被傳遞給一個reference-to-const參數時,轉換才發生。如果對象傳遞給一個reference-to-non-const對象,不會發生轉換。

例如:

[cpp] view plain copy
  1. void upper(string& str);  
  2.   
  3. char lower[] = "lower";  
  4.   
  5. upper(lower); // 此時不能轉換,編譯出錯  

此時如果編譯器對reference-to-non-const對象進行了類型轉換,那麼將會允許臨時對象的值被修改。而這和程序員的期望是不一致的。試想,在上面的代碼中,如果編譯器允許upper運行,將lower中的值轉換爲大寫,但是這是對臨時對象而言的,char lower[]的值還是“lower”,這和你的期望一致嗎?

有時候,這種隱式類型轉換不是我們期望的,那麼我們可以通過聲明constructor爲explicit來實現。explicit告訴編譯器,我們反對將constructor用於類型轉換。

例如:

[cpp] view plain copy
  1. explicit string(const char*);  

2、按值傳遞:這通常也是爲了讓函數調用成功而產生臨時對象。當按值傳遞對象時,實參對形參的初始化與T formalArg = actualArg的形式等價。

例如:

[cpp] view plain copy
  1. void test(T formalArg);  
  2.   
  3. T actualArg;  
  4. test(actualArg);  

此時編譯器產生的僞碼爲:

[cpp] view plain copy
  1. T _temp;  
  2.   
  3. _temp.T::T(acutalArg); // 通過拷貝構造函數生成_temp  
  4. g(_temp);  // 按引用傳遞_temp  
  5. _temp.T::~T(); // 析構_temp  

因爲存在局部參數formalArg,test()的調用棧中將存在formalArg的佔位符。編譯器必須複製對象actualArg的內容到formalArg的佔位符中。所以,此時編譯器生成了臨時對象。

3、按值返回:如果函數是按值返回的,那麼編譯器很可能爲之產生臨時對象。

例如:

[cpp] view plain copy
  1. class Integer {  
  2. public:  
  3.   friend Integer operator+(const Integer& a, const Integer& b);  
  4.     
  5.   Integer(int val=0): value(val) {  
  6.   }  
  7.     
  8.   Integer(const Integer& rhs): value(rhs.value) {  
  9.   }  
  10.     
  11.   Integer& operator=(const Integer& rhs);  
  12.     
  13.   ~Integer() {  
  14.   }  
  15.     
  16. private:  
  17.   int value;    
  18. };  
  19.   
  20. Integer operator+(const Integer& a, const Integer& b) {  
  21.   Integer retVal;  
  22.     
  23.   retVal.value = a.value + b.value;  
  24.     
  25.   return retVal;  
  26. }  
  27.   
  28. Integer c1, c2, c3;  
  29. c3 = c1 + c2;  

編譯器生成的僞代碼:

[cpp] view plain copy
  1. struct Integer _tempResult; // 表示佔位符,不調用構造函數  
  2. operator+(_tempResult, c1, c2); // 所有參數按引用傳遞  
  3. c3 = _tempResult; // operator=函數執行  
  4.   
  5. Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {  
  6.   struct Integer retVal;  
  7.   retVal.Integer::Integer(); // Integer(int val=0)執行  
  8.     
  9.   retVal.value = a.value + b.value;  
  10.     
  11.   _tempResult.Integer::Integer(retVal); // 拷貝構造函數Integer(const Integer& rhs)執行,生成臨時對象。  
  12.     
  13.   retVal.Integer::~Integer(); // 析構函數執行  
  14.     
  15.   return;  
  16. }  
  17.     
  18.   return retVal;  
  19. }  

如果對operator+進行返回值優化(RVO:Return Value Optimization),那麼臨時對象將不會產生。

例如:

[cpp] view plain copy
  1. Integer operator+(const Integer& a, const Integer& b) {    
  2.   return Integer(a.value + b.value);  
  3. }  

編譯器生成的僞代碼:

[cpp] view plain copy
  1. Integer operator+(const Integer& _tempResult, const Integer& a, const Integer& b) {  
  2.   _tempResult.Integer::Integer(); // Integer(int val=0)執行  
  3.   _tempResult.value = a.value + b.value;  
  4.     
  5.   return;  
  6. }  

對照上面的版本,我們可以看出臨時對象retVal消除了。

4、對象定義:

例如:

[cpp] view plain copy
  1. Integer i1(100); // 編譯器肯定不會生成臨時對象  
  2. Integer i2 = Integer(100); // 編譯器可能生成臨時對象  
  3. Integer i3 = 100; // 編譯器可能生成臨時對象  

然而,實際上大多數的編譯器都會通過優化省去臨時對象,所以這裏的初始化形式基本上在效率上都是相同的。


備註:

臨時對象的生命期:按照C++標準的說法,臨時對象的摧毀,是對完整表達式求值過程中的最後一個步驟。該完整表達式照成了臨時對象的產生。

完整表達式通常是指包含臨時對象表達式的最外圍的那個。例如:

((objA >1024)&&(objB <1024) ) ? (objA - objB) :(objB-objA)

這個表達式中一共含有5個表達式,最外圍的表達式是?。任何一個子表達式所產生的任何一個臨時對象,都應該在完整表達式被求值完成後,纔可以銷燬。

臨時對象的生命週期規則有2個例外:

1、在表達式被用來初始化一個object時。例如:

[cpp] view plain copy
  1. String progName("test");  
  2. String progVersion("ver-1.0");  
  3. String progNameVersion = progName + progVersion  

如果progName + progVersion產生的臨時對象在表達式求值結束後就析構,那麼progNameVersion就無法產生。所以,C++標準規定:含有表達式執行結果的臨時對象,應該保留到object的初始化操作完成爲止。

小心這種情況:

[cpp] view plain copy
  1. const char* progNameVersion = progName + progVersion  

這個初始化操作是一定會失敗的。編譯器產生的僞碼爲:

[cpp] view plain copy
  1. String _temp;  
  2. operator+(_temp, progName, progVersion);  
  3. progNameVersion = _temp.String::operator char*();  
  4. _temp.String::~String();  

2、當一個臨時對象被一個reference綁定時。例如:

[cpp] view plain copy
  1. const String& name = "C++";  

編譯器產生的僞碼爲:

[cpp] view plain copy
  1. String _temp;  
  2. temp.String::String("C++");  
  3. const String& name = _temp;  

針對這種情況,C++標準上是這樣說的:如果一個臨時對象被綁定於一個reference,對象將保留,直到被初始化的reference的生命結束,或直到臨時對象的生命範圍結束-----看哪種情況先到達而定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章