C++拷貝建構函式(複製建構函式)詳解

2020-07-16 10:04:22
複製建構函式是建構函式的一種,也稱拷貝建構函式,它只有一個引數,引數型別是本類的參照。

複製建構函式的引數可以是 const 參照,也可以是非 const 參照。 一般使用前者,這樣既能以常數物件(初始化後值不能改變的物件)作為引數,也能以非常數物件作為引數去初始化其他物件。一個類中寫兩個複製建構函式,一個的引數是 const 參照,另一個的引數是非 const 參照,也是可以的。

如果類的設計者不寫複製建構函式,編譯器就會自動生成複製建構函式。大多數情況下,其作用是實現從源物件到目標物件逐個位元組的複製,即使得目標物件的每個成員變數都變得和源物件相等。編譯器自動生成的複製建構函式稱為“預設複製建構函式”。

注意,預設建構函式(即無參建構函式)不一定存在,但是複製建構函式總是會存在。

下面是一個複製建構函式的例子。
#include<iostream >
using namespace std;
class Complex
{
public:
    double real, imag;
    Complex(double r, double i) {
        real= r; imag = i;
    }
};
int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //用複製建構函式初始化c2
    cout<<c2.real<<","<<c2.imag;  //輸出 1,2
    return 0;
}
第 13 行給出了初始化 c2 的引數,即 c1。只有編譯器自動生成的那個預設複製建構函式的引數才能和 c1 匹配,因此,c2 就是以 c1 為引數,呼叫預設複製建構函式進行初始化的。初始化的結果是 c2 成為 c1 的複製品,即 c2 和 c1 每個成員變數的值都相等。

如果編寫了複製建構函式,則預設複製建構函式就不存在了。下面是一個非預設複製建構函式的例子。
#include<iostream>
using namespace std;
class Complex{
public:
    double real, imag;
    Complex(double r,double i){
        real = r; imag = i;
    }
    Complex(const Complex & c){
        real = c.real; imag = c.imag;
        cout<<"Copy Constructor called"<<endl ;
    }
};

int main(){
    Complex cl(1, 2);
    Complex c2 (cl);  //呼叫複製建構函式
    cout<<c2.real<<","<<c2.imag;
    return 0;
}
程式的輸出結果是:
Copy Constructor called
1,2

第 9 行,複製建構函式的引數加不加 const 對本程式來說都一樣。但加上 const 是更好的做法,這樣複製建構函式才能接受常數物件作為引數,即才能以常數物件作為引數去初始化別的物件。

第 17 行,就是以 c1 為引數呼叫第 9 行的那個複製建構函式初始化的。該複製建構函式執行的結果是使 c2 和 c1 相等,此外還輸出Copy Constructor called

可以想象,如果將第 10 行刪去或改成real = 2*c.real; imag = imag + 1;,那麼 c2 的值就不會等於 c1 了。也就是說,自己編寫的複製建構函式並不一定要做複製的工作(如果只做複製工作,那麼使用編譯器自動生成的預設複製建構函式就行了)。但從習慣上來講,複製建構函式還是應該完成類似於複製的工作為好,在此基礎上還可以根據需要做些別的操作。

建構函式不能以本類的物件作為唯一引數,以免和複製建構函式相混淆。例如,不能寫如下建構函式:

Complex (Complex c) {...}

複製建構函式被呼叫的三種情況

複製建構函式在以下三種情況下會被呼叫。

1) 當用一個物件去初始化同類的另一個物件時,會引發複製建構函式被呼叫。例如,下面的兩條語句都會引發複製建構函式的呼叫,用以初始化 c2。
Complex c2(c1);
Complex c2 = c1;
這兩條語句是等價的。

注意,第二條語句是初始化語句,不是賦值語句。賦值語句的等號左邊是一個早已有定義的變數,賦值語句不會引發複製建構函式的呼叫。例如:
Complex c1, c2; c1 = c2 ;
c1=c2;
這條語句不會引發複製建構函式的呼叫,因為 c1 早已生成,已經初始化過了。

2) 如果函數 F 的引數是類 A 的物件,那麼當 F 被呼叫時,類 A 的複製建構函式將被呼叫。換句話說,作為形參的物件,是用複製建構函式初始化的,而且呼叫複製建構函式時的引數,就是呼叫函數時所給的實參。
#include<iostream>
using namespace std;
class A{
public:
    A(){};
    A(A & a){
        cout<<"Copy constructor called"<<endl;
    }
};

void Func(A a){ }

int main(){
    A a;
    Func(a);
    return 0;
}
程式的輸出結果為:
Copy constructor called

這是因為 Func 函數的形參 a 在初始化時呼叫了複製建構函式。

前面說過,函數的形參的值等於函數呼叫時對應的實參,現在可以知道這不一定是正確的。如果形參是一個物件,那麼形參的值是否等於實參,取決於該物件所屬的類的複製建構函式是如何實現的。例如上面的例子,Func 函數的形參 a 的值在進入函數時是隨機的,未必等於實參,因為複製建構函式沒有做複製的工作。

以物件作為函數的形參,在函數被呼叫時,生成的形參要用複製建構函式初始化,這會帶來時間上的開銷。如果用物件的參照而不是物件作為形參,就沒有這個問題了。但是以參照作為形參有一定的風險,因為這種情況下如果形參的值發生改變,實參的值也會跟著改變。

如果要確保實參的值不會改變,又希望避免複製建構函式帶來的開銷,解決辦法就是將形參宣告為物件的 const 參照。例如:
void Function(const Complex & c)
{
    ...
}
這樣,Function 函數中出現任何有可能導致 c 的值被修改的語句,都會引發編譯錯誤。

思考題:在上面的 Function 函數中,除了賦值語句,還有什麼語句有可能改變 c 的值?例如,是否允許通過 c 呼叫 Complex 的成員函數?

3) 如果函數的返冋值是類 A 的物件,則函數返冋時,類 A 的複製建構函式被呼叫。換言之,作為函數返回值的物件是用複製建構函式初始化 的,而呼叫複製建構函式時的實參,就是 return 語句所返回的物件。例如下面的程式:
#include<iostream>
using namespace std;
class A {
public:
    int v;
    A(int n) { v = n; };
    A(const A & a) {
        v = a.v;
        cout << "Copy constructor called" << endl;
    }
};

A Func() {
    A a(4);
    return a;
}

int main() {
    cout << Func().v << endl;
    return 0;
}
程式的輸出結果是:
Copy constructor called
4

第19行呼叫了 Func 函數,其返回值是一個物件,該物件就是用複製建構函式初始化的, 而且呼叫複製建構函式時,實參就是第 16 行 return 語句所返回的 a。複製建構函式在第 9 行確實完成了複製的工作,所以第 19 行 Func 函數的返回值和第 14 行的 a 相等。

需要說明的是,有些編譯器出於程式執行效率的考慮,編譯的時候進行了優化,函數返回值物件就不用複製建構函式初始化了,這並不符合 C++ 的標準。上面的程式,用 Visual Studio 2010 編譯後的輸出結果如上所述,但是在 Dev C++ 4.9 中不會呼叫複製建構函式。把第 14 行的 a 變成全域性變數,才會呼叫複製建構函式。對這一點,讀者不必深究。