類中指針數據成員的處理
一般而言,類中只含有內置類型時,只使用編譯器提供的默認constructor函數、默認destructor和默認overloaded assignment operator(重載操作符)即可,但是一旦有了指針數據成員,具體的說是指向堆中的值的指針數據成員,就得另當別論了。
由於編譯器添加的默認函數都比較簡單,對於比較簡單的類而言,通常沒有什麼問題,但是當類中有數據成員指向堆中的值時,什麼都需要程序員自己做了。
“實踐出真知”,現在拿出一個比較好的代碼分析:
這是一個簡單的animal的類,只有兩個數據成員,但其中又一個是string*類型的,它是我們討論的對象。
//Heap Data Member
//Demonstrates an object with a dynamically allocated data member
#include <iostream>
#include <string>
using namespace std;
class Animal
{
public:
Animal(const string& name = "", int age = 0);
~Animal(); //destructor prototype
Animal(const Animal& c); //copy constructor prototype
Animal& Animal::operator=(const Animal& c); //assignment operator
void Greet() const;
private:
string* m_pName; //★要注意的關鍵
int m_Age;
};
Animal::Animal(const string& name, int age)
{
cout << "(構造函數被調用)\n";
m_pName = new string(name); //另外分配內存空間
m_Age = age;
}
Animal::~Animal() //手工定義析構函數的必要性
{
cout << "(析構函數被調用)\n";
delete m_pName; //釋放內存空間
}
Animal::Animal(const Animal& c) //手工定義拷貝構造函數的必要性
{
cout << "(拷貝構造函數被調用)\n";
m_pName = new string(*(c.m_pName)); //實現“深拷貝”的關鍵
m_Age = c.m_Age;
}
Animal& Animal::operator=(const Animal& c) //手工定義操作符重載的必要性
{
cout << "(重載操作符被調用)\n";
if (this != &c)
{
delete m_pName;//別忘了釋放掉原先的內存
m_pName = new string(*(c.m_pName));
m_Age = c.m_Age;
}
return *this;
}
void Animal::Greet() const
{
cout << "你好,我叫" << *m_pName << " ,我今年" << m_Age << "歲了。 ";
cout << "&m_pName: " << cout << &m_pName << endl;
}
//聲明3個測試函數
void testDestructor();
void testCopyConstructor(Animal aCopy);
void testAssignmentOp();
int main()
{
testDestructor();
cout << endl;
Animal crit("Poochie", 5);
crit.Greet();
testCopyConstructor(crit);
crit.Greet();
cout << endl;
testAssignmentOp();
return 0;
}
void testDestructor()
{
Animal toDestroy("Rover", 3);
toDestroy.Greet();
} //運行結束時,toDestroy對象隨即被銷燬
void testCopyConstructor(Animal aCopy) //注:不是引用類型
{
aCopy.Greet();
}
void testAssignmentOp()
{
Animal ani1("ani1", 7);
Animal ani2("ani2", 9);
ani1 = ani2;
ani1.Greet();
ani2.Greet();
cout << endl;
Animal ani3("ani", 11);
ani3 = ani3;
ani3.Greet();
}
運行結果:
可以發現:Animal對象被銷燬、複製以及相互賦值是,對這些數據成員做出了不同的處理,具體分析如下。
析構函數
當對象的數據成員指向堆中的值時,可能產生的問題就是內存泄露。如果不編寫自己的析構函數,則編譯器會替程序員創建一個默認析構函數,但她並不嘗試釋放掉任何數據成員指向的堆中的內存。當類中有數據成員指向堆中值時,則應當編寫自己的析構函數,以便能在對象消失以前釋放掉與對象相關的堆中內存,避免內存泄露。所以在這裏的析構函數必須對申請的堆中內存做相應處理:
delete m_pName; //釋放內存空間
main函數中調用testDestructor()時測試了析構函數。它創建了一個toDestroy的對象,並且打印出m_pName中的堆中字符的地址。當testDestructor()調用完畢要返回main時,自動調用了析構函數,釋放掉toDestroy對象佔用的內存,包括“Rover”字符串佔用的堆中內存。析構函數對m_Age沒有做任何處理,這完全沒有問題,因爲m_Age不在堆中,而是toDestroy的一部分,並且會隨Animal對象的其餘部分被妥善的處理。
總之:如果在堆中分配內存,則應當編寫析構函數來清理與釋放堆中的內存。
拷貝構造函數
同構造函數和析構函數一樣簡單,編譯前生成的默認拷貝構造函數只是簡單地將每個數據成員的值複製給新對象同名數據成員,即按成員逐項進行復制。但在我們這個函數中卻不能在使用默認拷貝構造函數,原因還是m_pName的存在。如果只是用默認拷貝構造函數,對象的自動複製將會導致新的對象指向堆中的同一個字符串,因爲新對象的指針僅僅獲得存儲在原始對象的指針中地址的一個副本。這就造成了數據的淺拷貝。真正需要的拷貝構造函數是能讓新生成的對象在堆中擁有自己的內存塊,對象中的每個數據成員都指向一個隊中的對象,這就是深拷貝。所以在Animal類的默認拷貝構造函數需要對m_pName成員分配新的堆內存。
m_pName = new string(*(c.m_pName));
觀察程序,發現當testCopyConstructor()時使用的m_pName堆中地址與main中crit使用的m_pName地址並不相同,這就實現了堆內存數據的複製。當testCopyConstructor()執行結束時,析構函數被調用,釋放了對象內存。
總之:當類的數據成員指向堆中內存時,應當考慮編寫手動拷貝構造函數來爲新對象分配內存,實現深拷貝。
賦值運算符的重載
同上述幾個函數一樣,如果程序員沒有編寫自己的賦值運算符成員函數,編譯器就會爲程序員提供一個默認的成員函數,但這也是相當簡單的。
如果只是用默認的賦值運算符成員函數,也只是實現的淺拷貝。所以Animal的賦值運算符成員函數應當寫成:
Animal& Animal::operator=(const Animal& c) //手工定義操作符重載的必要性
{
cout << "(重載操作符被調用)\n";
if (this != &c)
{
delete m_pName;//別忘了釋放掉原先的內存
m_pName = new string(*(c.m_pName));
m_Age = c.m_Age;
}
return *this;
}
main中的testAssignmentOp()測試了賦值運算符的重載。
總之:當類中有數據成員指向堆中內存時,應當考慮爲該類重載賦值運算符。
如需轉載,請註明出處:http://write.blog.csdn.net/postedit/8046379