嵌入式C语言的设计思路---模式,封装,继承,多态

什么是设计模式?

设计模式是一套代码设计的实际经验总结,并且得到多方的验证,并得到确认。
设计模式的起源是1994年,由Gang of Four 提出,提出可复用的面向对象软件。整套基于两点进行设计:

  1. 对接口编程而非对实现编程
  2. 优先使用对象组合而不是继承

GOF一共提出23种设计模式,分为三大类:

  • 创建型模式
  • 结构性模式
  • 行为型模式

创建型模式: ⼯⼚模式,抽象⼯⼚模式,单例模式,建造者模式,原型模式。
结构型模式: 适配器模式,桥接模式,过滤器模式,组合模式,装饰器模式,外观模式,享元模式,代理模式。
⾏为型模式: 责任链模式,命令模式,解释器模式,迭代器模式,中介者模式,备忘录模式,观察者模式,状态模式,空对象模式,策略模式,模板模式,访问者模式。
设计模式会遵循以下六⼤原则:
开闭原则、⾥⽒代换原则、依赖倒转原则、接⼝隔离原则、最少知道原则(迪⽶特法则)、合成复⽤原则。

为什么嵌⼊式要⽤设计模式?

嵌⼊式开发⼀般脱离不了硬件底层设计,嵌⼊式初学者,他们所接触的代码例程都是以⾯向过程⾯向实现来进⾏演示,
这就容易让他们形成了“过程实现式”思维,⽽⾮“对象抽象化”思维。
设计模式的优势是可以使软件代码实现真正的⼯程化,让代码更容易被他⼈理解,并且保证代码的可靠性。因此,设计模式是软件⼯程的基⽯(此外,还有数据结构和算法)。
在嵌⼊式软件开发过程中,每种设计模式都有其对应的⼯作原理,并且针对特定的问题给出该问题核⼼的解决⽅案,这也是设计模式在很多⼤中型软件⼯程中被⼴泛使⽤的原因。
设计模式为软件开发⼈员提供了标准的术语,并且每种设计模式都具体到了特定的情景,所以,经验不⾜的软件开发⼈员可以通过对设计模式的学习,简单快捷地掌握基本的软件⼯程设计。使⽤C语⾔作为主要编程语⾔的Linux内核,⾥⾯⼤量使⽤了设计模式,没有⾯向对象设计思维,不会设计模式的⼯程师,⼀般很难理解Linux内核的设计精髓(前辈们的经验之谈)。

嵌入式软件如何使用设计模式?

设计模式不是⾼级编程语⾔(⾯向对象编程语⾔)独有的东⻄,⾯向对象编程语⾔由于其语法特性,可以很⽅便很简单地在实际软件开发中应⽤设计模式。⽽对于⼀名嵌⼊式软件⼯程师,学习设计模式,更多的是需要学习设计模式背后的原理,以及学习设计模式所针对的问题场景,从⽽养成设计模式的应⽤思维,⽽不是让思维受限于编程语⾔。
对于⼤多数嵌⼊式软件⼯程师⽽⾔,C语⾔是使⽤频率最⾼的编程语⾔,因此,在学习嵌⼊式软件如何使⽤设计模式之前,嵌⼊式软件⼯程师应该先学会如何使⽤C语⾔进⾏⾯向对象的设计。在⼤中型嵌⼊式软件开发过程中,嵌⼊式⼯程师应注重养成⾯向对象和⾯向接⼝开发的思维⽅式,这对整个软件架构的可靠性和扩展性,都是有很⼤好处的。需要掌握如何⽤C语⾔实现⾯向对象语⾔的⼀些特性,如继承、封
装、多态、组合,等等。关于面向对象设计的设计参考文章,点击链接

面向对象编程之“封装”

开发小型单片机程序可以使用面向过程的思路进行开发程序。但是,如果是需要用C语言进行开发大规模软件时比如(操作系统内核、文件系统底层、数据库底层等等)就需要用面向对象的思想设计整个软件框架了。
嵌入式Linux的内核,是用C语言编写的里面设计的大部分都使用了面向对象的编程思想。
封装是第一个基础概念,封装是把一个抽象的事物的属性和属性的操作函数打包在一起,外界的模块只能通过这个抽象事物对外提供的函数接口,对其属性进行访问。
在java 等高级语言,封装被称为“类”。而C语言使用结构体对事物进行封装。
下面用一个引用一个坐标类代码:
头文件:coordinate.h

#ifndef __COORDINATE_H_
#define __COORDINATE_H_

//创建一个坐标位置类对象,属性为坐标x,y
typedef struct coordinate
{
    short int x;
    short int y;
}coordinate_t,*p_coordinate_t;

extern p_coordinate_t coordinate_create(short int x,short int y);
extern void coordinate_destroy(p_coordinate_t p_coordinate);
extern void coordinate_moveby(p_coordinate_t p_coordinate,short int dx,short int dy);
extern short int coordinate_get_x(p_coordinate_t p_coordinate);
extern short int coordinate_get_y(p_coordinate_t p_coordinate);
extern void coordinate_test_function(void);

#endif // !__COORDINATE_H_

在头文件coordinate.h 里面,通过结构体封装了一个coordinate类,里面有两个坐标属性x和y。
coordinate_create 函数主要用于创建一个p_coordinate_t 指针类型的对象,并为其分配内存空间,内存分配成功后,设置两个坐标的初值,最后返回申请成功的对象指针。
coordinate_destroy 主要是释放对象之前申请的内存空间,然后把对象指针重置为NULL。
其他操作函数,主要是针对对象属性进行操作,例x和y的属性值,重置坐标的属性值。
下面是测试函数,在主函数中进行调用,可以测试 类coordinate对外提供的接口,测试代码通过创建了两个p_coordinate_t 指针类型的对象,然后打印初始坐标值,再通过对外提供的函数修改坐标值,然后再打印出来,进行测试函数。
在源文件coordinate.c

void coordinate_test_function(void)
{
    p_coordinate_t p_coordiante_1 = NULL;
    p_coordinate_t p_coordiante_2 = NULL;

    //创建两个坐标对象
    p_coordiante_1 = (p_coordinate_t)coordinate_create(100,200);
    p_coordiante_2 = (p_coordinate_t)coordinate_create(10,20);

    if((NULL == p_coordiante_1) || (NULL == p_coordiante_2)){
        printf("p_coordiante_1 or p_coordiante_2 create error! \n");
    }

    //分别打印出两个坐标对象里面的属性值
    printf("p_coordiante_1 x = %d, y = %d \n",coordinate_get_x(p_coordiante_1), coordinate_get_y(p_coordiante_1));
    printf("p_coordiante_2 x = %d, y = %d \n",coordinate_get_x(p_coordiante_2), coordinate_get_y(p_coordiante_2));

    //改变两个坐标对象里面的属性值
    coordinate_moveby(p_coordiante_1,50,50);
    coordinate_moveby(p_coordiante_2,50,50);

    //再次打印改变后的属性值
    printf("after moveby p_coordiante_1 x = %d, y = %d \n",coordinate_get_x(p_coordiante_1), coordinate_get_y(p_coordiante_1));
    printf("after moveby p_coordiante_2 x = %d, y = %d \n",coordinate_get_x(p_coordiante_2), coordinate_get_y(p_coordiante_2));

    //销毁两个坐标对象
    coordinate_destroy(p_coordiante_1);
    coordinate_destroy(p_coordiante_2);
}

通过上述代码可以看出,使用结构体可以很好的封装数据,并且需要通过指定的操作函数对结构体内的数据进行访问,每个操作函数的第一个参数是对象本身的指针,通过这个指针区访问具体对象里面的属性。

面向对象编程之“继承”

继承是基于一个已有的类,再去重新声明或创建一个新的类,这个类是子类或派生类。子类或派生类可以访问父类的数据和函数,然后子类里面可以添加自己的属性和数据。在C语言中,一般通过结构体嵌套的方式实现类的单继承,但需注意在结构体嵌套时,父类对象需要放在结构体成员的第一个位置。
根据 “封装” 部分的代码我们已经有了coordinate类作为父类,在重新定义一个rectangle派生类。所以对代码进行修改,并把操作函数通过函数指针的方式封装在结构体内,让对象的封装程度进一步提高,如下所示:
在头文件:coordinate.h

#ifndef __COORDINATE_H_
#define __COORDINATE_H_

//声明一个位置类,属性为坐标x,y,提供属性操作函数
typedef struct coordinate
{
    short int x;
    short int y;

    void (*moveby)(struct coordinate *p_coordinate,short int dx,short int dy);
    short int (*get_x)(struct coordinate *p_coordinate);
    short int (*get_y)(struct coordinate *p_coordinate);
}coordinate_t,*p_coordinate_t;

extern void coordinate_init(p_coordinate_t p_coordinate,short int x,short int y);
extern void coordinate_uninit(p_coordinate_t p_coordinate);

#endif // !__COORDINATE_H_

在头文件coordinate.h里,声明一个位置类,类里面提供了坐标属性x和y,还提供了属性的操作函数指针。头文件对外提供coordinate_init 和 coordinate_uninit函数,用来初始化对象和解初始化。
在源文件coordinate.c

#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "coordinate.h"

//修改coordinate的属性值
static void coordinate_moveby(struct coordinate *p_coordiante,short int dx,short int dy)
{
    if(NULL != p_coordiante){
        p_coordiante->x += dx;
        p_coordiante->y += dy;
    }
}

//获取coordinate的属性值x
static short int coordinate_get_x(struct coordinate *p_coordiante)
{
    return (NULL != p_coordiante) ? p_coordiante->x : -1;
}

//获取coordinate的属性值y
static short int coordinate_get_y(struct coordinate *p_coordiante)
{
    return (NULL != p_coordiante) ? p_coordiante->y : -1;
}

//创建一个coordinate对象
void coordinate_init(p_coordinate_t p_coordinate,short int x,short int y)
{
    if((x < 0) || (y < 0) || (NULL == p_coordinate)){
        printf("coordinate create error! x or y can not be less than zero \n");
        return;
    }

    p_coordinate->x = x;
    p_coordinate->y = y;
    p_coordinate->moveby = coordinate_moveby;
    p_coordinate->get_x = coordinate_get_x;
    p_coordinate->get_y = coordinate_get_y;
}

//销毁一个coordinate对象
void coordinate_uninit(p_coordinate_t p_coordinate)
{
    if(NULL != p_coordinate){
        p_coordinate->x = -1;
        p_coordinate->y = -1;
        p_coordinate->moveby = NULL;
        p_coordinate->get_x = NULL;
        p_coordinate->get_y = NULL;
    }
}

整个父类coordinate修改完成,父类把属性和属性函数封装在结构体内,封装程度很高,外部不能直接调用父类的属性函数进行操作必须通过函数指针的方式进行调用,下面基于父类coordinate,重新声明一个子类rectangle,子类在头文件中进行声明。在头文件里通过 #include 包含父类coordinate的接口,并创建了一个新的结构体,用于声明一个rectangle类,这个结构体把父类coordinate放在第一个成员位置,同时新增加两个属于自己的属性,宽度width和高度height。

在头文件rectangle.h

#ifndef __RECTANGLE_H_
#define __RECTANGLE_H_

#include "coordinate.h"    //包含基类的接口

//声明一个rectangle类,继承coordinate类
typedef struct rectangle
{
    coordinate_t coordinate; //父类,必须放在首位
    unsigned short width;
    unsigned short height;
}rectangle_t,*p_rectangle_t;

extern p_rectangle_t rectangle_create(short int x,short int y,unsigned short width,unsigned short height);
extern void rectangle_destroy(p_rectangle_t p_rectangle);
extern void rectangle_test_function(void);

#endif // !__RECTANGLE_H_

在源文件rectangle.c 里面,rectangle_create函数用于创建一个 p_rectangle_t 类型的对象,并为其分配内存空间。分配成功后,对调用分类coordinate_init函数,对父类的各种属性进行初始化,并同时对自身的属性width和height进行初始化,最后返回创建成功后的对象指针。
rectangle_destroy 用于父类对象属性的解除初始化,并为对象属性重新分配默认值,释放之前申请内存空间,销毁rectangle对象。
在源文件rectangle.c

#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "rectangle.h"

//创建一个rectangle类对象
p_rectangle_t rectangle_create(short int x,short int y,unsigned short width,unsigned short height)
{
    p_rectangle_t p_rectangle = NULL;

    p_rectangle = (p_rectangle_t)malloc(sizeof(rectangle_t));

    if(NULL != p_rectangle){       
        p_rectangle->width = width;
        p_rectangle->height = height;

        coordinate_init(&(p_rectangle->coordinate),x,y);
    }
    else{
        printf("rectangle create error! \n");
    }
    
    return p_rectangle;
}

//销毁一个rectangle类对象
void rectangle_destroy(p_rectangle_t p_rectangle)
{
    coordinate_uninit(&(p_rectangle->coordinate));

    if(NULL != p_rectangle){
        free(p_rectangle);
        p_rectangle = NULL;
    }
}

//测试函数,在main中调用,用于测试类 rectangle 的接口
void rectangle_test_function(void)
{
    p_rectangle_t p_rectangle_1 = NULL;
    p_rectangle_t p_rectangle_2 = NULL;

    //创建两个 p_rectangle_t 类型的类对象
    p_rectangle_1 = (p_rectangle_t)rectangle_create(0,0,150,150);
    p_rectangle_2 = (p_rectangle_t)rectangle_create(200,200,500,500);

    if((NULL != p_rectangle_1) && (NULL != p_rectangle_2)){

        //打印出类对象的初始化属性,通过函数指针的方式来调用属性操作函数
        printf("p_rectangle_1,x = %d,y = %d,width = %d,height = %d \n",\
               p_rectangle_1->coordinate.get_x(&(p_rectangle_1->coordinate)), \
               p_rectangle_1->coordinate.get_y(&(p_rectangle_1->coordinate)), \
               p_rectangle_1->width,p_rectangle_1->height);

        printf("p_rectangle_2,x = %d,y = %d,width = %d,height = %d \n",    \
               p_rectangle_2->coordinate.get_x(&(p_rectangle_2->coordinate)),   \
               p_rectangle_2->coordinate.get_y(&(p_rectangle_2->coordinate)),   \
               p_rectangle_2->width,p_rectangle_2->height);

        //修改类对象的属性,注意这里有两种方式,1、通过强制类型转换修改。2、通过正常方式修改
        p_rectangle_1->coordinate.moveby((p_coordinate_t)p_rectangle_1, 50, 50);
        p_rectangle_2->coordinate.moveby(&(p_rectangle_2->coordinate), 50, 50);

        //再次打印出类对象的修改后的属性
        printf("after moveby, p_rectangle_1,x = %d,y = %d,width = %d,height = %d \n",\
               p_rectangle_1->coordinate.get_x(&(p_rectangle_1->coordinate)), \
               p_rectangle_1->coordinate.get_y(&(p_rectangle_1->coordinate)), \
               p_rectangle_1->width,p_rectangle_1->height);

        printf("after moveby, p_rectangle_2,x = %d,y = %d,width = %d,height = %d \n",    \
               p_rectangle_2->coordinate.get_x(&(p_rectangle_2->coordinate)),   \
               p_rectangle_2->coordinate.get_y(&(p_rectangle_2->coordinate)),   \
               p_rectangle_2->width,p_rectangle_2->height);
    }

    //销毁类对象
    rectangle_destroy(p_rectangle_1);
    rectangle_destroy(p_rectangle_2);
}

从头文件rectangle.h 和源文件rectangle.c 可以看出,子类rectangle是基于其父类coordinate进行声明和构建的,因为矩形rectangle除了width 和height属性外,还包含了坐标x和y属性。把父类放在结构体成员的第一位是由于结构体内存的连续性,可以很安全地进行强制类型转换。例如:假如一个函数要求传入的参数是coordinate_t类型,但可以通过强制类型转换,传入rectangle_t 类型的参数,代码见上述源代码的测试函数。

段落总结:

  1. 外部函数可以通过子类直接使用父类的各个成员,但只能通过子类结构体的第一个成员来访问
  2. 父类放在结构体的第一个位置,由于结构体内存的连续性,因此通过强制类型转换来直接访问。
  3. 由于C语言的结构体的特性,即使子类存在与父类同名函数,父类的函数不会被子类的函数覆盖和重写,所以子类与父类之间不存在函数重载。

面向对象编程之“多态”

多态即 同一个接口多种状态
使用C语言实现多态,除了使用结构体构建出一个父类,还需要先构建出一个虚函数表,这个虚函数表就是一系列函数指针的结构体。简单来说,就是在结构体里面包含了函数指针作为函数的接口,而这个函数指针则可以根据程序运行时的情况,分别赋予不同的函数入口,从而实现同一个接口不同的功能调用。
通过虚函数表,就可以在应用层调用相同的接口,根据子类对虚函数表的不同赋值,实现不同的函数功能。实现了“接口相同,实现不同”的多态特性。

  1. 首先,virtual_func_tbl.h头文件里面,声明一个虚函数表结构体,为了理解简易,这个结构体目前只有一个函数指针,可以扩展更多的函数接口。
    virtual_func_tbl.h
#ifndef _VIRTUAL_FUNC_TBL_H_
#define _VIRTUAL_FUNC_TBL_H_

typedef void (*virtual_func_t)(void);

//声明一个虚函数表,
//为了简单,只有一个函数指针
typedef struct virtual_func_tbl
{
    virtual_func_t virt_func;
}virtual_func_tbl_t;

#endif
  1. 然后再创建一个父类base,在父类的头文件base.h里面,声明一个结构体,父类需要把虚函数表包含进来,以便于子类在继承父类的时候,可以对虚函数表里面的函数指针进行赋值,代码如下:
    base.h
#ifndef _BASE_H_
#define _BASE_H_

//父类需要包含虚函数表
#include "virtual_func_tbl.h"

//声明一个坐标基类,作为父类
//包含一个虚函数表,以及两个基本的坐标属性
typedef struct base
{
    //虚函数表
    virtual_func_tbl_t virt_func_tbl;    

    //坐标属性
    int x;
    int y;
}base_t;

#endif

  1. 父类也可以自己实现虚函数表里面的函数接口内容,在base.c文件里面,父类也可以有自己的构建函数,在构建函数里面,对虚函数表里面的指针进行赋值。
#include <stdio.h>
#include "base.h"

//父类 base 的打印函数
void base_printf(void)
{
    printf(">>>>>> this is base_printf >>>>>> \r\n");
}

//父类的构造函数
void base_create(base_t *p_obj)
{
    //设置虚函数表里面的成员函数
    p_obj->virt_func_tbl.virt_func = base_printf;
}

  1. 父类和虚函数表构建完成后,就可以在derive.h 里面声明两个子类结构体,C语言里面实现继承,父类的结构体变量必须要在子结构体的首位置,子类的声明代码如下:
    derive.h
#ifndef _DERIVE_H_
#define _DERIVE_H_

#include "base.h"

//声明一个子类derive_1,继承父类base
//子类有自己的属性,width 和 height
typedef struct derive_1
{
    //继承自父类
    base_t base;

    //子类自身的属性
    int width;
    int height;
}derive_1_t;

//derive_1的构造函数,被main函数调用
extern void derive_1_create(derive_1_t *p_obj);

//声明一个子类derive_2,继承父类base
//子类有自己的属性,width 和 height
typedef struct derive_2
{
    //继承自父类
    base_t base;

    //子类自身的属性
    int width;
    int height;
}derive_2_t;

//derive_2的构造函数,被main函数调用
extern void derive_2_create(derive_2_t *p_obj);

#endif
  1. 在子类的源文件derive.c里面,主要是实现了两个子类各自的虚函数表接口,以及其构造函数,在构造函数里面,两个子类分别对虚函数表里面的函数指针进行赋值
#include <stdio.h>
#include "derive.h"

//子类 derive_1 的打印函数
void derive_1_printf(void)
{
    printf(">>>>>> this is derive_1_printf >>>>>> \r\n");
}

//子类1的构造函数
void derive_1_create(derive_1_t *p_obj)
{
    //设置虚函数表里面的成员函数
    p_obj->base.virt_func_tbl.virt_func = derive_1_printf;
}


//子类 derive_2 的打印函数
void derive_2_printf(void)
{
    printf(">>>>>> this is derive_2_printf >>>>>> \r\n");
}

//子类 derive_2 的构造函数
void derive_2_create(derive_2_t *p_obj)
{
    //设置虚函数表里面的成员函数
    p_obj->base.virt_func_tbl.virt_func = derive_2_printf;
}
  1. 在应用层代买main函数里面,先定义一个父类之类(这个父类指针有用),然后再定义两个子类变量,通过调用两个子类各自的构造函数,对其虚函数表进行赋值操作。代码如下:
    mian.c
#include <stdio.h>
#include <string.h>
#include "base.h"
#include "derive.h"

int main(void)
{
    printf(">>>>>> c_object_polymorphism demo start >>>>>>\r\n");

    base_t *base;          //定义一个父类指针
    derive_1_t derive_1;   //定义一个子类1
    derive_2_t derive_2;   //定义一个子类2

    derive_1_create(&derive_1);     //使用构造函数初始化子类1
    derive_2_create(&derive_2);     //使用构造函数初始化子类2

    base = (base_t*)&derive_1;            //使父类指针指向子类1
    base->virt_func_tbl.virt_func();      //调用打印函数,此时的打印函数是子类1的

    base = (base_t*)&derive_2;            //使父类指针指向子类2
    base->virt_func_tbl.virt_func();      //调用打印函数,此时的打印函数是子类2的

    printf(">>>>>> c_object_polymorphism demo end >>>>>>\r\n");

    return 0;
}
  1. 上面代码所示,使用构造函数初始化子类后,就可以把子类的指针强制类型转换,再赋值给父类base指针,然后通过base调用虚函数表里面的接口,因为base指针代表了不同的子类,所以即使在应用层调用相同接口,也可以打印不同的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值