C++建構函式(從本質上理解)

2020-07-16 10:04:20
在 C++ 程式中,變數在定義時可以初始化。如果不進行初始化,變數的初始值會是什麼呢?對全域性變數和區域性變數來說,這個答案是不一樣的。

未初始化的全部變數

全域性變數在程式裝入記憶體時就已經分配好了儲存空間,程式執行期間其地址不變。對於程式設計師沒有初始化的全域性變數,程式啟動時自動將其全部初始化為 0(即變數的每個位元都是 0)。

在大多數情況下,這是一種穩妥的做法。而且,將全域性變數自動初始化為 0,是程式啟動時的一次性工作,不會花費多少時間,所以大多數 C++ 編譯器生成的程式,未初始化的全域性變數的初始值都是全 0。

未初始化的區域性變數

對於區域性變數,如果不進行初始化,那麼它的初始值是隨機的。區域性變數定義在函數內部,其儲存空間是動態分配在棧中的。

函數被呼叫時,棧會分配一部分空間存放該函數中的區域性變數(包括引數),這片新分配的儲存空間中原來的內容是什麼,區域性變數的初始內容也就是什麼,因此區域性變數的初始值是不可預測的。

函數呼叫結束後,區域性變數占用的儲存空間就被回收,以便分配給下一次函數呼叫中涉及的區域性變數。

為什麼不將區域性變數自動初始化為全 0 呢?因為一個函數的區域性變數在記憶體中的地址,在每次函數被呼叫時都可能不同,因此自動初始化的工作就不是一次性的,而是每次函數被呼叫時都耍做,這會帶來無謂的時間開銷。

當然,如果程式設計師在定義區域性變數時將其初始化了,那麼這個初始化的工作也是每次函數被呼叫時都要做的,但這是程式設計者要求做的,因而不會是無謂的。

物件的初始化

物件和基本型別的變數一樣,定義時也可以進行初始化。一個物件,其行為和內部結構可能比較複雜,如果不通過初始化為其某些成員變數賦予一個合理的值,使用時就會產生錯誤。例如,有些以指標為成員變數的類可能會要求其物件生成時,指標就已經指向一片動態分配的儲存空間。

物件的初始化往往不只是對成員變數賦值這麼簡單,也可能還要進行一些動態記憶體分配、開啟檔案等複雜的操作,在這種情況下,就不可能用初始化基本型別變數的方法來對其初始化。

雖然可以為類設汁一個初始化函數,物件定義後就立即呼叫它,但這樣做的話,初始化就不具有強制性,難保程式設計師在定義物件後不會忘記對其進行初始化。物件導向的程式設計語言傾向於物件一定要經過初始化後,使用起來才比較安全。因此,引入了建構函式(constructor)的概念,用於對物件進行自動初始化。

在C++語言中,“建構函式”就是一類特殊的成員函數,其名字和類的名字一樣,並且不寫返回值型別(void 也不寫)。

建構函式可以被過載,即一個類可以有多個建構函式。

如果類的設計者沒有寫建構函式,那麼編譯器會自動生成一個沒有引數的建構函式,雖然該無參建構函式什麼都不做。

無參建構函式,不論是編譯器自動生成的,還是程式設計師寫的,都稱為預設建構函式(default constructor)。如果編寫了建構函式,那麼編譯器就不會自動生成預設構造閒數。

物件在生成時,一定會自動呼叫某個建構函式進行初始化,物件一旦生成,就再也不會在其上執行建構函式。

初學者常因“建構函式”這個名稱而認為建構函式負責為物件分配記憶體空間,其實並非如此。建構函式執行時,物件的記憶體空間已經分配好了,建構函式的作用是初始化這片空間。

為類編寫建構函式是好的習慣,能夠保證物件生成時總是有合理的值。例如,一個“雇員”物件的年齡不會是負的。

來看下面的程式片段:
class Complex{
private:
    double real, imag;  //實部和虛部
public:
    void Set(double r, double i);  //設定實部和虛部
};
上面這個 Complex 類代表複數,沒有編寫建構函式,因此編譯器會為 Complex 類自動生成一個無參的建構函式。

下面兩條定義或動態生成 Complex 物件的語句,都會導致該無參建構函式被呼叫,以對 Complex 物件進行初始化。
Complex c;  //c用無參建構函式初始化
Complex *p = new Complex;  //物件 *p 用無參建構函式初始化
如果為 Complex 類編寫了構造閒數,如下所示:
class Complex{
private:
    double real, imag;
public:
    Complex(double r, double i = 0);  //第二個引數的預設值為0
};

Complex::Complex(double r,double i){
    real = r;
    imag = i;
}
那麼以下語句有的能夠編譯通過,有的則不行:
Complex cl;  //錯,Complex 類沒有無參建構函式(預設建構函式)
Complex* pc = new Complex;  //錯,Complex 類沒有預設建構函式
Complex c2(2);  //正確,相當於 Complex c2(2, 0)
Complex c3(2, 4), c4(3, 5);  //正確
Complex* pc2 = new Complex(3, 4);  //正確
C++ 規定,任何物件生成時都一定會呼叫構造閒數進行初始化。第 1 行通過變數定義的方式生成了 c1 物件,第 2 行通過動態記憶體分配生成了一個 Complex 物件,這兩條語句均沒有涉及任何關於建構函式引數的資訊,因此編譯器會認為這兩個物件應該用預設建構函式初始化。可是 Complex 類已經有了一個建構函式,編譯器就不會自動生成預設建構函式,於是 Complex 類就不存在預設建構函式,所以上述兩條語句就無法完成物件的初始化,導致編譯時報錯。

建構函式是可以過載的,即可以寫多個建構函式,它們的參數列不同。當編譯到能生成物件的語句時,編譯器會根據這條語句所提供的引數資訊決定該呼叫哪個建構函式。如果沒有提供引數資訊,編譯器就認為應該呼叫無參建構函式。

下面是一個有多個建構函式的 Complex 類的例子程式。
class Complex{
private:
    double real, imag;
public:
    Complex(double r);
    Complex(double r, double i);
    Complex(Complex cl, Complex c2);
};

Complex::Complex(double r)  //建構函式 1
{
    real = r;
    imag = 0;
}
Complex :: Complex(double r, double i)  //構造數 2
{
    real = r;
    imag = i;
}
Complex :: Complex(Complex cl, Complex c2)  //建構函式 3
{
    real = cl.real + c2.real;
    imag = cl.imag + c2.imag;
}
int main(){
    Complex cl(3), c2(1,2), c3(cl,c2), c4 = 7;
    return 0;
}
根據引數個數和型別要匹配的原則,c1、c2、c3、c4 分別用建構函式 1、建構函式 2、建構函式 3 和建構函式 4 進行初始化。初始化的結果是:c1.real = 3,c1.imag = 0 (不妨表示為 c1 = {3, 0}),c2 = {1, 2},c3 = {4, 2}, c4 = {7, 0}。