tft每日頭條

 > 生活

 > c基礎教程第36講

c基礎教程第36講

生活 更新时间:2024-10-05 14:51:26
引言

變量初始化的工作,要是不細想,可能會覺得是一件挺簡單的事兒,例如int a = 100;。但在 C 的世界裡,誰也不敢說哪件事是簡單的,要是寫成int a = {100};,不知道你有沒有見過?

其實後一種寫法是列表初始化方法,是 C 11 标準裡定義的,它的結果也和前一種一樣。我們在下文會對列表初始化的用法、原理,以及如何避坑進行詳細讨論。

如果這個變量int a是出現在類的聲明中,那又有什麼新花樣對它進行默認初始化呢?我們就從類的非靜态成員默認初始化說起吧。

c基礎教程第36講(C11精要學習)1

非靜态成員默認初始化

這要從一個痛點說起。

在 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"}; //這是列表初始化方法,下節會具體講述 };

是不是簡化得多了,是不是有一種清爽感撲面而來?構造函數裡,隻關注你想初始化的成員就好,所有默認值都可以放在聲明處,一次性解決。

雖然方便,但仍有兩點注意事項,以免掉坑。

  • 第一,不要使用括号()對非靜态數據成員進行初始化;
  • 第二,不要使用auto關鍵字聲明和初始化非靜态數據成員。

其實你真用了上面的方法,編譯也會報錯,隻是要知道錯在哪裡。

列表初始化

在介紹列表初始化之前,我們先回顧一下已有的兩種初始化方式:直接初始化和拷貝初始化。

直接初始化一般使用括号方式,将初始值賦予變量。而拷貝初始化,則是使用等号對變量進行初始化。

示例代碼如下:

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 的初始化就發生了隐式縮窄,信息發生了丢失。這種問題編譯時有可能警告都不會有,所以要千萬小心。

要避免這樣的問題,需要知道四條隐式縮窄規則:

  1. 從浮點類型轉換到整數類型;
  2. 從long double轉換到long或float。或從double轉換到float;
  3. 從整數類型或非強枚舉類型,轉換到浮點類型;
  4. 從整數類型或非強枚舉類型,轉換到不能代表所有原始類型值的整數類型。

以下代碼是錯誤示例,請仔細看:

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 高手,那是分分鐘被打臉。

不管怎樣,讓我們記住本文最重要的三個點,方便自己以後寫代碼吧:

  • 類的非靜态成員,可以在聲明處直接初始化;
  • 使用列表初始化方法,簡化多參數構造函數隐式執行;
  • STL 容器對象,可直接使用列表初始化方法。

參考資料:

謝丙堃 現代C 語言核心特性解析 人民郵電出版社

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关生活资讯推荐

热门生活资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved