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

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

struct?Class?{
????size_t?size;????/*?size?of?an?object?*/
????void?*?(*?ctor)?(void?*?this,?va_list?*?vl);
????void?*?(*?dtor)?(void?*?this);
????//....?clone?等
};
我們來實現(xiàn)以下new和delete:
//?要將參數(shù)透傳給對象的構(gòu)造函數(shù),所以使用?C?語言變長參數(shù)
//?type?是具體的類類型參數(shù)
void?*?new?(const?void?*?type,?...)?{
??//?因為?Class?放在第一個字段,所以可以直接做截斷,轉(zhuǎn)為?Class
????const?struct?Class?*class?=?type;
????//?分配對象內(nèi)存
????void?*this?=?calloc(1,?class->size);
????*(struct?Class**)this?=?class;??????//?這一步實際上是將每一個類構(gòu)造出的對象,填充上指向類類型的指針
????//?執(zhí)行構(gòu)造函數(shù)
????if(class->ctor)?{
??????//?變長參數(shù),C?語法
????????va_list?vl;
????????va_start(vl,?type);
????????this?=?class->ctor(this,?&vl);
????????va_end(vl);
????}
????return?this;
}
//?傳入待析構(gòu)的對象指針
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);
}
接著,我們基于這個Class來實現(xiàn)一個 String。
//?string.h
//?這就是需要傳入?new?函數(shù)的第一個參數(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的實現(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;
}
//?定義一個?Class?變量,即?String?類型的?Class
static?const?struct?Class?_String?=?{
????????sizeof(struct?String),
????????string_ctor,
????????string_dtor
};
//?然后將?_String?變量取地址賦值給定義在?string.h?的?StringNew
//?StringNew?就相當于構(gòu)造字符串的類模板了,以后需要將這個指針傳遞給?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);
}
是不是有點那味了?
就是每次都得顯示的傳 this參數(shù),這個沒辦法,語法不支持。
不過應(yīng)該是可以用宏包一下。
好了,整體的框架已經(jīng)搭好了,可以基于這種模式去實現(xiàn)繼承、多態(tài)了。
這部分我就放在第二篇寫了,可以自己先去試下,達到大概這種效果:

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