如何在C語言中實現C++的Class呢?

一些低級設備比如嵌入式,或者一些底層驅動、操作系統中,是不能使用C++語言的。網上有很多對這方面的解釋,除非對編譯器做一些設置或者修改,但這樣大大增加了開發的難度。並且靠修改編譯器參數來編譯,仍舊不能利用到C++的優點。比如C++的虛函數,垃圾回收,異常,在底層開發中使用,反而會造成很多不必要的麻煩。比如C++編譯器為了重載函數,其編譯出來的函數名會被改成包括參數的形式,而且每個編譯器都有自己的內部前綴和後綴,這一點尤其在操作系統編寫中會造成麻煩,因為操作系統的系統調用使用彙編,比如中斷的實現中,就需要調用彙編中斷服務,然後在其中回調操作系統內核的C函數,如果使用C++函數,就不能正確指定回調函數名。那麼在只能使用C語言來實現的情況下,如何讓這種過程式語言更具封裝性,而不讓代碼看起來「懶散」呢?C語言中可以和class類比的類型就是struct了,另外還有union, 但union並不具備class的條件。在struct中不能定義函數, 這一點可以在Microsoft Visual Studio中和Linux GCC下做個比較:typedef struct A {int data;

int Val() {

return data;}}A;A a;a.Val();在VS下這個struct能通過編譯,並且a.Val()能取到值, 這是因為C++編譯器在對兼容C語言的struct進行編譯時,是將struct按照public class來理解的,所以能支持內聯函數。但GCC是隻支持C語言的編譯器,編譯時就會報錯。那麼,如果使用C語言,如何才能讓struct媲美class呢?其實C類語言都支持函數指針的定義,並且struct中也支持函數指針定義。比如int func(int a, int b);定義的函數指針可以使這樣的:

int (*pfunc)(int, int);

當定義pfunc = func時,下面兩個調用是一樣的:func(10, 20);和pfunc(10, 20);那如上面所說,將函數指針定義到struct中:typedef struct A {int data;int (*Val)(int a);}A;int Val(int a) { return a;

}

A a;a.Val = Val;a.Val(10);這樣可以得到10的結果。我們知道class中隱含了一個this指針,那在Val函數中怎樣才能得到this呢?對了,就通過參數傳遞進去:typedef struct A A;struct A{int data;int (*Val)(A* that, int a);};

int Val(A* that, int a) {

return that->data + a;}A a;a.Val = Val;a.Val(&a, 10);上面就可以得到a.data + 10的結果。我們使用that來代替this,這樣如果這段代碼拿到C++編譯器下面時也不會跟struct中隱含的this衝突。這樣就定義了struct來代替class,唯一的缺點是定義好以後,每次調用函數需要將對象指針傳遞進去,這也是無可避免的。可以定義下面的宏來防止兩次指定的對象不一致:#define __CALL(o, f, ...) o.f(&o, __VA_ARGS__)__CALL(a, Val, 10);其中__VA_ARGS__是C語言關鍵字,用於將宏的變參傳遞到指定位置。在編譯期宏就已經被展開,因此Val已經是a的成員(函數)了,所以不用擔心Val這個參數在__CALL這個宏調用時沒有定義。

進階1: 構造函數

上一步中,a.Val = Val;寫在外面,如果有好幾個成員(函數),會造成代碼臃腫,需要定義一個構造函數。A * _A(A* that, int data) {that->data = data;that->Val = Val;return that;}A a;_A(&a, 20);a.Val(&a, 10);

這樣定義一個對象就需要兩行代碼,仍舊可以定義宏來實現新建對象,不過如果是new對象完全沒有必要。

A * a = _A(new A, 10);另外這裡構造函數只能是一個普通函數,不能作為成員(函數),因為構造函數只調用一次,沒有作為成員的必要,並且構造函數如果是成員也沒法在構造前知道構造函數是什麼,因此只能在外部指定。如果非要定義成成員(函數),這就變成了兩行代碼:a.init = _A;a.init(&a, 20);哈哈,但是如果想要重新設置對象a的值, 定義init成員則另當別論,不過最好還是在普通函數_A中定義。進階2:繼承我們已經有了一個很好的"class"了:typedef struct A A;struct A{

int data;

int (*Val)(A* that, int a);};int Val(A* that, int a) { return that->data + a;}A * _A(A * that, int data) {that->data = data;that->Val = Val;return that;

}

A a;_A(&a, 20);a.Val(&a, 10);接下來,我們要實現繼承。因為如果只是需要上面的代碼,就沒有使用類的必要,實現繼承纔是使用類的終極目標。這裡先暫時、而且也沒法實現虛函數之類的,不用考慮這些。實現繼承需要用到上面提到的union,比如繼承上面的A:typedef struct B B;struct B {union {A super;struct {

int data;

int (*Val)(A* that, int a);};};int val;};B* _B(B* that, int val) {_A(&that->super val);that->val = val;}B b;_B(&b, 30);在union中,定義了基類A,以及將基類A中的成員都拷貝到了一個匿名struct中。在C規範中,union的匿名struct後面定義一個變數名那麼使用變數名.成員才能得到變數,而如果沒有變數名,則直接使用成員名就能得到變數,如下:union {float f[2];struct {float f1;float f2;}uf;}um;要得到f[1]使用um.fu.f2可以得到,而union {float f[2];struct {float f1;float f2;};}um;只使用um.f2就能得到f[1]。我們的類就是利用了這點,可以讓基類的成員變成繼承類的成員。繼承類中super部分是基類,而自身又定義了val這個成員變數,是屬於基類以外的,而且更有意思的是,在B的構造函數中,可以直接通過that->super來構造a,並且構造函數完了以後,b.data和b.Val就是構造A以後的成員,它們分別等於b.super.data和b.super.Val。進階3:部分繼承和重寫(重載)另外我們還可以使用這種結構來實現部分繼承。來看下面A和B的定義(只有定義,前向聲明和調用省略):struct A{int data;int (*Val)(A* that, int a);int val;};struct B {union {A super;struct {int data;int (*Val)(A* that, int a);};};int val;};其中,B.val和A.val是不同的成員。如果要取得A.val使用b.super.val就可以了, 這個是union的特性來決定的。這種沒有繼承基類的情況叫部分繼承。那麼怎麼實現重寫呢?來看B的構造函數:B* _B(B* that, int val) {_A(&that->super, val);//overridethat->Val = myVal;that->val = val;}只要將繼承類的Val指針指向自定義的函數就可以了。不過注意必須在A構造完成之後,否則會被覆蓋回來。所以歸納起來,構造函數的寫法順序為:基類構造重寫函數子類構造(函數指針初始化)子類數據初始化其他初始化其中,「其他初始化」之前的所有都可以類比為C++類的構造函數:B : A(val), val(val) {}而重載函數是在C++類中定義內聯函數時就已經直接重載了。進階4:定義宏使結構更簡單C語言模擬C++類的大體如上。不過如果想要創建類更簡便,還需要一些宏定義的幫助。在任何情況下,我們都應該知道自己定義這個類將來是不是會成為基類。所以,對於A,我們知道它是基類,可以將它改寫成「模板」,但這個模板非C++的template,只是宏定義,用於簡化繼承類中基類的書寫:typedef struct A {#define A_Template int data;int (*Val)(A* that, int a);#define Template_A A_TemplateTemplate_Aint val;}A;雖然多了兩個#define,但是A的定義並沒有變化。int val;不是由子類繼承的所以不用寫在#define裡面。#define包括在花括弧內是為了讓代碼更加美觀。#define將會在下面宏中使用:#define __SUPER(Base) union { Base super; struct { Template_##Base }; }這就是B的union部分,我們將它提煉出來,使得以後所有的類都可以不失一般性地調用宏來繼承。typedef struct B {__SUPER(A);int val;}B;看,這樣就不用再去重新寫基類的成員了,所有基類成員只在基類中定義一遍,在子類中通過宏來進行展開。進階5:模板?我們通過上面的說明,能夠很快寫出一個類的例子,這是一個能編譯運行的C代碼(linux gcc):// Class.c//#include <stdio.h>#include <stdlib.h>

//////////////////////////////////////////////////////

#define __SUPER(Base) union { Base super; struct { Template_##Base }; }//////////////////////////////////////////////////////typedef struct A A;struct A {#define A_Template int data;int (*Val)(A* that, int a);#define Template_A A_TemplateTemplate_Aint val;};int Val(A* that, int a) { return that->data + a;}A * _A(A * that, int data) {that->data = data;that->Val = Val;return that;}//////////////////////////////////////////////////////typedef struct B {__SUPER(A);int val;}B;int myVal(B* that, int a) { return that->data * a;}B* _B(B* that, int val) {_A(&that->super, val);that->Val = myVal;that->val = val;}//////////////////////////////////////////////////////int main() { A a; _A(&a, 10); a.val = 1; printf("%d %d
", a.val, a.Val(&a, 20));

B b;

_B(&b, 20); b.val = 2; printf("%d %d
", b.val, b.Val(&b, 30)); return 0;}可以看到結果是1 20和2 600,說明成功了。但是編譯器報出警告:assignment from incompatible pointer type,這是因為基類Val是A類型參數,而B中重載時給的卻是B類型參數,由於是繼承關係,並且是指針,所以不用去理會也不會有什麼問題。但是如果真的要較真的話,就需要更改#define了:struct A {#define A_Template(T) int data;int (*Val)(T* that, int a);#define Template_A(T) A_Template(struct T)Template_A(A)int val;};上面_SUPER和下面B調用__SUPER的地方也一併改掉,不贅述。可以看到,當在A中時,Val使用的A類型參數,而在B中時使用B類型參數,應該不會有問題了。--------但這並不是模板!因為struct的侷限性,我們通過添加參數為函數傳入that指針代替this指針,我們定義類型T是為了除去繼承時指針類型不匹配的問題, 但並沒有引入模板。我們在繼承類中使用宏來展開基類,這一點跟模板很像,為什麼不能做成模板呢?哈哈,我們可以模仿一下模板,但真正的模板並不是這樣的:// Class.c//#include <stdio.h>#include <stdlib.h>

//////////////////////////////////////////////////////

#define __SUPER(Base, Type, ...) union { Base super; struct { Template_##Base(Type, __VA_ARGS__) }; }//////////////////////////////////////////////////////typedef struct A A;struct A {#define A_Template(T, Type) Type data;int (*Val)(T* that, int a);#define Template_A(T, Type) A_Template(struct T, Type)Template_A(A, int)int val;};int Val(A* that, int a) { return that->data + a;}A * _A(A * that, int data) {that->data = data;that->Val = Val;return that;}//////////////////////////////////////////////////////typedef struct B {__SUPER(A, B, int);int val;}B;int myVal(B* that, int a) { return that->data * a;}B* _B(B* that, int val) {_A(&that->super, val);that->Val = myVal;that->val = val;}//////////////////////////////////////////////////////int main() { A a; _A(&a, 10); a.val = 1; printf("%d %d
", a.val, a.Val(&a, 20));

B b;

_B(&b, 20); b.val = 2; printf("%d %d
", b.val, b.Val(&b, 30)); return 0;}看,模板就是#define template那裡,而A在定義的時候就已經將模板特化成int類型,B在定義時將模板特化成int類型。結果仍舊一樣。但這個仍舊不是模板,模板是在定義對象的時候特化,而這個是在定義類型時已經特化。由於成員函數並不是內聯的(所謂內聯,就是說每一個對象包含的函數都是在對象內部擴展開的),所以這些函數必須寫在外部,而外部必須保證struct進行了完全定義,所以C是沒有辦法做到真正的模板的。6.繼承鏈根據C的宏定義標準,宏是不可以嵌套的。因此如果要實現繼承鏈,__SUPER會造成嵌套。因此,再增加一個___SUPER表示二級繼承鏈,定義和__SUPER一模一樣,這樣,___SUPER會調用Template然後調用__SUPER,構成繼承鏈。如果還有更多繼承,則繼續定義____SUPER。另外,由於使用的是匿名union,因此基類的super都暴露在外面,繼承鏈中會出現重複super定義,可以將super定義為super+基類名來區別。二級繼承鏈定義如下,一級繼承鏈也修改為下面的定義:#define ___SUPER(Base, ...)union { Base super##Base; struct { Template_##Base(__VA_ARGS__) }; }好了,這樣就可以實現多級繼承鏈了。在_B構造函數中將super改為superA,然後添加C類繼承B類(二級繼承):struct C {___SUPER(B, C);};C * _C(C* that) {_B(&that->superB, 77);}這樣就構成了一個完整的繼承鏈。完整可運行代碼如下:// Class.c//#include <stdio.h>#include <stdlib.h>

typedef struct A A;

typedef struct B B;typedef struct C C;//////////////////////////////////////////////////////#define __SUPER(Base, ...)union { Base super##Base; struct { Template_##Base(__VA_ARGS__) }; }#define ___SUPER(Base, ...)union { Base super##Base; struct { Template_##Base(__VA_ARGS__) }; }//////////////////////////////////////////////////////struct A {#define A_Template(T) int data;int (*Val)(T* that, int a);#define Template_A(T) A_Template( T)Template_A(A)int val;};int Val(A* that, int a) { return that->data + a;}A * _A(A * that, int data) {that->data = data;that->Val = Val;return that;}//////////////////////////////////////////////////////struct B {#define B_Template( T) __SUPER(A, T);int val;#define Template_B( T) B_Template( T)Template_B( B)};int myVal(B* that, int a) { return that->data * a;}B* _B(B* that, int val) {_A(&that->superA, val);that->Val = myVal;that->val = val;}//////////////////////////////////////////////////////struct C {___SUPER(B, C);};C * _C(C* that) {_B(&that->superB, 77);}//////////////////////////////////////////////////////int main() { A a; _A(&a, 10); a.val = 1; printf("%d %d
", a.val, a.Val(&a, 20));

B b;

_B(&b, 20); b.val = 2; printf("%d %d
", b.val, b.Val(&b, 30)); C c; _C(&c); c.val = 3; printf("%d %d
", c.val, c.Val(&c, 30)); return 0;}輸出結果:1 30 2 600 3 2310,結果正確。

7.結束語

雖然沒能做到模板很遺憾,但是能夠保證一些class的元素能夠被使用在C語言中,已經很不錯了。本次class的應用是在看操作系統內核時,尋找用C語言解決class問題時,其中的一些研究成果,並且在實踐中已經證實可以使用----雖然有些晦澀而且並不是一個很好的編程體驗(比如下面記載的,對於習慣了class的人來說真的很難改變)但是需要強調幾點,這也是遺留的問題,以現在的能力還是沒有辦法解決,期待高人來解決吧:1.使用和定義類成員(函數)時一定要帶上類對象指針that以代替this2.struct的繼承和C++的繼承有很大區別,因為struct是通過在子類中重新定義基類來進行的,因此,基類設計一定要注意成員順序,比如上面例子中A的val是放在最後而不是最前,這樣通過子類繼承時子類纔不會將val繼承過去。3.由於宏定義的一些特性,導致繼承鏈需要定義多個級別的__SUPER,需要明確當前繼承是第幾級,然後確定使用哪個__SUPER4.第2點所帶來的麻煩並不止這些,如果是連續繼承,所有子類必須擁有基類的所有成員,繼承鏈越靠後類就會變得越臃腫,這僅僅是指類所佔的空間,然而在代碼中使用__SUPER並不會看到這种放大作用5.如上看到的,所有的成員函數並不是內聯函數,而僅僅是指向函數的指針,指向了一個struct外部的函數(在gcc中是不允許直接將函數寫在struct中的,因此只能定義函數指針,其實也相當於一個成員變數而已),所以成員函數沒有辦法做內聯優化。完(*^_^*)

stophin 2016/11/26


推薦閱讀:
相關文章