C++之匿名對象解析


我們知道在C++的創建對象是一個費時,費空間的一個操作。有些固然是必不可少,但還有一些對象卻在我們不知道的情況下被創建了。通常以下三種情況會產生臨時對象:

  1,以值的方式給函數傳參;
  2,類型轉換;
  3,函數需要返回一個對象時;
 
現在我們依次看這三種情況:
 
  一,以值的方式給函數傳參。
 
  我們知道給函數傳參有兩種方式。1,按值傳遞;2,按引用傳遞。按值傳遞時,首先將需要傳給函數的參數,調用拷貝構造函數創建一個副本,所有在函數里的操作都是針對這個副本的,也正是因為這個原因,在函數體里對該副本進行任何操作,都不會影響原參數。我們看以下例子:
 
#include <stdio.h>
class CTemp
{
public:
    int a;
    int b;
public:
    CTemp(CTemp& t){ printf("Copy function!\n");a = t.a;b = t.b;};
    CTemp(int m = 0,int n = 0);
    virtual ~CTemp(){};
public:
    int GetSum(CTemp ts);
};
CTemp::CTemp(int m , int n)
{
    printf("Construct function!\n");
    a = m;b=n;
    printf("a = %d\n",a);
    printf("b = %d\n",b);
}
int CTemp::GetSum(CTemp ts)
{
    int tmp = ts.a + ts.b;
    ts.a = 1000;           //此時修改的是tm的一個副本
    
    return tmp;
}
//--------------Main函數-----------------
void main()
{
    CTemp tm(10,20);
    printf("Sum = %d \n",tm.GetSum(tm));
    printf("tm.a = %d \n",tm.a);
}
--------------------------------------------------------
Output:
Construct function!
a = 10
b = 20
Copy function!
Sum = 30
tm.a = 10
------------------------------------------------------
我們看到有調用了拷貝構造函數,這是tm在傳給GetSum做參數時:
1,調用拷貝構造函數來創建一個副本為GetSum函數體內所用。
2,在GetSum函數體內對tm副本進行的修改並沒有影響到tm本身。
 
解決辦法:
  針對第一種情況的解決辦法是傳入對象引用(記住:引用只是原對象的一個別名(Alias)),我們將GetSum代碼修改如下:
int CTemp::GetSum(CTemp& ts)
{
    int tmp = ts.a + ts.b;
    ts.a = 1000;     //此時通過ts這個引用參考(refer to)對象本身
    return tmp;
}
----------------------------------------------------------
Output:
Construct function!
a = 10
b = 20
Sum = 30
tm.a = 1000
--------------------------------------------------------
可以通過輸出看本,通過傳遞常量引用,減少了一次臨時對象的創建。這個改動也許很小,但對多繼承的對象來說在構建時要遞歸調用所有基類的構造函數,這對於性能來說是個很大的消耗,而且這種消耗通常來說是沒有必要的。
 
  二,類型轉換生成的臨時對象。
 
  我們在做類型轉換時,轉換后的對象通常是一個臨時對象。編譯器為了通過編譯會創建一起我們不易察覺的臨時對象。再次修改如上main代碼:
 
void main()
{
 CTemp tm(10,20),sum;
 sum = 1000;  //調用CTemp(int m = 0,int n = 0)構造函數
 printf("Sum = %d \n",tm.GetSum(sum));
}
-----------------------------------------------------------
Output:
Construct function!
a = 10
b = 20
Construct function!
a = 0
b = 0
Construct function!
a = 1000
b = 0
Sum = 1000
----------------------------------------------------------
main函數創建了兩個對象,但輸出卻調用了三次構造函數,這是為什么呢?
關鍵在 sum = 1000;這段代碼。本身1000和sum類型不符,但編譯器為了通過編譯以1000為參調用構造函數創建了一下臨時對象。
 
解決辦法:
  我們對main函數中的代碼稍作修改,將sum申明推遲到“=”號之前:
void main()
{
     CTemp tm(10,20);
     CTemp sum = 1000; 
     printf("Sum = %d \n",tm.GetSum(sum));
}
----------------------------------------------------------
Output:
Construct function!
a = 10
b = 20
Construct function!
a = 1000
b = 0
Sum = 1000
----------------------------------------------------------
只作了稍稍改動,就減少了一次臨時對象的創建。
1,此時的“=”號由原本的賦值變為了構造。
2,對Sum的構造推遲了。當我們定義CTmep sum時,在main的棧中為sum對象創建了一個預留的空間。而我們用1000調用構造時,此時的構造是在為sum預留的空間中進行的。因此也減少了一次臨時對象的創建。
 
  三,函數返回一個對象。
 
  當函數需要返回一個對象,他會在棧中創建一個臨時對象,存儲函數的返回值。看以下代碼:
#include <stdio.h>
class CTemp
{
public:
    int a;
public:
    CTemp(CTemp& t) //Copy Ctor!
    { 
        printf("Copy Ctor!\n");
        a = t.a;
    };
    CTemp& operator=(CTemp& t) //Assignment Copy Ctor!
    {
        printf("Assignment Copy Ctor!\n");
        a = t.a;
        return *this;
    }
    CTemp(int m = 0);
    virtual ~CTemp(){};
};
CTemp::CTemp(int m) //Copy Ctor!
{
    printf("Construct function!\n");
    a = m;
    printf("a = %d\n",a);
}
CTemp Double(CTemp& ts)
{
    CTemp tmp;      //構建一個臨時對象
    tmp.a = ts.a*2;
    return tmp;
    
}
//-------------Main函數-----------------
void main()
{
    CTemp tm(10),sum;
    printf("\n\n");

    sum = Double(tm);

    
    printf("\n\nsum.a = %d \n",sum.a);
}
---------------------------------------------------------
Output:
Construct function!
a = 10
Construct function!
a = 0

Construct function!
a = 0
Copy Ctor!
Assignment Copy Ctor!

sum.a = 20
--------------------------------------------------------
我特地加寬了語句:
    sum = Double(tm);
這條語句竟生成了兩個對象,Horrible! 我們現在將這條語句逐步分解一下:
    1,我們顯式創建一個tmp臨時對象,               
      語句:CTemp tmp;
    2,將temp對象返回,返回過程中調用Copy cotr創建一個返回對象,              
      語句:return tmp;
 
    3,將返回結果通過調用賦值拷貝函數,賦給sum       
      語句: sum = 函數返回值;(該步並沒有創建對象,只是給sum賦值)
 
tm.Double返回一個用拷貝構造函數生成的臨時對象,並用該臨時對象給sum賦值.
  上面的第1步創建對象可以不用創建,我們可以直接對返回值進行操作,有些C++編譯器中會有一種優化,叫做(NRV,named return value).不過本人使用的VC++6.0並沒有這個啟用這個優化。
  第2步創建的返回對象是難以避免的,你或許想可以返回一個引用,但你別忘記了在函數里創建的局部對象,在返回時就被銷毀了。這時若再引用該對象會產生未預期的行為。(C#中解決了這個問題)。
 
解決方法:
  我們將對象直接操作(Manipulate)返回對象,再結合上面的減少臨時對象的方法,將函數Double的代碼,及main函數中的代碼修改如下:
 
CTemp Double(CTemp& ts)
{
    return ts.a*2;
}
/*--------上面的代碼相當於-------
CTemp _ret
void Double(CTemp& ts)
{
    _ret.a = ts.a*2;
}
---------------*/


//---------Main函數-----------
void main()
{
    CTemp tm(10);
    printf("\n\n");

    CTemp sum = Double(tm);
    
    printf("\n\nsum.a = %d \n",sum.a);
}
--------------------------------------------------------
Output:
Construct function!
a = 10

Construct function!
a = 20

sum.a = 20
-------------------------------------------------------
發現減少了一次構造函數調用(tmp),一次拷貝構造函數(tmp拷貝給返回對象)調用和一次賦值拷貝函數調用.(Assignment Copy Ctor),這是因為:
     返回對象直接使用為sum預留的空間,所以減少了返回臨時對象的生成——返回對象即是sum,返回對象的創建即是sum對象的創建.多么精妙!
 

************************************************************************************

C++中真正的臨時對象是看不見的,它們不出現在你的源代碼中,臨時對象的產生在如下幾個時刻:

1.       用構造函數作為隱式類型轉換函數時,會創建臨時對象。

例:

class Integer
{
public:
    Integer(int i)
    :m_val(i)
    {}

    ~Integer(){}

private:
       int   m_val;
};

void Calculate(Integer itgr)
{
      // do something
}        

             那么語句:  int  i = 10;

                                   Calculate(i);

              會產生一個臨時對象,作為實參傳遞到Calculate 函數中。

2.       建立一個沒有命名的非堆(non-heap)對象,也就是無名對象時,會產生臨時對象。

如:

        Integer& iref = Integer(5);           //用無名臨時對象初始化一個引用,等價於

                                                         //Integer iref(5);

 

        Integer  itgr = Integer(5);           //用一個無名臨時對象拷貝構造另一個對象

        按理說,C++應先構造一個無名的臨時對象,再用它來拷貝構造itgr,由於

該臨時對象拷貝構造 itgr 后,就失去了任何作用,所以對於這種類型(只起拷貝構造另一個對象的作用)的臨時對象,c++特別將其看做: Integer itgr(5); 即直接以相同參數構造目標對象,省略了創建臨時對象這一步。

 

Calculate( Integer(5) );                       //無名臨時對象作為實參傳遞給形參,函數調

//用表達式結束后,臨時對象生命期結束,被//析構.

3.       函數返回一個對象值時,會產生臨時對象,函數中的返回值會以值拷貝的形式拷貝到被調函數棧中的一個臨時對象。

如:

  Integer Func()
        {
               Integer itgr;
               return itgr;
        }
 
        void main()
        {
               Integer in;
               in = Func();
        }

表達式 Func() 處創建了一個臨時對象,用來存儲Func() 函數中返回的對象,臨時對象由 Func()中返回的 itgr 對象拷貝構造(值傳遞),臨時對象賦值給 in后,賦值表達式結束,臨時對象被析構。見下圖:

 

看看如下語句:

                      Integer& iRef = Func();

該語句用一個臨時對象去初始化iRef 引用,一旦該表達式執行結束,臨時對象的生命周期結束,便被結束,iRef引用的屍體已經不存在,接下來任何對 iRef 的操作都是錯誤的。

 

下面,來看看實際的測試結果,代碼如下:

class VECTOR3
{
public:
    VECTOR3()
       :x(0.0f),y(0.0f),z(0.0f)
    {
       std::cout<<"VECTOR3 Default Constructor "
               <<std::setiosflags(std::ios_base::hex)<<this
               <<std::endl;
    }
    VECTOR3(float fx, float fy, float fz)
       :x(0.0f),y(0.0f),z(0.0f)
    {
       std::cout<<"VECTOR3 Parameter Constructor "
               <<std::setiosflags(std::ios_base::hex)<<this
               <<std::endl;
    }
 
    VECTOR3(const VECTOR3& rht)
       :x(rht.x), y(rht.y), z(rht.z)
    {
       std::cout<<"VECTOR3 Copy Constructor "
           <<std::setiosflags(std::ios_base::hex)<<this
           <<" from rht : "
           <<std::setiosflags(std::ios_base::hex)<<&rht
           <<std::endl;
    }
 
    ~VECTOR3()
    {
       std::cout<<"VECTOR3 Destructor "
               <<std::setiosflags(std::ios_base::hex)<<this
               <<std::endl;
    }
 
    VECTOR3& operator = (const VECTOR3& rht)
    {
       if( &rht == this )
           return *this;
       x = rht.x;
       y = rht.y;
       z = rht.z;
std::cout<<"VECTOR3 operator = left oper : " <<std::setiosflags(std::ios_base::hex)<<this <<" right oper : " <<std::setiosflags(std::ios_base::hex)<<&rht <<std::endl; return *this; } private: float x; float y; float z; }; VECTOR3 Func1() { return VECTOR3(1.0f, 1.0f, 1.0f); } VECTOR3 Func2() { VECTOR3 ret; ret.x = 2.0f; ret.y = 2.0f; ret.z = 2.0f; return ret; } void main() {   VECTOR3 v1 = Func1();   v1 = Func1();   VECTOR3 v2 = Func2();   VECTOR3 v3;   v3 = Func2(); }

  

分析:

<1>.

VECTOR3 v1 = Func1();

該語句的執行過程本該是:

    1>. 在 Func1() 中構造一個無名對象

    2>. 由 Func1() 中的無名對象拷貝構造調用表達式處的臨時對象

    3>. 再由臨時對象拷貝構造v1

    4>. Func1() 返回,析構無名對象

    5>. 整個語句結束,析構臨時對象

但是c++ 會優化上述過程,省略了 1>. 2>. 處的臨時對象創建,直接以

1.0f, 1.0f, 1.0f 為參數構造v1,這樣只會有一次構造函數的調用。結果

如圖:

 

<2>.

    v1 = Func1();

該語句的執行過程本該是:

    1>. 在 Func1() 中構造一個無名對象

    2>. 由 Func1() 中的無名對象拷貝構造調用表達式處的臨時對象

    3>. 再由臨時對象賦值給v1 (賦值運算符)

    4>. Func1() 返回,析構無名對象

    5>. 整個語句結束,析構臨時對象

但是c++ 會優化上述過程,省略了 1>. 處的無名臨時對象創建,直接以

1.0f, 1.0f, 1.0f 為參數構造調用表達式處的臨時對象,因為是賦值,所以這個臨時對象是無法被優化的,賦值完畢后,表達式結束,臨時對象被析構。結果如圖:

 

<3>.

    VECTOR3 v2 = Func2();

該語句的執行過程本該是:

    1>. Func2() 中的 ret 拷貝構造調用表達式處的臨時對象

    2>. 該臨時對象拷貝構造v2

    3>. 析構臨時對象

但是c++ 會優化上述過程,省略了創建臨時對象這一步,直接由ret拷貝

構造v2,就一次拷貝構造函數的代價。

結果如圖:

 

<4>.

           VECTOR3 v3;

    v3 = Func2();

執行過程如下:

    1>. 構造v3

    2>. 進入Func2(),構造ret

    3>. 返回ret,用ret拷貝構造到調用表達式處的臨時對象

    4>. Func2()結束,ret被析構

    5>. 臨時對象賦值給v3

    6>. 賦值表達式結束,析構臨時對象

結果如圖:

 

 

 

綜上所述,可得如下結論:

<1>. 在使用一個臨時對象( 可能是無名對象 或者 返回對象值時 ) 創建構造另一個對象的過程的中,c++會優化掉該臨時對象的產生,直接以相同參數調用相關構造函數構或者 直接調用拷貝構造函數 到 目標對象.

     

<2>. 若不是對象創建,而是對象賦值,則在賦值表達式的右值處的臨時對象

      創建不能省略,臨時對象賦值給左值后,表達式結束,臨時對象被析構。 

 

 

補充知識點:匿名對象轉正!

///**********************************************
 /// @file    VECTOR.cc
 /// @author  alex(AlexCthon@qq.com)
 /// @date    2018-06-10 20:38:41
 ///**********************************************/
 
#include <iostream>
using namespace std;

class VECTOR
{
    public:
	VECTOR(){
	_ix=0;_iy=0;
	}
	VECTOR(int ix,int iy)
	    :_ix(ix)
	    ,_iy(iy)
	{
	    cout << "VECTOR(int,int) " << _ix <<","<< _iy << endl;    
	}
	VECTOR(const VECTOR &rhs)
	:_ix(rhs._ix)
	 ,_iy(rhs._iy)
	{
	}

	VECTOR & operator =(const VECTOR &rhs)
	{
	   _ix = rhs._ix;
	   _iy = rhs._iy;
	   return *this;
	}

    private:
	int _ix;
	int _iy;
};
//g函數:返回一個元素
//結論1:函數的返回值是一個元素(復雜類型的),返回的是一個新的匿名對象(所以會調用匿名對象類的copy構造函數) //結論2:有關匿名對象的去和留。
//    如果用匿名兌現給初始化,另一個同類型的對象,匿名對象專程有名對象
//    如果用匿名對象賦值給另一個同類型的對象,匿名對象就馬上被析構掉
//
VECTOR p() { VECTOR A(21,21); return A; };
void obj1()
{

  VECTOR a=p();//用匿名對象初始化m,此時c++編譯器,直接把匿名對象專程m(扶正),從匿名專程有名字了。
    //匿名對象,被扶正,不會被析構掉
}
void obj2()
{

  VECTOR a(1,2);
  a=p();//用匿名對象賦值給另一個同類型的對象,匿名對象就馬上被析構掉
}
int main()
{
    obj1();
   obj2(); return 0; }

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM