什麼情況下可能出現臨時對象?

 1、傳值參數(pass-by-value parameter)

代碼:
void func(String str) { cout << str; }

當進行這樣一個調用時:

代碼:
String s; func(s);

s將被拷貝構造一個備份(臨時對象),這個備份參與函數體內的運算,原件不會被改動。

2、返回值(return value)

代碼:
String func() { String s("Hello, world"); return s; }

當s被返回時,會被拷貝構造一個備份(臨時對象),這個變量被返回,函數體內的局部變量被銷燬。

3、隱式類型轉換(Implicitly Typecast)

代碼:
class String { public: String(); String(const char *str); operator=(const String &rhs); };

當實行一個賦值動作:

代碼:
String s; s = "Hello, world";

時,因爲String:perator=並沒有針對char *和const char *的重載,而最可能的做法就是將const char *隱式轉換爲一個String對象。在這個過程中就會產生一個String類的臨時對象。

4、++ 和 --

代碼:
std::list<int>::iterator iter; iter++;

因爲iter++的返回值需要返回原值,所以這個值在增1之前必須保留,這個保留就必須使用一個臨時對象。這相當於是返回值的一個典型情況。

5、值存儲的STL容器
主要針對std::vector,因爲vector具有自動擴充容量的功能,在每次擴充容量的時候,大部分實現的算法是new一個新的內存塊,將原有的每個元素拷貝複製到新的內存,然後刪除原有的內存塊。這期間要使用大量的臨時對象。

二、臨時對象的弊端
臨時對象實際上就是通過拷貝構造函數產生的,沒有變量名的const對象,所以每產生這樣一個對象,都會調用一次拷貝構造函數。如果這個類很“大”,那麼會佔用相當的資源和時間,從而降低效率。特別是在第5種情形中,會產生大量的臨時對象,情況尤其嚴重。

三、臨時對象的應對之道
解決臨時對象的主要方法就是使用指針和引用,減少構造函數調用的機會。

1、避免使用傳值參數
實際上絕大部分的傳值參數都可以使用常量引用(const &)來代替,而常量引用的參數不會產生臨時對象。

代碼:
void func(const String& str) { cout << str; }

這樣的寫法可以有效的避免在參數傳遞過程中產生臨時對象。

但是,引用通常是由指針實現的,對於基本類型來說,指針操作比直接操作要慢一些,並且增加了操作的複雜性。而基本類型的操作都不復雜,而且很快,所以,對於基本類型來說,沒有必要使用常量引用手法。

2、隱式類型轉換
隱式類型轉換的問題是定義了針對某個類型的構造函數,但是沒有定義針對該類型的operator=所導致的,所以,構造函數和operator=必須成對出現。

3、++ 和 --
這個問題已經討論過很多了,

代碼:
operator++() 的實現方法是 { return ++obj.value; } operator++(int) 的實現方法是 { Class tmp = obj; ++obj.value; return tmp; }

因此,在可能的情況下,使用++i代替i++,就可以避免出現臨時對象。

4、在STL容器(尤其是vector)中使用對象指針而不是對象實體作爲元素
vector因爲有重新分配內存的動作,在這個動作中會產生大量的臨時對象,極大的降低效率。解決方法就是使用對象指針作爲vector的元素,避免採用對象實體。另外,用reserve事先分配內存也是避免內存交換的方法。

代碼:
std::vector<String *> str_vector;

但是要注意,STL使用的是值(value)語意,所以對容器中指針的管理要自己負責,比如容器析構和copy時,可能要自己多做一些工作,以保證數據的正確性。

更好的方法當然是對指針語意的vector做一個封裝。

5、函數的返回值
函數的返回值不能以引用或指針的方式返回函數內部的局部變量,如果需要返回局部變量的值,必須以Object形式返回。在這種情況下,臨時對象是不可避免的。這個問題上,《Effective C++》Item 23 給出了更詳細的討論。
對於無法避免的臨時對象,我們至少可以將“危害”降到最低點。

利用編譯器優化
a) 上面提到,函數必須以值的形式返回局部變量。由於這種形式比較普遍並且無法避免,現代的編譯器大部分都可以實行一個叫做 Named Return Value(NRV)的優化。這個優化可以避免不必要的臨時對象的產生。在編譯器實施NRV優化時,如下的代碼:

代碼:
X bar() { X xx; //... operate xx return xx; }

將被優化爲類似這樣:

代碼:
void bar( X& __result) { __result.X::X(); //... operate __result return; }

很明顯的,這個優化減少了一個臨時對象的產生。

NRV優化由編譯器自動實施,並沒有什麼編譯選項,也無法人工干預。所以,不能依賴於該優化,不過該優化在某些情況下的確可以提高效率。

對NRV的討論,可以參考《深度探索C++對象模型》一書的“在編譯器層面做優化”一章。

b) 目前幾乎所有的編譯器都可以保證

代碼:
Class c = a + b;

這樣的形式將不會產生臨時對象。

6、其它
儘可能少的調用
這個問題最典型的現象就是循環的終止條件。

代碼:
for(std::vector<String *>::iterator iter = str_vector.begin(); iter != str_vector.end(); ++iter) { //... }

在這裏,每循環一次,iter != str_vector.end() 這個判斷就要進行一次,因爲str_vector.end()是一個函數調用,因爲它的返回值是一個iterator的Object,所以每次調用都會產生一個臨時對象。如果寫成這樣,臨時對象就會只被生成一次:

代碼:
std::vector<String *>::iterator end_iter = str_vector.end(); for(std::vector<String *>::iterator iter = str_vector.begin(); iter != end_iter; ++iter) { //... }

四、其它問題
臨時對象的生命週期
臨時對象是不可見的,但是,在特定的情況下,它的生存週期問題可能導致問題。對這個問題,標準規格上的定義是:

臨時對象的被摧毀,應該是對完整表達式求值過程中的最後一個步驟。該完整表達式造成臨時對象的產生

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