引言
朋友問了我一段代碼:
const string & foo(const string & a, const string & b)
{return a.empty() ? b : a;
}
int main ()
{auto & s = foo("", "foo"); // auto is const stringcout << s << '\n';return 0;
}
可以思考一下上面的代碼能否通過編譯?如果可以會輸出什么?
UB
對于上面的代碼,是可以通過編譯的。使用GCC
的話輸出foo
,那么代碼似乎沒有問題。
其實不然,上述的代碼發生了UB(UB是Undefined Behaviour的縮寫,意思是未定義行為既具體會發生什么由編譯器的實現決定沒有官方的要求)。
我們將上面的代碼稍作更改:
const string & foo(const string & a, const string & b)
{return a.empty() ? b : a;
}
int main ()
{auto & s = foo("", "foo"); // auto is const stringint a[100] = {0};cout << s << '\n';return 0;
}
在使用GCC
的情況下,上面的代碼輸出了空串。
為什么?臨時對象的生命周期
要知道為什么會出現上面的問題,我們需要先了解臨時對象的生命周期。
- 臨時對象:臨時對象往往是指右值(純右值和將亡值)。
- 生命周期:一個對象的生命周期可以理解為從調用構造函數開始到調用析構函數結束的整個過程。
當一個對象的生命周期結束后(調用析構函數后)其不能再被繼續使用否則會發生未定義行為。
例如:通過new
申請的對象,在delete
之后繼續進行解引用,此時會發生未定義行為(訪問野指針)。
對于上面提到的代碼實際上,在輸出的時候,""
與"foo"
的生命周期已然結束,所以出現了未定義行為,這是因為在string
中用于存放數據的內存已經被回收了(臨時對象調用過析構函數),但是s
中依然有指向數據的指針(或者引用),當沒有數據對對應地址進行寫操作的時候,依然能夠讀出之前的數據,但是增加int a[100] = {0}
之前的內存已經被覆蓋,因此此時輸出空串(但實際上由于是UB此時發生什么都是可以的,這里只是在根據結果解釋)。
一個問題:string
不是存放在堆上嗎,而申請的int a[100]
存放在棧上,為什么可以覆蓋堆上的內容?
在
C++
中string
的長度大于某個值時(這個值可能是16
),其數據才回被放置在堆上,否則還是存放在棧上。同時string
中存放著在棧上的數據,例如字符串長度變量,以及指針存放在棧上(使用new
可以使其存放在堆上),通過int a[100] = {0}
可以使長度清0
和指針變成空指針。
簡單說明了上面代碼的問題之后,知道了是因為臨時對象已經被析構了,導致其發生了未定義行為。那么,臨時對象的生命周期究竟如何呢?
- 對于沒有綁定對引用的臨時變量其創建完成后,即開始進行析構。例如
string("aaa");
從該語句的下一行開始,臨時變量已經被析構。需要注意的是:string a = string("aaa");
實際上會調用移動構造函數,所以該臨時變量已經綁定到引用上了,此時不屬于未綁定到引用上的臨時變量。 - 對于綁定到引用的臨時變量,其生命周期在引用脫離作用域時結束。例如:
{string &&a = string("xxx"); } // string("xxx") is finalized here.
- 特殊地,對于將同一個臨時變量綁定到兩個不同的引用上,其生命周期以第一個引用為準。例如:
{string &&a = string("xxx");{string &&b = std::move(a);}// using a here is OK } // string("xxx") is finalized here.
所以對于引言中的例子,在函數返回之后返回值賦值之前,臨時變量已經被析構了。
參考
Lifetime of a temporary cppreference