C 語言實(shí)現(xiàn)面向?qū)ο蟮谝徊?-對(duì)象模型
首先申明下,看完這篇文章的一些做法,你可能會(huì)覺得很傻x,但是我僅僅是抱著一種嘗試和學(xué)習(xí)的態(tài)度,實(shí)際中可能也并不會(huì)這么去用。
什么是 OOP(Object-oriented Programming, OOP)?
OOP 這種編程范式大概起源于 Simula。
它依賴于:
封裝(encapsulation) 繼承(inheritance) 多態(tài)(polymorphism)。
就 C++、Java 而言,OOP 的意思是利用類層級(jí)(class hierarchies)及虛函數(shù)進(jìn)行編程。
從而可以通過精制的接口操作各種類型的對(duì)象,并且程序本身也可以通過派生(derivation)進(jìn)行功能增量擴(kuò)展。
舉個(gè) Bjarne Stroustrup FAQ 用過的栗子:
比如可能有兩個(gè)(或者更多)設(shè)備驅(qū)動(dòng)共用一個(gè)公共接口:
class?Driver?{?//?公共驅(qū)動(dòng)接口
??public:
??virtual?int?read(char*?p,?int?n)?=?0;?//?從設(shè)備中讀取最多?n?個(gè)字符到?p
??//?返回讀到的字符總數(shù)
??virtual?bool?reset()?=?0;?//?重置設(shè)備
??virtual?Status?check()?=?0;?//?讀取狀態(tài)
};
Driver 僅僅是一個(gè)接口。
沒有任何數(shù)據(jù)成員,而成員函數(shù)都是純虛函數(shù)。
不同類型的驅(qū)動(dòng)負(fù)責(zé)對(duì)這個(gè)接口進(jìn)行相應(yīng)的實(shí)現(xiàn):
class?Driver1?:?public?Driver?{?//?某個(gè)驅(qū)動(dòng)
??public:
??Driver1(Register);?//?構(gòu)造函數(shù)
??int?read(char*,?int?n);
??bool?reset();
??Status?check();
??//?實(shí)現(xiàn)細(xì)節(jié)
};
class?Driver2?:?public?Driver?{?//?另一個(gè)驅(qū)動(dòng)
??public:
??Driver2(Register);
??int?read(char*,?int?n);
??bool?reset();
??Status?check();
??//?實(shí)現(xiàn)細(xì)節(jié)
};
這些驅(qū)動(dòng)含有數(shù)據(jù)成員,可以通過它們創(chuàng)建對(duì)象。它們實(shí)現(xiàn)了 Driver 中定義的接口。不難想象,可以通過這種方式使用某個(gè)驅(qū)動(dòng):
??void?f(Driver&?d)?//?使用驅(qū)動(dòng)
??{
????Status?old_status?=?d.check();
????//?...
????d.reset();
????char?buf[512];
????int?x?=?d.read(buf,512);
????//?...
??}
這里的重點(diǎn)是,f() 不需要知道它使用的是何種類型的驅(qū)動(dòng);
它只需知道有個(gè) Driver 傳遞給了它;
也就是說,有一個(gè)接口傳遞給了它。
我們可以這樣調(diào)用 f() :
void g() {
Driver1 d1(Register(0xf00)); // create a Driver1 for device
// with device register at address 0xf00
Driver2 d2(Register(0xa00)); // create a Driver2 for device
// with device register at address 0xa00
// ...
int dev;
cin >> dev;
if (dev==1)
f(d1); // use d1
else
f(d2); // use d2
// ...
}
當(dāng) f() 使用某個(gè)驅(qū)動(dòng)時(shí),與該驅(qū)動(dòng)相對(duì)應(yīng)的操作會(huì)在運(yùn)行時(shí)被隱式選擇。
例如,當(dāng) f() 得到 d1 時(shí),d.read() 使用的是 Driver1::read();
而當(dāng) f() 得到 d2 時(shí),d.read() 使用的則是 Driver2::read()。
這被稱為運(yùn)行時(shí)綁定,在一些動(dòng)態(tài)語言中,鴨子類型(duck typing) 常用來實(shí)現(xiàn)這種“多態(tài)”— 不關(guān)心是什么東西,只要覺得它可以run,就給他寫個(gè)叫 run的函數(shù)即可。
當(dāng)然 OOP 也并非萬能藥。
不能簡(jiǎn)單地把 “OOP” 等同于“好”。
OOP 的優(yōu)勢(shì)在于類層級(jí)可以有效地表達(dá)很多問題;OOP 的主要弱點(diǎn)在于太多人設(shè)法強(qiáng)行用層級(jí)模式解決問題。
并非所有問題都應(yīng)該面向?qū)ο蟆R部梢钥紤]使用普通類(plain class)(也就是常說的 C With Class)、泛型編程和獨(dú)立的函數(shù)(就像數(shù)學(xué)、C,以及 Fortran 中那樣)作為解決問題的方案。
當(dāng)然,OOP != 封裝、繼承、多態(tài)。
本文僅僅是想討論下在 C 中如何實(shí)現(xiàn)封裝、繼承、多態(tài)。
封裝可以借助 struct,將數(shù)據(jù)和方法都放到一個(gè)結(jié)構(gòu)體內(nèi),使用者可以無需關(guān)注具體的實(shí)現(xiàn)。
一種很直白簡(jiǎn)單的方式,就是使用函數(shù)指針表示成員方法和數(shù)據(jù)放在一個(gè)struct 內(nèi)。
比如在搜狗開源的服務(wù)端框架 Workflow 中就大量使用了這種方式:

這里可以看下 __poller_message這個(gè)結(jié)構(gòu)體:
struct?__poller_message
{
?int?(*append)(const?void?*,?size_t?*,?poller_message_t?*);
?char?data[0];?
};
這里 append 函數(shù)指針就算是一個(gè)成員方法,這樣會(huì)非常靈活,你可以給它賦任何一種具體實(shí)現(xiàn)。
(PS: char[0] 數(shù)組是一種 C 語言中常用技巧,通常放在結(jié)構(gòu)體的最后,常用來構(gòu)成緩沖區(qū)。
使用這樣的寫法最適合制作動(dòng)態(tài) buffer,可以這樣分配空間:malloc(sizeof(struct XXX)+ buff_len); 這樣就直接把 buffer 的結(jié)構(gòu)體和緩沖區(qū)一塊分配了**。**
用起來也非常方便,因?yàn)楝F(xiàn)在空數(shù)組其實(shí)變成了buff_len長(zhǎng)度的數(shù)組了。
感興趣的可以去看下源碼(學(xué)習(xí)分支):https://github.com/sogou/workflow/tree/study
當(dāng)然了,這里我選擇了模仿 C++ 對(duì)象模型,在《Inside the C++ Object Model》中提到了三種對(duì)象模型設(shè)計(jì)思路:
簡(jiǎn)單對(duì)象模型: 對(duì)象中只存儲(chǔ)每個(gè)成員(包括函數(shù)和數(shù)據(jù))的指針 表格驅(qū)動(dòng)對(duì)象模型: 對(duì)象中存儲(chǔ)兩個(gè)指針,一個(gè)指向存儲(chǔ)數(shù)據(jù)的表,一個(gè)指向存儲(chǔ)函數(shù)指針的表(虛函數(shù)的解決方案) C++ 實(shí)際對(duì)象模型: 對(duì)象存儲(chǔ) non-static 數(shù)據(jù),static成員(數(shù)據(jù)和函數(shù)) 和 non-static 函數(shù)都單獨(dú)存放(注意,并沒有指針指向它們,這可以在編譯時(shí)自動(dòng)確定地址), 還有一個(gè)虛表指針指向存儲(chǔ)虛函數(shù)指針的表格(這個(gè)表第一個(gè)元素可能存放的是 type_info object 以支持RTTI)
那這里選擇對(duì)象只存儲(chǔ)數(shù)據(jù)本身和函數(shù)指針。
我們需要一個(gè)創(chuàng)建對(duì)象和回收資源的方法,可以抄抄 C++ 的作業(yè),C++ 中構(gòu)造對(duì)象使用的是new運(yùn)算符,new運(yùn)算符完成了 內(nèi)存分配 + 調(diào)用類構(gòu)造函數(shù)兩件事。
delete則回收資源,主要是調(diào)用類的析構(gòu)函數(shù) + 釋放內(nèi)存。
new()方法必須知道當(dāng)前正在創(chuàng)建的是什么類型的對(duì)象,在 C++ 中,編譯器會(huì)自動(dòng)識(shí)別,并生成對(duì)應(yīng)的匯編。
但是在 C 中我們只能手動(dòng)將類型相關(guān)的信息作為參數(shù)。
然后在 new 方法內(nèi)使用一系列的 if 去分別處理每種類型?
這種方法顯然不合適,每個(gè)對(duì)象應(yīng)該知道怎么構(gòu)造自己以及如何析構(gòu),也就是類型信息應(yīng)該自帶構(gòu)造和析構(gòu)函數(shù)。
所以設(shè)計(jì)了一個(gè) Class 類,Class 類包含類的元信息,比如類的大小(分配內(nèi)存時(shí)會(huì)用)、構(gòu)造、析構(gòu)函數(shù)等。
其它所有的類都繼承自這個(gè)類。
所謂的繼承實(shí)際上就是將一個(gè)Class類型指針放在第一字段。
很簡(jiǎn)單,因?yàn)橹挥薪y(tǒng)一放在對(duì)象開頭,new 方法內(nèi)才能識(shí)別出這個(gè) Class 類型指針。
所以整個(gè)對(duì)象模型大概是這個(gè)樣子:

struct?Class?{
????size_t?size;????/*?size?of?an?object?*/
????void?*?(*?ctor)?(void?*?this,?va_list?*?vl);
????void?*?(*?dtor)?(void?*?this);
????//....?clone?等
};
我們來實(shí)現(xiàn)以下new和delete:
//?要將參數(shù)透?jìng)鹘o對(duì)象的構(gòu)造函數(shù),所以使用?C?語言變長(zhǎng)參數(shù)
//?type?是具體的類類型參數(shù)
void?*?new?(const?void?*?type,?...)?{
??//?因?yàn)?Class?放在第一個(gè)字段,所以可以直接做截?cái)啵D(zhuǎn)為?Class
????const?struct?Class?*class?=?type;
????//?分配對(duì)象內(nèi)存
????void?*this?=?calloc(1,?class->size);
????*(struct?Class**)this?=?class;??????//?這一步實(shí)際上是將每一個(gè)類構(gòu)造出的對(duì)象,填充上指向類類型的指針
????//?執(zhí)行構(gòu)造函數(shù)
????if(class->ctor)?{
??????//?變長(zhǎng)參數(shù),C?語法
????????va_list?vl;
????????va_start(vl,?type);
????????this?=?class->ctor(this,?&vl);
????????va_end(vl);
????}
????return?this;
}
//?傳入待析構(gòu)的對(duì)象指針
void?delete?(void?*?self)?{
??//?獲取?Class?類型指針
????const?struct?Class?**this?=?self;
????//?如果有析構(gòu)函數(shù),?就執(zhí)行析構(gòu)
????if(self?&&?*this?&&?(*this)->dtor)?{
????????self?=?(*this)->dtor(self);
????}
????//?釋放內(nèi)存
????free(self);
}
接著,我們基于這個(gè)Class來實(shí)現(xiàn)一個(gè) String。
//?string.h
//?這就是需要傳入?new?函數(shù)的第一個(gè)參數(shù),類型指針
extern?const?void?*?StringNew;
struct?String?{
????const?void?*class;???????/*?父類,?都是?Class?*/
????char?*?content;????????????/*?字符串內(nèi)容?*/
????char?*(*get_content)(struct?String*);?????//?獲取
????void?(*set_content)(struct?String*,?const?char?*);?//?設(shè)置
};
這是String的實(shí)現(xiàn):
//?string.c
//?getter
static?char?*get_content(struct?String?*str)?{
????return?str->content;
}
//?setter
static?void?set_content(struct?String?*str,?const?char?*newcontent)?{
????if(str->content)?{
????????free(str->content);
????}
????str->content?=?strdup(newcontent);
}
//?構(gòu)造函數(shù)
static?void*??string_ctor(void?*_this,?va_list?*args)?{
????struct?String?*?this?=?_this;
????//?初始化內(nèi)容
????const?char?*content?=?va_arg(*args,?const??char*);
????this->content?=?strdup(content);
????//?設(shè)置成員函數(shù)指針
????this->get_content?=?get_content;
????this->set_content?=?set_content;
????return?this;
}
//?析構(gòu)函數(shù)
static?void*?string_dtor(void?*_this)?{
????struct?String*?this?=?_this;
????//?釋放字符串內(nèi)存
????if(this->content)?{
????????free(this->content);
????????this->content?=?NULL;
????}
????return?this;
}
//?定義一個(gè)?Class?變量,即?String?類型的?Class
static?const?struct?Class?_String?=?{
????????sizeof(struct?String),
????????string_ctor,
????????string_dtor
};
//?然后將?_String?變量取地址賦值給定義在?string.h?的?StringNew
//?StringNew?就相當(dāng)于構(gòu)造字符串的類模板了,以后需要將這個(gè)指針傳遞給?new?函數(shù)
const?void?*StringNew?=?&_String;
來看下怎么用吧:
void?test_str()?{
????//?構(gòu)造
????struct?String?*str?=?new(StringNew,?"test");
????printf("%s\n",?str->get_content(str));
????str->set_content(str,?"newtest");
????printf("%s\n",?str->get_content(str));
????//?析構(gòu)
????delete(str);
}
是不是有點(diǎn)那味了?
就是每次都得顯示的傳 this參數(shù),這個(gè)沒辦法,語法不支持。
不過應(yīng)該是可以用宏包一下。
好了,整體的框架已經(jīng)搭好了,可以基于這種模式去實(shí)現(xiàn)繼承、多態(tài)了。
這部分我就放在第二篇寫了,可以自己先去試下,達(dá)到大概這種效果:

Circle 繼承自Graph,然后可以將 Circle 對(duì)象向上轉(zhuǎn)型為 Graph,但是Graph去調(diào)用具體 draw方法的時(shí)候,還是執(zhí)行的 Circle的 draw方法。
