為何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),編譯器把等號右側的值拷貝到新建立的物件中去。若不使用等號,則執行的是直接初始化
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為形參。當呼叫拷貝建構函式時候,必須將同類型的物件傳遞給它,而這裡是傳值形式,所以傳遞的物件需要在引數中進行拷貝,拷貝建構函式將一個物件的值一一拷貝到另外一個物件中。然而這裡僅給出了拷貝建構函式的定義,所以該拷貝建構函式將被遞迴呼叫,直到編譯器請求記憶體空間不足為止。這也就是為何拷貝建構函式必須要以引用的形式的根本原因。