C++ 構造函數的執行過程(一) 無繼承
本篇介紹了在無繼承狀況下, C++構造函數的執行過程, 即成員變量的構建先於函數體的執行, 初始化列表的數量和順序並不對構造函數執行順序形成任何影響.
還指出了初始化列表會影響成員變量的構造方式, 分析了爲什麼要儘量地使用初始化列表.html
關於在繼承的狀況下, C++構造函數的執行過程, 請期待第二篇.函數
本文所依賴的環境以下:性能
平臺: Windows 10 64位測試
編譯器: Visual Studio 2019優化
首先咱們聲明一個類:this
// Dog.h class Dog;
若是咱們建立一個該類的實例:指針
// main.cpp Dog myDog = Dog( );
那麼編譯器會申請一塊內存空間, 並調用Dog
的構造函數, 構造這個實例.調試
咱們一點點補全這個類.日誌
在這個類中, 添加一個構造函數, 一個析構函數.code
在函數體內, 各打印一條日誌, 方便咱們在調試的過程當中, 知道執行的順序.
// Dog.h class Dog { public: Dog( ) { std::cout << "Dog構造函數函數體"<< std::endl; } ~Dog( ) { } };
如今再次執行:
// main.cpp std::cout << "Dog構造函數 開始" << std::endl; Dog myDog = Dog( ); std::cout << "Dog構造函數 結束" << std::endl; std::cout << "程序即將結束" << std::endl;
程序會打印出日誌:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Dog構造函數函數體 3. Dog構造函數 結束 4. 程序即將結束
文明養狗, 每隻狗都應該有本身的項圈.
咱們給Dog
添加一個項圈collar
屬性.
注: 爲了方便驗證, 咱們讓collar
也是一個類的實例, 緣由在於, 咱們須要讓這個屬性在構造的時候, 打印出一條日誌, 這樣咱們才能判斷出它是在什麼時候被構造的.
// Collar.h class Collar { public: // 缺省構造函數 Collar( ) { std::cout << "Collar缺省構造函數" << std::endl; } };
如今咱們在Dog
中添加整個成員變量:
// Dog.h class Dog { public: Dog( ) { std::cout << "Dog構造函數函數體<< std::endl; } ~Dog(){ } private: Collar collar_; };
如今再次執行:
// main.cpp std::cout << "Dog構造函數 開始" << std::endl; Dog myDog = Dog(myCollar); std::cout << "Dog構造函數 結束" << std::endl; std::cout << "程序即將結束" << std::endl;
程序會打印出日誌:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Collar缺省構造函數 3. Dog構造函數函數體 4. Dog構造函數 結束 5. 程序即將結束
在建立一個類的實例的時候, 會先構造出它的成員變量, 而後纔會執行它的構造函數函數體的語句.
觀察上面的代碼, 咱們並無在任何地方, 顯式的調用Collar
的構造函數, 也就是說:
編譯器幫你完成了
Collar
構造函數的調用.
可是, 若是這個類, 不止有一個成員變量, 那麼編譯器先構造哪一個成員變量呢?
如今, 咱們給狗狗一個玩具.
// Toy.h class Toy { public: // 缺省構造函數 Toy( ) { std::cout << "Toy缺省構造函數" << std::endl; } };
在Dog
添加一個玩具Toy
屬性.
// Dog.h class Dog { // 構造和析構與1.3相同, 在此省略 private: Collar collar_; Toy toy_; };
如今執行程序, 獲得日誌:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Collar缺省構造函數 3. Toy缺省構造函數 4. Dog構造函數函數體 5. // 其他日誌與1.3相同, 在此省略
能夠看到, 咱們在class Dog
的聲明中, 先聲明瞭Collar
, 再聲明瞭Toy
, 實際執行過程, 就是先調用了Collar
缺省構造函數, 再調用了Toy
缺省構造函數.
若是修改成:
// Dog.h class Dog { // 構造和析構與1.3相同, 在此省略 private: Toy toy_; // 調換了位置 Collar collar_; // 調換了位置 };
日誌也會變成:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Toy缺省構造函數 3. Collar缺省構造函數 4. Dog構造函數函數體 5. // 其他日誌與1.3相同, 在此省略
類的成員變量, 是按照類的定義中, 成員變量的聲明順序進行構造的. 且構造都早於類構造函數的函數體.
咱們將對初始化列表作3個測試.
// Dog.h class Dog { public: Dog(const Collar& myCollar, const Toy& myToy) : collar_(myCollar) , toy_(myToy) { std::cout << "Dog構造函數函數體開始"<< std::endl; std::cout << "Dog構造函數函數體結束" << std::endl; } private: Collar collar_; Toy toy_; };
如今執行程序, 獲得日誌:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Collar缺省構造函數 3. Toy缺省構造函數 4. Dog構造函數函數體 5. // 其他日誌與1.3相同, 在此省略
// Dog.h class Dog { public: Dog(const Collar& myCollar, const Toy& myToy) : toy_(myToy) , collar_(myCollar) { std::cout << "Dog構造函數函數體開始"<< std::endl; std::cout << "Dog構造函數函數體結束" << std::endl; } private: Collar collar_; Toy toy_; };
如今執行程序, 獲得日誌:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Collar缺省構造函數 3. Toy缺省構造函數 4. Dog構造函數函數體 5. // 其他日誌與1.3相同, 在此省略
日誌沒有任何變化.
// Dog.h class Dog { public: Dog(const Collar& myCollar, const Toy& myToy) : collar_(myCollar) // 刪除了toy_(myToy) { std::cout << "Dog構造函數函數體開始"<< std::endl; std::cout << "Dog構造函數函數體結束" << std::endl; } private: Collar collar_; Toy toy_; };
如今執行程序, 獲得日誌:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Dog構造函數 開始 2. Collar缺省構造函數 3. Toy缺省構造函數 4. Dog構造函數函數體 5. // 其他日誌與1.3相同, 在此省略
日誌沒有任何變化.
初始化列表的數量和順序, 均不影響成員變量構造順序.
構造順序仍然是按照類的定義中, 成員變量的聲明順序進行構造的. 且構造都早於類構造函數的函數體.
如今, 咱們顯示的指定collar
的構造, 給Collar
添加另外一個構造函數:
// Collar.h class Collar { public: // 缺省構造函數 Collar( ) { std::cout << "Collar缺省構造函數" << std::endl; } // 含參構造函數 Collar(std::string color) { std::cout << "Collar含參構造函數" << std::endl; color_ = color; } // 拷貝構造函數, 這裏直接使用了const引用, 是出於性能考慮. 若是用值拷貝, 會多構造一個collar出來, 而後再析構它. Collar(const Collar& collar) { std::cout << "Collar拷貝構造函數" << std::endl; this->color_ = collar.color_; } // 拷貝賦值運算符 Collar& operator = (const Collar& collar) { std::cout << "Collar拷貝賦值運算符" << std::endl; this->color_ = collar.color_; return *this; } // 析構函數 ~Collar() { std::cout << "Collar析構函數" << std::endl; } private: std::string color_; };
主要作了幾個改動
Collar
添加了一個帶參構造函數. 便於和缺省構造函數進行區分.拷貝構造函數
.拷貝賦值運算符
.拷貝賦值運算符
其實就是咱們經常使用的"=
"(更準確的說是"operator =
"), 它存在於全部的類中, 當你在執行dog1 = dog2;
的時候, 就是調用了這個函數來完成的賦值工做.operator =
"函數, 你均可以使用它, 由於編譯器已經幫助你自動合成了它.operator =
"進行重載, 在這段代碼中, 我重載了這個函數, 額外添加了一條日誌.修改Dog
的構造函數:
// Dog.h class Dog { public: Dog(const Collar& myCollar) { std::cout << "Dog構造函數 函數體開始"<< std::endl; // 將參數`collar`賦值給成員變量`collar_` collar_= collar; std::cout << "Dog構造函數 函數體結束" << std::endl; } ~Dog(){ } private: Collar collar_; };
主要作了如下改動:
collar
賦值給成員變量collar_
.修改main.cpp
Collar myCollar = Collar("yellow"); std::cout << "Dog構造函數 開始" << std::endl; Dog myDog = Dog(myCollar); std::cout << "Dog構造函數 結束" << std::endl; std::cout << "程序即將結束" << std::endl;
實際運行後打印的日誌以下:
// 日誌, 每行開頭的數字序號, 是我手動添加的, 數字後纔是真實的日誌. 1. Collar含參構造函數 2. Dog構造函數開始 3. ----Collar缺省構造函數 4. ----Dog構造函數函數體開始 5. --------Collar拷貝賦值運算符 6. ----Dog構造函數函數體結束 7. Dog構造函數結束 8. 程序即將結束 9. Collar析構函數" 10. Collar析構函數"
可是第二行日誌指出, 編譯器仍是幫你完成了Collar
缺省構造函數的隱式調用, 而且該調用早於Dog
構造函數的調用.
> 第一條日誌, 調用`Collar`的含參構造函數, 構造出一個對象. > 第二條日誌, 標誌着程序開始調用`Dog`構造函數. > 第三條日誌, 調用成員變量的`Collar`缺省構造函數, 將`collar_`構造出來. > 第四條日誌, 進入`Dog`的構造函數的函數體. > 第五條日誌, 調用拷貝賦值運算符, 將參數`myCollar`賦值給成員變量`collar_`; > 第六條日誌, `Dog`的構造函數的函數體結束. > 第七條日誌, 標誌着`Dog`構造函數完全結束. > 第八條日誌, 標誌着程序即將結束, 開始進入析構階段. > 第九條日誌, 在析構`Dog`實例的過程當中, 會析構成員變量`collar_`, 執行`Collar`的析構函數. > 第十條日誌, 仍然是程序結束階段, 會析構第一步創建的`myCollar`, 執行`Collar`的析構函數.
在構造Dog
實例的過程當中, 總共有5個步驟涉及了Collar
:
在剛纔總結出的5個步驟中, 第2和3步, 存在浪費.
如今咱們單獨看這兩步:
第一步: 先使用缺省構造, 構造出collar_
對象.
這個缺省構造過程當中, 若是collar_
是一個很複雜的對象, 咱們假設它包含了多個成員變量, 且每一個成員變量要麼是類的對象, 要麼是結構體.
這個缺省構造, 將花費不少時間, 將每個成員變量正確構造出來, 給它們一個默認值, 記住, 默認值一般都是沒用的, 好比是'0'或者'nullptr'.
緊接着, 進入第二步, 拷貝賦值運算符:
在這個步驟以前, 咱們已經將myCollar
做爲參數傳遞了進來, 這個myCollar
早就已經構造完成了, 它全部的成員變量的值都是正確的且有意義的, 如今咱們把它複製給collar_
, 完成對collar_
的建立, 其中collar_
的默認值, 被一一覆蓋.
如今你可能意識到了問題:
第一步的默認值徹底是多餘的!
咱們須要執行第一步的前半部分, 將collar_
對象構造出來.
可是咱們不須要第一步的後半部分, 不須要默認值.
咱們直接使用第二步, 將myCollar
的值, 拷貝給collar_
就好了.
咱們僅僅對Dog.h
進行一些修改:
// Dog.h class Dog { public: Dog(const Collar& myCollar) : collar_(myCollar) { std::cout << "Dog構造函數函數體開始"<< std::endl; std::cout << "Dog構造函數函數體結束" << std::endl; } ~Dog(){ } private: Collar collar_; };
主要作了如下改動:
Dog
構造函數中, 添加初始化列表, 直接用myCollar
來初始化collar_
.collar_
已經初始化了, 函數體內的拷貝賦值運算符就能夠刪掉了.其餘內容保持不變, 執行:
1. Collar含參構造函數 2. Dog構造函數開始 3. Collar拷貝構造函數 4. Dog構造函數函數體開始 5. Dog構造函數函數體結束 6. Dog構造函數結束 7. 程序即將結束 8. Collar析構函數" 9. Collar析構函數"
對比上一次的日誌能夠發現:
本次運行使用了初始化列表, Collar拷貝構造函數
一個步驟, 替代了上次運行的Collar缺省構造函數
+拷貝賦值運算符
兩個步驟.
避免了Collar缺省構造
, 也就避免了多餘的默認值.
對於一個類的成員變量, 必定會在進入該類的構造函數以前構造完成.
若是成員變量在初始化列表中, 就會執行該變量類型的拷貝構造函數.
若是成員變量沒有在初始化列表中, 就會執行該變量類型的缺省構造函數.
使用初始化列表, 首要緣由是性能問題.
按照咱們剛纔的分析, 若是不使用初始化列表, 而是用構造函數函數體來完成初始化, 會額外調用一次缺省構造
.
對於內置類型, 如int
, double
, 在初始化列表和在構造函數函數體內初始化, 性能差異不是很大, 由於編譯器已經進行了優化.
可是對於類類型, 性能差異多是巨大的, 數倍的.
另外一個緣由是, 有一些狀況必須使用初始化列表:
常量成員, 由於常量只能初始化不能賦值, 因此必須放在初始化列表裏面.
引用類型, 引用必須在定義的時候初始化, 而且不能從新賦值, 因此也要寫在初始化列表裏面.
沒有默認構造函數的類類型, 由於使用初始化列表能夠沒必要調用缺省構造函數來初始化, 而是直接調用拷貝構造函數初始化.
注: 對於還不知道具體值的變量, 使用零值或沒有具體含義的值, 好比int類型使用0
, std::string類型使用""
, 指針類型使用nullptr
.