【C++】C++11列表初始化的一些使用細節

統一的初始化方法

在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 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中包含了私有的非靜態數據以及虛函數,用戶自定義構造函數,並且使用成員列表初始化方法可以使得非聚合類型的類也可以使用列表初始化方法,因此在這裏給各位看官提個建議,在對類的數據成員進行初始化的時候儘量在類的構造函數中用成員初始化列表的方式來對數據成員進行初始化,這樣可以防止一些意外的錯誤。

列表初始化防止類型收窄

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章