C++最詳細的棧對象返回說明【C++】(x)

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消失。

結論

可以返回棧對象,不可以返回棧對象的引用。

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