最近在看 C++ 類繼承中的欄位記憶體佈局,我就很好奇 C# 中的繼承鏈那些 private 欄位都哪裡去了? 在記憶體中是如何佈局的,畢竟在子類中是無法存取的。
為了方便講述,先上一個例子:
internal class Program
{
static void Main(string[] args)
{
Chinese chinese = new Chinese();
int num = chinese.b; //b 欄位無法存取,編譯報錯
Console.WriteLine(num);
}
}
public class Person
{
public int a = 10;
private int b = 11;
}
public class Chinese : Person
{
public int c = 12;
}
根據 C# 的類繼承原則,上面的 chinese.b
寫法肯定是無法被編譯的,因為它屬於父類別的 私有欄位,既然無法被存取,那這個 private b
到底去了哪裡呢? 要想找到答案,只能先從 chinese
範例處的組合程式碼看起,看看有沒有什麼意外收穫。
在 new chinese()
處下一個斷點,檢視 Visual Stduio 2022
的反組合視窗。
接下來我稍微解讀下:
07FD6176 mov ecx,87205C4h
07FD617B call CORINFO_HELP_NEWSFAST (06E30C0h)
這裡的 87205C4h
就是 Chinese 型別的 MT,然後通過 CLR 下的 CORINFO_HELP_NEWSFAST
處的方法進行範例化。
07FD6180 mov dword ptr [ebp-40h],eax
07FD6183 mov ecx,dword ptr [ebp-40h]
07FD6186 call CLRStub[MethodDescPrestub]@7e34871e07fd5d20 (07FD5D20h)
07FD618B mov eax,dword ptr [ebp-40h]
這裡的 eax 是 CORINFO_HELP_NEWSFAST
初始化方法的返回值,可以在 ecx,dword ptr [ebp-40h]
處下一個斷點,觀察它的記憶體佈局。
從佈局圖看,此時的 chinese 只是一個清零的預設狀態,此時的 a,b,c
三個欄位還沒有被賦值,那什麼時候被賦值呢? 這就是建構函式要做的事情了,也就是上面的 CLRStub[MethodDescPrestub]@7e34871e07fd5d20 (07FD5D20h)
指令,接下來在 07FD618B
處下一個斷點,再次觀察 0x02C9F528
處的記憶體地址,也就是 ebp-40
的位置,接下來我們繼續執行,截圖如下:
從圖中可以看到,當建構函式執行完之後,有三處記憶體地址(變紅)被賦值了,依次是 a,b,c
,這時候是不是讓人眼前一亮。
原來那個 b=11
並沒有丟,而是被 chinese
類給完全繼承下來的,而且佈局規則是 父類別
欄位在前, 子類
欄位在後的一種方式,有點意思,接下來的問題是如何把它提取出來?
如果是 C 語言,我們用 *(pointer+2)
就可以輕鬆提取,那用託管的 C# 如何去實現呢? 可以用複雜的 Marshal
包裝類,應該也可以變相的使用 Span
去搞定,這裡我就不麻煩了,直接用非安全程式碼下的 指標
去擺平,在 a
欄位偏移 +4 的位置上提取, 參考程式碼如下:
static void Main(string[] args)
{
unsafe
{
Chinese chinese = new Chinese();
fixed (int* ch = &chinese.a)
{
int b = *(ch + 1);
Console.WriteLine($"b={b}");
}
}
}
}
哈哈,是不是挺有意思。