面向對象編程(C++篇2)——構造
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)”的設計哲學。