C 語言返回棧對象
中間變量
函數在返回的過程中,將值賦給了變量。我們知道函數的調用會發生壓棧的行爲,這就會涉及到跨棧的問題,其內部是怎麼實現的呢,我們先從簡單的問題入手。
#include <stdio.h>
int func()
{
int a = 3;
return a; //a返回給CPU
}
int main(void)
{
int i = 4;
i = func();//i變成3 把CPU上的中間值賦值給i
return 0;
}
棧上的對象是可以返回的,不可以返回棧上的對象引用。
引用只是擴展作用域。
eax 寄存器
不管是返回指針還是返回值,return 將 return 之後的值存到 eax 寄存器中,回到父函數再將返回的值賦給變量。
編譯過程的彙編代碼:
c++返回棧對象
對於普通的對象,我們己經研究了,那對於 C++中的對象,該如何來傳遞呢?
以下展示的試驗,在不同的平臺,不同編譯器,相同編譯器的不同版本,均會有所不同。但最終的結論是一樣的,即棧對象是可以返回的。
傳值與傳引用
傳值
示例代碼如下:
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
void func(MyClass a)
{
}
int main()
{
MyClass a;
func(a);
return 0;
}
上面代碼使用傳值把類對象作爲函數參數
在vs2019平臺運行結果爲:
在Qt平臺運行結果爲:
都會發生一次構造,一次拷貝構造,兩次析構。
這個過程不會有差別。
引用
如果把上面的傳值改爲引用:
class代碼相同
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
void func(MyClass & a)
{
}
int main()
{
MyClass a;
func(a);
return 0;
}
vs2019平臺運行結果爲:
Qt平臺運行結果爲;
傳引用的話,會發生一次構造,一次析構。所以傳引用效率更高。
RVO / NRVO
關於RVO / NRVO的說明我們先給出引例進行說明然後進行解釋會更加容易理解。
子函數返回無名對象
Class代碼相同
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass foo()
{
return MyClass(); //無名對象
}
int main()
{
foo();
return 0;
}
Vs2019運行結果爲:
進行一次構造和析構
Qt平臺運行結果爲:
兩個平臺無差異。
子函數返回有名對象
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass foo()
{
MyClass b; //有名對象
return b;
}
int main()
{
foo();
return 0;
}
在vs2019平臺下運行結果爲:
會發生一次構造,一次拷貝構造,兩次析構。
在Qt平臺運行結果爲:
會發生一次構造,一次析構。
我們通過測試可以看到,對於Q同平臺,不管是匿名對象還是有名對象,運行結果爲都是沒有差別的。
在vs平臺則不一樣
使用有名對象,會發生一次構造,一次拷貝構造,兩次析構。
我們把有名對象運行結果截圖拿過來進行分析:
上面把地址E7拷貝到1B然後E7析構,之後1B析構。
過程分析:
執行:MyClass b; //有名對象
構造E7
執行return b;
在main函數開闢一段匿名空間地址爲1B將E7拷貝到1B
然後E7析構消失
main裏面的匿名空間在接收到foo返回的b對象之後沒有被接收就會析構消失
1B析構消失。
規範解釋說明:
在 main()函數調用 foo()之前,會在自己的棧幀中開闢一個臨時空間,該空間的地址作爲隱藏參數傳遞給 foo(),在需要返回 A 對象時,就在這個臨時空間上構造一個 A a,然後這個空間的地址再利用寄存器 eax 返回給 main()函數,這樣 main()函數就能獲得 foo()函數的返回值了。
那麼返回匿名對象
既然返回的對象要返回到main函數的調用空間,那麼就直接生成對象就在main函數的調用空間。就不需要再從原來子函數生成的對象去拷貝到main函數的調用空間。那麼匿名對象本質上生成和消失的空間都是main函數的調用空間。
例如上面生成對象的時候,97地址對象的生成和消失都在main函數的調用空間。所以只需要一次構造和析構即可完成。
RVO / NRVO說明
介紹
(具名)返回值優化((Name)Return Value Optimization
簡稱(N)RVO),這麼一種優化
機制
當函數需要返回一個對象的時候,如果自己創建一個臨時對象用戶返回,那麼這個 臨時對象會消耗一個構造函數(Constructor)的調用、一個複製構造函數的調用(Copy Constructor)以及一個析構函數(Destructor)的調用的代價。 通過優化的方式,可以減少這些開銷。
我們上面的測試過程中,匿名對象兩個平臺都會進行優化,無名對象的時候vs2019平臺不會進行優化,Qt平臺會進行優化。
讀者可以換其他平臺和版本進行測試。
接收棧對象
接收棧對象的方式不同,會影響優化
main函數創建一個類對象進行函數返回接收
返回有名對象
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass foo()
{
MyClass b; //有名對象
cout << &b << endl;
return b;
}
int main()
{
MyClass tmp = foo();
cout << &tmp << endl;
return 0;
}
在Qt平臺運行結果爲:
我們可以看到,整個過程完全是同一個地址,所以並不是返回值去接受,而是整個對象的操作過程都在main函數的調用空間。
在vs2019平臺運行結果爲:
由於vs平臺沒有優化有名對象的返回,通過子函數創建之後拷貝到main函數的調用空間,所以在函數裏面創建的對象和返回到main函數裏面對象的地址是不同的。
返回無名對象
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass foo()
{
return MyClass(); //無名對象
}
int main()
{
MyClass tmp = foo();
cout << &tmp << endl;
return 0;
}
Qt平臺運行結果爲:
vs2019平臺運行結果爲:
兩個平臺都進行了優化。
在main函數先定義再接收對象
接下來我們測試接受對象先定義再接收的情況:
先定義再接收無名對象
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass foo()
{
return MyClass(); //匿名對象
}
int main()
{
MyClass tmp;
tmp = foo();
cout << &tmp << endl;
return 0;
}
在vs2019平臺測試結果爲:
我們可以看到vs2019平臺沒有進行優化,執行了對象的賦值過程,進行了一次構造,一次賦值運算,兩次析構。
Qt平臺運行結果爲:
在Qt平臺,也沒有進行優化,也是執行了對象的賦值過程,進行了一次構造,一次賦值運算,兩次析構。
先定義再接收有名對象
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass foo()
{
MyClass b; //有名對象
return b;
}
int main()
{
MyClass tmp;
tmp = foo();
cout << &tmp << endl;
return 0;
}
Vs2019平臺運行結果爲:
對於沒有優化的vs平臺
在執行在執行MyClass tmp;
的時候創建對象地址爲A7
在執行在執行MyClass b;
的時候創建對象地址爲8F
在執行 return b
;的時候發生一次拷貝,
把地址爲8F的對象拷貝到地址爲DB的對象
然後子函數中地址爲8F的對象進行析構消失
A7地址的對象在main函數的匿名空間
在main函數執行tmp = foo();的時候再進行一次賦值。
把main函數中的調用空間地址爲DB的對象賦值給main函數執行MyClass tmp;
創建的A7對象。
main函數的調用空間地址爲DB的對象發生析構消失
main函數中執行MyClass tmp;
代碼創建的地址爲A7的對象發生析構消失。
Qt平臺運行結果爲:
我們可以看到返回之後發生一次賦值操作,但是對象在創建,賦值的時候都在main函數的調用空間。創建對象的時候先執行 MyClass tmp;
創建地址爲be的對象,然後創建地址爲bf的對象,把main函數內的匿名空間地址爲bf對象賦值給地址爲be的對象,然後bf對象發生析構消失,be對象發生析構消失。
返回引用
main函數無對象接收的返回引用
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass & foo()
{
MyClass c;
return c;
}
int main()
{
foo();
return 0;
}
Vs2019平臺運行結果爲:
Qt平臺運行結果爲:
兩個平臺都發生了一次構造和析構。
main函數有對象接收的返回引用
#include <iostream>
using namespace std;
class MyClass
{
public:
MyClass()//構造器
{
cout << this << " constructor" << endl;
}
~MyClass()//析構器
{
cout << this << " desstructor" << endl;
}
MyClass(const MyClass& another) //拷貝構造器
{
cout << this << " copy form" << &another << endl;
}
MyClass& operator=(const MyClass& another) //賦值運算符重載
{
cout << this << "operator =" << &another << endl;
return *this;
}
private:
};
MyClass& foo()
{
MyClass c;
return c;
}
int main()
{
MyClass d = foo();
return 0;
}
Vs2019平臺運行結果爲:
Vs平臺運行MyClass d;
構造d對象,然後析構d對象
接下來67地址是main函數的調用空間的匿名空間去拷貝一個非法的地址,
然後析構這個非法地址的內容。
Qt平臺運行結果爲:
Qt平臺先執行MyClass d;
構造地址爲8f的對象之後,
地址爲8f的對象d直接消失。
bf在拷貝的時候直接拷貝到了0,
然後進行析構bf消失。
結論
可以返回棧對象,不可以返回棧對象的引用。