1. 程式人生 > >C++中預設建構函式和建構函式初始化列表

C++中預設建構函式和建構函式初始化列表

1、預設建構函式和建構函式

(1)建構函式:C++用於構建類的新物件時需要呼叫的函式,該函式無返回型別!(注意:是“無”! 不是空!(void))。

(2)預設建構函式預設建構函式是在呼叫時不需要顯示地傳入實參的建構函式。

一個類如果自己沒有定義建構函式,則會有一個無參且函式體也是空的預設建構函式。只要程式設計師定義了建構函式,編譯器就不會再提供預設構造函數了。

定義預設建構函式有兩種方式,一是定義一個無參的建構函式,二是定義所有引數都有預設值的建構函式。

class testClass
{
public:
    testClass();                         /* 預設建構函式 */
    testClass(int a, char b);        	 /* 建構函式 */
    testClass(int a=10,char b='c');      /* 預設建構函式 */

private:
    int  m_a;
    char m_b;
};

(3)編譯器在什麼情況下會生成預設建構函式?

下面幾種情況下,編譯需要生成預設建構函式:

  • 當該類的類物件資料成員有預設建構函式時。
  • 當該類的基類有預設建構函式時。
  • 當該類的基類為虛基類時。
  • 當該類有虛擬函式時。

(4)避免“無引數的預設建構函式”和“帶預設引數的預設建構函式”同時存在

無引數的預設建構函式和帶預設引數的預設建構函式同時存在時,編譯器會產生二義性,從而生成編譯錯誤

class Sample {
public:
    // 預設建構函式
    Sample() {
        // do something
        printf("Sample()");
    }

    // 預設建構函式
    Sample(int m = 10) {
        // do something
        printf("Sample(int m = 10)");
    }
};


int main()
{
    Sample s; // error C2668: “Sample::Sample”: 對過載函式的呼叫不明確

    return 0;
}

2、建構函式初始列表

C++類中成員函式一般有兩種初始化,一種是在類的建構函式內部初始化,另一種是利用類的建構函式的初始化列表進行初始化。初始化和賦值對內建型別的成員沒有什麼大的區別。對非內建型別成員變數,為了避免兩次構造,推薦使用類建構函式初始化列表。

         初始化資料成員與對資料成員賦值的含義是什麼?有什麼區別?

首先把資料成員按型別分類並分情況說明:

(1)內建資料型別,如int,float等;複合型別(指標,引用)

    在成員初始化列表和建構函式體內進行,在效能和結果上都是一樣的

(2)使用者定義型別(類型別)

結果上相同,但是效能上存在很大的差別。因為類型別的資料成員物件在進入函式體前已經構造完成,也就是說在成員初始化列表處進行構造物件的工作,呼叫建構函式,在進入函式體之後,進行的是對已經構造好的類物件的賦值,又呼叫個拷貝賦值操作符才能完成(如果並未提供,則使用編譯器提供的預設按成員賦值行為)。因此,能使用初始化列表的時候儘量使用初始化列表。

class Test2
{
public:
    Test1 test1 ;
    Test2(Test1 &t1):test1(t1){}    //呼叫拷貝建構函式初始化test1,省去了呼叫預設建構函式的過程
}

3、必須用帶有初始化列表的建構函式:

(1)成員型別是沒有預設建構函式的類。若沒有提供顯示初始化式,則編譯器隱式使用成員型別的預設建構函式,若類沒有預設建構函式,則編譯器嘗試使用預設建構函式將會失敗。

class Test1
{
public:
    Test1(int a):i(a){}
    int i;
};
class Test2
{
public:
    Test1 test1 ;
    Test2(Test1 &t1)
    {test1 = t1 ;}
};

以上程式碼無法通過編譯,因為Test2的建構函式中test1 = t1這一行實際上分成兩步執行:

呼叫Test1的預設建構函式來初始化test1

由於Test1沒有預設的建構函式,所以1 無法執行,故而編譯錯誤。正確的程式碼如下,使用初始化列表代替賦值操作

class Test2
{
public:
    Test1 test1 ;
    Test2(int x):test1(x){}
}

(2)const成員引用型別的成員。因為const物件或引用型別只能初始化,不能對他們賦值。

#include <iostream>

using namespace std;

class A
{
public:
#if 0
	//因為成員變數中含有引用和常量則不能如此定義該建構函式
	A(){
		cout<<"aaaaaaaa"<<endl;
	}
#endif
#if 1
	A(int x1 = 0):x(x1),rx(x),pi(3.14) { }//帶有初始化列表的建構函式
	void print()
	{
		cout<<"x="<<x<<" rx="<<rx<<" pi="<<pi<<endl; 
	}
	~A(){}
private:
	int x;
	int &rx;		 //宣告為引用時則必須在建構函式基/成員初始值設定項列表中初始化
	const double pi;//宣告為const時則必須在建構函式基/成員初始值設定項列表中初始化
#endif
};

int main()
{
	A a(20);
	a.print();

	A a1;		//因為建構函式中有x1預設實參(int x1 = 111),則可以如此呼叫
	a1.print();

	system("pause");
	return 0;
}

注:如果一個建構函式為所以引數都提供預設實參,則它實際上也定義了預設建構函式

(3)基類未宣告預設建構函式

#include <iostream>

using namespace std;

class Animal
{
public:
	Animal(int weight,int height):        //基類未宣告預設建構函式
	  m_weight(weight),m_height(height){
		  cout<<"Animal "<<"weight="<<weight<<" height="<<height<<endl;
	  }

private:
	int m_weight;
	int m_height;
};

class Dog: public Animal
{
public:
	Dog(int weight = 10,int height = 20,int type = 1):
	  Animal(weight,height){//必須使用初始化列表增加對父類的初始化,否則父類Animal無合適建構函式
		cout<<"Dog"<<endl;
	}
private:
	int m_type;
};
int main()
{
	Dog d;

	system("pause");
	return 0;
}

最簡單的解決方法是將Animal的建構函式宣告為:Animal(int weight = 0,int height = 0);

4、初始化列表的成員初始化順序:

C++初始化類成員時,是按照宣告的順序初始化的,而不是按照出現在初始化列表中的順序。

class CMyClass {
    CMyClass(int x, int y);
    int m_x;
    int m_y;
};
 
 CMyClass::CMyClass(int x, int y) : m_y(y), m_x(m_y){
}

        你可能以為上面的程式碼將會首先做m_y=I,然後做m_x=m_y,最後它們有相同的值。但是編譯器先初始化m_x,然後是m_y,,因為它們是按這樣的順序宣告的。結果是m_x將有一個不可預測的值。有兩種方法避免它,一個是總是按照你希望它們被初始化的順序宣告成員,第二個是,如果你決定使用初始化列表,總是按照它們宣告的順序羅列這些成員。這將有助於消除混淆。

        如果可以的話儘可能避免使用某些成員初始化其他成員。而最好用建構函式的引數作為成員的初始值,這樣做的好處是可以比考慮成員的初始化順序。例如以上可改為:

class CMyClass {
     CMyClass(int x, int y);
     int m_x;
     int m_y;
 };
 
 CMyClass::CMyClass(int x, int y) : m_x(x), m_y(y) {
 }