C++構造函數、拷貝構造函數、賦值運算符漫談(二)——函數返回值
首先我們先看一下C程序的返回值處理情況,我們知道當C函數返回int等小型數據時直接將返回值放入eax寄存器。那當返回大的數據結構又是如何處理呢?看如下一段代碼:
#include <stdio.h> typedef struct big_thing { char buf[128]; }big_thing; big_thing return_test() { big_thing b; b.buf[0]=0; retutn b; } int main(int argc, char *argv[]) { big_thing n=return_test(); return 0; }
根據這段代碼的匯編代碼可以知道這段代碼是這樣執行的:
1. 首先main函數在棧上開辟了一片空間,並將這塊空間的一部分作為傳遞返回值的臨時對象temp;
2. 將temp對象的返回地址作為隱含參數傳遞給return_test();
3. return_tes()函數將數據拷貝給temp對象(對應下圖複製1),並將temp對象的地址用eax寄存器傳出;
4. return_test()函數返回後,main函數將eax指向的temp對象拷貝給對象n(對應下圖複製2)。
(上圖為函數調用的棧情況,綠色部分代表main函數的活動記錄,黃色部分為return_test的活動記錄。)
看過C程序的返回值得處理過程,我們不妨先推測一下C++的返回值處理過程:總體與C的返回過程類似,隻不過在上圖複製1中並不是直接執行“位逐次拷貝”,而是調用拷貝構造函數(因為此時產生了新對象——temp),在複製2的時候需要調用賦值運算符(因為此時沒有新的對象產生)。下麵我們進行驗證。
class X { public: X() {cout<<"X()"<<endl;}; X(const X& x) { cout<<"X(const X& x)"<<endl; } X& operator=(const X&) { cout<<"="<<endl; return *this; } ~X() {cout<<"destructor"<<endl;} }; X return_test() { X x; cout<<"before return"<<endl; return x; } int _tmain(int argc, _TCHAR* argv[]) { X xx; xx=return_test(); }
運行結果如下:
我們看到結果和我們預想的一樣,在return前調用了拷貝構造函數(用來生成臨時對象),函數返回後,函數內部的臨時變量x先銷毀,然後調用賦值運算符(對應圖中複製2),將臨時對象拷貝給xx;之後銷毀臨時對象temp,main退出時銷毀xx。
2.1 RVO
那麼如果我們將main中的兩句簡化為一句:
xx=return_test();
結果又如何呢?運行之後結果如下:
我們發現不再有賦值運算符被調用,也就是“複製2”不在發生,但是拷貝構造函數依然調用,說明“複製1”還是發生了。這是什麼原因呢?
首先,我們必須清楚C++創建對象的方式,不要以為創建對象都要調用無參構造函數,當有“=”時是需要調用拷貝構造函數的(這句話也不準確,祥見係列三)。那麼具體是如何發生的呢?這裏我們介紹一下C++編譯器采用的一個優化Return Value Optimization(RVO)。
RVO:
RVO其實就是編譯器直接將返回對象通過拷貝構造函數構建在目的對象位置上,而不是臨時對象(這種情況不再有臨時對象);
具體到編譯器是這樣執行的:
1. 首先為return_test加上一個額外參數(隱含參數);類型是Class Object的一個引用(同C程序中的臨時變量的目的地址,不過此處不再是臨時變量,而是目的對象的地址)。這個參數用來放置被“拷貝構建”而返回的值。
2. return前安插了一個拷貝構造函數的調用操作,一邊將返回的對象的內容(return_test中的局部變量x)當做上述新增參數的初值。
也就是說return_test將會被改變為:
void return_test(X& result)
{
X x;
result.X::X(x); //編譯器產生的操作
return;
}
而對 xx=return_test();的調用也會被修改為
X xx;//(注:這裏不再調用構造函數,隻分配空間)
return_test(xx);
2.2 擴展
如果return_test().memfun()又該怎麼被修改調用呢(其中memfun是X的成員函數)?
答案是:X temp;(注:此處仍不會調用構造函數,隻是開辟空間,內容由隨後的拷貝構造函數構造)
(return_test(temp),temp).memfun();(注意逗號表達式)
總結:無論采用什麼方式,C++返回對象時總是伴隨至少一次複製,所以應盡量避免返回對象。
最後更新:2017-04-03 12:55:57