C++ 構造函數的執行過程(一) 無繼承

 

引言

C++ 構造函數的執行過程(一) 無繼承
本篇介紹了在無繼承狀況下, C++構造函數的執行過程, 即成員變量的構建先於函數體的執行, 初始化列表的數量和順序並不對構造函數執行順序形成任何影響.
還指出了初始化列表會影響成員變量的構造方式, 分析了爲什麼要儘量地使用初始化列表.html

關於在繼承的狀況下, C++構造函數的執行過程, 請期待第二篇.函數

 

本文所依賴的環境以下:性能

平臺: Windows 10 64位測試

編譯器: Visual Studio 2019優化

 

一. 構造函數的執行順序

 

1.1 聲明一個類

首先咱們聲明一個類:this

// Dog.h
class Dog;

若是咱們建立一個該類的實例:指針

// main.cpp
Dog myDog = Dog( );

那麼編譯器會申請一塊內存空間, 並調用Dog的構造函數, 構造這個實例.調試

 

1.2 添加構造函數

咱們一點點補全這個類.日誌

在這個類中, 添加一個構造函數, 一個析構函數.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. 程序即將結束

 

1.3 添加成員變量

文明養狗, 每隻狗都應該有本身的項圈.

咱們給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構造函數的調用.

可是, 若是這個類, 不止有一個成員變量, 那麼編譯器先構造哪一個成員變量呢?

 

1.4 成員變量的構造順序

如今, 咱們給狗狗一個玩具.

// 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相同, 在此省略
目前的結論:

類的成員變量, 是按照類的定義中, 成員變量的聲明順序進行構造的. 且構造都早於類構造函數的函數體.

 

1.5 初始化列表的順序, 不影響成員變量構造順序

咱們將對初始化列表作3個測試.
 

測試1: 初始化列表的順序 和 成員變量聲明順序一致.
// 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相同, 在此省略

 

測試2: 初始化列表的順序 和 成員變量聲明順序不一致.
// 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相同, 在此省略

日誌沒有任何變化.

 

測試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相同, 在此省略

日誌沒有任何變化.

 

目前的結論:

初始化列表的數量和順序, 均不影響成員變量構造順序.

構造順序仍然是按照類的定義中, 成員變量的聲明順序進行構造的. 且構造都早於類構造函數的函數體.

 

1.6 目前的構造函數執行順序

  1. 開闢內存空間.
  2. 按照成員變量聲明的順序開始構形成員變量.
  3. 進入函數體, 執行語句.

 

二. 成員變量如何被構造

2.1 在構造函數體內, 給成員變量賦值

如今, 咱們顯示的指定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_;
};

主要作了幾個改動

  1. Collar添加了一個帶參構造函數. 便於和缺省構造函數進行區分.
  2. 添加一個拷貝構造函數.
    // todo 尚未解釋
  3. 添加一個拷貝賦值運算符.
    拷貝賦值運算符其實就是咱們經常使用的"="(更準確的說是"operator ="), 它存在於全部的類中, 當你在執行dog1 = dog2;的時候, 就是調用了這個函數來完成的賦值工做.
    無論你在類的定義中, 有沒有定義這個"operator ="函數, 你均可以使用它, 由於編譯器已經幫助你自動合成了它.
    C++容許用戶本身對"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_;
};

主要作了如下改動:

  1. 修改了Dog自身的構造函數聲明, 添加了一個參數.
  2. 在構造函數的函數體內, 將參數collar賦值給成員變量collar_.
  3. 因爲本構造函數內, 會調用其餘函數, 因此咱們在函數體內最上方和最下方都打印了一條日誌, 便於分析函數調用鏈.

修改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:

  1. 帶參構造
  2. 缺省構造
  3. 拷貝賦值運算符
  4. 析構"缺省構造"
  5. 析構"帶參構造"

 

2.2 問題在哪裏?

在剛纔總結出的5個步驟中, 第2和3步, 存在浪費.

如今咱們單獨看這兩步:

第一步: 先使用缺省構造, 構造出collar_對象.
這個缺省構造過程當中, 若是collar_是一個很複雜的對象, 咱們假設它包含了多個成員變量, 且每一個成員變量要麼是類的對象, 要麼是結構體.
這個缺省構造, 將花費不少時間, 將每個成員變量正確構造出來, 給它們一個默認值, 記住, 默認值一般都是沒用的, 好比是'0'或者'nullptr'.

緊接着, 進入第二步, 拷貝賦值運算符:
在這個步驟以前, 咱們已經將myCollar做爲參數傳遞了進來, 這個myCollar早就已經構造完成了, 它全部的成員變量的值都是正確的且有意義的, 如今咱們把它複製給collar_, 完成對collar_的建立, 其中collar_的默認值, 被一一覆蓋.

如今你可能意識到了問題:

第一步的默認值徹底是多餘的!

咱們須要執行第一步的前半部分, 將collar_對象構造出來.
可是咱們不須要第一步的後半部分, 不須要默認值.
咱們直接使用第二步, 將myCollar的值, 拷貝給collar_就好了.

 

2.3 使用初始化列表

咱們僅僅對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_;
};

主要作了如下改動:

  1. Dog構造函數中, 添加初始化列表, 直接用myCollar來初始化collar_.
  2. 既然collar_已經初始化了, 函數體內的拷貝賦值運算符就能夠刪掉了.

其餘內容保持不變, 執行:

1. Collar含參構造函數
2. Dog構造函數開始
3. Collar拷貝構造函數
4. Dog構造函數函數體開始
5. Dog構造函數函數體結束
6. Dog構造函數結束
7. 程序即將結束
8. Collar析構函數"
9. Collar析構函數"

對比上一次的日誌能夠發現:

本次運行使用了初始化列表, Collar拷貝構造函數一個步驟, 替代了上次運行的Collar缺省構造函數+拷貝賦值運算符兩個步驟.

避免了Collar缺省構造, 也就避免了多餘的默認值.

目前的結論:

對於一個類的成員變量, 必定會在進入該類的構造函數以前構造完成.
若是成員變量在初始化列表中, 就會執行該變量類型的拷貝構造函數.
若是成員變量沒有在初始化列表中, 就會執行該變量類型的缺省構造函數.

 

2.4 儘量地使用初始化列表

使用初始化列表, 首要緣由是性能問題.

按照咱們剛纔的分析, 若是不使用初始化列表, 而是用構造函數函數體來完成初始化, 會額外調用一次缺省構造.

對於內置類型, 如int, double, 在初始化列表和在構造函數函數體內初始化, 性能差異不是很大, 由於編譯器已經進行了優化.

可是對於類類型, 性能差異多是巨大的, 數倍的.

另外一個緣由是, 有一些狀況必須使用初始化列表:

  • 常量成員, 由於常量只能初始化不能賦值, 因此必須放在初始化列表裏面.

  • 引用類型, 引用必須在定義的時候初始化, 而且不能從新賦值, 因此也要寫在初始化列表裏面.

  • 沒有默認構造函數的類類型, 由於使用初始化列表能夠沒必要調用缺省構造函數來初始化, 而是直接調用拷貝構造函數初始化.

注: 對於還不知道具體值的變量, 使用零值或沒有具體含義的值, 好比int類型使用0, std::string類型使用"", 指針類型使用nullptr.

 

三 構造函數執行順序

  1. 開闢內存空間.
  2. 按照成員變量聲明的順序開始構形成員變量.
    • 若是成員變量在初始化列表中, 就會執行該變量類型的拷貝構造函數.
    • 若是成員變量沒有在初始化列表中, 就會執行該變量類型的缺省構造函數.
  3. 進入函數體, 執行語句.
相關文章
相關標籤/搜索