最近在學習C++對象模型,看的書是侯捷老師的《深度探索C++對象模型》,發現自己以前對構造函數存在很多誤解,作此筆記記錄。
預設構造函數的誤解##
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++對象模型》5.2節的内容。