C語言面向對象編程範式

面向對象編程不是銀彈。大部分場合,我對面向對象的使用很是謹慎,能不用則不用。相關的討論就不展開了。 

可是,某些場合下,採用面向對象的確是比較好的方案。好比 UI 框架,又好比 3d 渲染引擎中的場景管理。C 語言對面向對象編程並無原生支持,但沒有原生支持並不等於不適合用 C 寫面向對象程序。反而,咱們對具體實現方式有更多的選擇。 

大部分用 C 寫面向對象程序的程序員受 C++ 影響頗深。企圖用宏模擬出一個常見 C++ 編譯器已經實現的對象模型。於我愚見,這並非一個好的方向。C++ 的對象模型,本質上是爲了追求實現層的性能,並直接體現出來。就有如在 C++ 中被濫用的 inline ,的確有效,卻破壞了分離原則。C++ 的繼承是過緊的耦合。 

我所理解的面向對象,是讓不一樣的數據元有共同的操做方式,適合成組的處理。根據操做方式的不一樣,咱們會對數據元作不一樣的分組。一個數據可能出如今這個組裏,也能夠出如今那個組裏。這取決於你從不一樣的方面提取的共性。這些可供統一操做的共性稱之爲接口(Interface),接口在 C 語言中,表現爲一組函數指針的集合。放在 C++ 中,即爲虛表。 

我所偏心的面向對象實現方式(使用 C 語言)是這樣的: 

如有一組數據,咱們須要讓他們看起來都有一種叫做 foo 的共性。把符合這樣的數據都稱爲 foo_object 。一般,咱們會有以下 api 去操控 foo_object 。 

struct foo_object; 

struct foo_object * foo_create(); 
void foo_release(struct foo_object *); 
void foo_dosomething(struct foo_object *);在具體實現時,會在一個叫 foo.c 的實現文件中,定義出 foo_object 結構,裏面有一些 foo_dosomething 所需的數據成員。 

可是,以上還不能知足要求。由於,咱們會有不一樣的數據,他們只是表現出 foo_object 某些方面的特性。對於不一樣的數據,它們在 dosomething 時,實際所作的操做也有所區別。這時,咱們須要定義出一個接口,供 foo.c 內部使用。那麼,以上的頭文件就須要作一些修改,把接口 i_foo 的定義加進去,並修改 create 函數。 

struct i_foo { 
void (*foobar)(void *); 
}; 

struct foo_object * foo_create(struct i_foo *iface, void *data);這裏稍作解釋。i_foo 是供 foo_dosomething 內部使用的一組接口。構造 foo_object 時,咱們把一個外部數據 data 和爲 foo_object 相關特性定義出的 i_foo 接口捆綁在一塊兒,傳入構造函數 foo_create 。通常,我還會會每一個符合 foo_object 特性的對象實現一個方法來獲得對應的 i_foo ,如: 

struct foobar; 

struct i_foo * foobar_foo(void); 
struct foobar * foobar_create(void); 
void foobar_release(struct foobar *);建立一個 foo_object 對象的代碼看起來是這樣: 

struct foobar *foobar = foobar_create(); 
struct foo_object * fobj = foo_create(foobar_foo() , foobar);struct foo_object 的定義中,必然要記錄 i_foo 的接口指針和 data 數據指針。從 C++ 的觀點看,foo_object 是基類,它也會有一些基類成員和非虛的成員函數。具體的派生類在實現時,改寫了虛表 i_foo 的內容(重載了虛函數)。data 數據是在對基類 foo_object 繼承時擴展的數據成員。但,在這裏,咱們使用了組合的方式來擴展成員。這增長了一層間接性,但提供了更低的耦合。其中的優劣暫且不討論了。 

一般看起來會是這樣: 

struct foo_object { 
struct i_foo * vtbl; 
void * data; 
void * others; 
}; 

void 
foo_dosomething(struct foo_object *fobj) 
{ 
fobj->vtbl->foobar(fobj->data); 
// do something else 
}此處還有另外一個問題:data 的生命期該由誰來負責? 

生命期管理是個很大的課題。也是大多數使用 C/C++ 開發的軟件的複雜度重要來源。我我的傾向於把生命期管理獨立出來解決。因此 foo_object 模塊通常並不負責 data 的生命期管理。它只負責 struct foo_object 的資源釋放。 

本身經營本身,是個人 C 語言軟件開發的觀點之一。我傾向於採用混合語言編程來更好的解決這個問題。好比 C 和 Lua ,或者 C 和 C++ 。若是不採用混合語言編程,那麼也能夠在以後,增長一個一樣用 C 語言編寫的層次來管理。這個話題,留到下次來說。 

剝離出生命期管理,代碼量能夠減小不少,也不容易犯錯誤。 

ps. C 語言是一個弱類型的語言。至少比 C++ 要弱一些。這表如今: 

void * 在 C 語言中能夠指代任意數據指針。你能夠把任意數據指針賦值給一個 void * 變量,也能夠把一個 void * 變量賦給特定的指針類型變量。(這在 C++ 中不推薦,並會被編譯器警告) 

C 語言中的函數指針也比較有趣。一般,不一樣類型的函數指針相互賦值是會引發編譯器警告的(類型不一樣)。固然,咱們能夠用一個 void * 來解決問題。但有時候,咱們指望讓類型檢查嚴格一些,至少咱們不但願把一個數據指針賦值給一個函數指針。但但願編譯器不要理會函數參數的差別。 

在 C 語言中,void (*foo)() 能夠被賦予任意返回 void 的函數指針。即,你能夠把 void foobar(int) 的地址賦予前面的 foo 變量(這是由 C 標準的參數傳遞規則保證的)。 

因此,在 C 語言編程中須要注意。若是你想定義一個不接受參數的函數,並讓編譯器幫你檢查出那些錯誤的多傳遞了參數的語句。你必須在 .h 文件中嚴格定義 void foo(void) 以示 foo 函數不接受參數。 

在傳統的 C 語言中,對結構初始化須要很是當心。這裏,咱們的 i_foo 接口定義就使用了 C 裏的結構。這須要很是謹慎當心。(沒有 C++ 編譯器幫你作這件事) 

C99 新增長的語法加強了這點(在初始化結構時,能夠不依賴次序,而寫出成員的名字)。值得采用。
相關文章
相關標籤/搜索