C++中的臨時變量
它們是被神所遺棄的孩子,沒有人見過它們,更沒有人知道它們的名字.它們命中注定徘徊于命運邊緣高聳的懸崖和幽深的深淵之間,
用自己短暫的生命撫平了生與死之間的縫隙.譬如朝露,卻與陽光無緣.是該為它們立一座豐碑的時候了,墓銘志上寫著:我來了,我走了,我快樂過.
許多人對臨時變量的理解僅僅限于:
string temp;
其實,從C++的觀點來看,這根本就不是臨時變量,而是局部變量.
C++的臨時變量是編譯器在需要的時候自動生成的臨時性變量,它們并不在代碼中出現.但是它們在編譯器生成的二進制編碼中是存在的,
也創建和銷毀.在C++語言中,臨時變量的問題格外的重要,因為每個用戶自定義類型的臨時變量都要出發用戶自定義的構造函數和析構函數(如果用戶提供了)
又是該死的編譯器!又該有人抱怨編譯器總在自己背后干著偷偷摸摸的事情了.但是如果離開了編譯器的這些工作,我們可能寸步難行.
如果X是一個用戶自定義的類型,有默認構造函數,拷貝構造函數,賦值運算函數,析構函數(這也是類的4個基本函數),那么請考慮以下代碼:
X get(X arg)
{
return arg;
}
X a;
X b = get(a);
即使是這么簡單的代碼也是很難實現的
讓我們分析一下代碼執行過程中發生了什么?
首先我要告訴你一個秘密:對于一個函數來說,無論是傳入一個對象還是傳出一個對象其實都是不可能的.
讓一個函數傳入或傳出一個內置的數據類型,例如int,是很容易的,但是對于用戶自定義類型得對象卻非常的困難,因為編譯器總得找地方為這些對象
寫上構造函數和析構函數,不是在函數內,就是在函數外,除非你用指針或引用跳過這些困難
那么怎么辦?在這里,編譯器必須玩一些必要的小花招,嗯,其中的關鍵恰恰就是臨時變量
對于以對象為形參的函數:
void foo(X x0)
{
}
X xx;
foo(xx);
編譯器一般按照以下兩種轉換方式中的一種進行轉換
1.在函數外提供臨時變量
void foo(X& x0) //修改foo的聲明為引用
{
}
X xx; //聲明xx
X::X(xx); //調用xx的默認構造函數
X __temp0; //聲明臨時變量__temp0
X::X(__temp0, xx); //調用__temp0的拷貝構造函數
foo(__temp0); //調用foo
X::~X(__temp0); //調用__temp0的析構函數
X::~X(xx); //調用xx的析構函數
2.在函數內提供臨時變量
void foo(X& x0) //修改foo的聲明為引用
{
X __temp0; //聲明臨時變量__temp0
X::X(__temp0, x0); //調用__temp0的拷貝構造函數
X::~X(__temp0); //調用__temp0的析構函數
}
X xx; //聲明xx
X::X(xx); //調用xx的默認構造函數
foo(xx); //調用foo
X::~X(xx); //調用xx的析構函數
無論是在函數的內部聲明臨時變量還是在函數的外部聲明臨時變量,其實都是差不多的,這里的含義是說既然參數要以傳值的
語意傳入函數,也就是實參xx其實并不能修改,那么我們就用一個一摸一樣臨時變量來移花接木,完成這個傳值的語意
但是這樣做也不是沒有代價,編譯器要修改函數的聲明,把對象改為對象的引用,同時修改所有函數調用的地方,代價確實巨大啊,
但是這只是編譯器不高興而已,程序員和程序執行效率卻沒有影響
對于以對象為返回值的函數:
X foo()
{
X xx;
return xx;
}
X yy = foo();
編譯器一般按照以下方式進行轉換
void foo(X& __temp0) //修改foo的聲明為引用
{
X xx; //聲明xx
X::X(xx); //調用xx的默認構造函數
__temp0::X::X(xx); //調用__temp0的拷貝構造函數
X::~X(xx); //調用xx的析構函數
}
X yy; //聲明yy
X __temp0; //聲明臨時變量__temp0
foo(__temp0); //調用foo
X::X(yy, __temp0); //調用yy的拷貝構造函數
X::~X(__temp0); //調用__temp0的析構函數
X::~X(yy); //調用yy的析構函數
既然我們已經聲明了yy,為什么還要緊接著聲明__temp0,其實這里完全可以把yy和臨時變量合一
優化后,上面的代碼看起來象這個樣子:
void foo(X& __temp0) //修改foo的聲明為引用
{
X xx; //聲明xx
X::X(xx); //調用xx的默認構造函數
__temp0::X::X(xx); //調用__temp0的拷貝構造函數
X::~X(xx); //調用xx的析構函數
}
X yy; //聲明yy
foo(yy); //調用foo
X::~X(yy); //調用yy的析構函數
嗯,怎么說呢,這算是一種優化算法吧,其實這各個技巧已經非常普遍了,并擁有一個專門的名稱Named Return Value(NRV)優化
NRV優化如今被視為標準C++編譯器的一個義不容辭的優化操作(雖然其需求其實超出了正式標準之外)
除了以類為參數以外,如果參數的類型是const T&類型,這也可能導致臨時變量
void fun(const string& str)
const char* name = "wgs";
fun(name);
嗯,還記得在const文檔中的論述嗎?對于這種特殊的參數類型,編譯器是很樂意為你做自動轉換的工作的,代價嘛,就是一個臨時變量,
不過如果是你自己去做,大概就只能聲明一個局部變量了
為什么函數和臨時變量這么有緣,其實根本的原因在于對象傳值的語意,這一個也是為什么C++中鼓勵傳對象地址的原因
和函數的情況類似的,還有一大類情況是臨時變量的樂土,那就是表達式
string s,t;
printf("%s", s + t);
這里s+t的結果該放在什么地方呢?只能是臨時變量中.
這個printf語句帶來了新的問題,那就是"臨時變量的生命期"是如何的?
對于函數的情況,我們已經看到了,臨時變量在完成交換內容的使命后都是盡量早的被析構了,那么對于表達式呢?
如果在s+t計算后析構,那么print函數打印的就是一個非法內容了,因此C++給出的規則是:
臨時變量應該在導致臨時變量創建的"完整表達式"求值過程的最后一個步驟被析構
什么又是"完整表達式"?簡單的說,就是不是表達式的子表達式
這條規則聽起來很簡單,但具體實現起來就非常的麻煩了,例如:
X foo(int n)
if (foo(1) || foo(2) || foo(3) )
其中X中有operator int()轉換,所以可以用在if語句中
這里的foo(1)將產生一個臨時變量1,如果這部分為false,foo(2)將繼續產生一個臨時變量,如果這部分也為false,foo(3)...
一個臨時變量的參數居然是和運行時相關的,更要命的是你要記住你到底產生了幾個臨時變量并在這個表達式結束的時候進行析構以小心的維護對象構造和析構的一致
我猜想,這里會展開成一段復雜的代碼,并加入更多的if判斷才能搞定,呵呵,好在我不是做編譯器的
上面的規則其實還有兩條例外:
string s,t;
string v = 1 ? s + t : s - t;
這里完整表達式是?語句,但是在完整表達式結束以后臨時變量還不能立即銷毀,而必須在變量v賦值完成后才能銷毀,這就是例外規則1:
凡含有表達式執行結果的臨時變量,應該存留到對象的初始化操作完成后銷毀
string s,t;
string& v = s + t;
這里s+t產生的臨時變量即使在變量v的賦值完成后也不能銷毀,否則這個引用就沒用了,這就是例外規則2:
如果一個臨時變量被綁定到一個引用,這個臨時變量應該留到這個臨時變量和這個引用那個先超出變量的作用域后才銷毀
這篇文章可能有些深奧了,畢竟大多數內容來自于<<Inside The C++ Object Model>>
那么就留下一條忠告:
在stl中,以下的代碼是錯誤的
string getName();
char* pTemp = getName().c_str();
getName返回的就是一個臨時變量,在把它內部的char指針賦值給pTemp后析構了,這時pTemp就是一個非法地址
確實如C++發明者Bjarne Stroustrup所說,這種情況一般發生在不同類型的相互轉換上
在Qt中,類似的代碼是這樣的
QString getName();
char* pTemp = getName().toAscii().data();
這時pTemp是非法地址
希望大家不要犯類似的錯誤