類的成員變數的初始化細節
首先,來看兩個問題:
- 類的建構函式中,成員變數的列表初始化是如何實現的?
- 為什麼列表初始化效率上優於在建構函式中為成員變數賦值?
(後文中,將 “在建構函式中為成員變數賦值” 簡稱為 “構內賦值”。)
這兩個問題從何而來
通常,當你搜尋為什麼列表初始化優於構內賦值時,基本上所有的博文都會告訴你:“列表初始化使得成員變數在被定義時繫結初值;而在建構函式內賦值,成員變數會先在定義時被初始化為0,然後再被賦值為指定的初值。”總之,意思是,列表初始化相比於構內賦值少了一次對記憶體的寫入。至於這樣的說法是否正確?為什麼會這樣?列表初始化和構內賦值的實現細節是什麼樣的?很少有人分析。所以,本著“實踐出真知”的道理,我們在這本篇博文中做了一些列的實驗,並詳細的講述了為什麼初始化列表優於構內賦值?。
列表初始化 和 構內賦值 的實現細節
首先,先將提出的兩個問題回答一下:
- 類的列表初始化是透過成員變數的複製建構函式實現的。
- 列表初始化相比於構內賦值,減少了一半的函式呼叫,減少了一半的記憶體寫入。
列表初始化和構內賦值的具體的流程圖如下:
構內賦值 的實現細節
構內賦值分為兩步實現:
- 呼叫各級成員的預設建構函式
- 呼叫各級成員的賦值函式
首先,我們來解釋一下什麼是“各級成員”?如下,類 A 中包含型別為 B 的成員變數 b,而 B 型別還可能包含型別為 C 的成員變數 c,如此遞推。直至遞推到基礎型別(比如 int)。對於類 A 而言,b, c, d ... 就是它的各級成員。
class C{
D d;
};
class B{
C c;
};
class A{
B b;
};
我們透過如下程式碼來測試構內賦值是如何實現的:
#include<iostream>
class Test0{
int a;
public:
Test0():a(0){
std::cout<< "0預設構造\n";
}
Test0(const Test0& t1):a(t1.a){
std::cout<< "0複製構造\n";
}
Test0(Test0&& t1):a(t1.a){
std::cout<< "0移動構造\n";
}
void operator= (const Test0& t1){
std::cout<< "0賦值函式\n";
a = t1.a;
}
};
class Test1{
Test0 t;
public:
Test1(){
std::cout<< "1預設構造\n";
}
Test1(const Test1& t1):t(t1.t){
std::cout<< "1複製構造\n";
}
Test1(Test1&& t1):t(t1.t){
std::cout<< "1移動構造\n";
}
void operator= (const Test1& t1){
std::cout<< "1賦值函式\n";
t = t1.t;
}
};
class Test2{
private:
Test1 t1;
public:
Test2(const Test1& t1){
std::cout<< "2構造\n";
this->t1 = t1;
}
};
int main(){
Test1 t1; \\ main 函式第一行程式碼
std::cout<< "---------------\n";
Test2 t2(t1); \\ main 函式第三行程式碼
}
在上面的程式碼中,我們定義了三個類,並依次包含,最底層的類 Test0 包含了一個基礎型別 int。在 main 函式中,我們首先預設構造了一個 Test1 型別的物件 t1,而後將 t1 傳入到 Test2 的建構函式中。Test2 的建構函式,是透過構內賦值實現的。我們執行上述程式碼,結果如下:
0預設構造
1預設構造
---------------
0預設構造
1預設構造
2構造
1賦值函式
0賦值函式
可以看到,main 函式的第一行程式碼透過預設建構函式構建物件 t1,從輸出的第 1、2 行可以看出,t1 及其各級成員的建構函式自下而上的執行,即:int 的預設構造 -> Test0 的預設構造 -> Test1 的預設構造。預設建構函式,會定義變數,並初始化為 0。
main 函式的第三行我們定義變數 t2,將 main 函式第一行定義的 t1 傳入其建構函式。從輸出的第 4、5、6 行可以看出,t2 的成員變數 t1 首先經過了預設構造,然後才進入到 t2 的建構函式中。而後在 t2 的建構函式中,我們將 main 函式第一行定義的 t1 賦值給 t2 的成員變數 t1。從輸出的 7、8 行可以看出,這個賦值操作自上而下的呼叫了成員的賦值函式,即: Test1 的賦值函式 -> Test0 的賦值函式 -> int 的賦值函式。
至此完成了構內賦值。整個過程的資源分析如下:
- 呼叫了各級成員的預設構造
- 向基礎型別成員的記憶體中寫入 0
- 呼叫了各級成員的賦值函式
- 向基礎型別成員的記憶體中寫入初值
列表初始化的實現細節
我們透過如下程式碼來觀察列表初始化的實現細節:
#include<iostream>
class Test0{
int a;
public:
Test0():a(0){
std::cout<< "0預設構造\n";
}
Test0(const Test0& t1):a(t1.a){
std::cout<< "0複製構造\n";
}
Test0(Test0&& t1):a(t1.a){
std::cout<< "0移動構造\n";
}
void operator= (const Test0& t1){
std::cout<< "0賦值函式\n";
a = t1.a;
}
};
class Test1{
Test0 t;
public:
Test1(){
std::cout<< "1預設構造\n";
}
Test1(const Test1& t1):t(t1.t){
std::cout<< "1複製構造\n";
}
Test1(Test1&& t1):t(t1.t){
std::cout<< "1移動構造\n";
}
void operator= (const Test1& t1){
std::cout<< "1賦值函式\n";
t = t1.t;
}
};
class Test2{
private:
Test1 t1;
public:
Test2(const Test1& t1):t1(t1){
std::cout<< "2構造\n";
}
};
int main(){
Test1 t1;
std::cout<< "---------------\n";
Test2 t2(t1);
}
相比於構內賦值的實現細節中的程式碼,我們將 Test2 的建構函式改為列表初始化。程式碼的執行結果如下:
0預設構造
1預設構造
---------------
0複製構造
1複製構造
2構造
觀察輸出的 4,5 行,列表初始化透過呼叫各級成員的複製建構函式來完成。這種呼叫是自下而上的,即:int 的複製構造 -> Test0 的複製構造 -> Test1 的複製構造。基礎型別的成員經歷了一次記憶體寫入。
觀察輸出的 4, 5, 6 行,列表初始化在進入建構函式的函式體之前完成。
列表初始化的資源分析如下:
- 呼叫了各級成員的複製建構函式
- 向基礎型別成員的記憶體中寫入初值
列表初始化 與 構內賦值 所用資源比較
構內賦值的資源分析如下:
- 呼叫了各級成員的預設構造
- 向基礎型別成員的記憶體中寫入 0
- 呼叫了各級成員的賦值函式
- 向基礎型別成員的記憶體中寫入初值
列表初始化的資源分析如下:
- 呼叫了各級成員的複製建構函式
- 向基礎型別成員的記憶體中寫入初值
可見,列表初始化比構內賦值減少了一半的資源呼叫和一半的記憶體寫入。因此列表初始化由於構內賦值。
看來,大多數博文中說的不完全對,他們只說對了記憶體,卻沒有分析函式的呼叫次數。