關於 C++ 中的變量,數組,對象等都有不同的初始化方法,在這些繁瑣的初始化方法中沒有任何一種方式適用於所有的情況。爲了統一初始化方式,並且讓初始化行爲具有確定的效果,在 C++11 中提出了列表初始化的概念。
1. 統一的初始化
在 C++98/03 中,對應普通數組和可以直接進行內存拷貝(memcpy ())的對象是可以使用列表初始化來初始化數據的
// 數組的初始化 int array[] = { 1,3,5,7,9 }; double array1[3] = { 1.2, 1.3, 1.4 }; // 對象的初始化 struct Person { int id; double salary; }zhang3{1, 3000 };
在 C++11 中,列表初始化變得更加靈活了,來看一下下面這段初始化類對象的代碼:
#include <iostream> using namespace std; class Test { public: Test(int) {} private: Test(const Test&); }; int main(void) { Test t1(520); Test t2 = 520; Test t3 = { 520 }; Test t4{ 520 }; int a1 = { 1314 }; int a2{ 1314 }; int arr1[] = { 1, 2, 3 }; int arr2[]{ 1, 2, 3 }; return 0; }
具體地來解讀一下上面代碼中使用的各種初始化方式:
t1:最中規中矩的初始化方式,通過提供的帶參構造進行對象的初始化
t2:語法錯誤,因爲提供的拷貝構造函數是私有的。
如果拷貝構造函數是公共的,520 會通過隱式類型轉換被 Test(int) 構造成一個匿名對象,然後再通過對這個匿名對象進行拷貝構造得到 t2(這個錯誤在 VS 中不會出現,在 Linux 中使用 g++ 編譯會提示描述的這個錯誤,截圖如下。)
t3 和 t4:使用了 C++11 的初始化方式來初始化對象,效果和 t1 的方式是相同的。
在初始時,{} 前面的等號是否書寫對初始化行爲沒有任何影響。
t3 雖然使用了等號,但是它仍然是列表初始化,因此私有的拷貝構造對它沒有任何影響。
t1、arr1 和 t2、arr2:這兩個是基礎數據類型的列表初始化方式,可以看到,和對象的初始化方式是統一的。
t4、a2、arr2 的寫法,是 C++11 中新添加的語法格式,使用這種方式可以直接在變量名後邊跟上初始化列表,來進行變量或者對象的初始化。
既然使用列表初始化可以對普通類型以及對象進行直接初始化,那麼在使用 new 操作符創建新對象的時候可以使用列表初始化進行對象的初始化嗎?答案是肯定的,來看下面的例子:
int * p = new int{520}; double b = double{52.134}; int * array = new int[3]{1,2,3};
指針p 指向了一個 new 操作符返回的內存,通過列表初始化將內存數據初始化爲了 520
變量b 是對匿名對象使用列表初始之後,再進行拷貝初始化。
數組array 在堆上動態分配了一塊內存,通過列表初始化的方式直接完成了多個元素的初始化。
除此之外,列表初始化還可以直接用在函數返回值上:
#include <iostream> #include <string> using namespace std; class Person { public: Person(int id, string name) { cout << "id: " << id << ", name: " << name << endl; } }; Person func() { return { 9527, "華安" }; } int main(void) { Person p = func(); return 0; }
代碼中的 return { 9527, "華安" }; 就相當於 return (9527, "華安" );,直接返回了一個匿名對象。通過上面的幾個例子可以看出在 C++11 使用列表初始化是非常便利的,它統一了各種對象的初始化方式,而且還讓代碼的書寫更加簡單清晰。
2. 列表初始化細節
2.1 聚合體
在 C++11 中,列表初始化的使用範圍被大大增強了,但是一些模糊的概念也隨之而來,在前面的例子可以得知,列表初始化可以用於自定義類型的初始化,但是對於一個自定義類型,列表初始化可能有兩種執行結果:
#include <iostream> #include <string> using namespace std; struct T1 { int x; int y; }a = { 123, 321 }; struct T2 { int x; int y; T2(int, int) : x(10), y(20) {} }b = { 123, 321 }; int main(void) { cout << "a.x: " << a.x << ", a.y: " << a.y << endl; cout << "b.x: " << b.x << ", b.y: " << b.y << endl; return 0; }
程序執行的結果是這樣的:
a.x: 123, a.y: 321 b.x: 10, b.y: 20
在上邊的程序中都是用列表初始化的方式對對象進行了初始化,但是得到結果卻不同,對象 b 並沒有被初始化列表中的數據初始化,這是爲什麼呢?
對象 a 是對一個自定義的聚合類型進行初始化,它將以拷貝的形式使用初始化列表中的數據來初始化 T1 結構體中的成員。
在結構體 T2 中自定義了一個構造函數,因此實際的初始化是通過這個構造函數完成的。
現在很多小夥伴可能就一頭霧水了,同樣是自定義結構體並且在創建對象的時候都使用了列表初始化來初始化對象,爲什麼在類內部對對象的初始化方式卻不一樣呢?因爲如果使用列表初始化對對象初始化時,還需要判斷這個對象對應的類型是不是一個聚合體,如果是初始化列表中的數據就會拷貝到對象中。
那麼,使用列表初始化時,對於什麼樣的類型 C++ 會認爲它是一個聚合體呢?
普通數組本身可以看做是一個聚合類型
int x[] = { 1,2,3,4,5,6 }; double y[3][3] = { {1.23, 2.34, 3.45}, {4.56, 5.67, 6.78}, {7.89, 8.91, 9.99}, };
char carry[] = {'a', 'b', 'c', 'd', 'e', 'f'};
std::string sarry[] = {"hello", "world", "nihao", "shijie"};
滿足以下條件的類(class、struct、union)可以被看做是一個聚合類型:
無用戶自定義的構造函數。
無私有或保護的非靜態數據成員。
場景 1: 類中有私有成員,無法使用列表初始化進行初始化
struct T1 { int x; long y; protected: int z; }t{ 1, 100, 2 }; // error, 類中有私有成員, 無法使用初始化列表初始化
場景 2:類中有非靜態成員可以通過列表初始化進行初始化,但它不能初始化靜態成員變量。
struct T2 { int x; long y; protected: static int z; }t{ 1, 100, 2 }; // error
結構體中的靜態變量 z 不能使用列表初始化進行初始化,它的初始化遵循靜態成員的初始化方式。
struct T2 { int x; long y; protected: static int z; }t{ 1, 100 }; // ok // 靜態成員的初始化 int T2::z = 2;
無基類。
無虛函數。
類中不能有使用 {} 和 = 直接初始化的非靜態數據成員(從 c++14 開始就支持了)。
#include <iostream> #include <string> using namespace std; struct T2 { int x; long y; protected: static int z; }t1{ 1, 100 }; // ok // 靜態成員的初始化 int T2::z = 2; struct T3 { int x; double y = 1.34; int z[3]{ 1,2,3 }; }; int main(void) { T3 t{ 520, 13.14, {6,7,8} }; // error, c++11不支持,從c++14開始就支持了 return 0; }
從C++14開始,使用列表初始化也可以初始化在類中使用{}和=初始化過的非靜態數據成員。
2.2 非聚合體
對於聚合類型的類可以直接使用列表初始化進行對象的初始化,如果不滿足聚合條件還想使用列表初始化其實也是可以的,需要在類的內部自定義一個構造函數, 在構造函數中使用初始化列表對類成員變量進行初始化:
#include <iostream> #include <string> using namespace std; struct T1 { int x; double y; // 在構造函數中使用初始化列表初始化類成員 T1(int a, double b, int c) : x(a), y(b), z(c) {} virtual void print() { cout << "x: " << x << ", y: " << y << ", z: " << z << endl; } private: int z; }; int main(void) { T1 t{ 520, 13.14, 1314 }; // ok, 基於構造函數使用初始化列表初始化類成員 t.print(); return 0; }
另外,需要額外注意的是聚合類型的定義並非遞歸的,也就是說當一個類的非靜態成員是非聚合類型時,這個類也可能是聚合類型,比如下面的這個例子:
#include <iostream> #include <string> using namespace std; struct T1 { int x; double y; private: int z; }; struct T2 { T1 t1; long x1; double y1; }; int main(void) { T2 t2{ {}, 520, 13.14 }; return 0; }
可以看到,T1 並非一個聚合類型,因爲它有一個 Private 的非靜態成員。但是儘管 T2 有一個非聚合類型的非靜態成員 t1,T2 依然是一個聚合類型,可以直接使用列表初始化的方式進行初始化。
最後強調一下 t2 對象的初始化過程,對於非聚合類型的成員 t1 做初始化的時候,可以直接寫一對空的大括號 {},這相當於調用是 T1 的無參構造函數。
對於一個聚合類型,使用列表初始化相當於對其中的每個元素分別賦值,而對於非聚合類型,則需要先自定義一個合適的構造函數,此時使用列表初始化將會調用它對應的構造函數。
3. std::initializer_list
在 C++ 的 STL 容器中,可以進行任意長度的數據的初始化,使用初始化列表也只能進行固定參數的初始化,如果想要做到和 STL 一樣有任意長度初始化的能力,可以使用 std::initializer_list 這個輕量級的類模板來實現。
先來介紹一下這個類模板的一些特點:
它是一個輕量級的容器類型,內部定義了迭代器 iterator 等容器必須的概念,遍歷時得到的迭代器是隻讀的。
對於 std::initializer_list<T> 而言,它可以接收任意長度的初始化列表,但是要求元素必須是同種類型 T
在 std::initializer_list 內部有三個成員接口:size(), begin(), end()。
std::initializer_list 對象只能被整體初始化或者賦值。
3.1 作爲普通函數參數
如果想要自定義一個函數並且接收任意個數的參數(變參函數),只需要將函數參數指定爲 std::initializer_list,使用初始化列表 { } 作爲實參進行數據傳遞即可。
#include <iostream> #include <string> using namespace std; void traversal(std::initializer_list<int> a) { for (auto it = a.begin(); it != a.end(); ++it) { cout << *it << " "; } cout << endl; } int main(void) { initializer_list<int> list; cout << "current list size: " << list.size() << endl; traversal(list); list = { 1,2,3,4,5,6,7,8,9,0 }; cout << "current list size: " << list.size() << endl; traversal(list); cout << endl; list = { 1,3,5,7,9 }; cout << "current list size: " << list.size() << endl; traversal(list); cout << endl; //////////////////////////////////////////////////// ////////////// 直接通過初始化列表傳遞數據 ////////////// //////////////////////////////////////////////////// traversal({ 2, 4, 6, 8, 0 }); cout << endl; traversal({ 11,12,13,14,15,16 }); cout << endl; return 0; }
示例代碼輸出的結果:
current list size: 0 current list size: 10 1 2 3 4 5 6 7 8 9 0 current list size: 5 1 3 5 7 9 2 4 6 8 0 11 12 13 14 15 16
std::initializer_list擁有一個無參構造函數,因此,它可以直接定義實例,此時將得到一個空的std::initializer_list,因爲在遍歷這種類型的容器的時候得到的是一個只讀的迭代器,因此我們不能修改裏邊的數據,只能通過值覆蓋的方式進行容器內部數據的修改。雖然如此,在效率方面也無需擔心,std::initializer_list的效率是非常高的,它的內部並不負責保存初始化列表中元素的拷貝,僅僅存儲了初始化列表中元素的引用。
3.2 作爲構造函數參數
自定義的類如果在構造對象的時候想要接收任意個數的實參,可以給構造函數指定爲 std::initializer_list 類型,在自定義類的內部還是使用容器來存儲接收的多個實參。
#include <iostream> #include <string> #include <vector> using namespace std; class Test { public: Test(std::initializer_list<string> list) { for (auto it = list.begin(); it != list.end(); ++it) { cout << *it << " "; m_names.push_back(*it); } cout << endl; } private: vector<string> m_names; }; int main(void) { Test t({ "jack", "lucy", "tom" }); Test t1({ "hello", "world", "nihao", "shijie" }); return 0; }
輸出的結果:
jack lucy tom
hello world nihao shijie