在C++中,對于一個類,C++的編譯器都會為這個類提供四個預設函數,分别是:
A() //預設構造函數
~A() //預設析構函數
A(const A&) //預設拷貝構造函數
A& operator = (const A &) //預設指派函數。
這四個函數如果我們不自行定義,将由編譯器自動生成這四個預設的函數,下面讓我們來看看這四個函數(重點是後兩個)。
一. 構造函數
構造函數是一種特殊的成員函數,與其他成員函數不同,不需要使用者來調用它,而是在建立對象時自動執行。構造函數的功能是由使用者定義的,使用者根據初始化的要求設計函數體和函數參數,可以是一個,也可以是多個,可以把構造函數了解為重載的一種(函數名相同,不會傳回任何類型,也不可以是void類型,參數類型個數可不同)。
class Animal
{
private:
string name;
public:
Animal();//預設構造函數
Animal(string n);//也可以自定義構造函數
};
Animal::Animal()
{
//什麼都不做
}
Animal::Animal(string n)
{
this->name = n;
}
int main()
{
//第一種執行個體化對象的方法
Animal * a = new Animal(); //将調用預設構造函數
Animal * b = new Animal("花狗"); //将調用自定義的構造函數,對name變量初始化。
//第二種執行個體化對象的方法
Animal c; //将調用預設構造函數
//注意:對于無參構造函數,不可以使用Animal c(),
Animal c("花狗");//将調用自定義構造函數,對name變量初始化。
return 0;
}
複制
構造函數的作用就是對目前類對象起到一個初始化的作用,類對象不像我們基本類型那樣,在很多時候都需要初始化一些成員變量。
可以看到構造函數被聲明在public裡面,那麼可以聲明在private裡面嗎?是可以的,隻不過不能被外部執行個體化了,在設計模式中有一種單例模式,就是這樣設計的,有興趣的可以了解一下。
二. 析構函數
與構造函數相對立的是析構函數,這個函數在對象銷毀之前自動調用,例如在構造函數中,我們為成員變量申請了記憶體,我們就可以在析構函數中将申請的記憶體釋放,析構函數的寫法是在構造函數的基礎上加一個~符号,并且隻能有一個析構函數。
class Animal
{
private:
string name;
public:
Animal();//預設構造函數
~Animal(); //預設析構函數
};
複制
三. 拷貝構造函數
1.淺拷貝
class Animal
{
private:
string name;
public:
Animal()
{
name = "花狗";
cout << "Animal" << endl;
}
~Animal()
{
cout << "~Animal:" << (int)&name << endl;
}
};
int main()
{
Animal a;
Animal b(a);
return 0;
}
複制
運作結果:
這個例子調用的是預設的拷貝構造函數(注意看控制台顯示,調用了一次構造函數和兩次析構函數),可以看出兩個對象的成員變量位址是不一樣的,當成員變量不存在指針類型是,這樣做沒什麼問題,當類中有指針變量,自動生成的拷貝函數注定會出錯,往下看。
2.深拷貝
我們将成員變量換成指針變量,繼續實驗。
class Animal
{
private:
//string name;
string * name;
public:
Animal()
{
name = new string("花狗");
cout << "Animal" << endl;
}
~Animal()
{
cout << "~Animal:" << (int)name << endl;
}
};
int main()
{
Animal a;
Animal b(a);
return 0;
}
複制
運作結果:
可以看到兩個對象的指針成員所指的記憶體相同(記憶體裡面存着字元串:花狗),還記得析構函數的作用嗎,在對象銷毀之前自動調用,在構造函數中,我們為成員變量申請了記憶體,我們就可以在析構函數中将申請的記憶體釋放。
現在在析構函數中加上對name釋放的代碼:
~Animal()
{
cout << "~Animal:" << (int)name << endl;
delete name;
name = NULL;
}
複制
再運作發現程式崩潰了,調用一次構造函數,調用兩次析構函數,兩個對象的指針成員所指記憶體相同,name指針被配置設定一次記憶體,但是程式結束時該記憶體卻被釋放了兩次,導緻程式崩潰
而且發現當重複釋放的兩個指針分别屬于兩個類或者說是兩個變量的時候,會發生崩潰,如果對一個變量多次釋放則不會崩潰。
例如下面的代碼将不會發生奔潰
string * a = new string("花狗");
delete a;
a = NULL;
cout << "第一次完成\n";
delete a;
a = NULL;
cout << "第二次完成\n";
複制
現在我們已經知道對于指針進行淺拷貝會出現的奔潰的問題,那麼通過自定義拷貝構造函數來解決淺拷貝的問題。
Animal(const Animal & a)
{
//name = s.name;
name = new string(*a.name);
}
複制
之後運作程式不會崩潰,總結起來就是先開辟出和源對象一樣大的記憶體區域,然後将需要拷貝的資料複制到目标拷貝對象。
四. 指派函數
四個預設函數,當指派函數最為複雜。
Animal& operator=(const Animal&obj)
{
if(this !=&obj)
{
data=obj.data;
}
return *this;
}
複制
這是它的原型,類似 Animal a(b); Animal a = b; 這樣的寫法會調用拷貝構造函數。
而指派函數是在當年對象已經建立之後,對該對象進行指派的時候調用的,Animal a; a = b。
和拷貝構造函數一樣,若類中有指針變量,自動生成的指派函數注定會出錯,老樣子,先申請記憶體,再複制值即可完美解決。
Animal& operator=(const Animal&obj)
{
if(this !=&obj)
{
//預設是 name = obj.name;
name = new string(*obj.name);
}
return *this;
}
複制
還有一個知識點就是運算符重載這一塊,一個自定義類型的對象,如果想要進行預期的加減乘除之類的運算,或者是像内置類型一樣,用cout輸出一個類對象,這些都是需要我們來用代碼告訴機器怎麼做,都是需要我們來指定的。
還是拿這個類舉例子,例如運算符+重載
class Animal
{
private:
string * name;
int age;
int num;
public:
Animal()
{
name = new string("花狗");
age = 5;
num = 4;
}
Animal& operator+(const Animal&obj)
{
if(this !=&obj)
{
string * s = name;
name = new string(*name + *obj.name);
delete s;
s == NULL;
this->age+=obj.age;
this->num+=obj.num;
}
return *this;
}
};
int main()
{
Animal a;
Animal b;
a = a+b;
//這樣對象a裡面的age成員的值是5,num成員的值是8,而*name的值将是"花狗花狗";
return 0;
{
複制
cout輸出的定義,主要注意的是要用到友元函數。
class Animal
{
//中間代碼略
friend ostream& operator << (ostream& os, Animal& a)
{
os << *a.name << ":" << a.age << ":" << a.num;
return os;
}
};
複制
運作結果: