c++中的深拷貝和淺拷貝
我們有時候會聽說c++中有深拷貝和淺拷貝的說法,那麼深拷貝和淺拷貝的區別是什麼呢?簡單來說,淺拷貝就是簡單的賦值拷貝操作;而深拷貝是程式設計師手動在堆區重新申請一塊記憶體然後進行拷貝操作,那麼這麼做的意義何在呢,請看下面的一個場景,
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
string m_name;
int *m_age;
Student(string name, int age)//有參建構函式
{
cout << "有參建構函式呼叫。。" << endl;
m_name = name;
m_age = new int(age);//在堆區申請int型別的指標儲存傳入的年齡值
}
};
int main()
{
Student s1("Tom", 20);
cout << "s1姓名: " << s1.m_name << "年齡: " << *s1.m_age << endl;
Student s2(s1); //此處隱式呼叫編譯器預設的拷貝建構函式將物件s1的屬性拷貝給物件s2
cout << "s2姓名: " << s2.m_name << "年齡: " << *s2.m_age << endl;
system("pause");
return 0;
}
建立Student學生類,在其中自定義一個有參建構函式,在堆區手動new申請一塊記憶體使用一個int型別的指標接收建立例項物件時傳入的學生年齡,這裡隱式呼叫了編譯器預設的拷貝建構函式來建立物件s2:
Student(const Student& s)//(虛擬碼)編譯器預設的拷貝建構函式
{
m_name = s.m_name;//此處為淺拷貝
m_age = (s.m_age);//淺拷貝即簡單的賦值拷貝操作
}
執行結果如下:
看起來執行結果是沒什麼問題,在程式執行結束後由預設解構函式來自動釋放回收棧區記憶體(這裡沒有顯示);但是我們說,在堆區手動開闢的記憶體也要由程式設計師手動釋放,這一點尤其是對執行大型的複雜程式很有必要,所以我們嘗試在上面的Student類中過載解構函式來手動釋放堆區的記憶體:
~Student()//解構函式宣告
{
if ( m_age != NULL)
{
delete m_age;//手動釋放堆區記憶體
m_age = NULL;//防止野指標出現
}
}
然而函式執行結束呼叫解構函式時程式崩了,這是因為在呼叫預設拷貝建構函式建立物件s2時是淺拷貝,僅僅是將指標s1.m_age的值拷貝給了s2.m_age,此時s1.m_age和s2.m_age都指向同一塊記憶體空間(即儲存s1.m_age的那塊堆區的記憶體地址),程式執行結束呼叫解構函式時,根據先進後出的原則,他們指向的那塊堆區記憶體首先被s2的解構函式釋放掉,隨後當s1的解構函式也來試圖釋放這塊記憶體時就會出錯,因為那塊空間此時已經被s2的解構函式釋放掉了,所以淺拷貝帶來的問題:堆區記憶體的重複釋放。
解決方法也很簡單,在Student類中過載拷貝建構函式,在給新物件賦值同時new手動申請一塊新的記憶體空間即可:
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
string m_name;
int *m_age;
Student(string name, int age)//有參建構函式
{
cout << "有參建構函式呼叫。。" << endl;
m_name = name;
m_age = new int(age);//在堆區申請int型別的指標儲存年齡值
}
Student(const Student& s)//過載後的拷貝建構函式
{
cout << "拷貝建構函式呼叫。。" << endl;
m_name = s.m_name;
m_age = new int(*s.m_age);//深拷貝,在堆區手動申請一塊新的記憶體進行賦值
}
~Student()//解構函式宣告
{
cout << "解構函式呼叫。。" << endl;
if ( m_age != NULL)
{
delete m_age;//手動釋放堆區記憶體
m_age = NULL;//防止野指標出現
}
}
};
int main()
{
Student s1("Tom", 20);
cout << "s1姓名: " << s1.m_name << "年齡: " << *s1.m_age << endl;
Student s2(s1);//此處隱式呼叫過載後的拷貝建構函式進行深拷貝操作
cout << "s2姓名: " << s2.m_name << "年齡: " << *s2.m_age << endl;
system("pause");
return 0;
}
在呼叫拷貝建構函式建立物件s2時是深拷貝,在用一塊新的儲存空間來儲存指標s1.m_age指向的那塊記憶體的值,物件s1.m_age和s2.m_age分別指向一塊不同的堆區記憶體,所以在呼叫各自的解構函式釋放記憶體時互不影響。
總結:淺拷貝只是簡單的賦值拷貝操作,若在類中有在堆區開闢的屬性,一定要自己過載拷貝建構函式進行深拷貝,防止淺拷貝帶來的堆區記憶體重複釋放的問題。