C++臨時物件

licup123發表於2010-04-27
 書歸正傳,我們知道在C++的建立物件是一個費時,費空間的一個操作。有些固然是必不可少,但還有一些物件卻在我們不知道的情況下被建立了。通常以下三種情況會產生臨時物件:
  1,以值的方式給函式傳參;
  2,型別轉換;
  3,函式需要返回一個物件時;
 
現在我們依次看這三種情況:
 
  一,以值的方式給函式傳參。
 
  我們知道給函式傳參有兩種方式。1,按值傳遞;2,按引用傳遞。按值傳遞時,首先將需要傳給函式的引數,呼叫拷貝建構函式建立一個副本,所有在函式裡的操作都是針對這個副本的,也正是因為這個原因,在函式體裡對該副本進行任何操作,都不會影響原引數。我們看以下例子:
 

/*-----------------------
Platform.:WinXp + VC6
-----------------------*/

#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物件的建立.多麼精妙!

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10697500/viewspace-660896/,如需轉載,請註明出處,否則將追究法律責任。

相關文章