《深入探索C++對象模型》筆記 Chapter2 構造函數
《深入探索C++對象模型》筆記 Chapter3 成員變量
《深入探索C++對象模型》筆記 Chapter4 成員函數
第2章 構造函數
2.1 默認構造函數
之前以為沒有聲明構造函數,編譯器就會自動生成一個默認構造函數,然而事實上并不是這樣。默認構造函數分為 trival 和 nontrival ,對于無關緊要的默認構造函數,編譯器不會生成。(這里多說一句,一個類的一些構造函數以及析構函數是否 trival 等特性,可以用 type_traits 提取出來,在模板編程中十分有用,比如刪除一個區(qū)間的元素,如果析構函數是 trival ,那么就不需要一個個去調用析構函數。以上內容《STL源碼剖析》有詳細解釋)。
nontrival 默認構造函數又分為以下幾種情況:
- 成員變量有默認構造函數
即使用戶自己實現了構造函數,也要擴張構造函數,在用戶代碼執(zhí)行之前,調用必需的默認構造函數。 - 繼承的基類有默認構造函數
- 聲明了虛函數
在默認構造函數中初始化 vptr - 虛繼承
2.2 拷貝構造函數
拷貝分為兩種,memberwise copy 和 bitwise copy,前者是深拷貝,會遞歸拷貝成員變量,后者是淺拷貝,將內存中的每個字節(jié)拷貝過去。
諸如A a1;A a2=a1;這樣的代碼,即使A沒有聲明拷貝構造函數,編譯器也可以完成上述的行為,依賴的就是memberwise copy 和 bitwise copy。
同默認構造函數一樣,并不是沒有聲明拷貝構造函數而編譯器需要時就會生成一個,只有判斷 nontrival 才會生成。而是否 nontrival 取決于一個類是不是符合 bitwise copy 語義。判斷方式和2.1列的幾點類似。
2.3節(jié)有一句話說的很好,判斷是否 trival 關鍵在于 “class 是否含有編譯器內部產生的 member”。例如有虛函數,編譯器會給class 添加一個 vptr 成員,此時默認構造函數和拷貝構造函數都是 nontrival 了,因為編譯器要做 vptr 的設置和修改。
以及,如果成員都是POD類型, 那么沒有必要自己聲明拷貝構造函數,因為編譯器做的已經是最優(yōu)了。
2.3 NRV優(yōu)化
Named Return Value 優(yōu)化就是說一個函數生成并返回一個臨時對象,需要調用構造函數,拷貝構造函數,臨時對象的析構函數,這個過程效率很低下,編譯器可以做優(yōu)化。步驟如下:
- 函數添加一個參數,為欲返回對象的引用
- 在調用函數前,先創(chuàng)建該對象
- 進入函數后,就可以直接對這個對象的引用進行操作
- 返回值就可以為空了
于是 A a = fun(); 相當于A a; fun(a); 只有一次構造函數的消耗。可以寫個demo驗證一下:
#include <iostream>
using namespace std;
class B{
public:
B(){
cout<< "this is constructor without parameter of B" <<endl;
}
B(int num):k(num){
cout<< "this is constructor with parameter of B" <<endl;
}
B(const B& b){
k=b.k;
cout<< "this is copy constructor of B"<<endl;
}
B& operator=(const B& b){
k=b.k;
cout << "this is operator= of B" <<endl;
return *this;
}
~B(){
cout<< "this is destroyor of B"<<endl;
}
private:
int k;
};
B func(){
B b;
return b;
}
int main(){
cout<< "result of func(b)-----------"<<endl;
B b=func();
return 0;
}
打印結果為
result of func(b)-----------
this is constructor without parameter of B
this is destroyor of B
2.4 初始化成員列表
必須使用 member initialization list 的情況:
- 成員是引用
- 成員是 const member
- 基類的構造函數有一組參數
- 成員變量的構造函數有一組參數
如果不使用 member initialization list ,而是在構造函數的函數體內進行賦值操作,那么編譯器可能會產生一個臨時對象,再調用賦值操作符拷貝臨時對象,最后再將臨時對象刪除,效率會十分低下。
這里可以繼續(xù)前面的demo驗證一下:
#include <iostream>
using namespace std;
class B{
public:
B(){
cout<< "this is constructor without parameter of B" <<endl;
}
B(int num):k(num){
cout<< "this is constructor with parameter of B" <<endl;
}
B(const B& b){
k=b.k;
cout<< "this is copy constructor of B"<<endl;
}
B& operator=(const B& b){
k=b.k;
cout << "this is operator= of B" <<endl;
return *this;
}
~B(){
cout<< "this is destroyor of B"<<endl;
}
private:
int k;
};
class A{
public:
A(int num):k(num){}
A(){
k=2;
}
private:
B k;
};
int main(){
cout<< "result of A(1)--------------"<<endl;
A* a1=new A(1);
delete a1;
cout<< "result of A()---------------"<<endl;
A* a2=new A();
delete a2;
return 0;
}
輸出結果為:
result of A(1)--------------
this is constructor with parameter of B
this is destroyor of B
result of A()---------------
this is constructor without parameter of B
this is constructor with parameter of B
this is operator= of B
this is destroyor of B
this is destroyor of B
其中,帶參的構造函數使用了初始化成員列表,無參的構造函數采取了函數體內賦值,打印結果說明后者多出了臨時對象的構造、拷貝和析構等步驟。
另外需注意,初始化順序按照成員的聲明順序。
總結
不知不覺間,編譯器已經幫你做到了最好。