《深度探索c++對象模型》筆記總結(二)

2.1 默認構造函數

首先指出兩個誤解:

1)任何類如果沒有定義默認構造函數,就會被合成出來一個。

2)編譯器合成出來的默認構造函數會顯式設定“類內每一個數據成員的默認值”。

上述兩種說法都是錯誤的!

四種nontrivial的默認構造函數:

1)類成員中有成員是類對象,並且該成員的類含有默認構造函數。那麼C++編譯器會給這個類也生成一個默認構造函數,用來調用其成員對象的構造函數,完成該成員的初始化構造,但不會初始化當前類中所含的其他值(是程序員的責任)。P42-44寫的比較明白

//避免合成出多個默認構造函數:使用inline方式完成。

2)這個類的基類有默認構造函數。那麼C++編譯器也會幫你生成該派生類的默認構造函數,以調用基類的默認構造函數,完成基類的初始化。如果基類沒有提供這個默認構造的函數,那麼編譯器也不會爲派生類生成默認的構造函數(這裏包括兩層意思,第一,基類沒有任何形式構造函數;第二,基類存在其他形式的非默認構造函數,這種類型就是編譯不過的,道理很明顯)。

3)類中存在虛函數(新定義或繼承而得到)。那麼C++編譯器會爲你生成默認構造函數,在編譯期生成虛表和虛表指針。

4)存在虛基類(有直接虛擬基類或繼承鏈上有虛基類)。那麼C++編譯器會爲你生成默認構造函數,以初始化虛基類表(vbtable)。

這四種情況之外,且沒有聲明任何constructor的類,可以說它有無用的構造函數,但實際上它根本就不會被構建出來。

------------------------------------------------------------------

2.2 拷貝構造函數

三種情況出現默認拷貝構造函數:

1.顯示的對對象進行初始化

2.當object被當作參數交給某個函數

3.當函數返回一個class object時

位逐次拷貝(bitwise copy semantics)

Default constructors 和 copy constructors 在必要的時候才由編譯器產生出來。“必要”即意指當class不展現bitwise copy semantics時。

也就是,如果class中展現了位逐次拷貝,編譯器就不會產生出拷貝構造函數。當沒有產生拷貝構造的時候,我們的類怎麼產生呢。就是通過 bitwise copy 來搞定,也就是將源類中成員變量中的每一位都逐次拷貝到目標類中,這樣我們的類就構造出來了。

來看一下什麼是位逐次拷貝

對於第二種情況,類中含有String類的對象,因此此時並沒有位逐次拷貝,需要一個拷貝構造函數

類在下述四種情況下不展現位逐次拷貝(這時纔會而且有必要生成拷貝構造):

1)類中含有成員類對象,並且此類對象含有默認構造函數。即:有“對象”成員,而非只有基本數據類型成員。

2)基類帶有拷貝構造函數。

3)類中存在虛函數(新定義或繼承而得到)。重新設置vptr,並且,如果該對象是第一個對象的話還會構造vtbl。

4)存在虛基類(有直接虛擬基類或繼承鏈上有虛基類)。

前兩種情況下,都是需要生成該類的拷貝構造去調用類成員對象或基類的拷貝構造函數;後兩種情況下,需要編譯器來完成虛函數表(vbtl)的初始化和虛表指針(vptr)的初始化,所以如果沒有顯式的定義構造函數,需要編譯器構造默認的構造函數。

重新設定虛表指針

如果同類對象對同類對象進行初始化,沒有問題,位逐次拷貝已經足夠,但是如果是例如使用一個派生類的對象對基類的對象進行初始化(此時會發生切割),那麼必須使用拷貝構造函數

------------------------------------------------------------------

2.3程序轉化語義學

1.參數的初始化

對於這裏P63的例子我有一些疑問,是否一定需要把參數從X類對象改爲X類對象的引用?

個人感覺是不用的,只是說如果傳的不是引用類型的話,會使得參數需要先複製一份,那麼如果傳引用參數,可以節省拷貝構造函數和析構函數的開銷??

2.返回值的初始化

 

對於這樣的一個函數bar,內部的一個X類對象xx,那麼對於其返回值,是如何從這個局部對象xx中拷貝過來的呢?

(1)首先加上一個額外的參數,類型是類對象的一個引用

(2)在return之前其實調用了拷貝構造函數,將局部對象xx的值拷貝給了引用對象

(3)對於真實的return,其實並沒有返回值

也就是說,真實的過程如下:

 那麼同樣的,對於下面的調用,其實也進行了同樣的轉化實現

對於上面這個,其實不太理解後一個_temp0這樣的寫法,個人覺得是爲了示意前一部分產生了一個X類的對象,調用的是這個對象的menfunc函數?

 3.在使用者層面做優化

對於第一種寫法,就像我們前面提到過的,實際的bar函數return是void的,因此對於內部這個xx臨時對象,實際上我們是先生成他,然後處理完之後拷貝給_result的引用,這樣比較慢;而對於第二種,我們沒有生成這個臨時對象xx,直接使用y和x來進行處理,因此效率比較高

4.在編譯器層面做優化

NRV優化,也就是前面說到的,本來我調用bar函數,我先對臨時對象xx進行操作,然後在拷貝給_result,但此時編譯器不進行xx的操作,直接對於_result進行操作

這裏來看三個初始化的操作:


對於第一個xx0,沒有疑問,產生一個臨時對象,然後設定初值1024。那麼對於第二第三個來說呢,將臨時對象以拷貝構造的方式作爲explict對象的初值(也就是說調用了兩個構造函數)。

使用memcpy()或者memset()函數的時候,如果類中含有虛函數或者虛基類,此時要特別注意,這兩個函數不能有效運行,會導致vptr的初值被改寫(因爲例如用memset來進行清零操作,由於vptr需要在所有代碼執行之前安排好,那麼當vptr確定之後,我使用memset置零,此時會導致vptr又被置零了,此時是會發生錯誤的!!)

2.4成員們的初始化隊伍

初始化列表

下面四種情況必須使用初始化列表來初始化class 的成員:

1.當初始化一個reference member時;

2.當初始化一個const member時;

3.當調用一個base class 的 constructor ,而它擁有一組參數(其實就是自定義的構造函數)時;

4.當調用一個 member class 的 constructor,而它擁有一組參數時。

那麼來看爲什麼要使用初始化列表:

對於一般的在函數體內進行初始化,操作過程應該是先產生一個臨時的類對象,然後對這個類對象進行初始化,之後由這個臨時對象來進行初始化值,最後摧毀這個臨時對象。

來看:

要注意的一點:list中的項目順序是類中的成員聲明順序決定的,而不是由參數列表中的排列順序所決定的。所以寫的時候注意養成良好的習慣,兩者的順序一致!!

對於類似下面這樣的情況,比如說聲明是i,j的順序,之後使用j來初始化i,val來初始化j

 上面這個第二種寫法其實是正確的,因爲參數列表的項目都是放在explict的自寫代碼之前的,因此j先被賦值,再來賦值i

另外要注意的一點:

能否調用一個函數來設定一個成員的值?

此時建議:使用構造函數體內的成員函數可以,但是儘量不要使用存在於參數列表中的成員函數,因此此時並不知道這個成員函數對於類對象的依賴性有多高。

另外一點:對於派生類成員函數進行調用,其返回值作爲基類構造函數的一個參數的情況,不推薦。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章