不要被C++(自動(dòng)生成規(guī)則)所蒙騙
C++對(duì)象可以使用兩種方式進(jìn)行創(chuàng)建:構(gòu)造函數(shù)和復(fù)制構(gòu)造函數(shù)。假如我們定義了類(lèi)A,并使用它創(chuàng)建對(duì)象。
A a,b;
A c=a;
A d(b);
對(duì)象a和b使用編譯器提供的默認(rèn)構(gòu)造函數(shù)A::A()創(chuàng)建出來(lái),我們稱(chēng)這種創(chuàng)建方式為對(duì)象的定義(包含聲明的含義)。對(duì)象c和d則是使用已有的對(duì)象,通過(guò)編譯器提供的復(fù)制構(gòu)造函數(shù)A::A(const A&)創(chuàng)建,我們稱(chēng)這種創(chuàng)建方式為對(duì)象的初始化(包含定義和聲明的含義)。
可能不少人會(huì)把對(duì)象的初始化和對(duì)象的賦值混淆,比如。
c=d;
這里把對(duì)象d賦值給對(duì)象c并非創(chuàng)建新的對(duì)象,它不會(huì)調(diào)用任何構(gòu)造函數(shù)。編譯器默認(rèn)提供的賦值運(yùn)算符重載函數(shù)const A&operator=(const A&)為該語(yǔ)句提供支持。
編譯器除了提供默認(rèn)構(gòu)造函數(shù)、復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符重載函數(shù)之外,有可能還為我們提供了析構(gòu)函數(shù)A::~A(),但是這里的析構(gòu)函數(shù)并不是virtual的(相信會(huì)有童鞋忘記這一點(diǎn))。
這些基礎(chǔ)的語(yǔ)法對(duì)學(xué)習(xí)過(guò)C++的人或許并不陌生,我們自從學(xué)習(xí)了面向?qū)ο驝++后,一直都知道編譯器為我們提供了這樣的便利條件。經(jīng)過(guò)多年的編程實(shí)踐和體驗(yàn),我們絕對(duì)相信編譯器的確為我們做了這些工作,因?yàn)槲覀儧](méi)有遇到過(guò)任何問(wèn)題。甚至我們腦子中會(huì)默認(rèn)形成一個(gè)概念——即使我定義了一個(gè)空類(lèi)(類(lèi)內(nèi)什么都沒(méi)有),編譯器依然會(huì)“乖乖的”為我們生成上邊所說(shuō)的四個(gè)函數(shù)。
如果你真的形成了這種觀(guān)念的話(huà),那么恭喜你,因?yàn)槟阋呀?jīng)將C++基本規(guī)則運(yùn)用的十分熟練了。同時(shí)遺憾的是你我都看到了冰山一角,編譯器的工作方式遠(yuǎn)不像我們使用它的那樣。讀者可能會(huì)疑問(wèn),難道編譯器沒(méi)有生成這些函數(shù)嗎?答:要看你類(lèi)的定義。那么編譯器到底如何生成這些函數(shù)呢?和我一樣又好奇心的人都想一探究竟,而這些內(nèi)容在《Inside The C++ Object Model》被詮釋的比較徹底。筆者也通過(guò)“借花獻(xiàn)佛”的方式將該書(shū)所描述的對(duì)象構(gòu)造的內(nèi)幕結(jié)合個(gè)人的理解和大家一起分享。
首先我們從最簡(jiǎn)單的談起,編譯器為類(lèi)生成構(gòu)造函數(shù)了嗎?如果按照上邊描述的例子,只有一個(gè)空的類(lèi)定義的話(huà),我們可以肯定的說(shuō)——沒(méi)有。對(duì)編譯器這樣的做法,我們不必感到驚訝。試想一個(gè)空的類(lèi)——沒(méi)有數(shù)據(jù)成員,沒(méi)有成員函數(shù),即使生成了構(gòu)造函數(shù)又能做什么呢?即便是生成了,也只是一個(gè)空構(gòu)造函數(shù)而已。
A(){}
它什么也做不了,也什么都不必做。更“悲劇”,它的出現(xiàn)不僅沒(méi)有任何積極意義,還會(huì)為編譯器和程序運(yùn)行增加完全不必要的函數(shù)調(diào)用負(fù)擔(dān)。
既然如此,我們讓這個(gè)類(lèi)再?gòu)?fù)雜一點(diǎn),我們?yōu)樗黾訑?shù)據(jù)成員和成員函數(shù),比如下邊這段代碼(我們記它為例子1)。
class A
{
public:
int var;
void fun(){}
};
即便如此,結(jié)果還是和上邊的一樣,不生成構(gòu)造函數(shù)!因?yàn)闆](méi)有任何理由對(duì)var初始化,況且編譯器也不知道用什么值給它初始化。
果然,在主函數(shù)內(nèi)定義對(duì)象a后,沒(méi)有任何構(gòu)造函數(shù)被調(diào)用。
有人可能會(huì)說(shuō)用0初始化不行嗎?這只是我們的“一廂情愿”而已。一個(gè)沒(méi)有初始化的變量本身的值就可以是不確定的,何必要生成一個(gè)沒(méi)有任何意義的初始化為0的語(yǔ)句呢。
編譯器到底怎樣才能生成構(gòu)造函數(shù)呢?!或許你和我一樣有點(diǎn)“抓狂”了。不過(guò)現(xiàn)在還不是絕望的時(shí)候,因?yàn)榫幾g器需要我們給它一個(gè)“正當(dāng)?shù)睦碛伞鄙蓸?gòu)造函數(shù)。有四個(gè)正當(dāng)?shù)睦碛?,讓編譯器不得不生成構(gòu)造函數(shù),這里一一介紹。
首先,我們修改一下var的類(lèi)型。這里假設(shè)它不是內(nèi)置類(lèi)型int,而是一個(gè)定義好的類(lèi)B。
B var;
修改一下數(shù)據(jù)成員的類(lèi)型為自定義類(lèi)型能影響編譯器的抉擇嗎?答:可能。這要看類(lèi)B有沒(méi)有定義構(gòu)造函數(shù)。讀者可能有點(diǎn)明白了,是的,如果B沒(méi)有定義構(gòu)造函數(shù)(和這里的A一個(gè)樣子),那么編譯器仍然沒(méi)有理由生成構(gòu)造函數(shù)——為B初始化什么呢?反之,B一旦定義了默認(rèn)構(gòu)造函數(shù)B::B(),即便它是空的,編譯器就不得不為A創(chuàng)建默認(rèn)構(gòu)造函數(shù)了(這里不考慮編譯器的深度優(yōu)化)。因?yàn)锳的對(duì)象需要用B的默認(rèn)構(gòu)造函數(shù)初始化它自己的成員var,雖然B的構(gòu)造函數(shù)什么也沒(méi)做。因?yàn)榫幾g器不能假定B的構(gòu)造函數(shù)做了什么樣的操作(極端一點(diǎn):萬(wàn)一修改了一個(gè)全局變量了呢?),因此編譯器有絕對(duì)的必要生成A的構(gòu)造函數(shù),保證B類(lèi)型的數(shù)據(jù)成員的構(gòu)造函數(shù)正常執(zhí)行。

轉(zhuǎn)到編譯器為A生成的構(gòu)造函數(shù)處,我們發(fā)現(xiàn)了B的構(gòu)造函數(shù)被調(diào)用的語(yǔ)句(選中行)。

當(dāng)然,如果B提供了構(gòu)造函數(shù),但不是默認(rèn)的構(gòu)造函數(shù),那么必須要程序員介入為var初始化,否則編譯器就不客氣了——error!
因此,編譯器生成默認(rèn)構(gòu)造函數(shù)的第一個(gè)正當(dāng)理由是——類(lèi)內(nèi)數(shù)據(jù)成員是對(duì)象,并且該對(duì)象的類(lèi)提供了一個(gè)默認(rèn)構(gòu)造函數(shù)。
現(xiàn)在,我們回到例子1,這里我們不修改var的類(lèi)型,而是讓A繼承于另一個(gè)類(lèi)C。
class A:public C
我們都知道,在C++構(gòu)造函數(shù)初始化語(yǔ)法中,構(gòu)造函數(shù)會(huì)先初始化基類(lèi)C,再初始化自身的數(shù)據(jù)成員或者對(duì)象。因此,這里的問(wèn)題和對(duì)象成員var類(lèi)似。如果基類(lèi)C沒(méi)有提供任何構(gòu)造函數(shù),那么編譯器仍然不提供A的默認(rèn)構(gòu)造函數(shù)。如果C提供了默認(rèn)構(gòu)造函數(shù),結(jié)果和前邊類(lèi)似。

結(jié)果不出所料,編譯器為A生成了構(gòu)造函數(shù),并且調(diào)用了基類(lèi)C定義的默認(rèn)構(gòu)造函數(shù)。同樣,若C沒(méi)有提供默認(rèn)默認(rèn)構(gòu)造函數(shù),而提供了其他構(gòu)造函數(shù),編譯是無(wú)法通過(guò)的。
這也是編譯器生成默認(rèn)構(gòu)造函數(shù)的第二個(gè)正當(dāng)理由——類(lèi)的基類(lèi)提供了默認(rèn)的構(gòu)造函數(shù)。
我們?cè)俅位氐嚼?,這次我們修改成員函數(shù)fun。
virtual void fun(){}
我們把類(lèi)A的成員函數(shù)fun修改為虛函數(shù),再次看看是否產(chǎn)生了默認(rèn)構(gòu)造函數(shù)。

這次編譯器“毫不客氣”的為A生成了默認(rèn)構(gòu)造函數(shù),雖然它沒(méi)有調(diào)用任何其他的構(gòu)造函數(shù)!這是什么原因呢?原來(lái),C++為了實(shí)現(xiàn)多態(tài)機(jī)制,需要為類(lèi)維護(hù)一個(gè)虛函數(shù)表(vftable),而每個(gè)該類(lèi)的對(duì)象都保存一個(gè)指向該虛函數(shù)表的一個(gè)指針(一般保存在對(duì)象最開(kāi)始的四個(gè)四節(jié)處,多態(tài)機(jī)制的實(shí)現(xiàn)這里暫不介紹)。編譯器為A生成構(gòu)造函數(shù),其實(shí)不為別的,就為了保證它定義的對(duì)象都要正常初始化這個(gè)虛函數(shù)表的指針(vfptr)!
好了,因此我們得出編譯器生成默認(rèn)構(gòu)造函數(shù)的第三個(gè)正當(dāng)理由——類(lèi)內(nèi)定義了虛函數(shù)。這里可能還涉及一個(gè)更復(fù)雜點(diǎn)的情況:類(lèi)內(nèi)本身沒(méi)有定義虛函數(shù),但是繼承了基類(lèi)的虛函數(shù)。其實(shí)按照上述的原則,我們可以推理如下:基類(lèi)既然定義了虛函數(shù),那么基類(lèi)本身就需要生成默認(rèn)構(gòu)造函數(shù)初始化它本身的虛函數(shù)表指針。而基類(lèi)一旦產(chǎn)生了默認(rèn)構(gòu)造函數(shù),派生類(lèi)就需要產(chǎn)生默認(rèn)構(gòu)造函數(shù)調(diào)用它。同時(shí),如果讀者對(duì)多態(tài)機(jī)制了解清除的話(huà),派生類(lèi)在生成的默認(rèn)構(gòu)造函數(shù)內(nèi)還會(huì)初始化一次這個(gè)虛函數(shù)表指針的。
最后,我們?cè)俅位氐嚼?,這次仍然讓A繼承于C,但是這次C是一個(gè)空類(lèi)——什么都沒(méi)有,也不會(huì)自動(dòng)生成默認(rèn)構(gòu)造函數(shù)。但是A繼承C的方式要變化一下。
class A:public virtual C
A虛繼承于C,這次又有什么不同呢?

這次編譯器也生成了A的構(gòu)造函數(shù),并且初始化過(guò)程和虛函數(shù)時(shí)有點(diǎn)類(lèi)似。細(xì)心觀(guān)察下發(fā)現(xiàn),這次構(gòu)造函數(shù)也初始化了一張表——vbtable。了解虛繼承機(jī)制的讀者應(yīng)該不會(huì)陌生,這張表叫虛基類(lèi)表,它記錄了類(lèi)繼承的所有的虛基類(lèi)子對(duì)象在本類(lèi)定義的對(duì)象內(nèi)的偏移位置(至于虛繼承機(jī)制的實(shí)現(xiàn),我們以后詳細(xì)探討)。為了保證虛繼承機(jī)制的正確工作,對(duì)象必須在初始化階段維護(hù)一個(gè)指向該表的一個(gè)指針,稱(chēng)為虛表指針(vbptr)。編譯器因?yàn)樗峁〢的默認(rèn)構(gòu)造函數(shù)的理由和虛函數(shù)時(shí)類(lèi)似。
這樣,我們得出編譯器生成默認(rèn)構(gòu)造函數(shù)的第四個(gè)正當(dāng)理由——類(lèi)使用了虛繼承。
到這里,我們把編譯器為類(lèi)生成默認(rèn)構(gòu)造函數(shù)的正當(dāng)理由闡述完畢,相信大家應(yīng)該對(duì)構(gòu)造函數(shù)的生成時(shí)機(jī)有了一個(gè)大致的認(rèn)識(shí)。這四種“正當(dāng)理由”其實(shí)是編譯器不得不為類(lèi)生成默認(rèn)構(gòu)造函數(shù)的理由,《Inside The C++ Object Model》里稱(chēng)這種理由為nontrival的(候sir翻譯的很別扭,所以怎么翻譯隨你啦)。除了這四種情況外,編譯器稱(chēng)為trival的,也就是沒(méi)有必要為類(lèi)生成默認(rèn)構(gòu)造函數(shù)。這里討論的構(gòu)造函數(shù)生成準(zhǔn)則的內(nèi)容是寫(xiě)進(jìn)C++Standard的,如此看來(lái)標(biāo)準(zhǔn)就是“貼合正常思維”的一套準(zhǔn)則(簡(jiǎn)單YY一下),其實(shí)本就是這樣,編譯器不應(yīng)該為了一致化做一些沒(méi)有必要的工作。
通過(guò)對(duì)默認(rèn)構(gòu)造函數(shù)的討論,相信大家對(duì)復(fù)制構(gòu)造函數(shù)、賦值運(yùn)算符重載函數(shù)、析構(gòu)函數(shù)的生成時(shí)機(jī)應(yīng)該可以自動(dòng)擴(kuò)展了。沒(méi)錯(cuò),它們遵循著一個(gè)最根本的原則:只有編譯器不得不為這個(gè)類(lèi)生成函數(shù)的時(shí)候(nontrival),編譯器才會(huì)真正的生成它。
因此,正如標(biāo)題所說(shuō),我們不要被C++語(yǔ)法中所描述的那些條條框框所“蒙騙”了。的確,相信這些生成規(guī)則不會(huì)對(duì)我們的編程帶來(lái)多大的影響(不會(huì)產(chǎn)生錯(cuò)誤),但是只有了解它們的背后操作,我們才知道編譯器究竟為我們做了什么,我們才知道如何使用C++才能讓它變得更有效率——比如消除不必要的構(gòu)造和虛擬機(jī)制等(如果可以的話(huà))。相信本文對(duì)C++自動(dòng)生成的內(nèi)容的描述讓不少人認(rèn)清對(duì)象構(gòu)造函數(shù)產(chǎn)生的前因后果,希望本文對(duì)你有所幫助。
相關(guān)文章
Python擴(kuò)展C/C++庫(kù)的方法(C轉(zhuǎn)換為Python)
這篇文章主要介紹了Python擴(kuò)展C/C++庫(kù)的方法(C轉(zhuǎn)換為Python),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-10-10
vc++ 監(jiān)控指定路徑下文件變化實(shí)現(xiàn)代碼
這篇文章主要介紹了vc++ 監(jiān)控指定路徑下文件變化實(shí)現(xiàn)代碼,需要的朋友可以參考下2019-04-04
QT中在QLabel顯示圖片并且利用鼠標(biāo)點(diǎn)擊畫(huà)線(xiàn)問(wèn)題
這篇文章主要介紹了QT中在QLabel顯示圖片并且利用鼠標(biāo)點(diǎn)擊畫(huà)線(xiàn)問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-11-11
Qt連接數(shù)據(jù)庫(kù)并實(shí)現(xiàn)增刪改查操作
這篇文章主要為大家詳細(xì)介紹了Qt如何連接數(shù)據(jù)庫(kù)并實(shí)現(xiàn)增刪改查等基本操作,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09
基于OpenCV和C++ 實(shí)現(xiàn)圖片旋轉(zhuǎn)
這篇文章主要介紹了基于OpenCV和C++ 實(shí)現(xiàn)圖片旋轉(zhuǎn),幫助大家更好的利用c++處理圖片,感興趣的朋友可以了解下2020-12-12
C++實(shí)現(xiàn)LeetCode(186.翻轉(zhuǎn)字符串中的單詞之二)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(186.翻轉(zhuǎn)字符串中的單詞之二),本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
詳解C++中遞增運(yùn)算符重載的實(shí)現(xiàn)
本文主要詳解運(yùn)算符重載里的遞增運(yùn)算符重載;遞增和遞減原理是一樣的,這里就只分享遞增的重載;提到遞增遞減,我們都知道又前置和后置兩種方法, 那今天就詳解一下前置遞增和后置遞增的細(xì)節(jié),拿捏遞增運(yùn)算符重載2022-06-06
c++ 隊(duì)列相關(guān)知識(shí)總結(jié)
這篇文章主要介紹了c++ 隊(duì)列相關(guān)知識(shí)總結(jié),幫助大家更好的理解和學(xué)習(xí)使用c++,感興趣的朋友可以了解下2021-03-03
C++設(shè)計(jì)模式之備忘錄模式(Memento)
這篇文章主要為大家詳細(xì)介紹了C++設(shè)計(jì)模式之備忘錄模式Memento的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04

