如何用C語言實現(xiàn)OOP
我們知道面向對象的三大特性分別是:封裝、繼承、多態(tài)。很多語言例如:C++ 和 Java 等都是面向對象的編程語言,而我們通常說 C 是面向過程的語言,那么是否可以用 C 實現(xiàn)簡單的面向對象呢?答案是肯定的!C 有一種數(shù)據(jù)結構叫做結構體(struct)和函數(shù)指針,使用結構體和函數(shù)指針便可實現(xiàn)面向對象的三大特性。
C語言實現(xiàn)封裝
首先我們先簡單了解一下什么是封裝,簡單的說封裝就是類將屬性和屬性操作封裝在一個不可分割的獨立實體,只提供對外訪問屬性的操作方法。用戶無需知道對象的內部實現(xiàn)細節(jié),但能通過對外提供的接口訪問內部屬性數(shù)據(jù)。
由于 C 沒有像 C++ 一樣可以設置類內部數(shù)據(jù)的訪問權限,所以 C 的屬性和操作都是公有的,但是我們可以用 C 的函數(shù)指針模仿 C++ 實現(xiàn)簡單的封裝。后續(xù)的多態(tài)實現(xiàn)也用到 C 的函數(shù)指針。我們知道 C++ 所有的非靜態(tài)成員函數(shù)會有一個 this 指針,通過 this 指針可以訪問所有的成員變量和成員函數(shù)。而 C 可以通過傳入成員變量所在的結構體指針,達到 C++ this 指針的效果?,F(xiàn)在我們構建一個簡單的 Bird 類,Bird 有名稱(Name),顏色(Color),重量(Weight),棲居地(Addr)屬性和對應的操作方法。
enum{
INVALID_COLOR = 0,
RED = 1,
GREEN = 2,
};
struct Bird{
char *Name;
char *Addr;
int Color;
int Weight;
void (*SetName)(struct Bird *Bird, char *Name);
void (*SetAddr)(struct Bird *Bird, char *Addr);
void (*SetColor)(struct Bird *Bird, const int Color);
void (*SetWeight)(struct Bird *Bird, const int Weight);
char *(*GetName)(struct Bird *Bird);
int (*GetColor)(struct Bird *Bird);
};
代碼中 SetName, SetAddr, SetColor, SetWeight 函數(shù)指針相當于 C++ 類的成員函數(shù),是 Bird 類內部數(shù)據(jù)與外部交互的接口。在 C++ 中 this 指針是在編譯的時候由編譯器自己加上去的,所以每個接口都有一個 struct Bird* 類型形參,該指針的作用相當于 C++ 的 this 指針,通過該指針可以訪問類內部的所有成員變量和成員函數(shù)。接下來就需要實現(xiàn)具體的函數(shù),再在執(zhí)行構造函數(shù)時手動將函數(shù)指針指向最終的實現(xiàn)函數(shù)。
具體成員函數(shù)實現(xiàn)源碼如下:
void SetBirdName(struct Bird *Bird, const char * const Name)
{
if(Bird == NULL){
return;
}
Bird->Name = Name;
}
void SetBirdAddr(struct Bird *Bird, const char * const Addr)
{
if(Bird == NULL){
return;
}
Bird->Addr = Addr;
}
void SetBirdColor(struct Bird *Bird, const int Color)
{
if(Bird == NULL){
return;
}
Bird->Color = Color;
}
void SetBirdWeight(struct Bird *Bird, const int Weight)
{
if(Bird == NULL){
return;
}
Bird->Weight = Weight;
}
char *GetName(struct Bird *Bird)
{
if(Bird == NULL){
return NULL;
}
return Bird->Name;
}
int GetColor(struct Bird *Bird)
{
if(Bird == NULL){
return INVALID_COLOR;
}
return Bird->Color;
}
那么 C++ 的構造函數(shù)和析構函數(shù)如何使用 C 來實現(xiàn)呢?構造函數(shù)在創(chuàng)建一個對象實例時自動調用,析構函數(shù)則在銷毀對象實例時自動調用,實際上 C++ 的構造函數(shù)和析構函數(shù)在編譯期間由編譯器插入到源碼中。但是編譯 C 源碼時,編譯器沒有這種操作,需要我們手動去調用構造函數(shù)和析構函數(shù)。而且在調用 C 的構造函數(shù)時,需要我們手動將函數(shù)指針指向最終的實現(xiàn)函數(shù)。在調用 C 的析構函數(shù)時,需要我們手動的釋放資源。
構造函數(shù)源碼如下:
void BirdInit(struct Bird *Bird)
{
if(Bird == NULL){
return;
}
Bird->SetAddr = SetBirdAddr;
Bird->SetColor = SetBirdColor;
Bird->SetName = SetBirdName;
Bird->SetWeight = SetBirdWeight;
Bird->GetColor = GetColor;
Bird->GetName = GetName;
Bird->SetAddr(Bird, "Guangzhou");
Bird->SetColor(Bird, RED);
Bird->SetWeight(Bird, 10);
Bird->SetName(Bird, "Xiaoming");
}
析構函數(shù)源碼如下:
void BirdDeinit(struct Bird *Bird)
{
if(Bird == NULL){
return;
}
memset(Bird, 0, sizeof(struct Bird));
}
至此,C 如何實現(xiàn)面向對象的封裝特性已講完,下面看看我們實際運用的效果。
int main(int argc, char *argv[])
{
struct Bird *Bird = (struct Bird *)malloc(sizeof(struct Bird));
BirdInit(Bird); //調用構造函數(shù)
Bird->SetName(Bird, "Lihua"); //更改Bird的名稱
Bird->SetColor(Bird, GREEN); //更改Bird的顏色
printf("Bird name: %s, color: %d\n", Bird->GetName(Bird), Bird->GetColor(Bird));
BirdDeinit(Bird); //調用析構函數(shù)
free(Bird);
Bird = NULL;
return 0;
}
在 mac 上編譯執(zhí)行結果如下:
C語言實現(xiàn)繼承
我們繼續(xù)簡單了解一下什么是繼承,繼承就是使用已存在的類的定義基礎建立新類的技術。新類可以增加新的數(shù)據(jù)和方法,但不能選擇性的繼承父類。而且繼承是“is a”的關系,比如老鷹是鳥,但是你不能說鳥就是老鷹,因為還有其他鳥類動物也是鳥。因為 C 語言本身的限制,只能用 C 實現(xiàn) C++ 的公有繼承(除非使用 C 開發(fā)新的計算機語言)。在 C++ 使用公有繼承(沒有虛函數(shù)),編譯器會在編譯期間將父類的成員變量插入到子類中,通常是按照順序插入(具體視編譯器決定)。說到這里,我們很容易就能想到如何使用 C 語言實現(xiàn) C++ 的公有繼承了(不帶虛函數(shù)),就是在子類中定義一個父類的成員變量,而且父類的成員變量只能放在最開始的位置。依舊使用上面建立的 Bird 類作為父類,我們建立一個新的子類Eagle(老鷹),老鷹可以飛翔也吃肉(其他鳥類不一定會飛和吃肉),所以我們建立的子類如下:
struct Eagle
{
struct Bird Bird;
BOOL Fly;
BOOL EateMeat;
void (*CanFly)(struct Bird *Bird, const BOOL Fly);
void (*CanEateMeat)(struct Bird *Bird, const BOOL EateMeat);
BOOL (*IsFly)(struct Bird *Bird);
BOOL (*IsEateMeat)(struct Bird *Bird);
};
extern void EagleInit(struct Eagle *Eagle);
extern void EagleDeinit(struct Eagle *Eagle);
在 C++ 中 new 一個子類對象,構造函數(shù)的調用順序則是從繼承鏈的最頂端到最底端,依次調用構造函數(shù)。而 delete 一個子類對象時,析構函數(shù)的調用順序則是從繼承鏈的最底端到最頂端依次調用。按照這個模式,我們子類(Eagle)的構造函數(shù)和析構函數(shù)就很容易寫了,構造函數(shù)和析構函數(shù)源碼如下所示:
void EagleInit(struct Eagle *Eagle)
{
if(Eagle == NULL){
return;
}
BirdInit(&Eagle->Bird);
Eagle->CanFly = CanFly;
Eagle->CanEateMeat = CanEateMeat;
Eagle->IsFly = IsFly;
Eagle->IsEateMeat = IsEateMeat;
Eagle->CanFly((struct Bird *)Eagle, TRUE);
Eagle->CanEateMeat((struct Bird *)Eagle, TRUE);
}
void EagleDeinit(struct Eagle *Eagle)
{
if(Eagle == NULL){
return;
}
memset(Eagle, 0, sizeof(struct Eagle));
BirdDeinit(&Eagle->Bird);
}
在子類的構造函數(shù) EagleInit 中先調用父類的構造函數(shù) BirdInit,在子類的析構函數(shù)中先釋放子類的資源再調用父類的析構函數(shù) BirdDeinit。至此,我們完成了 C 語言實現(xiàn) C++ 的公有繼承(不帶虛函數(shù))。
C語言實現(xiàn)多態(tài)
所謂多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發(fā)出的方法調用在編程時并不確定,而是在程序運行期間才確定,即一個引用變量倒底會指向哪個類的實例對象,該引用變量發(fā)出的方法調用到底是哪個類中實現(xiàn)的方法,必須在由程序運行期間才能決定。因為在程序運行時才確定具體的類,這樣,不用修改源程序代碼,就可以讓引用變量綁定到各種不同的類實現(xiàn)上,從而導致該引用調用的具體方法隨之改變,即不修改程序代碼就可以改變程序運行時所綁定的具體代碼,讓程序可以選擇多個運行狀態(tài),這就是多態(tài)性。
老慣例,我們來看一下 C++ 是如何實現(xiàn)運行時多態(tài)的。C++ 的運行時多態(tài)是用虛函數(shù)實現(xiàn)的。在 C++ 中有虛函數(shù)的類存在一個虛函數(shù)表指針 vptr 指向一個虛函數(shù)表。而虛函數(shù)表則存放著,虛函數(shù)對應的實現(xiàn)函數(shù)。我們用 C 語言實現(xiàn)類似于 C++ 的多態(tài)性,可以模仿 C++ 用創(chuàng)建虛函數(shù)表和在類中定義一個虛函數(shù)表指針實現(xiàn)。但是我們一般不用這樣實現(xiàn),因為這種實現(xiàn)方式有幾個缺點:
添加和刪除一個虛函數(shù)時,虛函數(shù)表大小要隨著改變,函數(shù)在虛函數(shù)表里面存放的位置也要隨著改變。 會增加類的內存占用空間。 多層間接訪問虛函數(shù),增加了運行開銷和系統(tǒng)復雜度。
通過仔細觀察 C 語言實現(xiàn)繼承我們可以知道,父類的成員變量會全部放入到子類內存空間中。那么我們是否可以把虛函數(shù)表直接放在類中呢?這個時候函數(shù)指針又發(fā)揮作用了!我們可以把多個函數(shù)指針放在父類中,就可以在之類構造函數(shù)中直接將父類里的函數(shù)指針重新指向新的實現(xiàn)函數(shù),這就實現(xiàn)了我們想要的多態(tài)性!因為鳥類都會下蛋,所以我們定義一個下蛋的函數(shù) LayEggs。
Bird 類源碼如下:
struct Bird{
char *Name;
char *Addr;
int Color;
int Weight;
void (*SetName)(struct Bird *Bird, char *Name);
void (*SetAddr)(struct Bird *Bird, char *Addr);
void (*SetColor)(struct Bird *Bird, const int Color);
void (*SetWeight)(struct Bird *Bird, const int Weight);
char *(*GetName)(struct Bird *Bird);
int (*GetColor)(struct Bird *Bird);
void (*LayEggs)(struct Bird *Bird);
};
extern void BirdInit(struct Bird *Bird);
extern void BirdDeinit(struct Bird *Bird);
Bird 類構造函數(shù)源碼如下:
static void LayEggs(struct Bird *Bird)
{
if(Bird == NULL){
return;
}
printf("bird lay eggs\n");
}
void BirdInit(struct Bird *Bird)
{
if(Bird == NULL){
return;
}
Bird->SetAddr = SetBirdAddr;
Bird->SetColor = SetBirdColor;
Bird->SetName = SetBirdName;
Bird->SetWeight = SetBirdWeight;
Bird->GetColor = GetColor;
Bird->GetName = GetName;
Bird->LayEggs = LayEggs;
Bird->SetAddr(Bird, "Guangzhou");
Bird->SetColor(Bird, RED);
Bird->SetWeight(Bird, 10);
Bird->SetName(Bird, "Xiaoming");
}
Eagle 類構造函數(shù)源碼如下:
static void LayEggs(struct Bird *Bird)
{
if(Bird == NULL){
return;
}
printf("Eagle lay eggs\n");
}
void EagleInit(struct Eagle *Eagle)
{
if(Eagle == NULL){
return;
}
BirdInit(&Eagle->Bird);
Eagle->CanFly = CanFly;
Eagle->CanEateMeat = CanEateMeat;
Eagle->IsFly = IsFly;
Eagle->IsEateMeat = IsEateMeat;
Eagle->Bird.LayEggs = LayEggs;
Eagle->CanFly((struct Bird *)Eagle, TRUE);
Eagle->CanEateMeat((struct Bird *)Eagle, TRUE);
}
在 Eagle 構造函數(shù)中,我們將父類的函數(shù)指針指向了新的 LayEggs 函數(shù),在程序運行期間就會調用新的 LayEggs 函數(shù)。我們修改 main函數(shù),觀察運行結果。
main 函數(shù)修改如下:
int main(int argc, char *argv[])
{
struct Bird *Bird = (struct Bird *)malloc(sizeof(struct Bird));
BirdInit(Bird); //調用構造函數(shù)
Bird->SetName(Bird, "Lihua"); //更改Bird的名稱
Bird->SetColor(Bird, GREEN); //更改Bird的顏色
printf("Bird name: %s, color: %d\n", Bird->GetName(Bird), Bird->GetColor(Bird));
Bird->LayEggs(Bird);
BirdDeinit(Bird); //調用析構函數(shù)
free(Bird);
Bird = NULL;
Bird = (struct Bird *)malloc(sizeof(struct Eagle));
struct Eagle *Eagle = (struct Eagle *)Bird;
EagleInit((struct Eagle *)Bird);
Bird->SetName(Bird, "Tanmeimei");
Bird->SetAddr(Bird, "Shanghai");
Bird->SetColor(Bird, RED);
printf("Eagle is fly: %d, is eate meat: %d\n", Eagle->IsFly((struct Bird *)Eagle), Eagle->IsEateMeat((struct Bird *)Eagle));
printf("Eagle name is: %s,\n", Bird->GetName(Bird));
Bird->LayEggs(Bird);
EagleDeinit((struct Eagle *)Bird);
free(Bird);
Bird = NULL;
return 0;
}
運行結果如下:
到目前為止,我們已經用C語言實現(xiàn)了封裝、繼承和多態(tài)三大面向對象特性!
項目源碼:https://gitee.com/C-Cplusplusyiyezhiqiu/wechat-official-account.git
