· C语言真的是这个世界上的老古董了,1972年 Dennis MacAlistair Ritchie 创建它至今,虽然做过几次修改,但是它毕竟是面向过程的语言,所以大家使用起来还是很费力的。但是C语言仍然在嵌入式领域占据绝对优势,没有比C语言更快的高级语言了,著名的操作系统Linux就是C语言的最好实例。可以说Linux不被淘汰,C语言就不会过时。
· 后续产生了C++,Java,Python等等各种支持面向对象的语言,而面向对象作为一种先进思想,也深刻影响C语言的编程风格。虽然C语言不是面向对象的语言,但是C语言仍然可以使用面向对象的方式进行编程(虽然比较繁琐)。而大多数语言中的面向对象实现都是依据C语言开发的。本文为大家提供基本的面向对象的C语言实现方式,带大家搞清楚面向对象的实质,希望大家能够在后续的编程中使用。
· 本文中代码的实例的思想都来源于开源代码,本文代码实例在此可免费下载。
1.封装
· 封装就是“物以类聚”,将所有和某个对象相关的内容都放在一起,包括数据和操作函数。绝大多数的面向对象的语言都有关键字 class,但是C语言中没有,我们只能使用结构体 struct 作为封装对象的方法。
· 支持面向对象的语言,可以将相关代码内聚到一个对象中,但是C语言这边做不到,建议将数据放到 struct 中,相关函数也可以以函数指针的方式放入,3个以内的函数可以直接添加,函数指针太多的话就封装一个struct作为操作函数的集合。然后创建一个初始化函数,在函数初始化时对这些操作函数赋值。
· 这里讲一个例子:
struct OrderOperations {
int (*price)(struct Order *this);
};
struct Order {
int quantity; /*这是数据*/
int itemPrice;
struct OrderOperations *orderOp; /*这是操作函数集合*/
};
· 比如为了封装一个数据 quantity,相关的操作则以函数指针的方式赋值进来,如果操作函数比较多,建议也放到一个struct中来,由于C语言没有this可以用,这里将第一个入参都要用这个对象的指针作为第一个参数。这里举一个例子。
#define max(x,y) ((x)>(y)? (x):(y))
#define min(x,y) ((x)<(y)? (x):(y))
static int price(struct Order *this) {
//price is base price - quantity discount + shipping
return this->quantity * this->itemPrice -
max(0, this->quantity - 500) * this->itemPrice * 0.05 +
min(this->quantity * this->itemPrice * 0.1, 100);
}
· 操作函数可以在初始化的时候,要赋值进去,这里建议用指针的方式使用。比如下面创建一个构造函数 alloc_Order 用来初始化这个对象的指针,在初始化的时候后将这个静态的结构 orderOp 赋值进来,如果想做个性化操作,也可以新创建一个构造函数,增加入参来实现。
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 1
static struct OrderOperations orderOp = {
.price = price,
};
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
order->orderOp = &orderOp;
order->quantity = ORDER_DEFAULT_DATA;
order->itemPrice = ORDER_DEFAULT_DATA;
return order;
}
int main(int argc, char *argv[]) {
struct Order* order = alloc_Order();
order->itemPrice = 5;
order->quantity = 2;
printf("order price is %d.\n", order->orderOp->price(order));
}
· 当函数比较多的情况,更可以体现出这种方式的优势:
struct OrderOperations {
int (*price)(struct Order *this);
int (*getBasePrice)(struct Order *this);
int (*getQuantityDiscount)(struct Order *this);
int (*getShipping)(struct Order *this);
};
struct Order {
int quantity; /*这是数据*/
int itemPrice;
struct OrderOperations *orderOp; /*这是操作函数集合*/
};
int getBasePrice(struct Order *this) {
return this->quantity * this->itemPrice;
}
int getQuantityDiscount(struct Order *this) {
return max(0, this->quantity - 500) * this->itemPrice * 0.05;
}
int getShipping(struct Order *this) {
return min(this->quantity * this->itemPrice * 0.1, 100);
}
int getPrice(struct Order *this) {
return this->orderOp->getBasePrice(this) - this->orderOp->getQuantityDiscount(this) + this->orderOp->getShipping(this);
}
struct Order* alloc_Order(void) {
#define ORDER_DEFAULT_DATA 0
static struct OrderOperations orderOp = {
.price = getPrice,
.getBasePrice = getBasePrice,
.getQuantityDiscount = getQuantityDiscount,
.getShipping = getShipping,
};
struct Order* order = (struct Order* )malloc(sizeof(struct Order));
order->orderOp = &orderOp;
order->quantity = ORDER_DEFAULT_DATA;
order->itemPrice = ORDER_DEFAULT_DATA;
return order;
}
int main(int argc, char *argv[]) {
struct Order* order = alloc_Order();/* 申请对象 */
order->itemPrice = 5;
order->quantity = 2;
printf("order price is %d.\n", order->orderOp->price(order));/* 调用对象中的函数 */
}
· 可能有人发现,封装也可以分等级啊,private,protected 和 public这个怎么实现?说实话,在开源代码中,我没有见过将变量和函数设置不同的封装级别,我认为这个封装级别在C语言中实现的意义并不大,毕竟这些级别都是为了限制开发人员后续使用对象时的对其修改。这里给出一个建议,将private的变量使用以“_”为开头,提示后续的人员谨慎修改。
2.继承
· 继承就是将各个不同对象的共性提取,抽象出共同的基类,继承的层次可能有很多。
· 用C语言怎么做呢?好吧,没有办法还是使用struct,将基类放到一个struct里面,然后它的子类都包含这个基类。如果是基类的话,建议放在struct的开头。
· 拥有父子关系的两个 struct 怎么相互转化,你可能猜到了,用指针强转,由于地址的偏移都在struct的定义时就清楚了,因此这种方式实现时没有问题的。先看看如下例子:
/*基类定义*/
struct base_class {
int private_data;
};
/*派生类定义*/
struct derived_class {
struct base_class parent;
int private_data;
};
/*从子对象转成父对象*/
int main(int argc, char *argv[]) {
struct derived_class son;/*子类定义*/
struct base_class *parent = (struct base_class *)&son;/*父类指针初始化*/
return 0;
}
· 如果是父类转化成子类呢?其实这种情况的使用会比较多,父类就是更抽象的类,也有一些专有操作会被子类覆写,比如基类获取子类通常就在虚函数被覆写的时候。
· 如何获取子类,原理就是地址偏移,如果是单继承的偏移就是0,而多继承的便宜需要计算偏移位置。
/*基类1定义*/
struct base_class1 {
int private_data;
};
/*基类2定义*/
struct base_class2 {
int private_data;
};
/*派生类定义*/
struct derived_class{
struct base_class1 parent1;
struct base_class2 parent2;
int private_data;
};
int get_private_data(struct base_class2 *parent) {
/******************这里是重点*********************/
struct derived_class *son = (struct derived_class *)((char *)parent-(char *)(&((struct derived_class *)0)->parent2));
return son->private_data;
}
/*从父对象转成子对象*/
int main(void **argc,void *argv[]) {
struct derived_class son;/*子类定义*/
son.private_data = 3;
/* son 的各种操作*/
struct base_class2 *parent = (struct base_class *)&son.parent2;/*父类指针初始化*/
parent->private_data = get_private_data(parent);
printf("parent data = %d, son data = %d.\n",son.private_data, parent->private_data);
return 0;
}
· 这个方法有点绕,但是是可以实现的,使用宏定义会比较方便,见如下代码。
#define container_of(ptr, type, member) ({ \
(type *)((char *)ptr-(char *)(&((type *)0)->member));})
int get_private_data(struct base_class2 *parent) {
struct derived_class *son = container_of(parent, struct derived_class, parent2);
return son->private_data;
}
· 这个实现方式使用了强制转化,而Linux内核则使用了GCC提供的关键字typeof 来让这种更为合理的实现这种方式,typeof 是用来获取某一变量的类型。我们替换一下 container_of 也能得到相同的结果。(本人用的QT环境)
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
你可能觉得这个方式使用的不多,下面一节将告诉你怎么使用。
3.多态
· 多态就是子类可以决定是否覆写父类的虚函数,来实现不同的内容,主要是通过虚函数来实现。
· 虚函数的实现其实很简单,就是父类定义一个函数指针,如果纯虚函数就让指针为空,不是纯虚函数就给其初始化为一个默认函数,如果子类要对其覆写,就对其重新初始化为一个自设计的函数。
· 的确虚函数理解很简单,C++使用一个虚表的方式来实现,但是C语言使用这个思想并不用这么复杂,使用函数指针就行,见如下例子。
/*基类定义:圆*/
struct circle {
double radius;/*半径*/
double (*circumference)(struct circle *this);/*周长计算*/
};
double circle_circumference(struct circle *this) {
return this->radius * PI;/*默认圆的周长计算*/
}
struct circle* alloc_circle(int radius) {
struct circle* circle = (struct circle *)malloc(sizeof(struct circle));
circle->radius = radius;
circle->circumference = circle_circumference;
return circle;
}
/*派生类定义:圆方(圆平分2半,中间一个方形)*/
struct circle_square {
struct circle circle;
double side_length;/*边长*/
};
/*自有函数声明*/
double circle_square_circumference(struct circle *this) {
struct circle_square *cs = container_of(this, struct circle_square, circle);
return this->radius * PI + cs->side_length * 2;
}
struct circle_square* alloc_circle_square(double radius, double side_length) {
struct circle_square* cs = (struct circle_square *)malloc(sizeof(struct circle_square));
cs->circle.radius = radius;
cs->side_length = side_length;
/*覆写为自有函数*/
cs->circle.circumference = circle_square_circumference;
return cs;
}
/**** 调用新的计算周长函数 ****/
int main(void **argc,void *argv[]) {
struct circle_square *son = alloc_circle_square(1, 2);/*子类定义*/
/* son的各种操作 */
printf("circle_square circumference = %f.\n",son->circle.circumference(&son->circle));
free(son);
return 0;
}
· 虚表就是函数指针表,JAVA也是使用虚表来实现,这种方式主要是方便大家替换,大家用的就是同一张虚表,虚函数的覆写就变成了对表内容的修改,虚函数调用就是变成了查表,非常方便。C语言这么做就要加很多的判断,麻烦的多,但是我们能看到它的本质就是函数指针。
4.总结
· C语言虽然没有添加面向对象的语言特性,但是由于C语言是计算机操作的抽象,因此绝大多数的面向对象的操作都是通过C语言来实现的,这也让我们更能知道面向对象实现的精髓,知其然而知其所以然,更好的理解各种面向对象语言的实质。
· 写这篇的目的是为了《重构C语言版》打基础,因为重构中用到了太多的面向对象思想了,欢迎大家点个关注,及时获取我的后续文章。
下一篇:https://blog.csdn.net/weixin_42523774/article/details/105619681