變量初始化的工作,要是不細想,可能會覺得是一件挺簡單的事兒,例如int a = 100;。但在 C 的世界裡,誰也不敢說哪件事是簡單的,要是寫成int a = {100};,不知道你有沒有見過?
其實後一種寫法是列表初始化方法,是 C 11 标準裡定義的,它的結果也和前一種一樣。我們在下文會對列表初始化的用法、原理,以及如何避坑進行詳細讨論。
如果這個變量int a是出現在類的聲明中,那又有什麼新花樣對它進行默認初始化呢?我們就從類的非靜态成員默認初始化說起吧。
非靜态成員默認初始化
這要從一個痛點說起。
在 C 11 标準之前,如果一個類的非靜态數據成員較多,那麼用初始化列表方式,編寫構造函數時就會比較痛苦。如下例所示,會發現這幾乎就是在窮舉各種情況。
class OneInit {
public:
OneInit() : x(0), y(false), z("init") {}
OneInit(int x) : x(x), y(false), z("init") {}
OneInit(bool y) : x(0), y(y), z("init") {}
OneInit(const string &z) : x(0), y(0), z(z) {}
private:
int x;
bool y;
string z;
};
這還僅是舉個栗子,實際情況裡一個類裡十來個成員是常有的事,真要這樣寫默認初始化,費勁不說,還容易出錯。
C 11 提供了新的初始化方法,就是在類内聲明處,直接對非靜态成員賦值即可。經過改寫,看下新的版本:
class OneInit {
public:
OneInit() {}
OneInit(int x) : x(x) {}
OneInit(bool y) : y(y) {}
OneInit(const string &z) : z(z) {}
private:
int x = 0;
bool y = false;
string z{"init"}; //這是列表初始化方法,下節會具體講述
};
是不是簡化得多了,是不是有一種清爽感撲面而來?構造函數裡,隻關注你想初始化的成員就好,所有默認值都可以放在聲明處,一次性解決。
雖然方便,但仍有兩點注意事項,以免掉坑。
其實你真用了上面的方法,編譯也會報錯,隻是要知道錯在哪裡。
列表初始化在介紹列表初始化之前,我們先回顧一下已有的兩種初始化方式:直接初始化和拷貝初始化。
直接初始化一般使用括号方式,将初始值賦予變量。而拷貝初始化,則是使用等号對變量進行初始化。
示例代碼如下:
class OneInit {
public:
OneInit(int x) {}
};
int main() {
OneInit one(100); // 直接初始化
OneInit one2 = 101; // 拷貝初始化
OneInit *one3 = new OneInit(102); // 思考題:這是哪種初始化類型?
}
需要注意的是,拷貝初始化的等号并不是賦值的含義,而是隐式調用構造函數。如果在構造函數前加上explicit關鍵字,則編譯會失敗。explicit的作用就是強制構造函數必須顯式調用。
那麼,我們來看一下 C 11 中新增的列表初始化的方法,它使用花括号{}對變量進行初始化。列表初始化也有直接和拷貝兩種初始化類型。
還是用代碼說話:
class OneInit {
public:
OneInit(int x, string y) {}
};
int main() {
OneInit one{100, "init"}; // 直接初始化
OneInit one2 = {101, "init"}; // 拷貝初始化
OneInit *one3 = new OneInit{102, "init"}; // 直接初始化,知道上面思考題的答案了吧
}
上述代碼在構造函數中增加了一個string類型參數,以方便大家看到,在多參數的情況下,如何編寫列表初始化方法。
乍一看,這似乎和括号初始化方式差别不大,但它有一個妙用,就是在使用函數返回對象的時候,可以用列表初始化方式進行多參數的隐式構造,看代碼來體會一下:
class OneInit {
public:
OneInit(int x, string y) {}
};
OneInit get_object() {
return {103, "init"}; // 簡化了生成對象的過程
}
int main() {
OneInit one = get_object();
}
相信大家對列表初始化的規則已經了解,下面我們講一個高階應用。
容器初始化我們對于數組可以方便地以int array[] = {1, 2, 3};這樣的方法來初始化。但是對于 STL 庫中的衆多容器,以前卻沒什麼好辦法,隻能寫一個循環不斷地追加值。
現在借助于列表初始化,我們也可以做到像數組一樣初始化容器對象了。
int main() {
int array1[]{1, 2, 3};
int array2[] = {1, 2, 3};
vector<int> v1{1, 2, 3};
vector<int> v2 = {1, 2, 3};
list<int> l1{1, 2, 3};
list<int> l2 = {1, 2, 3};
map<string, int> m1{ {"one", 1}, {"two", 2}, {"three", 3} };
map<string, int> m2 = { {"one", 1}, {"two", 2}, {"three", 3} };
}
STL 容器對象的初始化,看起來也如絲般光滑了。這個實現離不開一個前提,就是容器類要支持std::initializer_list為形參的構造函數。
std::initializer_list是一個支持begin, end, size成員函數的類模闆。編譯器首先将花括号的内容構造為一個std::initializer_list對象,然後匹配容器類的構造函數,隻要形參是std::initializer_list類型,那麼執行相應的構造函數。
在構造函數中對begin, end範圍内的數據進行循環,就實現了多參數元素的初始化。說起來真不複雜,各位有心的完全可以自己定義一個類,然後支持std::initializer_list形參的構造函數,也能實現自有的列表初始化方法了。
不知道你注意到沒有,map 的初始化有點與衆不同,對于多參數的元素,似乎隻管嵌套就完了。如果深究的話,你會發現内層元素{"one", 1}其實是隐式調用了std::pair構造函數,而外層則是走的std::initializer_list方式。
注意事項列表初始化容易掉坑的地方,就是在隐式縮窄轉換上。例如int a = 10240; char b = a;中,變量 b 的初始化就發生了隐式縮窄,信息發生了丢失。這種問題編譯時有可能警告都不會有,所以要千萬小心。
要避免這樣的問題,需要知道四條隐式縮窄規則:
以下代碼是錯誤示例,請仔細看:
int main() {
int a = 10240;
double b = 99.9;
int c = {3.0}; // 對應規則1
float d = {b}; // 對應規則2
float e = {a}; // 對應規則3
char f = {a}; // 對應規則4
}
沒想到一個變量初始化,也能整出這麼多說法來。即使是一件看起來應該簡單的事情,在 C 裡面也能不斷挖出坑來。所以誰也不敢說自己是 C 高手,那是分分鐘被打臉。
不管怎樣,讓我們記住本文最重要的三個點,方便自己以後寫代碼吧:
參考資料:
謝丙堃 現代C 語言核心特性解析 人民郵電出版社
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!