1. 引述
在C++中,學習類的第一課往往就是建構函式。根據建構函式的定義,建構函式式是用於初始化類物件的資料成員的。無論何時,只要類被建立,就會執行建構函式:
class ImageEx
{
public:
ImageEx()
{
cout << "Execute the constructor!" << endl;
}
};
int main()
{
ImageEx imageEx;
return 0;
}
那麼問題來了,為什麼要有建構函式?
2. 詳述
2.1. 資料型別初始化
正如上一篇文章《物件導向程式設計(C++篇1)——引言》中提到的那樣:類是抽象的自定義資料型別。對於C++的內建資料型別,我們可以採用如下方式進行初始化:
double price = 109.99;
這種初始化行為很像賦值操作,但是初始化與賦值是兩種概念:初始化的含義是建立變數的時候賦予其一個初始值,而賦值的含義則是把物件的當前值擦除,以一個新的值來代替。實際上,我們同樣可以使用類似建構函式一樣的方式初始化內建資料型別:
double price(109.99);
那麼,我們在定義變數的時候不進行初始化會怎麼樣呢?答案是會進行預設初始化(其實不太準確,在某些情況下,會不被初始化,進而產生未定義的行為,是非常危險的):
double price;
price = 109.99;
在C++中,一個合理的原則是:變數型別定義時初始化。這個原則不僅可以避免未初始化可能產生的未定義行為,還節省了效能:避免定義(預設初始化)後再進行賦值操作。
2.2. 類初始化
可能你會認為,先定義(預設初始化)之後再進行賦值,對效能影響不大。這句話對於C#、Java、JavaScript這樣的語言來說是成立的,它們的應用場景很多時候可以不用關心這個(效能場景則不一定)。而對於C++這樣的面向底層的語言來說,追求的是"零成本抽象(zero overhead abstraction)"的設計原則,只是簡單的資料結構影響當然不太,但是對於一個非常複雜的資料型別,則可能存在不可忽視的效能開銷。
可以為一個類的資料成員提供一個類內初始值:
class ImageEx
{
int imgWidth = 0;
int imgHeight = 0;
int bandCount = 0;
};
類的資料成員如果不進行初始化,那麼就會如前所述,進行預設初始化:
class ImageEx
{
public:
void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
}
private:
int imgWidth;
int imgHeight;
int bandCount;
unsigned char data[10];
};
int main()
{
ImageEx imageEx;
imageEx.Print();
return 0;
}
執行結果:
預設初始化的未定義行為當然不是我們想要的,於是我們給他加一個初始化函式:
class ImageEx
{
public:
void Init()
{
imgWidth = 200;
imgHeight = 100;
bandCount = 3;
memset(data, 0, 10 * sizeof(unsigned char));
}
void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
cout << endl;
}
private:
int imgWidth;
int imgHeight;
int bandCount;
unsigned char data[10];
};
int main()
{
ImageEx imageEx;
imageEx.Print();
imageEx.Init();
imageEx.Print();
return 0;
}
執行結果:
從上例可以發現,如果我們自己給類的資料成員進行初始化函式,其實類的資料成員早就進行了一次預設初始化操作,這個初始化函式其實是一次額外的賦值。以這個類物件中的陣列資料成員data為例,假使這個陣列的容量很大,其額外的一次賦值操作對於底層來說,是不可忽略的效能開銷。
那麼使用建構函式的原因就很容易理解了,建構函式就是實現當類定義時初始化資料成員的,這樣可以避免額外的初始化效能開銷:
class ImageEx
{
public:
ImageEx()
{
cout << "Default initialization!" << endl;
Print();
cout << "Execute the constructor!" << endl;
Init();
}
void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
cout << endl;
}
private:
void Init()
{
imgWidth = 200;
imgHeight = 100;
bandCount = 3;
memset(data, 0, 10 * sizeof(unsigned char));
}
int imgWidth;
int imgHeight;
int bandCount;
unsigned char data[10];
};
int main()
{
ImageEx imageEx;
imageEx.Print();
return 0;
}
進一步探究,建構函式本質是個函式,函式是由語句組成,已經定義的資料型別只能賦值初始化,而無法再進行構造。也就是說,在呼叫建構函式之前,資料成員還是已經預設初始化了:
因此,初始化最好的實現是使用建構函式的初始值列表:
class ImageEx
{
public:
ImageEx() :
imgWidth(200),
imgHeight(100),
bandCount(3),
data{ 0, 1, 2 }
{
cout << "Execute the constructor!" << endl;
}
void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
cout << endl;
}
private:
int imgWidth;
int imgHeight;
int bandCount;
unsigned char data[10];
};
int main()
{
ImageEx imageEx;
imageEx.Print();
return 0;
}
執行結果:
通過這種實現,類中所有的資料成員都在定義時初始化,從而使類物件也實現了定義時初始化;避免了先定義後賦值的效能開銷,體現了C++"零成本抽象(zero overhead abstraction)"的設計哲學。