1. 程式人生 > >C++11新特性之列表初始化

C++11新特性之列表初始化

在我們實際程式設計中,我們經常會碰到變數初始化的問題,對於不同的變數初始化的手段多種多樣,比如說對於一個數組我們可以使用 int arr[] = {1,2,3}的方式初始化,又比如對於一個簡單的結構體:

struct A
{
	int x;
	int y;
}a={1,2};
這些不同的初始化方法都有各自的適用範圍和作用,且對於類來說不能用這種初始化的方法,最主要的是沒有一種可以通用的初始化方法適用所有的場景,因此C++11中為了統一初始化方式,提出了列表初始化(list-initialization)的概念。

統一的初始化方法

在C++98/03中我們只能對普通陣列和POD(plain old data,簡單來說就是可以用memcpy複製的物件)型別可以使用列表初始化,如下:

陣列的初始化列表: int arr[3] = {1,2,3}

POD型別的初始化列表:

struct A
{
	int x;
	int y;
}a = {1,2};
在C++11中初始化列表被適用性被放大,可以作用於任何型別物件的初始化。如下:
class Foo
{
public:
	Foo(int) {}
private:
	Foo(const Foo &);
};

int _tmain(int argc, _TCHAR* argv[])
{
	Foo a1(123); //呼叫Foo(int)建構函式初始化
	Foo a2 = 123; //error Foo的拷貝建構函式宣告為私有的,該處的初始化方式是隱式呼叫Foo(int)建構函式生成一個臨時的匿名物件,再呼叫拷貝建構函式完成初始化

	Foo a3 = { 123 }; //列表初始化
	Foo a4 { 123 }; //列表初始化

	int a5 = { 3 };
	int a6 { 3 };
	return 0;
}
由上面的示例程式碼可以看出,在C++11中,列表初始化不僅能完成對普通型別的初始化,還能完成對類的列表初始化,需要注意的是a3 a4都是列表初始化,私有的拷貝並不影響它,僅呼叫類的建構函式而不需要拷貝建構函式,a4,a6的寫法是C++98/03所不具備的,是C++11新增的寫法。

同時列表初始化方法也適用於用new操作等圓括號進行初始化的地方,如下:

int* a = new int { 3 };
double b = double{ 12.12 };
int * arr = new int[] {1, 2, 3};
讓人驚奇的是在C++11中可以使用列表初始化方法對堆中分配的記憶體的陣列進行初始化,而在C++98/03中是不能這樣做的。

列表初始化的一些使用細節

雖然列表初始化提供了統一的初始化方法,但是同時也會帶來一些使用上的疑惑需要各位苦逼碼農需要注意,比如對下面的自定義型別的例子:
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、任何長度的初始化列表

在C++11中,對於任意的STL容易都與和為顯示指定長度的陣列一樣的初始化能力,如:
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};
STL容易跟陣列一樣可以填入任何需要的任何長度的同類型的資料,而我們自定義的Foo型別卻不具備這種能力,只能按照建構函式的初始化列表順序進行依次賦值。實際上之所以STL容易擁有這種可以用任意長度的同類型資料進行初始化能力是因為STL中的容器使用了std::initialzer-list這個輕量級的類模板,std::initialzer-list可以接受任意長度的同類型的資料也就是接受可變長引數{...},那麼我們是否可以利用這個來改寫我們的Foo類,是的Foo類也具有這種能力呢?看下面例子:
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
在上面的例子中我們用std::initialzer-list將類改寫,是的類Foo也具有了接受可變長引數的能力,在Foo類中定義了三個變數,分別在main函式中使用1個2個3個4個引數的列表初始化方法來初始化foo變數,可見,由程式的輸出結果可知,對於這種擁有固定數目的資料成員來說使用std::initialzer-list來改寫後,如果列表初始化的引數剛好是3個則資料成員完全初始化,如果列表初始化的個數小於3個則未給予的值是一個隨機值,而大於3個的話超出的列表初始化引數將被拋棄。雖然std::initialzer-list可以改寫我們自定義的類,但是對於用於固定的資料成員的類來說這種改寫意義不大,若列表初始化個數少於資料成員個數則會導致某些資料成員未被初始化,容易引起程式出BUG,BUT如果我們自定義的類也是一個容器類情況呢?
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改寫我們自定義的容器類可以使得我們自定義的容器類和STL中的容器類一樣擁有接受可變長相同資料型別的資料的能力,注意資料型別必須相同。
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;
}
因此在以後碰到需要使用可變長度的同類型的資料時,可以考慮使用std::initialzer-list。


2、std::initialzer-list的使用細節

簡單瞭解了initialzer-list後,看看它擁有哪些特點呢? 1、它是一個輕量級的容器型別,內部定義了迭代器iterator等容器必須的一些概念。 2、對於initialzer-list<T>來說,它可以接受任意長度的初始化列表,但是元素必須是要相同的或者可以轉換為T型別的。 3、它只有三個成員介面,begin(),end(),size(),其中size()返回initialzer-list的長度。 4、它只能被整體的初始化和賦值,遍歷只能通過begin和end迭代器來,遍歷取得的資料是可讀的,是不能對單個進行修改的。 類似下面的操作時不合法的
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 };
}
雖然看起來沒有任何問題,且能正常編譯通過,但是a,b是在func內定義的區域性變數,但程式離開func時變數a,b就銷燬了,initialzer-list卻儲存的是變數的引用,因此返回的將是非法未知的內容。


列表初始化防止型別收窄

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的列表初始化方法定義變數從源頭了遏制了型別收窄,使得不恰當的用法就不會用在程式中,避免了某些位置型別的錯誤,因此建議以後再實際程式設計中儘可能的使用列表初始化方法定義變數。