• C 语言实现面向对象编程


    转载 https://blog.csdn.net/onlyshi/article/details/81672279

    C 语言实现面向对象编程
    1、引言
    面向对象编程(OOP)并不是一种特定的语言或者工具,它只是一种设计方法、设计思想。它表现出来的三个最基本的特性就是封装、继承与多态。很多面向对象的编程语言已经包含这三个特性了,例如 Smalltalk、C++、Java。但是你也可以用几乎所有的编程语言来实现面向对象编程,例如 ANSI-C。要记住,面向对象是一种思想,一种方法,不要太拘泥于编程语言。

    2、封装
    封装就是把数据和方法打包到一个类里面。其实C语言编程者应该都已经接触过了,C 标准库
    中的 fopen(), fclose(), fread(), fwrite()等函数的操作对象就是 FILE。数据内容就是 FILE,数据的读写操作就是 fread()、fwrite(),fopen() 类比于构造函数,fclose() 就是析构函数。这个看起来似乎很好理解,那下面我们实现一下基本的封装特性。

    #ifndef SHAPE_H
    #define SHAPE_H
    
    #include <stdint.h>
    
    // Shape 的属性
    typedef struct {
        int16_t x; 
        int16_t y; 
    } Shape;
    
    // Shape 的操作函数,接口函数
    void Shape_ctor(Shape * const me, int16_t x, int16_t y);
    void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
    int16_t Shape_getX(Shape const * const me);
    int16_t Shape_getY(Shape const * const me);
    
    #endif /* SHAPE_H */
    


    这是 Shape 类的声明,非常简单,很好理解。一般会把声明放到头文件里面 “Shape.h”。

    来看下 Shape 类相关的定义,当然是在 “Shape.c” 里面。

    #include "shape.h"
    
    // 构造函数
    void Shape_ctor(Shape * const me, int16_t x, int16_t y)
    {
    me->x = x;
    me->y = y;
    }
    
    void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) 
    {
    me->x += dx;
    me->y += dy;
    }
    
    // 获取属性值函数
    int16_t Shape_getX(Shape const * const me) 
    {
    return me->x;
    }
    int16_t Shape_getY(Shape const * const me) 
    {
    return me->y;
    }
    

     

    再看下 main.c

     1 #include "shape.h" /* Shape class interface */
     2 #include <stdio.h> /* for printf() */
     3 
     4 int main() 
     5 {
     6 Shape s1, s2; /* multiple instances of Shape */
     7 
     8 Shape_ctor(&s1, 0, 1);
     9 Shape_ctor(&s2, -1, 2);
    10 
    11 printf("Shape s1(x=%d,y=%d)
    ", Shape_getX(&s1), Shape_getY(&s1));
    12 printf("Shape s2(x=%d,y=%d)
    ", Shape_getX(&s2), Shape_getY(&s2));
    13 
    14 Shape_moveBy(&s1, 2, -4);
    15 Shape_moveBy(&s2, 1, -2);
    16 
    17 printf("Shape s1(x=%d,y=%d)
    ", Shape_getX(&s1), Shape_getY(&s1));
    18 printf("Shape s2(x=%d,y=%d)
    ", Shape_getX(&s2), Shape_getY(&s2));
    19 
    20 return 0;
    21 }

    编译之后,看看执行结果:

    Shape s1(x=0,y=1)
    Shape s2(x=-1,y=2)
    Shape s1(x=2,y=-3)
    Shape s2(x=0,y=0)

    整个例子,非常简单,非常好理解。以后写代码时候,要多去想想标准库的文件IO操作,这样也有意识的去培养面向对象编程的思维。

    3、继承
    继承就是基于现有的一个类去定义一个新类,这样有助于重用代码,更好的组织代码。在 C 语言里面,去实现单继承也非常简单,只要把基类放到继承类的第一个数据成员的位置就行了。

    例如,我们现在要创建一个 Rectangle 类,我们只要继承 Shape 类已经存在的属性和操作,再添加不同于 Shape 的属性和操作到 Rectangle 中。

    下面是 Rectangle 的声明与定义:

     1 #ifndef RECT_H
     2 #define RECT_H
     3 
     4 #include "shape.h" // 基类接口
     5 
     6 // 矩形的属性
     7 typedef struct {
     8 Shape super; // 继承 Shape
     9 
    10 // 自己的属性
    11 uint16_t width;
    12 uint16_t height;
    13 } Rectangle;
    14 
    15 // 构造函数
    16 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
    17 uint16_t width, uint16_t height);
    18 
    19 #endif /* RECT_H */
    #include "rect.h"
    
    // 构造函数
    void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
    uint16_t width, uint16_t height)
    {
    /* first call superclass’ ctor */
    Shape_ctor(&me->super, x, y);
    
    /* next, you initialize the attributes added by this subclass... */
    me->width = width;
    me->height = height;
    }

    我们来看一下 Rectangle 的继承关系和内存布局

    因为有这样的内存布局,所以你可以很安全的传一个指向 Rectangle 对象的指针到一个期望传入 Shape 对象的指针的函数中,就是一个函数的参数是 “Shape *”,你可以传入 “Rectangle *”,并且这是非常安全的。这样的话,基类的所有属性和方法都可以被继承类继承!

     1 #include "rect.h" 
     2 #include <stdio.h>
     3 
     4 int main() 
     5 {
     6 Rectangle r1, r2;
     7 
     8 // 实例化对象
     9 Rectangle_ctor(&r1, 0, 2, 10, 15);
    10 Rectangle_ctor(&r2, -1, 3, 5, 8);
    11 
    12 printf("Rect r1(x=%d,y=%d,width=%d,height=%d)
    ",
    13 Shape_getX(&r1.super), Shape_getY(&r1.super),
    14 r1.width, r1.height);
    15 printf("Rect r2(x=%d,y=%d,width=%d,height=%d)
    ",
    16 Shape_getX(&r2.super), Shape_getY(&r2.super),
    17 r2.width, r2.height);
    18 
    19 // 注意,这里有两种方式,一是强转类型,二是直接使用成员地址
    20 Shape_moveBy((Shape *)&r1, -2, 3);
    21 Shape_moveBy(&r2.super, 2, -1);
    22 
    23 printf("Rect r1(x=%d,y=%d,width=%d,height=%d)
    ",
    24 Shape_getX(&r1.super), Shape_getY(&r1.super),
    25 r1.width, r1.height);
    26 printf("Rect r2(x=%d,y=%d,width=%d,height=%d)
    ",
    27 Shape_getX(&r2.super), Shape_getY(&r2.super),
    28 r2.width, r2.height);
    29 
    30 return 0;
    31 }

    输出结果:

    Rect r1(x=0,y=2,width=10,height=15)
    Rect r2(x=-1,y=3,width=5,height=8)
    Rect r1(x=-2,y=5,width=10,height=15)
    Rect r2(x=1,y=2,width=5,height=8)

    4、多态
    C++ 语言实现多态就是使用虚函数。在 C 语言里面,也可以实现多态。
    现在,我们又要增加一个圆形,并且在 Shape 要扩展功能,我们要增加 area() 和 draw() 函数。但是 Shape 相当于抽象类,不知道怎么去计算自己的面积,更不知道怎么去画出来自己。而且,矩形和圆形的面积计算方式和几何图像也是不一样的。

    下面让我们重新声明一下 Shape 类

     1 #ifndef SHAPE_H
     2 #define SHAPE_H
     3 
     4 #include <stdint.h>
     5 
     6 struct ShapeVtbl;
     7 // Shape 的属性
     8 typedef struct {
     9 struct ShapeVtbl const *vptr;
    10 int16_t x; 
    11 int16_t y; 
    12 } Shape;
    13 
    14 // Shape 的虚表
    15 struct ShapeVtbl {
    16 uint32_t (*area)(Shape const * const me);
    17 void (*draw)(Shape const * const me);
    18 };
    19 
    20 // Shape 的操作函数,接口函数
    21 void Shape_ctor(Shape * const me, int16_t x, int16_t y);
    22 void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy);
    23 int16_t Shape_getX(Shape const * const me);
    24 int16_t Shape_getY(Shape const * const me);
    25 
    26 static inline uint32_t Shape_area(Shape const * const me) 
    27 {
    28 return (*me->vptr->area)(me);
    29 }
    30 
    31 static inline void Shape_draw(Shape const * const me)
    32 {
    33 (*me->vptr->draw)(me);
    34 }
    35 
    36 
    37 Shape const *largestShape(Shape const *shapes[], uint32_t nShapes);
    38 void drawAllShapes(Shape const *shapes[], uint32_t nShapes);
    39 
    40 #endif /* SHAPE_H */

    看下加上虚函数之后的类关系图


    4.1 虚表和虚指针
    虚表(Virtual Table)是这个类所有虚函数的函数指针的集合。

    虚指针(Virtual Pointer)是一个指向虚表的指针。这个虚指针必须存在于每个对象实例中,会被所有子类继承。

    在《Inside The C++ Object Model》的第一章内容中,有这些介绍。

    4.2 在构造函数中设置vptr
    在每一个对象实例中,vptr 必须被初始化指向其 vtbl。最好的初始化位置就是在类的构造函数中。事实上,在构造函数中,C++ 编译器隐式的创建了一个初始化的vptr。在 C 语言里面, 我们必须显示的初始化vptr。

     1 下面就展示一下,在 Shape 的构造函数里面,如何去初始化这个 vptr。
     2 
     3 #include "shape.h"
     4 #include <assert.h>
     5 
     6 // Shape 的虚函数
     7 static uint32_t Shape_area_(Shape const * const me);
     8 static void Shape_draw_(Shape const * const me);
     9 
    10 // 构造函数
    11 void Shape_ctor(Shape * const me, int16_t x, int16_t y) 
    12 {
    13 // Shape 类的虚表
    14 static struct ShapeVtbl const vtbl = 
    15 { 
    16 &Shape_area_,
    17 &Shape_draw_
    18 };
    19 me->vptr = &vtbl; 
    20 me->x = x;
    21 me->y = y;
    22 }
    23 
    24 
    25 void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy)
    26 {
    27 me->x += dx;
    28 me->y += dy;
    29 }
    30 
    31 
    32 int16_t Shape_getX(Shape const * const me) 
    33 {
    34 return me->x;
    35 }
    36 int16_t Shape_getY(Shape const * const me) 
    37 {
    38 return me->y;
    39 }
    40 
    41 // Shape 类的虚函数实现
    42 static uint32_t Shape_area_(Shape const * const me) 
    43 {
    44 assert(0); // 类似纯虚函数
    45 return 0U; // 避免警告
    46 }
    47 
    48 static void Shape_draw_(Shape const * const me) 
    49 {
    50 assert(0); // 纯虚函数不能被调用
    51 }
    52 
    53 
    54 Shape const *largestShape(Shape const *shapes[], uint32_t nShapes) 
    55 {
    56 Shape const *s = (Shape *)0;
    57 uint32_t max = 0U;
    58 uint32_t i;
    59 for (i = 0U; i < nShapes; ++i) 
    60 {
    61 uint32_t area = Shape_area(shapes[i]);// 虚函数调用
    62 if (area > max) 
    63 {
    64 max = area;
    65 s = shapes[i];
    66 }
    67 }
    68 return s;
    69 }
    70 
    71 
    72 void drawAllShapes(Shape const *shapes[], uint32_t nShapes) 
    73 {
    74 uint32_t i;
    75 for (i = 0U; i < nShapes; ++i) 
    76 {
    77 Shape_draw(shapes[i]); // 虚函数调用
    78 }
    79 }


    注释比较清晰,这里不再多做解释。

    4.3 继承 vtbl 和 重载 vptr
    上面已经提到过,基类包含 vptr,子类会自动继承。但是,vptr 需要被子类的虚表重新赋值。并且,这也必须发生在子类的构造函数中。下面是 Rectangle 的构造函数。

     1 #include "rect.h" 
     2 #include <stdio.h>
     3 
     4 // Rectangle 虚函数
     5 static uint32_t Rectangle_area_(Shape const * const me);
     6 static void Rectangle_draw_(Shape const * const me);
     7 
     8 // 构造函数
     9 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y,
    10 uint16_t width, uint16_t height)
    11 {
    12 static struct ShapeVtbl const vtbl = 
    13 {
    14 &Rectangle_area_,
    15 &Rectangle_draw_
    16 };
    17 Shape_ctor(&me->super, x, y); // 调用基类的构造函数
    18 me->super.vptr = &vtbl; // 重载 vptr
    19 me->width = width;
    20 me->height = height;
    21 }
    22 
    23 // Rectangle's 虚函数实现
    24 static uint32_t Rectangle_area_(Shape const * const me) 
    25 {
    26 Rectangle const * const me_ = (Rectangle const *)me; //显示的转换
    27 return (uint32_t)me_->width * (uint32_t)me_->height;
    28 }
    29 
    30 static void Rectangle_draw_(Shape const * const me) 
    31 {
    32 Rectangle const * const me_ = (Rectangle const *)me; //显示的转换
    33 printf("Rectangle_draw_(x=%d,y=%d,width=%d,height=%d)
    ",
    34 Shape_getX(me), Shape_getY(me), me_->width, me_->height);
    35 }

    4.4 虚函数调用
    有了前面虚表(Virtual Tables)和虚指针(Virtual Pointers)的基础实现,虚拟调用(后期绑定)就可以用下面代码实现了。

    1 uint32_t Shape_area(Shape const * const me)
    2 {
    3 return (*me->vptr->area)(me);
    4 }

    这个函数可以放到.c文件里面,但是会带来一个缺点就是每个虚拟调用都有额外的调用开销。为了避免这个缺点,如果编译器支持内联函数(C99)。我们可以把定义放到头文件里面,类似下面:

    1 static inline uint32_t Shape_area(Shape const * const me) 
    2 {
    3 return (*me->vptr->area)(me);
    4 }


    如果是老一点的编译器(C89),我们可以用宏函数来实现,类似下面这样:

    1 #define Shape_area(me_) ((*(me_)->vptr->area)((me_)))


    1
    看一下例子中的调用机制:


    4.5 main.c

     1 #include "rect.h" 
     2 #include "circle.h" 
     3 #include <stdio.h>
     4 
     5 int main() 
     6 {
     7 Rectangle r1, r2; 
     8 Circle c1, c2; 
     9 Shape const *shapes[] = 
    10 { 
    11 &c1.super,
    12 &r2.super,
    13 &c2.super,
    14 &r1.super
    15 };
    16 Shape const *s;
    17 
    18 // 实例化矩形对象
    19 Rectangle_ctor(&r1, 0, 2, 10, 15);
    20 Rectangle_ctor(&r2, -1, 3, 5, 8);
    21 
    22 // 实例化圆形对象
    23 Circle_ctor(&c1, 1, -2, 12);
    24 Circle_ctor(&c2, 1, -3, 6);
    25 
    26 s = largestShape(shapes, sizeof(shapes)/sizeof(shapes[0]));
    27 printf("largetsShape s(x=%d,y=%d)
    ", Shape_getX(s), Shape_getY(s));
    28 
    29 drawAllShapes(shapes, sizeof(shapes)/sizeof(shapes[0]));
    30 
    31 return 0;
    32 }

    输出结果:

    largetsShape s(x=1,y=-2)
    Circle_draw_(x=1,y=-2,rad=12)
    Rectangle_draw_(x=-1,y=3,width=5,height=8)
    Circle_draw_(x=1,y=-3,rad=6)
    Rectangle_draw_(x=0,y=2,width=10,height=15)

    5、总结
    还是那句话,面向对象编程是一种方法,并不局限于某一种编程语言。用 C 语言实现封装、单继承,理解和实现起来比较简单,多态反而会稍微复杂一点,如果打算广泛的使用多态,还是推荐转到 C++ 语言上,毕竟这层复杂性被这个语言给封装了,你只需要简单的使用就行了。但并不代表,C 语言实现不了多态这个特性。

  • 相关阅读:
    我爬取了爬虫岗位薪资,分析后发现爬虫真香
    红薯,撑起父亲的快乐,让我揪心
    跨域问题服务端解决办法 Request header field Authorization is not allowed by Access-Control-Allow-Headers
    antdvue2.x 使用阿里iconfont自定义组件iconfont
    前端 crypto-js aes 加解密
    jsencrypt加密解密字符串
    CryptoJS base64使用方法
    客户端js生成rsa 密钥对
    js动态添加style样式
    PHP 使用非对称加密算法(RSA)
  • 原文地址:https://www.cnblogs.com/buzhidao1/p/12377363.html
Copyright © 2020-2023  润新知