深度探索C 物件模型筆記 第二章

2022-09-17 17:27:10 字數 4055 閱讀 1682

explicit關鍵字能夠制止單參建構函式被當作型別轉換運算子.

編譯器的隱式操作只是為了滿足編譯器本身的需求, 而不是程式本身, 乙個被編譯器隱式生成的預設建構函式, 多數情況下對於程式本身來說是無用的.

如果乙個類沒有任何建構函式, 但類中的乙個非內建型別成員變數有預設建構函式, 那麼這個類被隱式生成的預設建構函式則不是無用的.

只有建構函式真正被呼叫了, 編譯器才會為類隱式生成預設建構函式.

為了避免乙個沒有預設構造的類被放在多個檔案中實現而導致生成多個預設建構函式, 構造,析構,拷貝構造,拷貝賦值 這些函式都會被生成為inline或者explict no-inline static 的.

編譯器的行為可以簡單記為 編譯器不會為內建型別(不包含建構函式) 生成/擴充 建構函式; 對於下方的**, 編譯器會為bar隱式生成乙個預設建構函式, 該預設建構函式中會呼叫foo::foo()來對bar::foo做初始化, 但並不會為str做初始化操作.

class foo ;​

class bar;​

//code

void foo_bar()

如果乙個類包含建構函式, 但建構函式中並未對非內建型別成員變數做初始化, 那麼編譯器會擴充已有的建構函式, 在其中呼叫相應成員變數的建構函式.

當編譯器對建構函式進行擴充時, 擴充的建構函式會被安插在顯式使用者**之前, 安插建構函式的順序將以變數宣告的順序為基準.

編譯器會為不包含任何建構函式的派生類生成乙個預設建構函式, 其中會按繼承先後順序呼叫基類的預設建構函式.

如果乙個不包含預設建構函式的類, 其中宣告/繼承了虛函式 或 該類派生自乙個繼承鏈,繼承鏈中有乙個或多個虛基類, 則編譯器的預設建構函式生成行為如下

編譯器生成出來的預設建構函式中, 只有基類子物件,成員類物件會被初始化, 所有其他的非靜態成員變數(整數,整數指標,整數陣列等)都不會被初始化.

以乙個物件作為另乙個類物件的初值的三種情況 :

如果乙個類沒有提供顯式的拷貝構造, 當對這個類的物件做拷貝初始化時, 內部會對類內每個成員施以預設的構造操作, 即將每乙個內建的或派生的成員變數的值拷貝至當前物件的變數上(淺拷貝/按位逐次拷貝), 這個操作是遞迴的.

預設構造和拷貝構造只有在類不展現淺拷貝需求時才會被編譯器合成出來.

不展現淺拷貝需求的四種情況 ( 對於前兩種情況, 編譯器會將拷貝構造操作加入新合成的拷貝建構函式中 )

為了支援多型, 對於編譯器而言, 每乙個新生成的類物件中的vptr和vtbl都必須被正常設初值, 這就要求, 當vptr被編譯器擴張加入乙個類中時, 該類就不再展現淺拷貝需求了.

class canimal

virtual ~canimal() {}

​virtual string sound()

virtual string eat()

​private:

string food;};​

class cdog : public canimal

virtual ~cdog() {}

​string sound()  //雖然沒有宣告virtual,但其實是virtual的

virtual string eat()

​private:

string dog_food;};​

//dogglas會呼叫預設構造做初始化, dogglas的vptr被設定指向cdog的vtbl, 因此將dogglas拷貝給doggy是安全的.

cdog dogglas;

cdog doggy = dogglas;

當乙個基類物件以其派生類物件的內容做初始化操作時, vptr的設定同樣要保證安全

/*

* 由於派生類虛函式重寫後, 可能會呼叫派生類自身的私有成員變數, 如果將派生類物件中的vptr直接

* 淺拷貝給基類物件中的vptr, 又因為不是使用指標或引用, 必將導致執行期間記憶體崩潰

*/cdog doggy;

canimal animal = doggy;  //此處發生了切割(sliced)

canimal *dog = new cdog();​/*

* animal eat something

* dog eat steak

*/printf("animal eat %s\n", animal.eat().c_str());

printf("dog eat %s\n", dog->eat().c_str());​/*

* 這裡也是個很有趣的點, 我們delete了乙個基類指標, 是否會造成記憶體洩漏?

* 假定兩種情況: 1.派生類的構造存在額外的堆記憶體申請, 析構函式有正常釋放這個堆記憶體. 2.派生類的構造中沒有額外的堆記憶體申請

* 在上述情況下, 如果基類析構函式為virtual, 且沒有過載new和delete運算子, 則不會造成記憶體洩露

* 詳細原因可以檢視new和delete運算子定義, 返回值和入參均為void, 也就是說, 對於new和delete而言, 能否正常申請釋放記憶體是型別無關的.

*/delete dog;

當類派生自乙個繼承鏈, 其中有乙個或多個虛基類時, 編譯器則要保證初始化時_vbcbase(虛基類子成員物件)被正確設定, 該類則不展現淺拷貝需求.

顯式初始化在程式轉化時有兩個必要的階段: 重寫每乙個變數定義, 剝離初始化操作; 呼叫類的拷貝構造.

class cbase;​

cbase g_base;

​//原始函式

void foo_bar()

​//編譯器轉化後 偽**

void foo_bar_tran()

引數初始化把乙個類物件當做引數傳給乙個函式/作為函式返回值, 相當於以引數值為物件對形參/返回值做初始化操作

void foo (cbase base) 

​//相當於 cbase base = g_base

foo(g_base);

返回值初始化分兩個階段: 首先加上乙個額外引數, 型別是類物件的乙個引用, 該引數用來放置拷貝構造的返回值; 其次在 return 指令前安插乙個拷貝構造呼叫操作, 將返回值物件賦給新增的額外引數.

//原始函式

cbase bar()

​//轉換後 偽**

void bar_tran(cbase &__result)​/*

* 上述的這種轉換方式被稱為nrv(named return value)優化

* 該轉換方式被視為c++編譯器的乙個義不容辭的優化標準

* 當然,該優化方式要求類內存在拷貝建構函式(無論是顯式編寫的還是編譯器合成的)*/​

必須使用列表初始化的幾種情況

class cword};​

//上方構造可能會被編譯器轉化成如下形式

//偽**

cword::cword(this)

​//如果cword類的構造使用了列表初始化

cword::cword

: _name(0)

//構造可能會被編譯器轉化成如下形式

//偽**

cword::cword(this)

當我們使用列表初始化時, 編譯器會一一操作初始化列表中的成員, 以適當的順序在建構函式中的編寫者顯式**之前安插初始化操作, 而這個安插的順序並不是按照我們列表初始化中編寫的順序來排列的, 而是這些變數在類中的宣告順序決定的.

//形如下列**,由於誤以為初始化順序會按照列表初始化編寫的順序操作而產生了錯誤

class cword}​

//偽**

cword::cword(this, int val)

深度探索C 物件模型 第二章讀書筆記

1.如果設計者提供多個constructors,但其中都沒有default constructor呢?編譯器會擴張現在的每一次constructors,將 用以呼叫所有必要之default constructor 的程式 加進去。它不會合成乙個新的default constructor,因為其他由u...

《深度探索C 物件模型》第二章 建構函式語意學

default constructor的構建操作 default constructors在需要的時候被編譯器產生。例 clas foo void foo bar 上述的 情況中,並不會生成乙個deafult constructor。需要注意的地方是 全域性的object內存在被啟用時會清0,而區域...

深度探索C 物件模型 第二章建構函式語意學

在使用c 時,常常會好奇或者抱怨,編譯器為我們做了什麼事呢?為什麼建構函式沒有為我初始化呢?為什麼我還要寫預設建構函式呢?2.1 default constructor 的構造操作 如果沒有宣告預設建構函式,編譯器會在需要的時候幫我們產生出來。為了避免在多個地方被需要導致重複,則編譯器將產生的建構函...