1. 程式人生 > 其它 >為何C++拷貝建構函式引數必須為引用形式

為何C++拷貝建構函式引數必須為引用形式

技術標籤:C++語言

文章目錄

1. 引數傳遞

     在C++中,有三種方法可將資料傳遞給函式,分別是:引用、傳值和指標(C風格),它們在效率、儲存以及效能方面都有著不同的特點。對於引用,本質上就是指標,它只是作為指標的語法糖(“語法糖”一詞,來自閱讀 前橋和彌《征服C指標》一書)。因此著重分析傳值和引用兩者的區別。

1.1 傳值

     對於傳值方式,當物件或內建型別(eg:int、double、float等)資料傳遞給函式時候,函式將會為每個實參(即待傳給函式的引數)做一份拷貝操作,這也就是通常所說的“副本”,這樣一來,就需要分配額外的記憶體空間,所有值和子物件都被分別複製和儲存。對於物件型別,會呼叫拷貝建構函式拷貝一份資料,而內建型別,其拷貝成本相對較低、通常也不會帶來其他效能的影響。然而,若傳遞給函式的資料是一個大的值(比如vector、list或結構體陣列等),那麼這個複製過程對於CPU、記憶體和儲存方面都有較大的速度影響及效能損失。

      比如,一個vector中擁有一萬個字串變數,若以傳值方式給fun函式,絕對不是明智的選擇。


void fun(vector<string> v1/*v1.size() = 10000*/){}

1.2 引用方式

     當物件或內建型別以引用方式將資料傳遞給函式時,將不會產生資料拷貝的動作,因為函式被賦予的是資料本身的記憶體地址。沒有拷貝操作,也就意味著不需要額外申請記憶體地址、儲存空間和CPU。這極大的提高了CPU的處理速度和效能。

2. 初始化方式

      C++有幾種不同的初始化方式。若使用等號(“=”)來初始化一個變數,則執行的是拷貝初始化(Copy Initializition),編譯器把等號右側的值拷貝到新建立的物件中去。若不使用等號,則執行的是直接初始化

(Direct Initialization)。


string s1 = "hello world.";   //拷貝初始化
string s2("good");            //直接初始化
string s3(5, 'h');            //直接初始化

     通常情況下,當初始化值僅有一個時候,使用拷貝初始化或是直接初始化均可(比如上面示例中的s1、s2)。但是若初始化的值有多個(比如s3),往往直接使用直接初始化。當然也可以使用拷貝初始化方式,如:


string s1 = string(5, 'h');

      它等價於:


string s1;
string s2(5, 'h'); s1 = s2; 或是 string s2(5, 'h'); string s1 = s2;

2.1 拷貝建構函式

      在瞭解C++變數初始化方式及引數傳遞形式之後,進入本文主題“拷貝建構函式”。《C++ Primer》13.1.1中描述到,“如果一個建構函式的第一個引數是自身型別的引用,且任何額外引數都有預設值,則該建構函式是拷貝建構函式。” 如下面程式碼中註釋處的函式為拷貝建構函式。

class TCopy
{
    TCopy(const Tcopy &t){}   //拷貝建構函式
};

     現在有兩個疑問:

     (1) 拷貝建構函式引數一定需要以const形式麼?

     (2) 拷貝建構函式引數可不可以用傳值方式?

     我們先來看第一個問題,當拷貝建構函式的引數以非const的引用形式時,是否能夠正常工作。示例程式碼如下:

class TCopy
{
public:
    TCopy(int a)
        :m_a(a){}
        
    TCopy(TCopy &t){}
    
    void Display(){
        cout << "m_a: "<<m_a<<endl;
    }
    
private:
    int m_a;
};

int main()
{
    TCopy t(1);     //建構函式-直接初始化
    TCopy t1 = t;   //拷貝建構函式初始化
    t.Display();    //列印結果: m_a: 1
    return 0;
}

在這裡插入圖片描述
                         demo列印結果

     通過列印結果可看到,不加const修飾的引用是可以的。對於拷貝建構函式中的const引用,主要有兩處作用(強烈建議都加上)。首先,避免被調函式內部意外修改實參的值;其次,避免使用者將臨時物件繫結到非const引用上。如下示例:

class TCopy
{
public:
    TCopy(){}
    TCopy(TCopy &t){}
};

TCopy Test()
{
    TCopy t;
    return t;
}

int main()
{
    TCopy t;
    TCopy t1 = Test();
    
    return 0;
}

     編譯器將錯誤提示:

D:\code\01_09\main.cpp:25: error: no matching function for call to 'TCopy::TCopy(TCopy)'
     TCopy t1 = Test();
                     ^
D:\code\01_09\main.cpp:12: note:   no known conversion for argument 1 from 'TCopy' to 'TCopy&'

      現在來看第二個問題,以普通傳值方式,將資料傳遞給函式是否可以的問題。

class TCopy
{
public:

    TCopy(){}

    TCopy(TCopy t){}
};

      直接編譯報錯:

D:\code\1\main.cpp:12: error: invalid constructor; you probably meant 'TCopy (const TCopy&)'
     TCopy(TCopy t){}
                  ^

     當主調函式中把資料傳遞給被調函式形參時候,若以值形式傳遞,則相當於:

                 資料型別 a = b ;

     此處b為實參, a為形參。當呼叫拷貝建構函式時候,必須將同類型的物件傳遞給它,而這裡是傳值形式,所以傳遞的物件需要在引數中進行拷貝,拷貝建構函式將一個物件的值一一拷貝到另外一個物件中。然而這裡僅給出了拷貝建構函式的定義,所以該拷貝建構函式將被遞迴呼叫,直到編譯器請求記憶體空間不足為止。這也就是為何拷貝建構函式必須要以引用的形式的根本原因