C++拷貝構造函數(複製構造函數)詳解

c語言小新 發佈 2024-03-01T21:02:58.039825+00:00

拷貝和複製是一個意思,對應的英文單詞都是copy。對於計算機來說,拷貝是指用一份原有的、已經存在的數據創建出一份新的數據,最終的結果是多了一份相同的數據。

拷貝和複製是一個意思,對應的英文單詞都是copy對於計算機來說,拷貝是指用一份原有的、已經存在的數據創建出一份新的數據,最終的結果是多了一份相同的數據。例如,將 Word 文檔拷貝到U盤去複印店列印,將 D 盤的圖片拷貝到桌面以方便瀏覽,將重要的文件上傳到百度網盤以防止丟失等,都是「創建一份新數據」的意思。

中,拷貝並沒有脫離它本來的含義,只是將這個含義進行了「特化」,是指用已經存在的對象創建出一個新的對象。從本質上講,對象也是一份數據,因為它會占用內存。

嚴格來說,對象的創建包括兩個階段,首先要分配內存空間,然後再進行初始化:

  • 分配內存很好理解,就是在堆區、棧區或者全局數據區留出足夠多的字節。這個時候的內存還比較「原始」,沒有被「教化」,它所包含的數據一般是零值或者隨機值,沒有實際的意義。
  • 初始化就是首次對內存賦值,讓它的數據有意義。注意是首次賦值,再次賦值不叫初始化。初始化的時候還可以為對象分配其他的資源(打開文件、連接網絡、動態分配內存等),或者提前進行一些計算(根據價格和數量計算出總價、根據長度和寬度計算出矩形的面積等)等。說白了,初始化就是調用構造函數。


很明顯,這裡所說的拷貝是在初始化階段進行的,也就是用其它對象的數據來初始化新對象的內存。

那麼,如何用拷貝的方式來初始化一個對象呢?其實這樣的例子比比皆是,string 類就是一個典型的例子。

#include <iostream>
#include <string>
using namespace std;
void func(string str){
    cout<<str<<endl;
}
int main(){
    string s1 = "http://c.biancheng.net";
    string s2(s1);
    string s3 = s1;
    string s4 = s1 + " " + s2;
    func(s1);
    cout<<s1<<endl<<s2<<endl<<s3<<endl<<s4<<endl;
   
    return 0;
}

運行結果:
http://c.biancheng.net


http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net http://c.biancheng.net

s1、s2、s3、s4 以及 func() 的形參 str,都是使用拷貝的方式來初始化的。

對於 s1,表面上看起來是將一個字符串直接賦值給了 s1,實際上在內部進行了類型轉換,將 const char * 類型轉換為 string 類型後才賦值的,這點我們將在《C++轉換構造函數》一節中詳細講解。s4 也是類似的道理。

對於 s1、s2、s3、s4,都是將其它對象的數據拷貝給當前對象,以完成當前對象的初始化。

對於 func() 的形參 str,其實在定義時就為它分配了內存,但是此時並沒有初始化,只有等到調用 func() 時,才會將其它對象的數據拷貝給 str 以完成初始化。

拷貝構造函數(Copy Constructor)

下面的例子演示了拷貝構造函數的定義和使用:

#include <iostream>
#include <string>
using namespace std;
class Student{
public:
    Student(string name = "", int age = 0, float score = 0.0f);  //普通構造函數
    Student(const Student &stu);  //拷貝構造函數(聲明)
public:
    void display();
private:
    string m_name;
    int m_age;
    float m_score;
};
Student::Student(string name, int age, float score): m_name(name), m_age(age), m_score(score){ }
//拷貝構造函數(定義)
Student::Student(const Student &stu){
    this->m_name = stu.m_name;
    this->m_age = stu.m_age;
    this->m_score = stu.m_score;
   
    cout<<"Copy constructor was called."<<endl;
}
void Student::display(){
    cout<<m_name<<"的年齡是"<<m_age<<",成績是"<<m_score<<endl;
}
int main(){
    Student stu1("小明", 16, 90.5);
    Student stu2 = stu1;  //調用拷貝構造函數
    Student stu3(stu1);  //調用拷貝構造函數
    stu1.display();
    stu2.display();
    stu3.display();
   
    return 0;
}

運行結果:
Copy constructor was called.
Copy constructor was called.
小明的年齡是16,成績是90.5
小明的年齡是16,成績是90.5
小明的年齡是16,成績是90.5

第 8 行是拷貝構造函數的聲明,第 20 行是拷貝構造函數的定義。拷貝構造函數只有一個參數,它的類型是當前類的引用,而且一般都是 const 引用。

1) 為什麼必須是當前類的引用呢?

如果拷貝構造函數的參數不是當前類的引用,而是當前類的對象,那麼在調用拷貝構造函數時,會將另外一個對象直接傳遞給形參,這本身就是一次拷貝,會再次調用拷貝構造函數,然後又將一個對象直接傳遞給了形參,將繼續調用拷貝構造函數……這個過程會一直持續下去,沒有盡頭,陷入死循環。

只有當參數是當前類的引用時,才不會導致再次調用拷貝構造函數,這不僅是邏輯上的要求,也是 C++ 語法的要求。

2) 為什麼是 const 引用呢?

拷貝構造函數的目的是用其它對象的數據來初始化當前對象,並沒有期望更改其它對象的數據,添加 const 限制後,這個含義更加明確了。



以上面的 Student 類為例,將 const 去掉後,拷貝構造函數的原型變為:

Student::Student(Student &stu);

此時,下面的代碼就會發生錯誤:

const Student stu1("小明", 16, 90.5);Student stu2 = stu1;Student stu3(stu1);

stu1 是 const 類型,在初始化 stu2、stu3 時,編譯器希望調用Student::Student(const Student &stu),但是這個函數卻不存在,又不能將 const Student 類型轉換為 Student 類型去調用Student::Student(Student &stu),所以最終調用失敗了。

默認拷貝構造函數

在前面的教程中,我們還沒有講解拷貝構造函數,但是卻已經在使用拷貝的方式創建對象了,並且也沒有引發什麼錯誤。這是因為,如果程式設計師沒有顯式地定義拷貝構造函數,那麼編譯器會自動生成一個默認的拷貝構造函數。這個默認的拷貝構造函數很簡單,就是使用「老對象」的成員變量對「新對象」的成員變量進行一一賦值,和上面 Student 類的拷貝構造函數非常類似。

對於簡單的類,默認拷貝構造函數一般是夠用的,我們也沒有必要再顯式地定義一個功能類似的拷貝構造函數。但是當類持有其它資源時,如動態分配的內存、打開的文件、指向其他數據的
指針、網絡連接等,默認拷貝構造函數就不能拷貝這些資源,我們必須顯式地定義拷貝構造函數,以完整地拷貝對象的所有數據,這點我們將在《C++深拷貝和淺拷貝》一節中深入講解。

關鍵字: