C++類成員冒號初始化以及構造函數內賦值

原文鏈接:https://blog.csdn.net/zj510/article/details/8135556

轉載自:https://blog.csdn.net/zj510/article/details/8135556

通常我們對類成員進行“初始化”有兩種方式:

1. 構造函數後面跟冒號;

2. 構造函數裏面對成員進行賦值。

有些人不太注意這個小細節,或者根本不知道他們的區別,認爲兩種方式是一樣的。這個誤解有時可能會對程序帶來影響,這裏我來介紹一下這兩種方式。

首先我們看這麼一段代碼:

class A
{
public:
	A(int& c)
	{
		_a = 1;
	}
protected:
	int _a;
	const int _b;
	int& _c;
};

這段代碼正確嗎?答案是否定,這段代碼無法通過編譯。我們會看到下面的編譯錯誤

1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_b' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(20) : see declaration of 'A::_b'
1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_c' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(21) : see declaration of 'A::_c'


意思是說成員_b和_c必須在構造函數的成員初始化列表裏面初始化。那麼_a爲什麼沒有報錯呢?看看成員的聲明,我們看到_a是一個int類型,_b是一個const int類型,_c是一個int&類型。根據C++的規則,const類型和引用不可以被賦值,只能被初始化。這裏我們先花一點點時間來看一下const類型和引用。

大家看看下面的這段代碼是否正確:
 

int _tmain(int argc, _TCHAR* argv[])
{
	int a;
	const int b;
	int& c;
 
	return 0;
}

編譯一下就會看到這2個錯誤:
 

1>d:\study\myconsole\myconsole\myconsole.cpp(30) : error C2734: 'b' : const object must be initialized if not extern
1>d:\study\myconsole\myconsole\myconsole.cpp(31) : error C2530: 'c' : references must be initialized

 哦,原來const和引用必須在聲明的時候就初始化(其實就是因爲const和引用不可以在變量創建完成後再被賦值,所以編譯器做了這個限制)。ok,把代碼改一下:
 

int _tmain(int argc, _TCHAR* argv[])
{
	int a;
	const int b=5;
	int& c = a;
 
	return 0;
}

在 編譯就沒有問題了。細心的朋友會發現我這裏在b和c聲明代碼那裏使用的=號而不是(),其實我們也可以這麼做:

int _tmain(int argc, _TCHAR* argv[])
{
	int a;
	const int b(5);
	int& c(a);
 
	return 0;
}

在這種情況下用括號和等於號初始化,效果是一樣的。具體就不細講了。

OK,我們舉這個小例子的目的就是想加深大家對const和引用的印象:const和引用必須在聲明的時候就初始化,換句話說就是在給const和引用類型變量分配內存的時候就初始化。

好了,現在我們回到class A的問題,類A裏面有const成員和引用成員,當系統要給類A的對象分配內存的時候,系統需要給A的對象的3個成員_a, _b, _c分配內存。_a沒有問題,系統直接給它一塊內存。_b和_c就出問題了,分配內存的時候沒有初始化。所以編譯就出問題了。其實C++給類成員初始化的唯一方式就是成員初始化列表,也就是構造函數後面跟冒號的那種形式。將class A的代碼調整一下:
 

class A
{
public:
	A(int& c): _b(2), _c(c)
	{
		_a = 1;
	}
protected:
	int _a;
	const int _b;
	int& _c;
};
 
 
 
 
int _tmain(int argc, _TCHAR* argv[])
{
	int number = 3;
	A a(number);
 
	return 0;
}

我們在A的構造函數的後面用冒號來初始化_b和_c。現在可以通過編譯了。因爲系統可以在給_b和_c分配內存的時候就初始化了。那麼假如我們把代碼改成下面的形式:

class A
{
public:
	A(int& c)
	{
		_a = 1;
		_b = 2;
		_c = c;
	}
protected:
	int _a;
	const int _b;
	int& _c;
 
};

這樣 能行嗎?編譯一下就得到下面的錯誤:

1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_b' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(22) : see declaration of 'A::_b'
1>d:\study\myconsole\myconsole\myconsole.cpp(14) : error C2758: 'A::_c' : must be initialized in constructor base/member initializer list
1>        d:\study\myconsole\myconsole\myconsole.cpp(23) : see declaration of 'A::_c'
1>d:\study\myconsole\myconsole\myconsole.cpp(17) : error C2166: l-value specifies const object

這3個錯誤包含2個意思:

1. const和引用變量沒有初始化;

2. 不可以對const變量_b進行賦值,也可以說const變量不可以當作左值(error C2166: l-value specifies const object)。

現在我們就可以知道了,其實在構造函數裏面調用等於號並不是真正意義上的“初始化”。這個過程相當於:

1. 系統創建成員變量;

2. 創建完後再進行賦值操作。

而在構造函數後面跟冒號,就相當於:

1. 系統創建成員變量並且初始化。也就是系統爲成員變量分配了一塊內存並且把相應的數據給填了進去。而構造函數裏面調用等於號的方式是分配好後再進行賦值,多了一個步驟。

 下面我們再來做一個實驗:
 

class A
{
public:
	A(int& c): _b(2), _c(c)
	{
		_a = 1;
	}
protected:
	int _a;
	const int _b;
	int& _c;
 
};
 
class B
{
public:
	B(int& c):_objA(c)
	{
		printf("B constructor\n");
	}
 
protected:
	A _objA;
};
 
 
 
int _tmain(int argc, _TCHAR* argv[])
{
	int number = 3;
	B obj2(number);
 
	return 0;
}

類B裏面有個一個類A的對象,在類B的構造函數裏面用冒號來初始化成員_objA。那麼_objA是什麼時候被初始化的呢?有圖有真相:


從callstack裏面可以清楚的看到:

1. 進入B的構造函數;

2. 進入A的構造函數。

也就是說冒號後面的代碼是在一進入構造函數的時候就被調用了。

然後從左下角的Watch裏面也可以看到,在系統調用構造函數括號裏面的第一行代碼之前,_a,_b, _c就已經分配好了。我們可以看到_a是個沒有初始化過的值(系統自己生成了一個),_b和_c都是我們初始化的。那麼我可以得出一個結論:

構造函數後面跟的冒號代碼是在進入構造函數並且在括號裏面的第一行代碼之前被執行。

 

假如在B的構造函數裏面不顯式初始化_objA,會發生什麼事呢?用代碼模擬一下就知道了,系統會調用A的默認構造函數來初始化_objA。

 

好了,講完了。通俗的講,構造函數後面的冒號就是初始化,而括號裏面的等於號並不是初始化,而是變量生成以後的賦值而已(永遠都是2個步驟)。

附:

本文前面我提到一句話:const和引用不可以被賦值,只能被初始化。可能會有些朋友對這句話有意見,看下面的代碼,這段代碼是正確的,沒有問題。那麼怎麼說不能被賦值呢?其實b=12只是把a的內容給改掉了(a和b的值都是12),而不是把引用b指向另外一個變量。換句話說:引用b初始化完成後,就永遠指向初始化時候的那個變量,無法再改變了。我這裏的“引用不可以被賦值”是指不能給引用本身賦值來改變它的指向,並不是說不可以改變引用指向的內存的內容。可能言語上面會有不同的理解,但是隻要知道是這麼回事情就可以了。
 

int _tmain(int argc, _TCHAR* argv[])
{
	int a = 1;
	int& b = a;
	b = 12;
 
	return 0;
}

 

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