最近在用 C 做項目,之前用慣了 C++ ,轉回頭來用C 還真有點不適應。 C++ 語言中自帶面向對象支持,如封裝、繼承、多態等面向對象的基本特征。 C 原本是面向過程的語言,自身沒有內建這些特性,但我們還是可以利用 C 語言本身已有的特性來實現面向對象的一些基本特征。接下來我們就一一來細說封裝、繼承、多態、純虛類等面向對象特性在 C 語言中如何實現,並且給出實例。
這篇文章中我們先說封裝和繼承。
先來看封裝。
所謂封裝,通俗地說,就是一個姑娘化了妝,只給你看她想讓你看的那一面,至於裡面是否刮了骨、墊了東西,不給你看。說到封裝就得說隱藏,這是對兄弟概念;其實我理解隱藏是更深的封裝,完全不給你看見,而封裝可能是猶抱琵琶半遮面。封裝在 C++ 語言中有 protected 、 private 關鍵字在語言層面上支持,而 C 語言中沒有這些。 C 有結構體( struct ),其實可以實現封裝和隱藏。
在 QT 中,為了更好的隱藏一個類的具體實現,一般是一個公開頭文件、一個私有頭文件,私有頭文件中定義實現的內部細節,公開頭文件中定義開放給客戶程序員的接口和公共數據。看看 QObject (qobject.h ),對應有一個 QObjectPrivate (qobject_p.h ) ,其他的也類似。而代碼框架如下:
QObject{ public: xxx xxx private: QObjectPrivate * priv; };我們在 C 語言中完全可以用同樣的方法來實現封裝和隱藏,只不過是放在結構體中而已。代碼框架如下:
struct st_abc_private; struct st_abc { int a; xxx; void (*xyz_func)(struct st_abc*); struct st_abc_private * priv; };上面的代碼,我們只前向聲明結構體 struct st_abc_private ,沒人知道它裡面具體是什麼東西。假如 struct st_abc 對應的頭文件是 abc.h ,那麼把 st_abc_private 的聲明放在 abc_p.h 中,abc.c 文件包含 abc_p.h ,那麼在實現 struct st_abc 的函數指針 xyz_func 時如何使用 struct st_abc_private ,客戶程序員根本無須知道。
這樣做的好處是顯而易見的,除了預定義好的接口,客戶程序員完全不需要知道實現細節,即便實現經過重構完全重來,客戶程序員也不需要關注,甚至相應的模塊連重新編譯都不要——因為 abc.h 自始至終都沒變過。
上面代碼有個問題,客戶程序員如何得到 struct st_abc 的一個實例,他不知道 struct st_abc_private 如何實現的呀。 C 中沒有構造函數,只好我們自己提供了:我們可以在 abc.h 中聲明一個類似構造函數的函數來生成 struct st_abc 的實例,名字就叫作 new_abc() ,函數原型如下:
struct st_abc * new_abc();至於實現,我們放在 abc.c 中,客戶程序員不需要知道。相應的,還有個類似析構函數的函數,原型如下:
void delete_abc(struct st_abc *);
什麼是繼承?在面向對象層面上不講了,只說語法層面。語法層面上講,繼承就是派生類擁有父類的數據、方法,又添了點自己的東西,所謂子承父業,發揚光大。在 C 語言中可以用結構體的包含來實現繼承關系。代碼框架如下:
struct st_base{ xxx; }; struct st_derived{ struct sb_base base; yyy; };代碼上就是這麼簡單,不過有一點要注意:第一點就是派生類(結構體)中一定要把父類類型的成員放在第一個。
繼承在語法層面上看,有數據成員、函數,數據成員通過上面的方法自動就“繼承”了,至於函數,在結構體中是函數指針,其實在 C 語言中也是一個數據成員,是個指針而已,也會自動“繼承”。之所以還要在這裡列出來說明,是因為面向對象語言中繼承包含的幾個特性:覆蓋、重載、虛函數。這有點兒麻煩,不過都可以實現。
覆蓋,就是說基類和派生類中可以有相同簽名的函數(名字、返回值、參數完全一樣),派生類的中的函數會覆蓋基類的同簽名函數。而 C 語言中,也可以實現,base 和 derived 可以實現同名的函數指針,不過容易引起混淆,不到不得已不建議這麼干。
重載,函數重載,指函數名字一樣,參數個數、類型不一樣的函數聲明和實現。由於 C 編譯器的緣故,不支持。不過這個影響不大。
虛函數是實現多態的關鍵,可以在結構體中使用函數指針來表達,這個完全可以,我們接下來會細講。
好了,第一篇就到這裡,有時間會往下續。