使用C语言实现面相对对象三大特性
前言
在学习C++中或者其他的面对对象语言的中,我们常常使用面向对象的思维来编写代码。的确,在语言的语法上,C++确实存在语法规则是适应于面向对象的开发,联想到C++很类似的C语言,它是否也可以实现面向对象的方式来进行编程,答案是确定,本章笔记用于记录在使用C语言进行面向对象思维开发的一些过程的思考。
面向对象的思维
说起面向对象,许多人脑海中立即浮现面向对象的三个特性,封装,继承和多态,当然还有那个程序员的老梗。咳咳咳
C语言实现封装特性
在说明封装的特点,可以先思考这两个问题。
首先什么是封装?
封装给我们编写代码带来什么方便?
简单的说封装就是将类将属性和对属性的操作封装在一个不可分割的独立实体当中,对外提供访问属性的接口或者方法,用户在使用这个实体时,不用知道具体内部的方法是怎么实现,只要知道这个实体可以提供什么操作即可。
在C++中类的内部成员函数有三种访问的权限,pubilic,private,protocol。三种不同关键词来限制对类的内部的成员的访问,但是在C语言中,不存在这三个关键词,所以在C语言中的封装的内部的成员都是公开的。在C++成员中还存在一个特殊的指针this,通过this指针可以访问成员变量和成员函数,当然在C语言中可以使用在指向结构体本身的指针来完后和this指针相同的功能。
这样说可能还是有点抽象,现在咱们通过实际的代码来实现C语言的封装特性。假设有一个对象animal类,animal类的内部成员有名字(name)、重量(weight)、颜色(color)、高度(height)和栖息地(addr)这几个属性。
enum
{
BLACK = 0,
RED = 1,
GREEN = 2,
};
struct animal
{
/* 相当类中的成员函数 */
char *name;
char* addr;
int color;
int weight;
int height;
/* 成员函数 */
void (*p_set_name)(struct animal* animal, char* const name);
void (*p_set_addr)(struct animal* animal, char* addr);
void (*p_set_color)(struct animal* animal, const int color);
void (*p_set_weight)(struct animal* animal, const int weight);
void (*p_set_height)(struct animal* animal, const int height);
char* (*p_get_name)(struct animal* animal);
int (*p_get_color)(struct animal* animal);
};
其中类似void (p_set_name)(struct animal animal, char* const name);这种形式的相当于的C++成员函数,在这里我们使用C语言的指针函数来代替了,有的同学可能会疑惑为什么使用函数指针来指向一个函数,这是因为使用函数指针可以将结构体和函数相连接实现封装的目的,并且细心的同学估计还发现每个函数指针的参数列表中还存在一个struct *animal,这个就相当C++在给成员函数中隐藏了this指针。在实现了类成员函数和成员变量的定义,接下去就一个一个实现成员函数实现。
void set_name(struct animal* animal, const char* const name)
{
if (animal == NULL)
{
return;
}
animal->name = name;
}
void set_addr(struct animal* animal, char* addr)
{
if (animal == NULL)
{
return;;
}
animal->addr = addr;
}
void set_color(struct animal* animal, const int color)
{
if (animal == NULL)
{
return;
}
animal->color = color;
}
void set_weight(struct animal* animal, const int weight)
{
if (animal == NULL)
{
return;
}
animal->weight = weight;
}
void set_height(struct animal* animal, const int height)
{
if (animal == NULL)
{
return;
}
animal->height = height;
}
char* get_name(struct animal* animal)
{
if (animal == NULL)
{
return NULL;
}
return animal->name;
}
const int p_get_color(struct animal* animal)
{
if (animal == NULL)
{
return 0;
}
return animal->color;
}
到了这一步后,我们有类的成员函数,也有类的成员函数,但是目前还没有将会两者给联系联系在一起。在C++中,我们根据在类中定义或者使用:号来可以联系成员函数的声明和定义。但是在C语言中,我们必须手动进行声明和定义的联系,我们可以通过析构函数,在C中可以简单的理解为类的初始化函数,具体实现如下所示。
/* 相当于类中的析构函数 */
void animal_init(struct animal* animal)
{
if (animal == NULL)
{
return;
}
/* 将函数进行装载 */
animal->p_set_name = set_name;
animal->p_set_addr = set_addr;
animal->p_set_color = set_color;
animal->p_set_weight = set_weight;
animal->p_set_height = set_height;
animal->p_get_color = set_color;
animal->p_get_name = get_name;
/* 初始化一些初值 */
animal->p_set_name(animal, "xiaohuang");
animal->p_set_addr(animal, "china");
animal->p_set_color(animal, 1);
animal->p_set_weight(animal, 100);
animal->p_set_height(animal, 20);
}
相同类中的析构函数可以简单理解为一个手动的内存释放函数,在类不需要使用通过人手动的释放,目前我还没有想到什么办法自动释放,下面为一个本例子中析构函数:
void animal_destroy(struct animal* animal)
{
if (animal == NULL)
{
return;
}
memset(animal, 0, sizeof(struct animal));
}
到这里使用C语言实现OOP中封装特性的方案已经讲完,接下去就是使用C语言实现OPP中继承特性。
C语言实现继承特性
在实现C语言实现继承特性之前,先说下继承的特点。简单说,继承就是在原本的类的基础上建立子类/派生类的技术,新类拥有父类/基本的成员函数和成员变量,这样来大大的实现代码的复用。需要注意的是:基类可以在父类的基础上增加新的成员函数和新的成员函数,但是不能选择性的继承父类的成员函数和成员方法,但是父类可以对成员函数和成员变量进行public等关键字进行修饰限制子类的对父类的访问。正如C语言在封装中,由于C语言的本身的特点,只能实现封装的公共函数和公共成员。在继承技术中,C语言只能实现公共继承(没有虚函数这种概念),我们继续从上面的例子中实现C语言的继承,我们以动物为父类来实现其子类飞行动物(fly_animal),假设其中有两个成员函数is_fly(能否飞行),is_eat_meat(是否吃肉),所以我们根据这两个特点实现fly_animal这个类。
struct fly_animal
{
struct animal animal;
bool is_fly;
bool is_eat_meat;
void (*p_can_fly)(struct animal* animal, const bool fly_state);
void (*p_can_eat_meat)(struct animal* animal, const bool eat_meat);
bool (*p_is_fly)(struct animal* animal);
bool (*p_is_eat_meat)(struct animal* animal);
};
extern void fly_animal_init(struct fly_animal* fly_animal);
extern void fly_animal_destroy(struct fly_animal* fly_animal);
在C++中,我们使用new关键词建立一个子类对象时,构造函数调用的顺序是从继承链的最顶层慢慢一层一层构造到最底层,依次使用构造函数。而delete子类对象时,析构函数的调用顺序正相反。根据这个模式,就可以实现子类构造函数和析构函数。
void fly_animal_init(struct fly_animal* fly_animal)
{
if (fly_animal == NULL)
{
return;
}
// 先进行父类的构造函数
animal_init(&fly_animal->animal);
fly_animal->is_fly = can_fly;
fly_animal->p_is_eat_meat = is_eat_meat;
fly_animal->is_fly = TRUE;
fly_animal->is_eat_meat = TRUE;
}
同理析构函数也是同样的方法
void fly_animal_destroy(struct fly_animal* fly_animal)
{
if (fly_animal == NULL)
{
return;
}
memset(fly_animal, 0, sizeof(fly_animal));
animal_destroy((fly_animal->animal)
}
C语言实现多态特性
在使用C实现多态特性之前,咱们先多OPP中的多态特性进行分析。所谓多态就是引用的变量所指向的具体类型和使用该引用发出的方法在编程时是不确定,而是在程序运行器才可以确定。这样不用修改源代码就可以一个变量实现不用类的实现,如果说继承是大大实现代码的复用,而多态是实现了代码框架的复用。在C++中运行多态是使用虚函数实现的,在C++中有虚函数的类中存在一个虚函数指针指向的虚函数表,而虚函数表则放置虚函数对应的实现函数,我们可以使用C语言来模仿C++一样的方式来创造一个虚函数表,但是一般不这么做来实现多态,因为这么存在以下几个缺点:
- 在我们添加或者删除一个虚函数时,虚函数表的大小会发生改变,而存放在虚函数表中函数存在的 位置也会随着发生改变。
- 这种方式增加了类的内存占用
- 多层间接的访问虚函数,会提高了运行时间和空间开销和代码复杂度。
在上一节实现继承的代码中,父类的成员变量会全部放入子类的空间中,那么我们是否可以把虚函数直接放入类中?答案当然是可以,不然我白打这么废话干嘛。我们可以使用函数指针来实现,我们定义一个测试多态的函数sleep()
animal类源码如下:
enum
{
BLACK = 0,
RED = 1,
GREEN = 2,
};
struct animal
{
/* 相当类中的成员函数 */
char *name;
char* addr;
int color;
int weight;
int height;
/* 成员函数 */
void (*p_set_name)(struct animal* animal, char* const name);
void (*p_set_addr)(struct animal* animal, char* addr);
void (*p_set_color)(struct animal* animal, const int color);
void (*p_set_weight)(struct animal* animal, const int weight);
void (*p_set_height)(struct animal* animal, const int height);
void (*p_sleep)(struct animal* animal)
char* (*p_get_name)(struct animal* animal);
int (*p_get_color)(struct animal* animal);
};
animal类的构造函数如下:
void sleep(struct animal *animal)
{
if(animal == NULL){
return;
}
printf("animal_sleep\n");
}
/* 相当于类中的析构函数 */
void animal_init(struct animal* animal)
{
if (animal == NULL)
{
return;
}
/* 将函数进行装载 */
animal->p_set_name = set_name;
animal->p_set_addr = set_addr;
animal->p_set_color = set_color;
animal->p_set_weight = set_weight;
animal->p_set_height = set_height;
animal->p_get_color = set_color;
animal->p_get_name = get_name;
animal->p_sleep = sleep;
/* 初始化一些初值 */
animal->p_set_name(animal, "xiaohuang");
animal->p_set_addr(animal, "china");
animal->p_set_color(animal, 1);
animal->p_set_weight(animal, 100);
animal->p_set_height(animal, 20);
}
在fly_animal的构造函数中,我们将父类的指针指向新的sleep函数,在程序运行期就会指向新的sleep函数。test的主函数如下
main,c函数如下
int main(void)
{
struct animal *animal = (struct animal*)malloc(sizeof(animal));
// 调用构造函数
animal_init(animal);
animal->sleep(animal);
// 调用析构函数
animal_destory(animal);
free(animal);
animal = NULL;
struct animal *animal = (struct animal*)malloc(sizeof(animal));
struct fly_animal *fly_animal = (struct fly_aniaml*)animal;
fly_animal_init((struct fly_animal*)animal);
fly_animal->sleep;
}
到目前为止,咱们就是通过C语言将C++中的OOP的三大特征,封装、继承、多态实现。通过C代码我们可以知道C++编译器在后面给咱们做了多少手脚,背着咱们做了多少不可告人的事情。