在我們實際編程中,我們經常會碰到變量初始化的問題,對於不同的變量初始化的手段多種多樣,比如說對於一個數組我們可以使用 int arr[] = {1,2,3}的方式初始化,又比如對於一個簡單的結構體:
這些不同的初始化方法都有各自的適用范圍和作用,且對於類來說不能用這種初始化的方法,最主要的是沒有一種可以通用的初始化方法適用所有的場景,因此C++11中為了統一初始化方式,提出了列表初始化(list-initialization)的概念。
統一的初始化方法
在C++98/03中我們只能對普通數組和POD(plain old data,簡單來說就是可以用memcpy復制的對象)類型可以使用列表初始化,如下:
數組的初始化列表: int arr[3] = {1,2,3}
POD類型的初始化列表:
在C++11中初始化列表被適用性被放大,可以作用於任何類型對象的初始化。如下:
由上面的示例代碼可以看出,在C++11中,列表初始化不僅能完成對普通類型的初始化,還能完成對類的列表初始化,需要注意的是a3 a4都是列表初始化,私有的拷貝並不影響它,僅調用類的構造函數而不需要拷貝構造函數,a4,a6的寫法是C++98/03所不具備的,是C++11新增的寫法。
讓人驚奇的是在C++11中可以使用列表初始化方法對堆中分配的內存的數組進行初始化,而在C++98/03中是不能這樣做的。如下:
int* a = new int { 3 }; double b = double{ 12.12 }; int * arr = new int[] {1, 2, 3};
表初始化的一些使用細節
雖然列表初始化提供了統一的初始化方法,但是同時也會帶來一些使用上的疑惑需要各位苦逼碼農需要注意,比如對下面的自定義類型的例子:
- struct A
- {
- int x;
- int y;
- }a = {123, 321};
- //a.x = 123 a.y = 321
- struct B
- {
- int x;
- int y;
- B(int, int) :x(0), y(0){}
- }b = {123,321};
- //b.x = 0 b.y = 0
對於自定義的結構體A來說模式普通的POD類型,使用列表初始化並不會引起問題,x,y都被正確的初始化了,但看下結構體B和結構體A的區別在於結構體B定義了一個構造函數,並使用了成員初始化列表來初始化B的兩個變量,,因此列表初始化在這里就不起作用了,b采用的是構造函數的方式來完成變量的初始化工作。
那么如何區分一個類(class struct union)是否可以使用列表初始化來完成初始化工作呢?關鍵問題看這個類是否是一個聚合體(aggregate),首先看下C++中關於類是否是一個聚合體的定義:
(1)無用戶自定義構造函數。
(2)無私有或者受保護的非靜態數據成員
(3)無基類
(4)無虛函數
(5)無{}和=直接初始化的非靜態數據成員。下面我們逐個對上述進行分析。
1、首先存在用戶自定義的構造函數的情況,示例如下:
- struct Foo
- {
- int x;
- int y;
- Foo(int, int){ cout << "Foo construction"; }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo{ 123, 321 };
- cout << foo.x << " " << foo.y;
- return 0;
- }
輸出結果為:Foo construction -858993460 -858993460
可以看出對於有用戶自定義構造函數的類使用初始化列表其成員初始化后變量值是一個隨機值,因此用戶必須以用戶自定義構造函數來構造對象。
2、類包含有私有的或者受保護的非靜態數據成員的情況
- struct Foo
- {
- int x;
- int y;
- //Foo(int, int, double){}
- protected:
- double z;
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo{ 123,456,789.0 };
- cout << foo.x << " " << foo.y;
- return 0;
- }
實例中z是一個受保護的成員變量,該程序直接在VS2013下編譯出錯,error C2440: 'initializing' : cannot convert from 'initializer-list' to 'Foo',而如果將z變量聲明為static則,可以用列表初始化來,示例:
- struct Foo
- {
- int x;
- int y;
- //Foo(int, int, double){}
- protected:
- static double z;
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo{ 123,456};
- cout << foo.x << " " << foo.y;
- return 0;
- }
程序輸出:123 456,因此可知靜態數據成員的初始化是不能通過初始化列表來完成初始化的,它的初始化還是遵循以往的靜態成員的額初始化方式。
3、類含有基類或者虛函數
- struct Foo
- {
- int x;
- int y;
- virtual void func(){};
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo {123,456};
- cout << foo.x << " " << foo.y;
- return 0;
- }
上例中類Foo中包含了一個虛函數,該程序也是非法的,編譯不過的,錯誤信息和上述一樣cannot convert from 'initializer-list' to 'Foo'。
- struct base{};
- struct Foo:base
- {
- int x;
- int y;
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo {123,456};
- cout << foo.x << " " << foo.y;
- return 0;
- }
上例中則是有基類的情況,類Foo從base中繼承,然后對Foo使用列表初始化,該程序也一樣無法通過編譯,錯誤信息仍然為cannot convert from 'initializer-list' to 'Foo',
4、類中不能有{}或者=直接初始化的費靜態數據成員
- struct Foo
- {
- int x;
- int y= 5;
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo {123,456};
- cout << foo.x << " " << foo.y;
- return 0;
- }
在結構體Foo中變量y直接用=進行初始化了,因此上述例子也不能使用列表初始化方法,需要注意的是在C++98/03中,類似於變量y這種直接用=進行初始化的方法是不允許的,但是在C++11中放寬了,是可以直接進行初始化的,對於一個類來說如果它的非靜態數據成員使用了=或者{}在聲明同時進行了初始化,那么它就不再是聚合類型了,不適合使用列表初始化方法了。
在上述4種不再適合使用列表初始化的例子中,需要注意的是一個類聲明了自己的構造函數的情形,在這種情況下使用初始化列表是編譯器是不會給你報錯的,操作系統會給變量一個隨機的值,這種問題在代碼出BUG后是很難查找到的,因此這種情況不適合使用列表初始化需要特別注意,而其他不適合使用的情況編譯器會直接報錯,提醒你這些場景下使用列表初始化時不合法的。
那么是否有一種方法可以使得在類不是聚合類型的時候可以使用列表初始化方法呢?相信你肯定猜到了,作為一種很強大的語言不應該也不會存在使用上的限制。自定義構造函數+成員初始化列表的方式解決了上述類是非聚合類型使用列表初始化的限制。看下面的例子:
- struct Foo
- {
- int x;
- int y= 5;
- virtual void func(){}
- private:
- int z;
- public:
- Foo(int i, int j, int k) :x(i), y(j), z(k){ cout << z << endl; }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo {123,456,789};
- cout << foo.x << " " << foo.y;
- return 0;
- }
輸出結果為 789 123 456 ,可見,盡管Foo中包含了私有的非靜態數據以及虛函數,用戶自定義構造函數,並且使用成員列表初始化方法可以使得非聚合類型的類也可以使用列表初始化方法,因此在這里給各位看官提個建議,在對類的數據成員進行初始化的時候盡量在類的構造函數中用成員初始化列表的方式來對數據成員進行初始化,這樣可以防止一些意外的錯誤。
初始化列表
在上面的使得一個類成為非聚合類的例子2、3、4中,這些非法的用法編譯器都報出的錯誤是cannot convert from 'initializer-list' to 'Foo',那么這個initializer-list是什么呢?為什么使用列表初始化方法是將initializer-list轉換成對應的類類型呢?下面我們就來看看這個神秘的東西
1、任何長度的初始化列表
- int arr[] = { 1, 2, 3, 4, 5 };
- std::map < int, int > map_t { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
- std::list<std::string> list_str{ "hello", "world", "china" };
- std::vector<double> vec_d { 0.0,0.1,0.2,0.3,0.4,0.5};
- struct Foo
- {
- int x;
- int y;
- int z;
- Foo(std::initializer_list<int> list)
- {
- auto it= list.begin();
- x = *it++;
- y = *it++;
- z = *it++;
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- Foo foo1 {123,456,789};
- Foo foo2 { 123, 456};
- Foo foo3{ 123};
- Foo foo4{ 123, 456, 789,258 };
- cout << foo1.x << " " << foo1.y << " " << foo1.z<<endl;
- cout << foo2.x << " " << foo2.y << " " << foo2.z << endl;
- cout << foo3.x << " " << foo3.y << " " << foo3.z << endl;
- cout << foo4.x << " " << foo4.y << " " << foo4.z << endl;
- return 0;
- }
- 程序的輸出結果為:
- 123 456 789
- 123 456 -858993460
- 123 -858993460 -858993460
- 123 456 789
- class FooVec
- {
- public:
- std::vector<int> m_vec;
- FooVec(std::initializer_list<int> list)
- {
- for (auto it = list.begin(); it != list.end(); it++)
- m_vec.push_back(*it);
- }
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- FooVec foo1 { 1, 2, 3, 4, 5, 6 };
- FooVec foo2 { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
- return 0;
- }
std::initialzer-list不僅可以用於自定義類型的列表初始化方法,也可以用於傳遞相同類型數據的集合:
- void func(std::initializer_list<int> list)
- {
- for (auto it = list.begin(); it != list.end(); it++)
- {
- cout << *it << endl;
- }
- }
- int _tmain(int argc, _TCHAR* argv[])
- {
- func({});//傳遞一個空集
- func({ 1, 2, 3 });//傳遞int類型的數據集
- return 0;
- }
2、std::initialzer-list的使用細節
- std::initializer_list<int> list_t ={ 1, 2, 3, 4 };
- int _tmain(int argc, _TCHAR* argv[])
- {
- for (auto it = list_t.begin(); it != list_t.end; it++)
- (*it) = 1;
- return 0;
- }
此外initialzer-list<T>保存的是T類型的引用,並不對T類型的數據進行拷貝,因此需要注意變量的生存期。比如我們不能這樣使用:
- std::initializer_list<int> func(void)
- {
- auto a = 2, b = 3;
- return{ a, b };
- }
列表初始化防止類型收窄
C++11的列表初始化還有一個額外的功能就是可以防止類型收窄,也就是C++98/03中的隱式類型轉換,將范圍大的轉換為范圍小的表示,在C++98/03中類型收窄並不會編譯出錯,而在C++11中,使用列表初始化的類型收窄編譯將會報錯:- int a = 1.1; //OK
- int b{ 1.1 }; //error
- float f1 = 1e40; //OK
- float f2{ 1e40 }; //error
- const int x = 1024, y = 1;
- char c = x; //OK
- char d{ x };//error
- char e = y;//error
- char f{ y };//error
上面例子看出,用C++98/03的方式類型收窄並不會編譯報錯,但是將會導致一些隱藏的錯誤,導致出錯的時候很難定位,而利用C++11的列表初始化方法定義變量從源頭了遏制了類型收窄,使得不恰當的用法就不會用在程序中,避免了某些位置類型的錯誤,因此建議以后再實際編程中盡可能的使用列表初始化方法定義變量。