一、什麼是拷貝構造函數
拷貝構造函數是一種特殊的構造函數,它在對象作為參數傳遞給函數或函數返回對象時被調用。拷貝構造函數可以將一個對象作為另一個對象的副本來創建,即初始化。這個副本跟原對象有着一模一樣的數據和屬性。
可以用下面的代碼來定義一個簡單的類,並為它定義一個拷貝構造函數。
class MyString { public: MyString(const char* str = NULL); // 構造函數 MyString(const MyString& other); // 拷貝構造函數 ~MyString(); // 析構函數 private: char* m_data; };
調用拷貝構造函數的方式有兩種:一種顯式調用,另一種隱式調用。
顯式調用拷貝構造函數的方式是創建一個對象並將其初始化為另一個對象,比如下面的代碼。
// 顯式調用 MyString str1 = "Hello world"; MyString str2(str1);
在這裡,我們創建了兩個 MyString 類型的對象 str1 和 str2。第一個對象被顯式初始化為字符串 “Hello world”,這個過程會調用構造函數。第二個對象 str2 被顯式初始化為第一個對象 str1 的副本,這個過程會調用拷貝構造函數。
隱式調用拷貝構造函數的方式有很多,比如函數參數傳遞、函數返回值等。比如下面的代碼:
// 隱式調用 void func(MyString str) { // do something... } MyString func2() { MyString str = "Hello world"; return str; } int main() { MyString str = "Hello world"; func(str); MyString str2 = func2(); return 0; }
在這裡,我們定義了一個函數 func,它的參數是 MyString 類型的對象 str。當我們調用這個函數時,會創建一個 MyString 類型的對象並將其初始化為傳入的參數對象,這個過程會調用拷貝構造函數。類似地,我們還定義了一個函數 func2,它返回一個 MyString 類型的對象。當我們將其賦值給一個 MyString 類型的對象 str2 時,也會調用拷貝構造函數。
二、淺拷貝和深拷貝
在定義拷貝構造函數時,需要考慮到對象內存的複製問題。如果只是簡單地將一個對象的內存複製到另一個對象中,叫做淺拷貝。淺拷貝可能會導致問題,主要是因為多個對象共享同一塊內存資源,導致其中一個對象的內存被釋放後,其他對象仍然指向該內存地址,訪問該地址的結果就可能是未定義的。
為了避免這種問題,我們需要使用深拷貝。深拷貝將不僅複製對象的內存,而且會分配一段新的內存,將原來對象的數據複製到這段新的內存中,從而避免多個對象共享同一塊內存資源的問題。
下面是深拷貝和淺拷貝的示例代碼。
class MyString { public: MyString(char* str = NULL); //構造函數 MyString(const MyString& other); //拷貝構造函數,進行深拷貝 ~MyString(); //析構函數 private: char* m_data; }; MyString::MyString(char* str) { if (str == NULL) { m_data = new char[1]; *m_data = '\0'; } else { int len = strlen(str); m_data = new char[len + 1]; strcpy(m_data, str); } } MyString::MyString(const MyString& other) { int len = strlen(other.m_data); m_data = new char[len + 1]; strcpy(m_data, other.m_data); } MyString::~MyString() { delete[] m_data; } int main() { char* str = "Hello world"; MyString myStr(str); // 調用構造函數 MyString myStr2(myStr); // 調用拷貝構造函數 return 0; }
在這裡,我們定義了一個 MyString 類,並為它定義了構造函數和拷貝構造函數。構造函數會根據傳入的字符串分配內存,並將字符串複製到這段內存中。拷貝構造函數進行深拷貝,它會為新對象分配一段新的內存,並將原對象的數據複製到這段新的內存中。
在 main 函數中,我們調用了 MyString 的構造函數和拷貝構造函數,分別創建出兩個對象 myStr 和 myStr2,這兩個對象在內存中擁有不同的地址。
三、默認拷貝構造函數
如果我們沒有定義自己的拷貝構造函數,編譯器就會自動生成一個默認的拷貝構造函數。默認拷貝構造函數與複製構造函數的行為類似:將原對象的所有屬性值都複製到新對象中。默認拷貝構造函數的實現非常簡單,就是將原對象的內存複製到新對象中。
下面是默認拷貝構造函數的示例代碼:
class MyString { public: char* m_data; }; int main() { char* str = "Hello world"; MyString myStr1; myStr1.m_data = new char[strlen(str) + 1]; strcpy(myStr1.m_data, str); MyString myStr2(myStr1); // 調用默認拷貝構造函數 return 0; }
在這裡,我們定義了一個 MyString 類,並為它定義了一個屬性 m_data。在 main 函數中,我們創建了一個對象 myStr1,並為它分配了一段內存,並將字符串 “Hello world” 複製到這段內存中。然後我們用 myStr1 創建了一個新對象 myStr2,這個過程會調用默認拷貝構造函數。
默認拷貝構造函數的實現就是逐個複製原對象的屬性值到新對象中,代碼如下:
MyString(const MyString& other) { m_data = other.m_data; }
可以看到,這個拷貝構造函數實現非常簡單,只是將原對象的內存地址複製到新對象中。由於兩個對象共享同一塊內存,這就導致了上面提到的多個對象共享同一塊內存資源的問題,因此必須自己定義拷貝構造函數,避免這個問題。
結論
拷貝構造函數是一種特殊的構造函數,它可以將一個對象作為另一個對象的副本來創建,即初始化。拷貝構造函數有兩種調用方式:一種是顯式調用,另一種是隱式調用。拷貝構造函數需要根據對象內存的複製問題來考慮如何實現,如果只是簡單地將一個對象的內存複製到另一個對象中,叫做淺拷貝;否則,需要使用深拷貝避免共享同一地址的問題。如果沒有定義自己的拷貝構造函數,編譯器會自動生成一個默認的拷貝構造函數,但這個函數會導致多個對象共享同一塊內存資源的問題,因此自己定義拷貝構造函數是必須的。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/235802.html