C++函數返回對象效率以及使用建議 局部變量作爲函數返回值 C++中函數返回值是一個對象時的問題

C++中函數返回值與拷貝

五一假期最後一天,看JUC看的頭疼,寫寫blog放鬆一下。作爲自己的第一篇blog,自己就先來談談對C++中函數返回return的理解,自己本來在學Java,但是平時學校的項目是用的C++,所以在平時搬磚時經常會有一些問題,今天就來談談前段時間注意到的一個很小的知識點,話不多說,先上列子。

首先我們創建一個簡單的Man類,實現它的無參構造函數、有參構造函數和析構函數:

class Man
{
public:
	Man() {
		cout << "構造" << endl;
		data = new int(0); }

	Man(const Man& m)
	{
		cout << "拷貝構造" << endl;
		this->data = m.data;
	}
	
	~Man() 
	{ 
		cout << "析構" << endl;
		delete data; 
	}
	
	int* data;
};

聲明一個get函數獲取一個Man的對象

Man get(Man& m)
{
	cout << "----" << endl;
	return m;
}

在main函數中執行下列代碼

 void main()
{
		Man m, n;
		//cout << "before m=" << &m << "n=" << &n << endl;
		*m.data = 5;
		printf("m.data is %d\n", *m.data);
		n = get(m); 

		printf("m.data is %d\n", *m.data);
		printf("n.data is %d\n", *n.data);
	
	    system("pause");
	    }

你可以試着想一想三個printf的輸出結果分別是多少

執行結果如下圖所示:

在這裏插入圖片描述

在輸出結果裏我們可以清楚的看到,Man m, n; 創建了m,n兩個對象,調用了構造函數,對m對象中的data賦值,然後我們調用get(Man& man) 函數,注意這裏函數參數是引用類型,因此傳入的對象是m對象本身,這裏我們要區別get(Man man) 兩種函數參數類型的區別,我稍後再提。get(Man& man) 函數調用完畢後,返回對象m。
按照我們過去的分析會認爲對象n等於get函數返回的m對象 (n=m) (注意這裏等號=被重載過),m對象中的int* data 成員值直接賦值給了n對象中的data成員,輸出時照理說m和n的data值都應該等於5的,但是:

爲什麼這裏輸出結果卻表明這個data指針指向的空間被銷燬了?

爲什麼get函數執行裏會多出了拷貝構造和析構這兩個過程呢?

如果我們返回值爲Man&會有什麼區別變化呢?

這裏我們做一個對比,填加一個getR函數,返回值爲Man& 引用類型:

Man& getR(Man& m)
{ 
    cout << "----" << endl;
	return m;
}

接下來我們調用getR這個函數看一看輸出結果:

void main()
{
		Man m, n;
		*m.data = 5;
		printf("m.data is %d\n", *m.data);
		n = getR(m);
		
		printf("m.data is %d\n", *m.data);
		printf("n.data is %d\n", *n.data);
		
	    system("pause");
	    }

執行結果如下圖所示:

在這裏插入圖片描述
可以看到,當我們返回的是m對象的的引用時,getR 函數執行時沒有調用拷貝構造和析構函數

這裏我向你詳細的解釋一下返回值不是引用的情況時整個函數執行的過程(個人拙劣的理解)

我們再回到get這個函數:

Man get(Man& m)
{
	cout << "----" << endl;
	return m;
}

首先函數參數傳入m這個對象的引用我們毋庸置疑,關鍵就在return這裏。我們捋一捋函數從開始到結束這個過程,隨着Main函數調用get函數,get函數入棧,同時get方法對應的棧幀(儲存函數局部變量、返回地址等信息)也入棧,這裏的局部變量也就是m對象的引用。
當我們return這個m對象時,會在內存中創建一個臨時的Man temp對象,同時這個temp對象調用其拷貝構造函數,也就是Man temp(m) 。
在這裏插入圖片描述
完成temp對象的創建後,get函數出棧,對應的棧區內容被銷燬,這時系統會調用m對象的析構函數,注意這裏有一個陷阱!!!!,由於m對象是在main方法下的棧區創建的,因此get方法出棧後,系統調用m析構函數並沒有真正把m對象在棧區銷燬(因爲它根本就不是在get方法的棧區上),調用析構函數僅僅是將data指針所指向的內存空間被銷燬了(delete data;),這也解釋了爲什麼m.data的值爲-572662307。當main方法執行完畢後,m對象纔會調用析構函數真正被銷燬,當然,這也會帶來另一個問題,data指向的內存區被執行了兩次delete,運行結束後你也就會發現還會有一個**“析構”**和一個內存問題報錯。
在這裏插入圖片描述
回到我們返回的值上:

n = get(m); 

這裏實際上可以理解成

Man temp(m);
n=temp;

當然由於get函數的退出調用析構函數時,data指針指向的內存區域數據已經被銷燬,自然n和m得到的值是一個錯誤值了。

總結

對於函數返回值類型爲非引用類型(當然引用類型也可以理解爲Man& temp=m),都是會在內存中創建一個臨時變量,將返回值拷貝到臨時變量中,而返回值是作爲函數調用棧區中的局部變量,隨着函數的返回,棧區的銷燬,而被銷燬。

如果有理解上的錯誤還請各位指正!~


 

C++函數返回對象效率以及使用建議

 
128 篇文章9 訂閱

函數的傳參和傳返回值,分爲兩種模式:傳值和傳引用。

傳值就是傳整個對象,傳引用就是傳對象的引用(地址)。當對象本身比較小時,可以直接傳值,但當對象比較大時我們一般會傳引用以節省內存和減少拷貝。這是C++的機制,在java中都是傳引用,所以不用像C++一樣區分值語義和引用語義。

對於傳參一般建議都使用傳引用,但對於返回值而言,我們不能直接返回對象引用,因爲,函數調用完後會清棧,引用指飛。但傳值我們又擔心臨時變量的拷貝降低性能,所以我們可能寫出如下代碼,返回值用指針包裹。

shared_ptr<A> fun()
{
    make_shared<A> ptr;
    ...
    return ptr;
}

這當然萬無一失,但明顯變麻煩了。實際上我們可以放心大膽地返回對象。下面進行論述:

C/C++函數返回對象的原理:

A fun2()
{
    A a1();
    ...
    return a1;
}
void fun1()
{
    A a2=fun2();
}

a1是在函數fun2中堆棧中的對象,在return時,會將其拷貝構造到外層函數fun1的堆棧中(這個對象是一個匿名對象,是一個右值,馬上會被析構),之後將其拷貝構造產生對象a2。這是沒有任何編譯器優化的情況。

一般情況下,編譯器會採用RVO(return value optimization)優化,參看《深度探索C++對象模型》。下面是一個例子

//原函數
Point3d factory()
{
    Point3d po(1,2,3)
    return po;
}
int main()
{
    Point3d p = factory();
    return 1;
}

//
編譯器優化後的示例代碼
//
factory(const Point3d &_result)
{
    Point3d po;
    po.Point3d::Point3d(1,2,3);
    _result.Point3d::Point3d(po);   //用po拷貝構造_result;
    po.Point3d::~Point3d();         //po對象析構
    return;
}
int main()
{
    Point3d _result;
    factory(_result);
    Point3d p=result;
    return 1;
}

大體意思是說,會在值傳遞返回值的函數中,將返回值改寫成傳引用參數。這樣可以少一次臨時變量的構造。這是編譯器優化時幫我們做的。

總結起來就是放心大膽地返回一個對象實體,這並不會有多餘的開銷,編譯器會幫我們優化。


c++ 如何高效傳遞對象,避免不必要的複製

 
今天在看c11的右值引用特性,遇到個毀三觀的問題。在我認知中,函數返回變量會經歷兩次複製過程,如下例子:
#include <iostream>

class A {
public:

    A() {
        std::cout << " constructor" << std::endl;
    }

    A(const A& orig) {
        std::cout << " copy constructor" << std::endl;
    }

    ~A() {
        std::cout << " destructor" << std::endl;
    }
    
    A& operator=(const A& orig) {
        std::cout << " operator=" << std::endl;
    }

};

A func() {
    return A();
}

int main() {
    A a = func();
    return 0;
}

按我之前的認知,應該輸出
 constructor
 copy constructor
 destructor
 copy constructor
 destructor
 destructor
在func中調用複製構造函數來複制return語句中創建的對象,用於返回到main函數,然後析構return語句中創建的對象;func函數返回後,調用a的複製構造函數來複制func返回後的臨時對象,然後析構臨時對象。最後main函數返回後再析構a。
然而實際在gcc中輸出是
 constructor
 destructor
網上查,才知道編譯器做了點手腳,它把main中的a直接指向了func中return那句構造的對象,然後func返回時,構造的對象當然不會被析構。這個手腳就是返回值優化(RVO)。不過我們也可以關閉這個優化,只要加上編譯選項 -fno-elide-constructors。當加上這個選項後,運行輸出
 constructor
 copy constructor
 destructor
 copy constructor
 destructor
 destructor
 

輸出時,同時再輸出this指針地址,能更直觀的看出整個過程:

 

#include <iostream>

class A {
public:

    A() {
        std::cout << this << " constructor" << std::endl;
    }

    A(const A& orig) {
        std::cout << this << " copy constructor" << std::endl;
    }

    ~A() {
        std::cout << this << " destructor" << std::endl;
    }
    
    A& operator=(const A& orig) {
        std::cout << this << " operator=" << std::endl;
    }
    
    void printAddr() {
        std::cout << this << std::endl;
    }

};

A func() {
    return A();
}

int main() {
    A a = func();
    a.printAddr();
    return 0;
}
加上編譯選項-fno-elide-constructors,編譯運行輸出
0x7ffd892d9d6f constructor
0x7ffd892d9d9f copy constructor
0x7ffd892d9d6f destructor
0x7ffd892d9d9e copy constructor
0x7ffd892d9d9f destructor
0x7ffd892d9d9e
0x7ffd892d9d9e destructor
不加-fno-elide-constructors編譯運行輸出
0x7ffde009f03f constructor
0x7ffde009f03f
0x7ffde009f03f destructor
另外,若func函數改爲返回有名對象:
A func() {
    A local;
    return local;
}
結果是一樣的,即有名返回值優化(NRVO)。

 

若func中傳入A對象的引用,再直接返回:

A func(A& r) {
    return r;
}

int main() {
    A f;
    A a = func(f);
    a.printAddr();
    return 0;
}
輸出
0x7ffe76589caf constructor
0x7ffe76589cae copy constructor
0x7ffe76589cae
0x7ffe76589cae destructor
0x7ffe76589caf destructor
嗯,知道r到a應該複製一份,合情合理,編譯器還挺聰明的嘛。若func中直接傳入A對象,再直接返回:
A func(A r) {
    return r;
}

int main() {
    A f;
    A a = func(f);
    a.printAddr();
    return 0;
}
輸出
0x7ffead26280e constructor
0x7ffead26280f copy constructor
0x7ffead26280d copy constructor
0x7ffead26280f destructor
0x7ffead26280d
0x7ffead26280d destructor
0x7ffead26280e destructor
除了會多一個從形參到實參的複製,還會多一個形參到a的複製。

接下來講一個c11右值引用的例子,A中新增一個顯式移動構造函數:

 

#include <iostream>

class A {
public:

    A() {
        std::cout << this << " constructor" << std::endl;
    }

    A(const A& orig) {
        std::cout << this << " copy constructor" << std::endl;
    }

    A(A&& orig) {
        std::cout << this << " move constructor" << std::endl;
    }

    ~A() {
        std::cout << this << " destructor" << std::endl;
    }

    A& operator=(const A& orig) {
        std::cout << this << " operator=" << std::endl;
    }

    void printAddr() {
        std::cout << this << std::endl;
    }

};

A func() {
    return std::move(A());
}

int main() {
    A a = func();

    return 0;
}
因爲右值引用是c11新特性,編譯時需加上-std=c++0x(或-std=c++11/14/17)。由於RVO,以上代碼輸出
0x7ffc248d7c8f constructor
0x7ffc248d7c8f destructor
控制變量,我們需要看到新特性右值引用的作用,去掉編譯器優化,加上-fno-elide-constructors編譯運行輸出
0x7ffe158f029f constructor
0x7ffe158f02cf move constructor
0x7ffe158f029f destructor
0x7ffe158f02ce move constructor
0x7ffe158f02cf destructor
0x7ffe158f02ce destructor
從結果能看出,通過std::move()函數,我們可以“移動”對象內存所有權,使得免去邏輯上多餘複製的操作,達到資源再利用,提高效率。以上是顯式移動,得益於c11新特性,c11從語義上默認支持移動,所以還可以隱式移動,把func改爲:
A func() {
//    return std::move(A());
    return A();
}
輸出不變。
綜上在c11下寫返回局部對象的函數,編譯器會先自動優化,若某些場景下優化未觸發(具體哪些場景可搜索關鍵字RVO),還會通過移動來避免複製。
此外,還能通過移動來避免右值傳參過程中的複製,如下,A不變,新增函數func2:
void func2(A s) {
}

int main() {
    A a;

    std::cout << "copy" << std::endl;
    func2(a);

    std::cout << "\nexplicit move" << std::endl;
    func2(std::move(a));

    std::cout << "\nimplicit move" << std::endl;
    func2(A());

    std::cout << "\ndone" << std::endl;
    return 0;
}
輸出
0x7fff2b988efb constructor
copy
0x7fff2b988efc copy constructor
0x7fff2b988efc destructor

explicit move
0x7fff2b988efd move constructor
0x7fff2b988efd destructor

implicit move
0x7fff2b988eff constructor
0x7fff2b988efe move constructor
0x7fff2b988efe destructor
0x7fff2b988eff destructor

done
0x7fff2b988efb destructor
以上std::move(a)和A()都是右值,邏輯上沒必要複製到形參,得益於c11新特性,可以通過移動來避免實參到形參的複製。需要注意的:
1. std::move(a)後,a雖沒有被立即釋放,訪問其對象語法上是可以的,但我們清楚其資源已經是別的對象的了,所以訪問成員變量(包括析構時析構成員變量)是不允許的,所以在移動構造函數中,形參orig的成員必須被置爲nullptr,以防止其訪問已不再屬於它的資源。
2. 要儘量保證移動構造函數不發生異常(大概因爲實參移動到實參或者臨時對象返回時拋異常不好處理?),可以通過noexcept關鍵字,這裏可以保證移動構造函數中拋出來的異常會直接調用terminate終止程序。


參考:
https://www.zhihu.com/question/22111546
http://www.cnblogs.com/lengender-12/p/6659833.html
http://book.2cto.com/201306/25367.html
http://blog.csdn.net/immiao/article/details/46876799
http://blog.csdn.net/virtual_func/article/details/48709617

C++以對象作爲返回值時編譯器的優化,以及臨時變量的析構時機

 
18 篇文章1 訂閱

印象中,函數調用的時候,參數past by value、返回值return by value,總是伴隨着對象的複製。

 

實際上參數傳遞是這樣的,但是返回值有可能不是這樣的(雖然大部分都是面臨拷貝構造函數的調用),這取決於編譯器。

 

#include<string>  
#include<list>  
#include<iostream>  

using namespace std;  
  
class C  
{  
   public:
   	    C()
   	    {
   	   	    cout<<"C default constructor(),this="<<this<<endl; 
   	    }  
            
        C(const C &c)  
        {  
            cout<<"C const copy constructor(),this="<<this<<",reference="<<&c<<endl;                                  
        }  
          
        C(C &c)  
        {  
            cout<<"C nonconst copy constructor(),this="<<this<<",reference="<<&c<<endl;            
        }  
          
        const C & operator=(const C &c)  
        {  
            cout<<"C assignment(),this="<<this<<endl;  
            return *this;  
        }  
        ~C()  
        {  
            cout<<"C destructor(),this="<<this<<endl;  
        } 
};  
      
      
C test_1(int i)  
{  
	 cout<<"entering test_1"<<endl; 
   C x;  
   C a;           //a會析構
   cout<<"leaving test_1"<<endl; 
   return x;      //return之後棧不清空,x不會析構,即使編譯器已經將優化設置成-O0  
  
}  
      
C test_2(int i) 
{  
	 cout<<"entering test_2"<<endl; 
   C x;  
   C a;  
   
   cout<<"leaving test_2"<<endl;
   if(i>0) 
       return x;   
   else  
       return a;    //x和a都會析構,返回的時候,先調用拷貝構造函數,初始化返回值(此處爲main裏面的z),  
                    //然後再析構a和x  
} 


C test_3(C t)
{
	return t;        //此處導致t的構造和析構
} 

C test_4(C t)
{
	C x=t;
	return x;        //此處導致t的構造和析構,但是x只會構造不會析構
} 

int main()
{
   cout<<"invoking test_1"<<endl;
   C y=test_1(1); //這種調用不會有拷貝構造函數,y直接爲test_1函數棧裏面生成的對象,編譯器優化的結果
   cout<<"end invoke test_1"<<endl;
   
   cout<<"================華麗分割線================="<<endl;
   
   cout<<"invoking test_2"<<endl;
   C z=test_2(1);  //這種情況會調用拷貝構造函數(nonconst版本),初始化z
   cout<<"end invoke test_2"<<endl; 
   
   cout<<"================華麗分割線================="<<endl;
   
   cout<<"invoking test_3"<<endl;
   C a=test_3(y);  
   cout<<"end invoke test_3"<<endl; 
   
   
   cout<<"================華麗分割線================="<<endl;
   
   cout<<"invoking test_4"<<endl;
   C b=test_4(y);  
   cout<<"end invoke test_4"<<endl;  
   
  cout<<"================華麗分割線================="<<endl;
   
   cout<<"開始測試臨時變量何時析構"<<endl;
   test_2(1),                         //(注意結束處是逗號)此處返回的C沒有指定任何變量,編譯器會生成臨時變量                                         
   cout<<"結束測試臨時變量何時析構"<<endl;//臨時變量會再語句的第一個分號處析構,cout完成之後析構
   
   cout<<"================華麗分割線================="<<endl;
   
   cout<<"開始測試臨時變量何時析構"<<endl;
   test_2(1);                         //(注意結束處是分號)此處返回的C沒有指定任何變量,編譯器會生成臨時變量                                         
   cout<<"結束測試臨時變量何時析構"<<endl;//臨時變量會再語句的第一個分號處析構,cout開始之前析構
   
   cout<<"================華麗分割線================="<<endl;
   cout<<"================下面開始析構棧裏面的變量了,啦啦啦================="<<endl;
   cout<<"================析構順序按照入棧的順序,後進先出,後構造,先析構==========="<<endl;
   return 0;

}

 

 

運行結果:

 

AlexdeMacBook-Pro:~ alex$ a.out
invoking test_1
entering test_1
C default constructor(),this=0x7fff5929baa8
C default constructor(),this=0x7fff5929b8d8
leaving test_1
C destructor(),this=0x7fff5929b8d8
end invoke test_1
================華麗分割線=================
invoking test_2
entering test_2
C default constructor(),this=0x7fff5929b8d8
C default constructor(),this=0x7fff5929b8d0
leaving test_2
C nonconst copy constructor(),this=0x7fff5929ba98,reference=0x7fff5929b8d8
C destructor(),this=0x7fff5929b8d0
C destructor(),this=0x7fff5929b8d8
end invoke test_2
================華麗分割線=================
invoking test_3
C nonconst copy constructor(),this=0x7fff5929ba88,reference=0x7fff5929baa8
C nonconst copy constructor(),this=0x7fff5929ba90,reference=0x7fff5929ba88
C destructor(),this=0x7fff5929ba88
end invoke test_3
================華麗分割線=================
invoking test_4
C nonconst copy constructor(),this=0x7fff5929ba78,reference=0x7fff5929baa8
C nonconst copy constructor(),this=0x7fff5929ba80,reference=0x7fff5929ba78
C destructor(),this=0x7fff5929ba78
end invoke test_4
================華麗分割線=================
開始測試臨時變量何時析構
entering test_2
C default constructor(),this=0x7fff5929b8d8
C default constructor(),this=0x7fff5929b8d0
leaving test_2
C nonconst copy constructor(),this=0x7fff5929ba70,reference=0x7fff5929b8d8
C destructor(),this=0x7fff5929b8d0
C destructor(),this=0x7fff5929b8d8
結束測試臨時變量何時析構
C destructor(),this=0x7fff5929ba70
================華麗分割線=================
開始測試臨時變量何時析構
entering test_2
C default constructor(),this=0x7fff5929b8d8
C default constructor(),this=0x7fff5929b8d0
leaving test_2
C nonconst copy constructor(),this=0x7fff5929ba68,reference=0x7fff5929b8d8
C destructor(),this=0x7fff5929b8d0
C destructor(),this=0x7fff5929b8d8
C destructor(),this=0x7fff5929ba68
結束測試臨時變量何時析構
================華麗分割線=================
================下面開始析構棧裏面的變量了,啦啦啦=================
================析構順序按照入棧的順序,後進先出,後構造,先析構===========
C destructor(),this=0x7fff5929ba80
C destructor(),this=0x7fff5929ba90
C destructor(),this=0x7fff5929ba98
C destructor(),this=0x7fff5929baa8
AlexdeMacBook-Pro:~ alex$
AlexdeMacBook-Pro:~ alex$ 

結論:

一:return by value時候編譯器的優化

編譯器在能夠做優化的時候,會盡量幫你做優化,比如test_1,總是將棧裏面的x直接給調用者,避免了多一次的析構和構造。即使在關閉編譯器優化的時候,它依然給你做了這個動作。但是在test_2裏面,返回值是動態的,隨着參數變動而變動,編譯器是沒有辦法得知保留哪個的,於是索性都析構了。

 

在Effective C++ Item 21,page 94, Don't try to return a reference when you must return an object.

作者說:C++和所有編程語言一樣,允許編譯器實現者施行最優化,用以改善產出碼的效率卻不改變其可觀察的行爲。

 

g++確實對此做了優化,但是動態返回值,編譯器卻無能爲力。你無法要求編譯器按照每個分支,生成不同的代碼。否則在複雜的程序下,生成的可執行文件大小那將無法估計了

 

二:臨時變了的析構時機

臨時變量,總是在執行完生成臨時變量的那一行代碼之後析構

(是不是比較拗口?)

那就這樣說吧:生成臨時變量之後,遇到第一個分號,析構函數開始調用


C++函數返回值,你必須注意的問題

 
20 篇文章1 訂閱

C++太繁雜了,先接觸C++後接觸python這樣的語言,你就再也不想碰它,因爲,就連一個函數返回值都一大堆的說道,這裏面的玄機,連工作三年的C++熟手都未必能準確的理解和運用。

         歸根結底,C++所面臨的問題要求它提供各種各樣的機制以保證性能,也許,這輩子也見不到C++能安全有效的自己進行內存垃圾回收。。。。。

        老程序猿都會提醒菜鳥,注意函數的返回值,因爲,很可能,你的函數返回的數據在後續的使用中會出錯。那麼函數在返回值時要注意什麼呢?

         本篇博客嘗試用最簡練的普通大白話,講解函數返回值的問題。

        C++把內存交給了程序猿,但是,請你注意,它可沒把所有的內存都交給你,交給你的只是堆上的內存,也就是你通過malloc函數  和new 關鍵字申請來的內存,除了這些內存以外,其他的內存,你最好別碰,最好別碰,最好別碰,重要的事情說三遍。

         如果你的函數返回值在後續使用中出錯了,尤其是返回函數內的局部變量這種事情,那麼,基本可以肯定,你碰了不該碰的內存。這時候,你會覺得自己很冤枉啊,我沒有啊。但事實是,沒有冤枉你,所以,爲了不被bug檢察院起訴你,作爲一個C++程序猿,你必須學會甄別那些內存是能碰的,那些內存是不能碰的。

	char *pstr = "This is the buffer text";  
	return pstr;

          如果你的函數是這麼寫的,那麼恭喜你,返回正確,因爲這個pstr指向的是常量存儲區,這裏的內存,你是可以碰的,但是注意,這個碰,僅僅是讀,你想修改,那是萬萬不可以的。

	char buffer[] = "This is the buffer text";  
	return buffer;
         如果你的函數是這麼寫的,那麼恭喜你,等着bug聯邦檢察院起訴你吧。這裏的buffer指向的是棧上內存,這個,就是你碰不得的,前面的pstr就好比公園,公園嘛,大家都可以來玩,但是你不能把公園裏的假山拆了,你也不能把公園裏的樹砍了,你只能是來玩,不能修改它,棧上的內存,就好比是私家花園,你一個外人,是不能進去的。那麼怎麼甄別的,方法倒也簡單,你見到帶中括號的,就應該明白,這東西是棧上的,出了這個函數,你就別想再碰的,你只要敢碰,bug聯邦檢察院就會起訴你。
	static char buffer[] = "This is the buffer text";  
	return buffer;
如果你的函數是這麼寫的,那麼恭喜你,返回正確,可是剛纔不是明明說,這裏是私家花園嘛,沒錯,但是你注意看,前面還加了一個static,只要加了這個關鍵字,就相當於說國家把這個私家花園徵用了,那麼,它就從私家花園變成了靜態存儲區裏的一個小花園,靜態存儲區裏的內存,國家說,靜態存儲區對外開放,你們都可以來。

             函數返回的都是值拷貝,棧上的內存,在函數結束的時候,都會被收回。在函數內部,你可以碰棧上的內存,那是因爲這個時候你是在棧的家裏做客,那他們家的內存小花園當然允許你訪問,可是函數結束了,就相當於你離開了棧的家,棧把內存小花園的門關上了,你怎麼可以進去,你進去了,就會被bug聯邦法院起訴!

 

             但是呢,總有一些奇怪的現象讓你以爲你可以在函數結束後仍然可以訪問棧上的內存。

            我們定義一個結構體

 

struct person
{
	int age;
}

            寫一個函數
person*  getperson2()
{
	person p;
	p.age = 99;
	return &p;
}
          在得到函數的返回值以後,你可以輸出對象的年齡
	person *p2 = getperson2();
	cout<<p2->age<<endl;

           你會發現,這段代碼居然可以正確執行!在函數getperson2內部,p這個變量是局部變量,必然是在棧上申請的,返回的是&p,這不就是棧上的內存地址麼,那爲啥在函數外部,卻仍然可以輸出age呢?

           雖然,函數結束後,對象被銷燬,但是銷燬的不夠徹底,似乎計算機在管理內存時也不需要那麼徹底的銷燬一個對象,你之所以能輸出age,那是因爲那個區域,沒有被徹底銷燬,這一小塊的內存(存儲age的4個byte)沒有發生變化。你可以暫時的碰這塊內存,但遲早是要出問題的,如果某一刻,計算機打算用這塊內存,發現你在非法使用,那麼必然會報警,然後bug聯邦檢察院會起訴你。

           爲了讓問題更透明一些,我們修改一下結構體

struct person
{
	int age;
	char* name;
	person()
	{
		name = new char(10);
		strcpy(name,"sheng");
	}
	~person()
	{
		name = NULL;
	}
};
person*  getperson2()
{
	person p;
	p.age = 99;
	return &p;
}
	person *p2 = getperson2();
	cout<<p2->age<<endl;
	cout<<p2->name<<endl;

          這一次,函數結束後,對象的銷燬要比上一次徹底的多,雖然,age的區域還是沒有被徹底銷燬,但是name區域被徹底銷燬了,如果你訪問name的區域,就必然出錯,這就好比啊,私家花園關門了,可是花園好大的,所以不是每一處都安裝了攝像頭和報警器,比如age這片區域,所以,你偷偷的從age這個區域溜進去時,花園的主人沒發現,直到花園的巡防大隊到age區域巡防時,發現你竟然在這裏偷偷菜花,結果就是把你打的崩潰了。而name這邊區域,在~person這個析構函數中安裝了攝像頭和報警器,你只要來,就立刻報警,然後把你打的崩潰。

          千言萬語,匯成一句話,函數不要返回指向棧的內存地址,切記,是地址,別被嚇的所有的函數內的變量都不敢返回,只要不是棧的內存地址,你儘管放心的返回。


 

char *strA()

{

  char str[] = "hello word";

  return str;

}

上述程序有什麼問題?

簡單的來說,str是個局部變量的地址,作爲返回值,有可能被提前回收。

那麼局部變量可以作爲函數的返回值嗎,這個問題不能一概而論。局部變量作爲返回值時,一般是系統先申請一個臨時對象存儲局部變量,也就是找個替代品,這樣系統就可以回收局部變量,返回的只是個替代品。

瞭解完局部變量返回的過程,那麼如果返回的是一個基本類型的變量,比如:

int a;

a = 5;

return a;

那麼就會有一個臨時對象也等於a的一個拷貝,即5返回,然後a就被銷燬了。儘管a被銷燬了,但它的副本5還是成功地返回了,所以這樣做沒有問題。

那麼如果是指針,這麼返回就問題很大,因爲你返回的局部變量是地址,地址雖然返回了,但地址所指向的內存中的值已經被回收了,你主函數再去調,就有問題了。這個問題也是可以解決的,可以把局部變量變爲靜態變量或者全局變量,這樣就不存放在棧中了,而是存放在靜態存儲區,不會被回收。

 

char str[] = "hello word";//分配一個局部變量

char *str= "hello word";//分配一個全局變量


C++中函數返回值是一個對象時的問題 

 

問題描述

在C++程序中,一個函數返回值是一個對象時,返回的是函數內部的局部變量本身,
還是會產生一箇中間對象(匿名對象)呢?

經過測試,在win平臺和Linux平臺效果不同

代碼如下

//
// Created by YANHAI on 2019/5/28.
//
#include <iostream>
using namespace std;

class Test {
public:
    Test(const char *name)
    {
        this->name = name;
        printf("%s: 執行了構造函數, 我的地址是 %p\n", name, this);
    }

    Test(const Test &obj)
    {
        this->name = obj.name;
        printf("%s: 執行了拷貝構造函數,我的地址是 %p,拷貝來自%s %p\n",
               name.c_str(), this, obj.name.c_str(), &obj);
    }

    ~Test()
    {
        printf("%s: 執行了析構函數, 我的地址是 %p\n", name.c_str(), this);
    }

public:
    string name;
};

Test fun()
{
    Test t("我是在fun函數中創建的");
    printf("in fun: %p\n", &t);
    return t;
}

void test1()
{
    // 這裏t1對象就是fun函數裏面創建的?
    cout << "fun start.." << endl;
    Test t1 = fun();
    cout << "fun end.." << endl;
    t1.name = "我是在test函數中被創建的";
    printf("我是在test函數中被創建的對象,我的地址是: %p\n", &t1);
}

int main()
{
    cout << "--------test1 start ...-----" << endl;
    test1();
    cout << "--------test1 end ...-----" << endl;
    return 0;
}

測試過程

在win平臺

使用VS2019編譯並運行

運行結果:

--------test1 start ...-----
fun start..
我是在fun函數中創建的: 執行了構造函數, 我的地址是 010FFAC4
in fun: 010FFAC4
我是在fun函數中創建的: 執行了拷貝構造函數,我的地址是 010FFBD4,拷貝來自我是在fun函數中創建的 010FFAC4
我是在fun函數中創建的: 執行了析構函數, 我的地址是 010FFAC4
fun end..
我是在test函數中被創建的對象,我的地址是: 010FFBD4
我是在test函數中被創建的: 執行了析構函數, 我的地址是 010FFBD4
--------test1 end ...-----

過程解釋:

  1. 在fun函數中,t對象被創建,執行t對象的構造函數(t對象地址爲 010FFAC4)
  2. 在fun函數執行return時,會產生一個匿名對象,會執行匿名對象的拷貝構造函數,相當於執行了 Test tmp = t; (匿名對象tmp地址爲010FFBD4)
  3. fun函數執行結束,局部變量對象t被釋放,執行t對象的析構函數,fun函數將匿名對象(tmp)返回(返回的是010FFBD4地址的匿名對象)
  4. 在test1函數中,t1對象被創建時 使用了fun函數的返回值,故匿名對象tmp直接變爲t1對象(而不是執行拷貝構造函數給t1,就比如執行了Test t1 = Test("xx");)(t1對象的地址即爲匿名對象地址 010FFBD4)
  5. test1函數執行完畢後,t1對象被釋放,執行t1的析構函數

在linux平臺

使用g++編譯

運行結果:

--------test1 start ...-----
fun start..
我是在fun函數中創建的: 執行了構造函數, 我的地址是 0x7ffe5a2488c0
in fun: 0x7ffe5a2488c0
fun end..
我是在test函數中被創建的對象,我的地址是: 0x7ffe5a2488c0
我是在test函數中被創建的: 執行了析構函數, 我的地址是 0x7ffe5a2488c0
--------test1 end ...-----

過程解釋:

  1. 在fun函數中,t對象被創建,執行t對象的構造函數(t對象地址爲 0x7ffe5a2488c0)
  2. 在fun函數結束時,並沒有產生匿名對象,而是將t對象返回(返回的是0x7ffe5a2488c0地址的對象t)
  3. 在test1函數中,t1對象被創建時 使用了fun函數的返回值,故返回對象t直接變爲t1對象(而不是執行拷貝構造函數給t1,就比如執行了Test t1 = Test("xx");)(t1對象的地址即爲t對象地址 0x7ffe5a2488c0)
  4. test1函數執行完畢後,t1對象被釋放,執行t1的析構函數

結論

  1. 在linux平臺上,少產生了一個匿名對象,提高了執行效率
  2. 原本僅在fun函數內有效(局部變量生存週期)的t對象,由於被返回,在test1函數中仍然有效
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章