C風格的面向對象設計,是從linux內核代碼流行開的一種設計模式。
C++并不適合編寫系統內核代碼,但內核里的很多模塊又非常的OOP[呲牙]所以Linux之父就想出了這么一套C風格的OOP,然后被大量的程序員有樣學樣[呲牙]
C風格的面向對象,是使用結構體加函數指針來模擬C++的多態的。
它們的區別只是,C++的虛函數表是編譯器自動生成的,而C語言的“虛函數表”則需要創建對象時由程序員手動初始化。
我覺得C語言的函數指針聲明比較麻煩,所以我在scf編譯器框架里讓函數既可以當作函數,也可以當作類型。
然后,就可以用函數名直接聲明它的指針變量,來作為函數指針。
因為當函數指針的參數里也有函數指針時,C語言代碼會變得很復雜,例如:
類似這樣的多級函數指針的嵌套,會讓代碼的可讀性變得很差。
在C語言里遇到這種情況,一般也是用typedef去定義一個函數指針類型,然后再去聲明函數指針變量:
typedef int (*cmp)(...);
cmp c0;
所以我在scf框架里就直接讓函數名也可以當類型用了:先聲明1個函數,然后用它的函數名加上星號去定義函數指針變量。例如:
int file_close_pt(int fd);
file_close_pt* close; //這就是函數指針
在scf框架里,我直接去掉了typedef關鍵字。反正它也只是C語言的版本演化而導致的一個補丁關鍵字[捂臉]
全局結構體的初始化如下:
file_ops test_file_ops = {file_open, file_close}; // “虛函數表”
file f = {NULL, &test_file_ops}; // "類"的對象
如果是動態申請的對象結構體,需要手動設置它的“虛函數表”:
file* f = malloc(sizeof(file));
f->ops = &test_file_ops;
然后這么調用它:f->ops->open(),與C++的f->open()也差不多。
C++的虛函數的查找是由編譯器自動實現的,C語言則只能明確的寫出來:調用的是ops函數指針結構體里的open()函數。
C++的虛函數表,實際上也是函數指針組成的結構體,只不過實現細節被編譯器隱藏了。
我在scf框架里沒有區分這2種語法:結構體取成員和結構體指針取成員,前者在C語言里用點號運算符(.),后者用指針運算符(->),我全部使用了->運算符。
在更底層的匯編代碼里,讀取結構體的成員都是通過指針進行的:都是先把結構體的內存地址加載到某個寄存器,然后以這個寄存器為基礎,讀取偏移量是什么位置的多少個字節。
如果file結構體的指針在rdx寄存器,讀取它的ops成員到rax,那么就是:mov rax, 8(rdx),其中8是以字節計算的偏移量。
讀取的長度也是8字節,這是由rax寄存器的位數指定的。
如果直接讀取file結構體的ops成員,即C語言的f.ops,那么首先要把f的地址加載它rdx:
lea rdx, f(rip) // 這里的f是個重定位符號,需要連接器后來修改
mov rax, 8(rdx)
因為這兩個語法過于類似,所以我使用了同一個運算符,而沒有像C語言那樣使用2個。
結構體變量與結構體指針的區別在于,結構體變量需要分配內存。
全局的結構體變量,內存需要分配在程序的數據段.data里,然后在程序啟動時被操作系統映射到內存里(通過Linux的mmap系統調用)。
局部的結構體變量,內存需要分配在函數的棧上。
而結構體指針,只是個8字節的普通變量。
main函數對f->ops->open的調用
匯編代碼對全局變量的加載,都是通過rip寄存器加一個偏移量實現的。
匯編代碼對函數指針的調用都是call *register,其中register是存放著函數指針的寄存器。
.data段的重定位符號
函數也是全局的,函數的地址也是個全局常量,所以test_file_ops結構體(“虛函數表”)的內容也是需要連接器填寫的,編譯器只能給出一個重定位符號。
全局變量的符號表
本文代碼的運行結果,當然是打印一行"file_open",main()函數的返回值是0。
在Linux上,查看main()函數的返回值,使用命令:echo $?
運行結果