1. 程式人生 > >C++默認構造函數的合成

C++默認構造函數的合成

什麽 更多 fault 導致 插入 這樣的 是你 允許 過程

默認構造函數的誤解

1.當程序猿定義了默認構造函數,編譯器就會直接使用此默認構造函數

來一個簡單的栗子

class Student;
class School
{
public:
School(){}
...
Student students;
};

我們知道,一個對象,在定義的時候就一定會調用其構造函數。而在我們上面的默認構造函數,明顯沒有調用students的構造函數,所以,編譯器絕對沒有直接使用我們的默認構造函數。具體細節,請看下文,這裏只提問題。

2.當程序猿沒有定義構造函數的時候,編譯器就會幫我們定義默認構造函數

接下來,就讓我們帶著這些錯誤的看法來進入正文。

為什麽要幫我們定義默認構造函數

再來個簡單的栗子

class Student
{
public:
Student(){} //有定義
void Study(); //只給出了聲明,沒有定義
...
};
void main()
{
Student stu;
//stu.Study(); //調用沒有定義的函數
}

上面是一個可以編譯,連接,運行的例子完整代碼。其中,Study()函數只有聲明,但是沒有定義,但是卻通過了編譯?為什麽呢?因為你沒有用到它。即使,你將註釋的那行代碼取消註釋,它也不會在編譯期出錯,只會等到連接的時候編譯器才會提示錯誤。具體可以參考這篇博客,依樣畫葫蘆。你也可以先不看,記住這樣的話:編譯器沒有具體需要用到某個函數時(上面是因為代碼中沒有調用Study函數),這個函數可以沒有實現。所以,你可以在你的代碼中很不負責任地聲明很多沒有用的函數並且不對其中的任何一個進行實現。

回到我們的內容,“為什麽要幫我們定義默認構造函數”,答案就是編譯器要用到,但是你卻沒有給出明確定義。註意,這裏不是程序需要,而是編譯器需要。程序需要用到,是指我們希望class中的成員,基類等能夠正常地值初始化。而編譯器需要,是指,沒有這個函數,編譯連接工作就沒辦法正常地進行。

那麽問題就來了,編譯器具體什麽時候有這個需求。

在四個需求下,編譯器需要一個默認構造函數

第一個需求

如果一個class沒有任何constructor,但它內含一個member object,而後者有default constructor,那麽這個class的implicit default constructor就是“nontrivial",編譯器需要為該class合成一個default constructor。不過,這個合成操作只有在constructor真正需要被調用時才會發生。

這裏引用了書裏的話。nontrivial的意思就是有用的。舉個例子說明一下。

class Student
{
public:
Student(){}
...
};
class School
{
Student students; //不是繼承,是內含
char* name;
...
};
void main()
{
int a;
School school; //合成操作在這裏發生
}

上面的例子中,編譯器為School類合成了一個default constructor,因為School中包含的Student具有默認的構造函數,而我們在構造School的時候,需要調用Student的默認構造函數,所以編譯器就幫我們合成了一個大概樣子如下的默認構造函數。

School::School()
{
students.Student();
}

註意:School::name初始化是程序的需求,而不是編譯器的需求。所以,合成的構造函數不會完成對name的初始化。時刻分清,編譯器的需求與程序的需求不是一回事。

回到上面的程序,編譯器在main中的第二行代碼中才進行了合成。還記得在上一部分中我們提到的那些只聲明沒有定義的函數,無獨有偶,假如我們在上面的代碼中沒有實例化School,那麽這個合成操作永遠不會進行,因為編譯器不需要!!!只有當需要用到這個默認構造函數的時候,編譯器才會進行合成。

這裏還有一個問題,假如我們自己定了構造函數,卻沒有調用內部對象的構造函數時,編譯器還會合成一個新的構造函數嗎?否。編譯器只會在已有的構造函數裏面插入”編譯器需要“的代碼。再來個簡單的栗子。

class Student
{
public:
Student(){}
...
};
class School
{
public:
School(){name = NULL} //沒有初始化students
Student students; //不是繼承,是內含
Student students2;
char* name;
...
};
//編譯器插入自己需要的代碼,最後的構造函數類似如下
School::School()
{
//在用戶自己的代碼前插入,確保所有的對象在使用前已經初始化
students.Student();
students2.Student(); //存在多個對象,按照聲明的順序進行插入
name = NULL;
}

第二個需求

如果一個沒有任何constructor的class派生自一個"帶有default constructor"的base class,那麽這個derived class的default constructor會被視為nontrivial,並因此需要被合成出來。

這一點與第一個需求很相似。需要記住的有以下幾點。

1.在derived class的constructor(已有或者合成)中,編譯器除了插入member class object的constructor外,還會插入base class constructor。

2.base class constructor的調用時間在member class object之前。

第三個需求

class聲明(或繼承)一個virtual function,當缺乏程序猿聲明的constructor,編譯器合成一個default constructor。

我們知道,virtual function在調用的過程中,具體的函數是在編譯器是不可知的。比如

class Base
{
public:
Base();
virtual void Print();
};
class Derived:public Base
{
public:
Derived();
virtual void Print();
};
void Print(Base *para)
{
para->Print();
}
void main()
{
Base a;
Derived b;
Print(a); //調用Base::Print();
Print(b); //調用Derived::Print();
}

編譯器如何得知調用哪一個Print()呢?當class中含有virtual function的時候,編譯器會在class中插入“虛表指針",並在構造函數中進行初始化。虛表指針指向了虛函數表,裏面記錄了各個虛函數的地址。程序之所以能夠實現多態,實際上就是因為調用虛函數的時候,動態地使用了這個表裏面的地址。

回歸一下正題,在這裏要強調的是,當存在虛函數的時候,編譯器需要構造虛表指針。所以,假如我們沒有任何構造函數的時候,編譯器就會合成一個默認構造函數,裏面滿足除了前面“第一個需求,第二個需求”外,還會在在Base class完成構造之後,完成虛表指針的初始化。假如我們已經定義了構造函數,那麽就會在base class constructors之後,member initialzation list之前完成虛表指針的初始化。

第四個需求

class派生自一個繼承串鏈,其中有一個或更多的virtual base classes,當沒有程序猿定義的constructor的時候,編譯器就會合成一個默認構造函數。舉個例子

class X
{
public:
int i;
};
class A:public virtual X{};
class B:public virtual X{};
class C:public A,public B{};
void Foo(const A *pa)
{
pa->i = 1024;
}
void main()
{
Foo(new A);
Foo(new C);
}

編譯器沒辦法確切地知道“經由pa"而存取的X::i的實際偏移位置,因為pa的真正類型可以改變。編譯器必須改變”執行存取操作“的那些代碼,使X::i可以延遲至執行期才決定下來。對於class定義的每個constructor,編譯器會安插那些”允許每一個virtual base class“執行期存取操作的代碼。如何class沒有聲明任何constructor,編譯器必須為它合成一個default constructor。

C++默認構造函數

默認構造函數

默認的構造函數是指為所有參數都提供了默認值的構造函數,通常是指無參的構造函數。比如下面的類Test,它的默認構造函數就是Test()。

class Test
{
public:
Test(){} // default constructor
} ;

如果你沒有為你的類提供任何構造函數,那麽編譯器將自動為你生成一個默認的無參構造函數。一旦你為你的類定義了構造函數,哪怕只是一個,那麽編譯器將不再生成默認的構造函數。

為你的類提供默認的構造函數

有很多原因,列舉如下:

  1. 當你使用靜態分配的數組,而數組元素類型是某個類的對象時,就要調用默認的構造函數,比如下面的代碼。

Object buffer[10]; // call default constructor

  1. 當你使用動態分配的數組,而數組元素類型是某個類的對象時,就要調用默認的構造函數,比如下面的代碼,如果Object沒有默認的構造函數,是無法通過編譯的,因為new操作符要調用Object類的無參構造函數類初始化每個數組元素。

Object* buffer = new Object[10];

  1. 當你使用標準庫的容器時,如果容器內的元素類型是某個類的對象時,那麽這個類就需要默認的構造函數,原因同上。

vector<Object> buffer;

  1. 一個類A以另外某個類B的對象為成員時,如果A提供了無參構造函數,而B未提供,那麽A則無法使用自己的無參構造函數。下面的代碼將導致編譯錯誤。

class B
{
B(int i){}
};

class A
{
A(){}
B b;
};

int main(void)
{
A a(); // error C2512: ‘B‘ : no appropriate default constructor available

getchar() ; 
return 0 ; 

}

再比如下面的代碼,類A定義了拷貝構造函數,而沒有提供默認的構造函數,B繼承自A,所以B在初始化時要調用A的構造函數來初始化A,而A沒有默認的構造函數,故產生編譯錯誤。

class A
{
A(const A&){}
};

class B : public A
{

};

int main(void)
{
B b; //error C2512:‘B‘: no appropriate default constructor available

getchar() ; 
return 0 ; 

}

以上是雲棲社區小編為您精心準備的的內容,在雲棲社區的博客、問答、公眾號、人物、課程等欄目也有的相關內容,歡迎繼續使用右上角搜索按鈕進行搜索編譯器 , class , 函數 , 程序 , 代碼 virtual 合成的默認構造函數、合成默認構造函數、c 默認構造函數、c 默認拷貝構造函數、c 不存在默認構造函數,以便於您獲取更多的相關知識。

C++默認構造函數的合成