帶你掌握 C++ 中三種類成員初始化方式

語言: CN / TW / HK

摘要:在 C++11 之後,宣告時初始化->初始化列表->建構函式初始化。

本文分享自華為雲社群《如何編寫高效、優雅、可信程式碼系列(3)——類成員初始化的三種方式》,原文作者:我是一顆大西瓜。

首先,先得了解一下 C++支援哪幾種類成員初始化的方式,你常用的又是哪一種。

  • 初始化方式一:初始化列表

class A
{
public:
int a; // 初始化列表
A(int a_):a(a_){}
};

  • 初始化方式二:建構函式初始化

class A
{
public:
int a; // 初始化列表
A(int a_, bool b) { a = a_; }
};

  • 初始化方式三:宣告時初始化(也稱就地初始化,c++11 後支援)

class A
{
public:
int a = 1; // 宣告時初始化
A() {}
};

在 C++98 中,支援了在類宣告中使用等號“=”加初始值的方式,來初始化類中靜態成員常量。這種宣告方式我們也稱之為“就地”宣告。就地宣告在程式碼編寫時非常便利,不過 C++98 對類中就地宣告的要求卻非常高。如果靜態成員不滿足常量性,則不可以就地宣告,而且即使常量的靜態成員也只能是整型或者列舉型才能就地初始化。而非靜態成員變數的初始化則必須在建構函式中進行。比如,如下程式碼在 c++98 中編譯

class Init
{
public:
    Init(): a(0) []
    Init(int d): a(d) {}
private:
    int a;
    const static int b = 0;


    int c = 1;           // member, cannot pass build
    static int d = 0;    // member, cannot pass build


    static const double e = 1.3;      // not int or enum type, cannot pass build
    stati const char* const f = "e";  // not int or enum type, cannot pass build
}

這非常不方便,所以在 C++11 中,標準允許非靜態成員變數的初始化有多種形式。具體而言,除了初始化列表外,在 C++11 中,標準還允許使用等號= 或者 花括號{} 進行就地的非靜態成員變數初始化。

struct init {
    int a = 1;
    double b {1.2};
};

大家知道,有幾種情況下推薦優先使用列表初始化

  • const 成員變數只能用成員初始化列表來完成初始化,而不能在建構函式內賦值
  • 初始化的資料成員是物件
  • 需要初始化引用成員資料

具體的原因這裡不細述,大家可以去看一下《C++ Primer》。

建構函式初始化的本質是賦值操作("="),這個方法存在兩個問題,一個是比起初始化列表和就地初始化,此方式的效率偏低;第二個是可能存在錯誤隱患。

先說第一個,賦值過程中會產生臨時物件,臨時物件的構造析構會造成效率損耗,初始化列表的方式就避免了產生臨時物件縮帶來的問題。

第二個是,如果你沒有重寫或者禁止賦值建構函式,c++會悄悄的加上預設的賦值建構函式,這個時候也有可能帶來問題。

從 C++11 之後,這三種初始化的方法都可以使用,並不會存在衝突,但是,他們之間是有優先順序順序的,這個優先順序來源於他們在初始化的時間順序,後面初始化的會把前面的覆蓋掉,成員變數的初始化順序是宣告時初始化->初始化列表->建構函式初始化

因此假如三種初始化方式同時存在的話,那麼最後保留的成員變數值肯定是建構函式中初始化的值。

#include <iostream>
using namespace std;
class A 
{
public:
    int a = 1;
    A(int a_) :a(2) { a = 3; }
};


int main()
{
    A a;
    cout << "a.a=" << a.a << endl;
    return 0;
}


// a.a=3

既然初始化方式這麼多,那麼什麼時候適用哪種呢?

1. 宣告時初始化的使用場景

  • 一個優點是直觀,你在宣告的時候順便給一個初始值,bravo,別人在看你程式碼的時候,點一下調到宣告也能看到你賦予的初始值,不用再去看建構函式那裡給的什麼值

  • 第二個優點更有用了,比如你要定義多個建構函式,每個建構函式都用列表初始化的方法初始化,多麻煩呀,請看下面的例子,媽媽看了再也不用擔心我想用其他初始化方法了

class Group {
public:
Group() {}
Group(int a): data(a) {}
Group(Mem m): mem(m) {}
Group(int a, Mem m, string n): data(a), mem(m), name(n) {}
private:
int data = 1;
Mem mem{0};
string name{"Group"};
};

2. 列表初始化的使用場景

前面說過了三個場景,這裡贅述一下

  • const 成員變數只能用成員初始化列表來完成初始化,而不能在建構函式內賦值
  • 初始化的資料成員是物件
  • 需要初始化引用成員資料

但是,需要注意列表初始化的順序,不過 IDE 會提示你的

3. 建構函式初始化的使用場景

  • 第一個就是拷貝和賦值建構函式裡(不然怎麼叫賦值建構函式呢)
  • 第二個就是比較無聊的情況了,比如你想把幾個成員函式都初始化成一個值,請看下面例子

class Group {
public:
Group() {data1 = data2 = data3 = 0;}
private:
int data1;
int data2;
int data3;
};

一言以蔽之,優先就地初始化和列表初始化。

點選關注,第一時間瞭解華為雲新鮮技術~