到編譯器編譯的(非可控,由微軟決定編譯))
class { ... }想成一個實體盒子認為大括號裡寫的類型、變數和函數最後都要一起塞進每個物件內部的 0/1 裡。--這是錯誤的類別宣告是編譯器閱讀的一份規則文件不是一整塊會原樣存在於執行期記憶體中的資料。Epic 也把 class 描述成建立 ObjectActor 的模板類別標頭裡是「宣告」函數與屬性不是說每個物件都攜帶一份函數。(Epic Games Developers)看這個struct FVectorLike { using Scalar float; float X; float Y; float Z; float LengthSquared() const { return X * X Y * Y Z * Z; } };你在源碼中看到它們都寫在同一對{}裡但編譯後會被分流到完全不同的地方。FVectorLike 的宣告 ├─ using Scalar float │ └─ 只給編譯器建立一個類型別名 │ └─ FVectorLike 物件裡不佔空間 │ ├─ float X, Y, Z │ └─ 決定每個物件的資料布局 │ └─ 每個 FVectorLike 物件真正擁有三個 float │ └─ LengthSquared() └─ 編譯成一份機器指令 └─ 所有 FVectorLike 物件共同使用一個物件真正存了什麼例如FVectorLike A; FVectorLike B;執行期可以近似理解成A 的記憶體 ├─ X4 bytes ├─ Y4 bytes └─ Z4 bytes B 的記憶體 ├─ X4 bytes ├─ Y4 bytes └─ Z4 bytes在典型 Windows C 實作中它們通常各自是 12 bytes實際布局仍由型別、對齊及編譯器 ABI 決定。C 所謂 object layout主要就是描述資料成員如何排列在物件記憶體裡。(Microsoft Learn)它們不會變成A ├─ X ├─ Y ├─ Z ├─ Scalar 類型資訊 └─ LengthSquared 函數的全部機器碼函數沒有複製進 A也沒有複製進 B。!!!那A.LengthSquared()怎麼知道操作 A這是關鍵。你寫float Result A.LengthSquared();概念上編譯器可以把它理解成float Result FVectorLike_LengthSquared(A);也就是函數只有一份但呼叫時偷偷多傳入this A;因此原來的return X * X Y * Y Z * Z;概念上等於return this-X * this-X this-Y * this-Y this-Z * this-Z;C 的非靜態成員函數具有隱含的this指標指向目前正在被操作的物件。(Microsoft Learn)所以A.LengthSquared(); B.LengthSquared();使用的是同一份函數機器碼區別只在傳進去的物件地址呼叫 A.LengthSquared() └─ this A 的地址 呼叫 B.LengthSquared() └─ this B 的地址這就是「資料屬於每個物件行為由所有物件共用」。「類別裡可以存類型」也不是把類型塞進物件例如struct FContainer { using ValueType float; struct FIterator { int Index; }; ValueType Value; };這裡FContainer ├─ ValueType │ └─ 一個作用域內的類型名稱 │ ├─ FIterator │ └─ 另一個類型的宣告 │ └─ Value └─ 真正的資料成員你可以寫FContainer::ValueType Number; FContainer::FIterator Iterator;FContainer::在這裡更像一個名稱作用域FContainer └─ 裡面有哪些名稱 ├─ ValueType ├─ FIterator └─ Value巢狀類型本身不會增加FContainer物件大小。只有你真的寫struct FContainer { FIterator Iterator; };這時Iterator才是物件資料才會佔記憶體。因此要區分struct FIterator {}; // 宣告一種類型 FIterator Iterator; // 放置一個該類型的物件第一行只是讓編譯器知道「有這種形狀」。第二行才是真的要求按照FIterator的布局給每個FContainer實例留出一塊記憶體。最底層的 0/1 到底怎麼區分資料、類型和函數0 和 1 本身沒有「這是 float」「這是函數」的天然標籤。同一串 bits01000000 01001001 00001111 11011011只有在不同解讀規則下才可能被理解成└─ 當 float 解讀 └─ 一個浮點數 └─ 當 int32 解讀 └─ 一個整數 └─ 當機器指令的一部分解讀 └─ 某些 CPU opcode / operand意義來自「誰在解讀」以及「按什麼規則解讀」。完整鏈路是C 源碼文字 └─ 編譯器解析 ├─ 類型宣告 │ └─ 用於檢查尺寸、成員偏移、呼叫是否合法 │ ├─ 資料成員 │ └─ 形成物件的記憶體布局 │ └─ 函數 └─ 編譯成 CPU 機器指令 可執行檔 ├─ Code / .text │ └─ 函數的機器指令 │ ├─ Data │ └─ 全域與靜態資料 │ └─ 編譯器與連結器資訊 └─ 符號、重定位、除錯資訊等 執行時 ├─ CPU 指令指標指向 Code │ └─ CPU 把那些 bytes 當指令執行 │ └─ Load / Store 指令指向物件記憶體 └─ CPU 把那些 bytes 當資料讀寫不是 bits 自己聲稱「我是函數」。而是CPU 從指令指標位置取 bytes └─ 按指令集解碼 └─ 它們成為函數指令 CPU 執行讀取 float 的指令 └─ 從某地址取 4 bytes └─ 按浮點格式使用!!!函數如果是虛函數,則以base類裡的所有Virtual函數給這個類湊成表例如struct Base { virtual void F(); virtual void G(); }; struct ChildA : Base { void F() override; }; struct ChildB : Base { void G() override; };概念上的布局是Base 的表 ├─ slot 0 → Base::F └─ slot 1 → Base::G ChildA 的表 ├─ slot 0 → ChildA::F └─ slot 1 → Base::G ChildB 的表 ├─ slot 0 → Base::F └─ slot 1 → ChildB::G不是一張總表 ├─ Base::F ├─ ChildA::F ├─ ChildB::F ├─ Base::G ├─ ChildA::G └─ ChildB::G而是每個類型都有一套固定槽位槽位代表虛函數接口槽位裡放的是對這個具體類型而言該虛函數最終應該調用哪個實現。Microsoft 的資料也把 vtable 描述為函數指標構成的表虛呼叫會從固定 offset 的槽位讀出函數地址再調用。(Microsoft Learn)所以class { ... }究竟是什麼不要再把它理解成「一個裡面裝了資料和函數的盒子」。更準確是class / struct ├─ 一個名稱作用域 │ ├─ 可放類型名稱 │ ├─ 可放函數名稱 │ └─ 可放變數名稱 │ ├─ 一份物件布局規則 │ └─ 非 static 資料成員決定物件裡有哪些 bytes │ ├─ 一組可作用於該物件的函數 │ └─ 通過 this 指標找到具體物件 │ └─ 一套存取與型別檢查規則 ├─ public ├─ protected └─ private它不是單一物理實體而是 C 把幾種相關規則收束到同一個名字下。最終可以壓成一句類別大括號裡混合的是「宣告」不是同一種執行期資料編譯器會把類型規則留在編譯期把資料成員變成物件布局把函數變成共用機器碼再用this把函數接到具體物件。它們根本沒有作為同一坨 0/1 被塞進物件只是在源碼層被放進同一個語義作用域。