学会C语言面向对象编程,弄清面向对象实质。

· 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章