C++類默認拷貝構造函數的弊端
C++類的中有兩個特殊的構造函數,(1)無參構造函數,(2)拷貝構造函數。它們的特殊之處在於:
(1) 當類中沒有定義任何構造函數時,編譯器會默認提供一個無參構造函數且其函數體為空;
(2) 當類中沒有定義拷貝構造函數時,編譯器會默認提供一個拷貝構造函數,進行成員變量之間的拷貝。(這個拷貝操作是淺拷貝)
這里只講拷貝構造函數。在C語言中,
int a = 5; //初始化
int b;
b = 6; //賦值
上面的初始化及賦值操作是最正常不過的語法,C++語言肩挑兼容C語言語法的責任,所以在類的設計上,也兼容這種操作:
class cls
{
pubic:
//...
}
int main(void)
{
cls c1;
cls c2 = c1; //初始化類,還可以 cls c2(c1);
cls c3;
c3 = c1; //賦值類
//...
return 0;
}
如上的初始化類需要調用到cls類的默認實現的拷貝構造函數,為類賦值需要調用的是cls類的默認實現的賦值操作符重載函數,它們都是淺度拷貝的。前者其原型為:
cls(const cls& c)
默認的拷貝構造函數存在弊端,看如下類定義:
class TestCls{
public:
int a;
int *p;
public:
TestCls() //無參構造函數
{
std::cout<<"TestCls()"<<std::endl;
p = new int;
}
~TestCls() //析構函數
{
delete p;
std::cout<<"~TestCls()"<<std::endl;
}
};
類中的指針p在構造函數中分配的空間,在析構函數中釋放。
int main(void)
{
TestCls t;
return 0;
}
編譯運行確實不會出錯:
類在我們沒有定義拷貝構造函數的時候,會默認定義默認拷貝構造函數,也就是說可以直接用同類型的類間可以相互賦值、初始化:
int main(void)
{
TestCls t1;
TestCls t2 = t1; //效果等同於TestCls t2(t1);
return 0;
}
編譯通過,運行卻出錯了:
原因就在於,默認的拷貝構造函數實現的是淺拷貝。
深度拷貝和淺拷貝
深度拷貝和淺拷貝在c語言中就經常遇到的了,在這里我簡單描述。
一般的賦值操作是深度拷貝:
//深度拷貝
int a = 5;
int b = a;
簡單的指針指向,則是淺拷貝:
//淺拷貝
int a = 8;
int *p;
p = &a;
char* str1 = "HelloWorld";
char* str2 = str1;
將上面的淺拷貝改為深度拷貝后:
//深度拷貝
int a = 8;
int *p = new int;
*p = a;
char* str1 = "HelloWorld";
int len = strlen(str1);
char *str2 = new char[len];
memcpy(str2, str1, len);
總而言之,拷貝者和被拷貝者若是同一個地址,則為淺拷貝,反之為深拷貝。
例:以字符串拷貝為例,淺拷貝后,str1和str2同指向0x123456,不管哪一個指針,對該空間內容的修改都會影響另一個指針。
深拷貝后,str1和str2指向不同的內存空間,各自的空間的內容一樣。因為空間不同,所以不管哪一個指針,對該空間內容的修改都不會影響另一個指針。
解決默認拷貝構造函數的弊端
類的默認拷貝構造函數只會用被拷貝類的成員的值為拷貝類簡單初始化,也就是說二者的p指針指向的內存空間是一致的。以前面TestCls可以知道,編譯器為我們默認定義的拷貝構造函數為:
TestCls(const TestCls& testCls)
{
a = testCls.a;
p = testCls.p; //兩個類的p指針指向的地址一致。
}
解釋:main函數將要退出時,拷貝類t2的析構函數先得到執行,它把自身p指向的堆空間釋放了;接下來,t1的析構函數得到調用,被拷貝類t1的析構函數得到調用,它同樣要去析構自身的p指向指向的堆空間,但是該空間和t2類中p指向的空間一樣,造成重復釋放,程序運行崩潰。
解決辦法十分簡單,自定義拷貝構造函數,里面用深度拷貝的方式為拷貝類初始化:
class TestCls{
public:
int a;
int *p;
public:
TestCls()
{
std::cout<<"TestCls()"<<std::endl;
p = new int;
}
TestCls(const TestCls& testCls)
{
std::cout<<"TestCls(const TestCls& testCls)"<<std::endl;
a = testCls.a;
//p = testCls.p;
p = new int;
*p = *(testCls.p); //為拷貝類的p指針分配空間,實現深度拷貝
}
~TestCls()
{
delete p;
std::cout<<"~TestCls()"<<std::endl;
}
};
int main(void)
{
TestCls t1;
TestCls t2 = t1;
return 0;
}
編譯運行正常:
關於c++拷貝構造函數的深度拷貝和淺拷貝的介紹到這里,其實還可以將它們的地址打印出來看看,不過這一步就不再贅述了。
拷貝構造函數其它妙用
自定義拷貝構造函數,並設置為private屬性,其實現體可以什么都不寫,那么這個類將變成一個不可被復制的類了。
[參考博文: https://blog.csdn.net/qq_29344757/article/details/76037255]