深度探索C++對象模型(筆記)

第二章 對象

第二章第一節 類對象所佔用的空間
  • 成員函數不佔用類對象的內存空間
  • 一個類對象至少佔用1個字節的內存空間
  • 成員變量是佔用對象的內存空間
  • 成員函數 每個類只誕生 一個(跟着類走),而不管你用這個類產生了多少個該類的對象;
第二章第二節 對象結構的發展和演化
  • 非靜態的成員變量(普通成員變量)跟着類對象走(存在對象內部),也就是每個類對象都有自己的成員變量;
  • 靜態成員變量跟對象沒有什麼關係,所以肯定不會保存在對象內部,是保存在對象外面(表示所佔用的內存空間和類對象無關)的。
  • 成員函數:不管靜態的還是非靜態,全部都保存在類對象之外。所以不管幾個成員函數,不管是否是靜態的成員函數,對象的sizeof的大小都是不增加的;
  • 虛函數:不管幾個虛函數,sizeof()都是多了4個字節
    • 類裏只要有一個虛函數(或者說至少有一個虛函數),這個類會產生一個指向虛函數表的指針
    • 類本身 指向虛函數的指針(一個或者一堆)要有地方存放,存放在一個表格裏,這個表格我們就稱爲“虛函數表(virtual table【vtbl】)”;這個虛函數表一般是保存在可執行文件中的,在程序執行的時候載入到內存中來。虛函數表是基於類的,跟着類走的
    • 因爲有了虛函數的存在,導致系統往類對象中添加了一個指針,這個指針正好指向這個虛函數表,很多資料上把這個指針叫vptr;這個vptr的值由系統在適當的時機(比如構造函數中通過增加額外的代碼來給值);
  • 總結
    • 靜態數據成員不計算在類對象sizeof()內;
    • 普通成員函數和靜態成員函數不計算在類對象的sizeof()內
    • 虛函數不計算在類對象的sizeof()內,但是虛函數會讓類對象的sizeof()增加4個字節以容納虛函數表指針
    • 虛函數表[vtbl]是基於類的(跟着類走的,跟對象沒關係,不是基於對象的)
    • 如果有多個數據成員,那麼爲了提高訪問速度,某些編譯器可能會將數據成員之間的內存佔用比例進行調整。(內存字節對齊)
    • 不管什麼類型指針char *p,int *q;,該指針佔用的內存大小是固定的
    • 第一個基類子對象的開始地址和派生類對象的開始地址相同
    • 後續這些基類子對象的開始地址 和派生類對象的開始地址相差多少呢?就需要從開始的那些基類子對象所佔用的內存空間進行相應的偏移大小
    • 你調用哪個子類的成員函數,這個this指針就會被編譯器自動調整到對象內存佈局中 對應該子類對象的起始地址那去
第二章第三節 this指針調整

在這裏插入圖片描述

  • 派生類對象 它是包含 基類子對象的。
  • 如果派生類只從一個基類繼承的話,那麼這個派生類對象的地址和基類子對象的地址相同
第二章第四節分析obj目標文件,構造函數語義
  • 默認構造函數(缺省構造函數):沒有參數的構造函數;
  • 該類MBTX沒有任何構造函數,但包含一個類類型的成員ma,而該對象ma所屬於的類MATX 有一個缺省的構造函數。換句話說:編譯器合成了默認的MBTX構造函數,並且在其中 安插代碼,調用MATX的缺省構造函數。
第二章第五節 構造函數語義續
  • 父類帶缺省構造函數,子類沒有任何構造函數,那因爲父類這個缺省的構造函數要被調用,所以編譯器會爲這個子類合成出一個默認構造函數。合成的目的是爲了調用這個父類的構造函數。換句話說,編譯器合成了默認的構造函數,並在其中安插代碼,調用其父類的缺省構造函數。
  • 如果一個類含有虛函數,但沒有任何構造函數時
    • 因爲虛函數的存在編譯器會給我們生成一個基於該類的虛函數表vftable。
    • 編譯給我們合成了一個構造函數,並且在其中安插代碼: 把類的虛函數表地址賦給類對象的虛函數表指針 (賦值語句/代碼);
    • 我們可以把 虛函數表指針 看成是我們表面上看不見的一個類的成員變量
  • 編譯器給我們往MBTX缺省構造函數中增加了代碼:
    • 生成了類MBTX的虛函數表
    • 調用了父類的構造函數
    • 因爲虛函數的存在,把類的虛函數表地址賦給對象的虛函數表指針。
  • 當我們有自己的默認構造函數時,編譯器會根據需要擴充我們自己寫的構造函數代碼,比如調用父類構造函數,給對象的虛函數表指針賦值。編譯器幹了很多事,沒默認構造函數時必要情況下幫助我們合成默認構造函數,如果我們有默認構造函數,編譯器會根據需要擴充默認構造函數裏邊的代碼。
  • 如果一個類帶有虛基類,編譯器也會爲它合成一個默認構造函數
第二章第六節 拷貝構造函數語義
  • 成員變量初始化手法,比如int這種簡單類型,直接就按值就拷貝過去,編譯器不需要合成拷貝構造函數的情況下就幫助我們把這個事情辦了,比如類A中有類類型ASon成員變量asubobj,也會遞歸的去拷貝類ASon的每個成員變量

  • 那編譯器在什麼情況下會幫助我們合成出拷貝構造函數來呢?那這個編譯器合成出來的拷貝構造函數又要幹什麼事情呢?

    • 如果一個類A沒有拷貝構造函數,但是含有一個類類型CTB的成員變量m_ctb。該類型CTB含有拷貝構造函數,那麼當代碼中有涉及到類A的拷貝構造時,編譯器就會爲類A合成一個拷貝構造函數。
    • 如果一個類CTBSon沒有拷貝構造函數,但是它有一個父類CTB,父類有拷貝構造函數當代碼中有涉及到類CTBSon的拷貝構造時,編譯器會爲CTBSon合成一個拷貝構造函數 ,調用父類的拷貝構造函數
    • 如果一個類CTBSon沒有拷貝構造函數,但是該類聲明瞭或者繼承了虛函數,當代碼中有涉及到類CTBSon的拷貝構造時,編譯器會爲CTBSon合成一個拷貝構造函數 ,往這個拷貝構造函數裏插入語句:這個語句的含義 是設定類對象myctbson2的虛函數表指針值。虛函數表指針,虛函數表等概念。
    • 如果一個類沒有拷貝構造函數,但是該類含有虛基類,當代碼中有涉及到類的拷貝構造時,編譯器會爲該類合成一個拷貝構造函數
      C cc; C cc2 = cc;//當代碼中有涉及到類的拷貝構造時
第二章第九節 拷貝構造函數,深淺拷貝
  • 當需要處理很複雜的成員變量類型的時候。因爲我們增加了自己的拷貝構造函數,導致編譯器本身的bitwise拷貝能力失效,所以結論:如果你增加了自己的拷貝構造函數後,就要對各個成員變量的值的初始化負責了。(在拷貝構造函數裏自己申請內存,深拷貝)
第二章第十節 成員初始化列表說
  • 何時必須用成員初始化列表
    • 如果這個成員是個引用
    • 如果是個const類型成員
    • 如果你這個類是繼承一個基類,並且基類中有構造函數,這個構造函數裏邊還有參數。
    • 如果你的成員變量類型是某個類類型,而這個類的構造函數帶參數時;
  • 使用初始化列表的優勢(提高效率)
    • 對於類類型成員變量xobj放到初始化列表中能夠比較明顯的看到效率的提升
  • 初始化列表中的代碼可以看作是被編譯器安插到構造函數體中的,只是這些代碼有些特殊
    • 這些代碼 是在任何用戶自己的構造函數體代碼之前被執行的。所以大家要區分開構造函數中的用戶代碼 和 編譯器插入的初始化所屬的代碼
    • 這些列表中變量的初始化順序是定義順序,而不是在初始化列表中的順序

第三章 虛函數

第三章第一節虛函數表指針位置分析
  • 類:有虛函數,這個類會產生一個虛函數表。
  • 類對象,有一個指針,指針(vptr)會指向這個虛函數表的開始地址。
  • 虛函數表指針位於對象內存的開頭
第三章第三節 虛函數表分析

在這裏插入圖片描述

  • 一個類只有包含虛函數纔會存在虛函數表,同屬於一個類的對象共享虛函數表,但是有各自的vptr(虛函數表指針),當然所指向的地址(虛函數表首地址)相同
  • 父類中有虛函數就等於子類中有虛函數。
  • 但不管是父類還是子類,(單繼承時)都只會有一個虛函數表,不能認爲子類中有一個虛函數表+父類中有一個虛函數表
  • 如果子類中完全沒有新的虛函數,則我們可以認爲子類的虛函數表和父類的虛函數表內容相同.但僅僅是內容相同,這兩個虛函數表在內存中處於不同位置,換句話來說,這是內容相同的兩張表。
  • 虛函數表中每一項,保存着一個虛函數的首地址,但如果子類的虛函數表某項和父類的虛函數表某項代表同一個函數(這表示子類沒有覆蓋父類的虛函數),則該表項所執行的該函數的地址應該相同。

第三章第四節 多重繼承虛函數分析

  • 一個類,如果它的類有多個基類(基類有虛函數),則有多個虛函數表,分別對應多個基類。子類新的虛函數加在第一個基類的虛函數表裏。
  • 一個對象,如果它的類有多個基類(基類有虛函數)則有多個虛函數表指針(注意是多個虛函數表指針,而不是兩個虛函數表);
  • 在多繼承中,對應各個基類的vptr按繼承順序依次放置在類的內存空間中,且子類與第一個基類共用一個vptr(第二個基類有自己的vptr)(這裏表述不太嚴禁)
  • 在這裏插入圖片描述
第三章第五節 vptr、vtbl創建時機-01

在這裏插入圖片描述

  • 實際上,對於這種有虛函數的類,在編譯的時候,編譯器會往相關的構造函數中增加 爲vptr賦值的代碼,這是在編譯期間編譯器爲構造函數增加的
  • 程序運行的時候,遇到創建對象的代碼,執行對象的構造函數,那麼這個構造函數裏有 給對象的vptr(成員變量)賦值的語句,自然這個對象的vptr就被賦值了
  • 實際上,虛函數表是編譯器在編譯期間(不是運行期間)就爲每個類確定好了對應的虛函數表vtbl的內容,就是說編譯後虛函數表已經確定了。(虛函數表的地址不再發生變化了)

第四章 數據語義學

typedef放在類的最開頭。編譯器是對成員函數的解析,是整個A類成員變量定義完畢後纔開始的
當運行一個可執行文件時,操作系統就會把這個可執行文件加載到內存;此時進程有一個虛擬的地址空間(內存空間)
在這裏插入圖片描述

第四章 數據成員佈局
  • 普通成員變量的存儲順序 是按照在類中的定義順序從上到下來的;比較晚出現的成員變量在內存中有更高的地址;
    類定義中pubic,private,protected的數量,不影響類對象的sizeof

  • 靜態成員變量,可以當做一個全局量,但是他只在類的空間內可見;引用時用 類名::靜態成員變量名靜態成員變量只有一個實體,保存在可執行文件的數據段的;

  • 邊界調整,字節對齊

    • 某些因素會導致成員變量之間排列不連續,就是邊界調整(字節對齊),調整的目的是提高效率,編譯器自動調整;調整:往成員之間填補一些字節,使用類對象的sizoef字節數湊成 一個4的整數倍,8的整數倍
  • 成員變量偏移值,就是這個成員變量的地址,離對象首地址偏移多少

  • 非靜態成員變量的存取(普通的成員變量),存放在類的對象中。存取通過類對象(類對象指針)

    • 對於普通成員的訪問,編譯器是把類對象的首地址加上成員變量的偏移值
  • 一個子類對象,所包含的內容,是他自己的成員,加上他父類的成員的總和;從偏移值看,父類成員先出現,然後纔是孩子類成員。
    在這裏插入圖片描述

  • 單個類帶虛函數的數據成員佈局

    • 類中引入虛函數時,會有額外的成本付出
    • 編譯的時候,編譯器會產生虛函數表,參考三章五節
    • 對象中會產生 虛函數表指針vptr,用以指向虛函數表的
    • 增加或者擴展構造函數,增加給虛函數表指針vptr賦值的代碼,讓vptr指向虛函數表;
    • 析構函數中也被擴展增加了虛函數表指針vptr相關的賦值代碼,感覺這個賦值代碼似乎和構造函數中代碼相同;
      ![單個類](https://img-blog.csdnimg.cn/20200508192445516.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FzbGFkZmE=,size_16,color_FFFFFF,t_70
      在這裏插入圖片描述
  • 多重繼承且父類都帶虛函數的數據成員佈局

    • 在這裏插入圖片描述
    • 通過this指針打印,我們看到訪問Base1成員不用跳 ,訪問Base2成員要this指針要偏移(跳過)8字節;
    • this指針,加上偏移值 就的能夠訪問對應的成員變量,this指針+偏移值

- 虛繼承
- 在這裏插入圖片描述
- 虛基類初探
- 兩個概念:(1)虛基類表 vbtable(virtual base table).(2)虛基類表指針 vbptr(virtual base table pointer)
- virtual虛繼承之後,A1,A2裏就會被編譯器插入一個虛基類表指針,這個指針,有點成員變量的感覺
- A1,A2裏因爲有了虛基類表指針,因此佔用了4個字節
- 虛基類表內容之5-8字節內容分析
- 虛基類表 一般是8字節,四個字節爲一個單位。每多一個虛基類,虛基類表會多加4個字節
- 再增加虛繼承類時,虛基類表會再多四個字節,標識相應的虛基類成員變量距離虛基類指針的偏移值
- 虛基類成員與虛基類表指針之間的偏移量
- 編譯器因爲有虛基類,會給A1,A2類增加默認的構造函數,並且這個默認構造函數裏,會被編譯器增加進去代碼,給vbptr虛基類表指針賦值。
- 虛基類表內容之1-4字節內容分析(實繼承一個類,再虛繼承一個類時 1-4字節有值)
- 在這裏插入圖片描述
- 實繼承的成員變量之後纔是虛基類表指針
- 多繼承時,子類訪問數據成員是用棧底指針。
- 虛基類表指針成員變量的首地址和本對象A1首地址之間的偏移量 也就是:虛基類表指針 的首地址 - A1對象的首地址(一般是負的)
- 此時是用棧底指針 + 虛函數表中的偏移 - 一個值(只有一個實繼承一個虛繼承時才用棧底指針算)
- 這個值是虛函數表指針與棧底差值(最後一個變量內存後的八個字節是棧底指針
- x + 8; x = 虛函數表指針到類對象所佔據最後一塊內存的距離
- 就是此時虛基類對象的地址 ebp+ecx(虛函數表中的偏移值)-14h
- 結論:只有對虛基類成員進行處理比如賦值的時候,纔會用到虛基類表,取其中的偏移,參與地址的計算;
- 三層結構時虛基類表分析
- 在這裏插入圖片描述

第五章 函數語義學

第一節 普通成員函數調用方式
  • 編譯器內部實際上是將對成員函數myfunc()的調用轉換成了對 全局函數的調用;
  • 成員函數有獨立的內存地址,是跟着類走的,並且成員函數的地址 是在編譯的時候就確定好的
  • 編譯器額外增加了一個叫this的形參,是個指針,指向的其實就是生成的對象
  • 常規成員變量的存取,都通過this形參來進行
第二節 虛成員函數、靜態成員函數調用方式
  • 虛成員函數(虛函數)調用方式
    • 要通過虛函數表指針查找虛函數表,通過虛函數表在好到虛函數的入口地址,完成對虛函數的調用
  • 靜態成員函數調用方式
    • 靜態成員函數沒有this指針,這點最重要
    • 無法直接存取類中普通的非靜態成員變量;
    • 靜態成員函數不能在屁股後使用const,也不能設置爲virtual
    • 可以用類對象調用,但不非一定要用類對象調用。
    • 靜態成員函數等同於非成員函數,有的需要提供回調函數的這種場合,可以將靜態成員函數作爲回調函數;
第五節單繼承虛函數

![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200508194006427.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FzbGFkZmE=,size_16,color_FFFFFF,t_70

第六節 多繼承函數深釋,第二基類,虛析構必加

在這裏插入圖片描述
在這裏插入圖片描述

  • 如何成功刪除用第二基類指針new出來的繼承類對象
    • 我們要刪除的實際是整個子類對象
    • 要能夠保證Derive()對象的析構函數被正常調用
    • 編譯器會調用Base2的析構函數,還是調用Derive的析構函數呢?
    • 執行delte pb2時,系統的動作會是?
      • 如果Base2裏沒有析構函數,編譯器會直接刪除以pb2開頭的這段內存,一定報異常,因爲這段內存壓根就不是new起始的內存;
      • 如果Base2裏有一個析構函數,但整個析構函數是個普通析構函數(非虛析構函數),那麼當delte pb2,
        這個析構函數就會被系統調用,但是delete的仍舊是pb2開頭這段內存,所以一定報異常
        。因爲這段內存壓根就不是new起始的內存;析構函數如果不是虛函數,編譯器會實施靜態綁定,靜態綁定意味着你delete Base2指針時,刪除的內存開始地址就是pb2的當前位置;所以肯定是錯誤的
      • 如果Base2裏是一個虛析構函數,
      • 子類裏就就算沒有虛析構函數,因爲Base2裏 有虛析構函數,編譯器也會爲此給子類生成虛析構函數,爲了調用基類中的虛析構函數
    • 凡是涉及到繼承的,所有類都必須要寫虛析構函數;
  • 類的第二個虛函數表中發現了thunk字樣:
    • 一般用在多重繼承中(從第二個虛函數表開始可能就 會有);用於this指針調整,調用Derive析構函數
第七節 多繼承第二基類虛函數支持、虛繼承帶虛函數
  • 多重繼承第二基類對虛函數支持的影響(this指針調整作用)
    • 子類繼承了幾個父類,子類就有幾個虛函數表
    • this指針調整,調整的目的是幹什麼?
      • this指針調整的目的就是讓對象指針正確的指向對象首地址,從而能正確的調用對象的成員函數或者說正確確定數據成員的存儲位置。
    • 通過指向第二個基類的指針調用繼承類的虛函數;
    • 一個指向派生類的指針,調用第二個基類中的虛函數
第八節 RTTI運行時類型識別回顧與存儲位置介紹
  • c++運行時類型識別RTTI,要求父類中必須至少有一個虛函數;如果父類中沒有虛函數,那麼得到RTTI就不準確;
  • RTTI就可以在執行期間查詢一個多態指針,或者多態引用的信息了;
  • RTTI能力靠typeid和dynamic_cast運算符來體現

在這裏插入圖片描述

第十節 指向成員函數的指針及vcall進一步談
  • 指向成員函數的指針
    • 成員函數地址,編譯時就確定好的。但是,調用成員函數,是需要通過對象來調用的;
    • 所有常規(非靜態)成員函數,要想調用,都需要 一個對象來調用它;
  • 指向虛成員函數的指針及vcall進一步談
    • vcall (vcall trunk) = virtual call:虛調用
    • 它代表一段要執行的代碼的地址,這段代碼去執行正確的虛函數
    • 或者我們直接把vcall看成虛函數表,如果這麼看待的話,那麼vcall{0}代表的就是虛函數表裏的第一個函數
    • &A::myvirfunc:打印出來的是一個地址,這個地址中有一段代碼,這個代碼中記錄的是該虛函數在虛函數表中的一個偏移值,有了這個偏移值,再有了具體的對象指針(this指針),我們就能夠知道調用的是哪個虛函數表裏邊的哪個虛函數了;

第六章對象構造語義學

第六節 new,delete的進一步認識
		A *pa = new A(); //函數調用
		A *pa2 = new A;
  • new類對象時加不加括號的差別
    • 帶括號的初始化會把一些和成員變量有關的內存清0,但不是整個對象的內存全部清0
  • new 幹了兩個事:一個是調用operator new(malloc),一個是調用了類A的構造函數
  • delete幹了兩個事:一個是調用了類A的析構函數,一個是調用operator delete(free)
第七節 new細節探祕,重載類內operator new、delete
  • 我們注意到,一塊內存的回收,影響範圍很廣,遠遠不是10個字節,而是一大片
  • 分配內存這個事,絕不是簡單的分配出去4個字節,而是在這4個字節周圍,編譯器做了很多處理,比如記錄分配出的字節數等等
  • 分配內存時,爲了記錄和管理分配出去的內存,額外多分配了不少內存,造成了浪費;尤其是你頻繁的申請小塊內存時,造成的浪費更明顯,更嚴重
  • 構造和析構函數被調用3次,但是operator new[]和operator delete[]僅僅被調用一次;(數組操作)
第八節 內存池概念
  • 內存池的概念和實現原理概述
    • malloc:內存浪費,頻繁分配小塊內存,則浪費更加顯得明顯
  • 內存池”,要解決什麼問題?
    • 減少malloc的次數,減少malloc()調用次數就意味着減少對內存的浪費
    • 減少malloc的調用次數,是否能夠提高程序運行效率? 會有一些速度和效率上的提升,但是提升不明顯;
  • 內存池的實現原理
    • 用malloc申請一大塊內存,當你要分配的時候,我從這一大塊內存中一點一點的分配給你,當一大塊內存分配的差不多的時候,我再用malloc再申請一大塊內存,然後再一點一點的分配給你;
  • 減少內存浪費,提高運行效率;
第十節 重載全局new、delete,定位new
  • 定位new(placement new)
    • 有placement new,但是沒有對應的placement delete
    • 功能:在已經分配的原始內存中初始化一個對象
      • 已經分配,定位new並不分配內存,你需要提前將這個定位new要使用的內存分配出來
      • 初始化一個對象(初始化一個對象的內存),我們就理解成調用這個對象的構造函數;定位new就是能夠在一個預先分配好的內存地址中構造一個對象
    • 格式:new (地址) 類類型();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章