1、 C++98/03初始化
我們先來總結一下C++98/03的各種不同的初始化情況:
//普通數組 int i_arr[3] = {1, 2, 3}; //POD(plain old data) struct A { int x; struct B { int i; int j; }b; }a = {1, {2, 3}}; //拷貝初始化 int i = 0; class Foo { public: Foo(int){}; }Foo = 123; //直接初始化 int j(0);
這些不同的初始化方法都有各自的適用范圍和方法,但是種類繁多的方法卻沒有一種可以通用的。所以C++11提出了初始化列表的方法來解決通用問題。
2、 統一初始化方法
其實C++98/03中已經存在初始化列表的方法,只是范圍比較窄,只適用於常規POD類型。
int i_arr[3] = {1, 2, 3}; int i_arr2[] = {1, 2, 3, 4}; struct B { int i; int j; }b = {1, 2};
而C++11將這種初始化方法適用於所有類型的初始化。我們先來看一組例子:
class Foo { public: Foo(int){}; private: Foo(const Foo &){}; }; void testFunc(void) { Foo val1(123); //Foo val2 = 123; // error:Foo::Foo(const Foo &) is private. Foo val3 = {123}; Foo val4{123}; int a5 = {2}; int a6{3}; }
val3、val4使用了初始化列表來初始化對象,a3雖然使用等號,但是並不影響到私有拷貝,仍然是初始化列表的方式,等統一val1的直接初始化,而val2則調用私有拷貝函數會報錯。a5、a6則是一般類型的初始化,val4和a6都是C++11特有的,而C++98/03並不支持。
新的初始化方法是變量名后面加{}來進行初始化,{}內則是初始化的內容,等號是否存在並不影響。
type val {};
C++11的新方式同樣支持new操作符:
int *a = new int{5}; double b = double {12.34}; int *arr = new int[3]{1,2,3};
a指向了new操作符分配的一塊內存,通過初始化列表將內存的初始值指定為了5;
b是對匿名對象進行初始化之后然后進行拷貝初始化;
arr則是通過new動態申請一個數組,並通過初始化列表進行初始化。
初始化列表還有一個特殊的地方,就是作為函數的返回值。
struct Foo { Foo(int, double){}; }; Foo testFunc(void) { return {12, 12.3}; }
在C++11中,初始化列表是非常方便的,不僅統一了對象的初始化方式,還使代碼更加簡潔清晰。
3、 使用細節
3.1 自定義類型初始化
當我們在C++11中使用初始化列表時,可能有以下情況:
struct A { int x; int y; }a = {123,123}; //a.x = 123, a.y = 123 struct B { int x; int y; B(int, int) : x(0), y(0) {}; }b = {123,123}; //b.x = 0, b.y = 0
這個例子說明什么問題呢,a是以C++98/03的聚合類型來初始化的,用拷貝的方式初始化a中的成員,而b呢,由於自定義了構造函數,所以初始化是以構造函數來初始化的。所以有以下結論:
當使用初始化列表時,如果是聚合類型,則以拷貝的方式來初始化成員,如果是非聚合類型,則是以構造函數來初始化成員。
3.2 聚合類型
提了這么多的聚合類型,那么到底什么是聚合類型呢?我們來看聚合類型的定義:
1) 類型是普通數組(int[10],char[],long[2][3]等)。
2) 類型是一個類,且:
- 無用戶自定義構造函數;
- 無私有或者保護的非靜態成員;
- 無基類;
- 無虛函數;
- 無{}和=直接初始化的非靜態數據成員。
3.2.1 數組
對於數組而言,就很簡單了,只要該類型是一個普通的數組,如果數組的元素並不是聚合類型,那么這個數組也是一個聚合類型:
int [] = {1,2,3}; std::string s_arr[3] = {“hello”, “C++”, “11”};
3.2.2 存在自定義構造函數
struct A { int x; int y; int z; A(int, int){}; }; A a = {123, 123, 12};
當一個自定義類擁有自己的構造函數使,無法將該類看作一個聚合類型,必須通過自定義的構造函數才能構造對象。
3.2.3 存在私有或者非靜態成員
struct A { int x; int y; protected: int z; }; A a = {123, 123, 12}; //error struct B { int x; int y; protected: static int z; }; B b = {123, 123}; //ok
例子中,A的實例化是失敗的,因為z是一個受保護的非靜態成員。而b是成功的,因為z是一個受保護的靜態數據成員,所以,類成員里面的靜態數據成員是不能通過初始化列表來初始化的,靜態數據成員的初始化遵循靜態成員的初始化方式。
3.2.4 有基類或者虛函數
有基類或者虛函數同樣不適用於使用初始化列表。
struct A { int x; int y; virtual void fun(){}; }; A a = {123, 123}; //error class B {}; struct C : public B { int x; int y; }; B b = {123, 123}; //error
3.2.5 {}和=初始化的非靜態數據成員
struct A { int x; int y = 2; }; A a = {123, 123}; //error
在類型A中,y在聲明時即被=初始化為2,所以A不再是一個聚合類型。
這個例子中需要注意的是,C++11中放寬了類型申明的初始化操作,即在非靜態數據成員的聲明時調用{}或者=來對成員進行初始化,但是造成的影響是該類型不再是聚合類型,所以不能直接使用初始化列表。所以,如果要使用初始化列表就必須自己定義一個構造函數。
3.2.6 聚合類型並非遞歸
struct A { int x; int y; private: int z; }; A a{1, 2, 3}; // error A a1{}; //ok struct B { A a; int x; double y; }; B b{{}, 1, 2.5};
A有一個私有化的非靜態成員,所以使用A a{1, 2, 3}是錯誤的,但是可以調用他的無參構造函數,所以在B中,即使成員a是一個非聚合類型,但是B仍然是一個聚合類型,可以直接使用初始化列表。
3.2.6 小結
根據這么多例子,我們得到以下結論:
對於一個聚合類型,使用初始化列表相當於對其中每個元素分別賦值;而對於非集合類型,則需要先定義一個合適的構造函數,此時使用初始化列表將調用它對應的構造函數。
4、 初始化列表
4.1 任意長度的初始化列表
在c++中,對於stl容器和未顯示數組長度的數組可以進行任意長度的初始化,在初始化的時候可以書寫任意長度的內容。
int i_arr[] = {1,2,3,4}; std::vector<int> veci_t = {1,2,3,4}; std::map<std::string, int> mapsi_t = {{"1", 1}, {"2", 2}, {"3", 3}};
但是對於自定義類型不具備這種能力,但是C++11解決了這個問題,C++11中可以通過輕量級模板std::initalizer_list來解決這個問題。我們只需要添加一個std::initializer_list的構造函數,這個自定義類型即可擁有這種任意長度初始化列表來初始化的能力。
class Foo { public: Foo( std::initializer_list<int> list ) {}; }; Foo foo = {1,2,3,4,5};
std::initializer_list負責接收初始化列表,可以通過for循環來讀取其中的元素,並將元素做操作。不僅可以作為類型的初始化,同樣的,可以作為函數參數傳遞同類型的數據集合。在任何需要的時候,都可以使用std::initializer_list來一次性傳遞多個參數。
// code1 class FooVector { public: FooVector(std::initializer_list<int> list) { for(auto it = list.begin(); it != list.end(); ++it) { mveci_content.push_back(*it); } } private: std::vector<int> mveci_content; }; FooVector foo1 = {1,2,3,4,5}; //code2 using pair_t = std::map<int, int>::value_type; class FooMap { public: FooMap(std::initializer_list<pair_t> list) { for(auto it = list.begin(); it != list.end(); ++it) { mmapii_content.insert(*it); } } private: std::map<int, int> mmapii_content; }; FooMap foo2 = {{1,2}, {3,4}, {5,6}}; //code3 void vFunc(std::initializer_list<int> list) { for(auto it = list.begin(); it != list.end(); ++it) { std::cout << *it << std::endl; } } void vCallFunc(void) { vFunc({}); vFunc({1,2,3,4}); }
4.2 std::initializer_list使用細節
std::initializer_list的特點如下:
- 它是一個輕量級的容器類型,內部定義了iterator等容器等必須的概念;
- 可以接收任意長度的初始化列表,但是要求元素必須都是同種類型;
- 有三個成員接口,size(),begin(),end();
- 只能被整體初始化或者賦值。
//獲取長度
std::initializer_list<int> list = {1,2,3}; //初始化 size_t len = list.size(); //len = 3
std::initializer_list的訪問只能通過begin()和end()來進行循環遍歷,遍歷取得的迭代器是只讀的,所以無法修改其中元素的值,但是可以整體賦值來修改其中的元素。
std::initializer_list<int> list; size_t len = list.size(); //len = 0 list = {1,2,3,4,5}; len = list.size(); //len = 5 list = {1,2}; len = list.size(); //len = 2
在研究了std::initializer_list的用法之后,我們來看std::initializer_list的效率。很多時候,如果容器內部是自定義類型或者數量較大,那么是不是就像vector之類的容器一樣,把每個元素都賦值一遍呢?答案是不是!std::initializer_list是非常高效的,它的內部並不保存初始化列表元素中的拷貝,僅僅保存初始化列表中的引用。
如果我們按照下面的代碼來使用std::initializer_list是錯誤的,雖然可以正常通過編譯,但是可能無法得到我們希望的結果,因為a,b在函數結束時生存周期也結束了,返回的是不確定的內容。
std::initializer_list<int> func1(void) { int a = 1, b = 2; return {a, b}; //a,b在返回時並沒有拷貝 }
正確的用法應該是這樣,通過真正的容器或者具有轉移拷貝語意的物件來替代std::initializer_list返回的結果。
std::vector<int> func2(void) { int a = 1, b = 2; return {a, b}; //ok }
我們應該將std::initializer_list看作保存對象的引用來使用,在它持有的對象的生命周期結束之前來完成傳遞。
5、 防止類型收窄
5.1 類型收窄的情況
我們先來看一段代碼:
struct Foo { Foo(int i) { std::cout << i << std::endl;} }; Foo foo(1.2);
這個例子就是類型收窄的情況,雖然說能夠正常通過編譯,但是在傳遞i之后不能完整的保存浮點數的數據。
我們來看C++中有哪些情況會有類型收窄的情況:
- 從一個浮點數隱式轉換為一個整數,如int I = 2.2;
- 從高精度浮點數隱式轉換為低精度浮點數,如long doule隱式轉換為double或者float;
- 從一個整數隱式轉換為一個浮點數,並且超出了浮點數的范圍,如float f = (unsigned long long ) – 1;
- 從一個整型隱式轉換為一個長度較短的整型數,並且超出了長度較短的整型數范圍,如char x = 65536;
這些類型收窄的情況,在編譯器並不會報錯,但是可能存在潛在的錯誤。
5.2 C++11的改善
C++11中可以通過初始化列表來檢查,防止類型的收窄。我們來看一組例子:
int a = 1.1; //ok int b = {1.1}; //error float fa = 1e40; //ok float fb = {1e40}; //error float fc = (unsigned long long) -1; //ok float fd = { (unsigned long long) -1 }; //error float fe = (unsigned long long)1; //ok float ff = {(unsigned long long)1}; //ok const int x = 1024, y = 1; char c = x; //ok char d = {x}; //error char e = y; //ok char f = {y}; //ok
在C++11中,遇到各種類型收窄的情況,初始化列表是不允許這種轉換的,上述例子中,如果x,y去掉const限定符,最后的f也會因為類型收窄而報錯。