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;
}


//---------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對象的創建.多麼精妙!


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