如何用C语言实现OOP
我们知道面向对象的三大特性分别是:封装、继承、多态。很多语言例如:C++ 和 Java 等都是面向对象的编程语言,而我们通常说 C 是面向过程的语言,那么是否可以用 C 实现简单的面向对象呢?答案是肯定的!C 有一种数据结构叫做结构体(struct
)和函数指针,使用结构体和函数指针便可实现面向对象的三大特性。
C语言实现封装
首先我们先简单了解一下什么是封装,简单的说封装就是类将属性和属性操作封装在一个不可分割的独立实体,只提供对外访问属性的操作方法。用户无需知道对象的内部实现细节,但能通过对外提供的接口访问内部属性数据。
由于 C 没有像 C++ 一样可以设置类内部数据的访问权限,所以 C 的属性和操作都是公有的,但是我们可以用 C 的函数指针模仿 C++ 实现简单的封装。后续的多态实现也用到 C 的函数指针。我们知道 C++ 所有的非静态成员函数会有一个 this 指针,通过 this 指针可以访问所有的成员变量和成员函数。而 C 可以通过传入成员变量所在的结构体指针,达到 C++ this 指针的效果。现在我们构建一个简单的 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 函数指针相当于 C++ 类的成员函数,是 Bird 类内部数据与外部交互的接口。在 C++ 中 this 指针是在编译的时候由编译器自己加上去的,所以每个接口都有一个 struct Bird* 类型形参,该指针的作用相当于 C++ 的 this 指针,通过该指针可以访问类内部的所有成员变量和成员函数。接下来就需要实现具体的函数,再在执行构造函数时手动将函数指针指向最终的实现函数。
具体成员函数实现源码如下:
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++ 的构造函数和析构函数如何使用 C 来实现呢?构造函数在创建一个对象实例时自动调用,析构函数则在销毁对象实例时自动调用,实际上 C++ 的构造函数和析构函数在编译期间由编译器插入到源码中。但是编译 C 源码时,编译器没有这种操作,需要我们手动去调用构造函数和析构函数。而且在调用 C 的构造函数时,需要我们手动将函数指针指向最终的实现函数。在调用 C 的析构函数时,需要我们手动的释放资源。
构造函数源码如下:
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");
}
析构函数源码如下:
void BirdDeinit(struct Bird *Bird)
{
if(Bird == NULL){
return;
}
memset(Bird, 0, sizeof(struct Bird));
}
至此,C 如何实现面向对象的封装特性已讲完,下面看看我们实际运用的效果。
int main(int argc, char *argv[])
{
struct Bird *Bird = (struct Bird *)malloc(sizeof(struct Bird));
BirdInit(Bird); //调用构造函数
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); //调用析构函数
free(Bird);
Bird = NULL;
return 0;
}
在 mac 上编译执行结果如下:
C语言实现继承
我们继续简单了解一下什么是继承,继承就是使用已存在的类的定义基础建立新类的技术。新类可以增加新的数据和方法,但不能选择性的继承父类。而且继承是“is a”的关系,比如老鹰是鸟,但是你不能说鸟就是老鹰,因为还有其他鸟类动物也是鸟。因为 C 语言本身的限制,只能用 C 实现 C++ 的公有继承(除非使用 C 开发新的计算机语言)。在 C++ 使用公有继承(没有虚函数),编译器会在编译期间将父类的成员变量插入到子类中,通常是按照顺序插入(具体视编译器决定)。说到这里,我们很容易就能想到如何使用 C 语言实现 C++ 的公有继承了(不带虚函数),就是在子类中定义一个父类的成员变量,而且父类的成员变量只能放在最开始的位置。依旧使用上面建立的 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 一个子类对象,构造函数的调用顺序则是从继承链的最顶端到最底端,依次调用构造函数。而 delete 一个子类对象时,析构函数的调用顺序则是从继承链的最底端到最顶端依次调用。按照这个模式,我们子类(Eagle)的构造函数和析构函数就很容易写了,构造函数和析构函数源码如下所示:
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);
}
在子类的构造函数 EagleInit 中先调用父类的构造函数 BirdInit,在子类的析构函数中先释放子类的资源再调用父类的析构函数 BirdDeinit。至此,我们完成了 C 语言实现 C++ 的公有继承(不带虚函数)。
C语言实现多态
所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
老惯例,我们来看一下 C++ 是如何实现运行时多态的。C++ 的运行时多态是用虚函数实现的。在 C++ 中有虚函数的类存在一个虚函数表指针 vptr 指向一个虚函数表。而虚函数表则存放着,虚函数对应的实现函数。我们用 C 语言实现类似于 C++ 的多态性,可以模仿 C++ 用创建虚函数表和在类中定义一个虚函数表指针实现。但是我们一般不用这样实现,因为这种实现方式有几个缺点:
- 添加和删除一个虚函数时,虚函数表大小要随着改变,函数在虚函数表里面存放的位置也要随着改变。
- 会增加类的内存占用空间。
- 多层间接访问虚函数,增加了运行开销和系统复杂度。
通过仔细观察 C 语言实现继承我们可以知道,父类的成员变量会全部放入到子类内存空间中。那么我们是否可以把虚函数表直接放在类中呢?这个时候函数指针又发挥作用了!我们可以把多个函数指针放在父类中,就可以在之类构造函数中直接将父类里的函数指针重新指向新的实现函数,这就实现了我们想要的多态性!因为鸟类都会下蛋,所以我们定义一个下蛋的函数 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 类构造函数源码如下:
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 类构造函数源码如下:
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 构造函数中,我们将父类的函数指针指向了新的 LayEggs 函数,在程序运行期间就会调用新的 LayEggs 函数。我们修改 main函数,观察运行结果。
main 函数修改如下:
int main(int argc, char *argv[])
{
struct Bird *Bird = (struct Bird *)malloc(sizeof(struct Bird));
BirdInit(Bird); //调用构造函数
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); //调用析构函数
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语言实现了封装、继承和多态三大面向对象特性!
项目源码:https://gitee.com/C-Cplusplusyiyezhiqiu/wechat-official-account.git