加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
    • 4 多态
    • 5 小节
  • 推荐器件
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

嵌入式软件开发的对象在哪(下)

05/15 10:50
348
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

接上集《嵌入式软件开发的对象在哪(上)》继续...

4 多态

多态字面含义就是具有“多种形式”。从调用者的角度看对象,会发现它们非常相似,但内部处理实际上却各不相同。换句话说,各对象虽然内部处理不同,但对于使用者(调用者)来讲,它们却是相同的。

4.1 学生的“自我介绍”

在前面提到的学生类,包含姓名、学号、性别、身高、体重等属性,并对外提供了一个“自我介绍”方法。

//微信公众号:嵌入式系统
void student_self_introduction(struct student *p_this)
{
    printf("Hi! My name is %s, I'm a %s. My school number is %d. My height is %fcm and weight is %fkg",
           p_this->name,
           (p_this->sex == 'M') ? "boy" : "girl",
           p_this->id,
           p_this->height,
           p_this->weight);
}

假设一个场景,开学第一课所有同学依次作一个简单的自我介绍,调用所有同学的自我介绍方法即可,范例程序如下:

void first_class(struct student *p_students, int num)
{
    int i;
    for(i = 0; i < num; i++)
    {
        student_self_introduction(&p_students[i]);
    }
}

调用该函数前,需要将所有学生对象创建好,并存于一个数组中,假定一个班级有 50个学生,则调用示意代码如下:

int main()
{
    struct student student[50];

    /*根据每个学生的信息,依次创建各个学生对象*/
    student_init(&student[0], "zhangsan", 2024001, 'M', 173, 60);
    student_init(&student[1], "lisi", 2024002, 'F', 168, 65);
 // ...

    /*上第一节课    */
    first_class(student, 50);
}

上面的实现代码,假定了学生的“自我介绍”格式是完全相同的,都是将个人信息陈述一遍,显然,这样的自我介绍无法体现每个学生的个性和差异。例如,一个名叫张三的学生,其期望这样介绍自己:

“亲爱的老师,同学们!我叫张三,来自湖北仙桃,是一个自信开朗,积极向上的人,我有着广泛的兴趣爱好,喜欢打篮球、看书、下棋、听音乐……”

每个学生自我介绍的内容并不期望千篇一律。若不基于多态的思想,最简单粗暴的方式是每个学生都提供一个自我介绍方法,例如 student_zhangsan_introduction()。这种情况下每个学生提供的方法都不相同(函数名不同),根本无法统一调用,此时,第一节课的调用将会大改,需要依次调用每个学生提供的不同的自我介绍方法,例如:

void first_class()
{
    student_zhangsan_introduction(&zhangshan); // 张三自我介绍
    student_lisi_introduction(&lisi);   // 李四自我介绍
 // ….
}

无法使用同样的调用形式(函数)完成不同对象的“自我介绍”。对于调用者来讲,需要关注每个对象提供的特殊方法,复杂度将提升。

使用多态的思想即可很好的解决这个问题,进而保证 firstt_class()的内容不变,虽然每个对象方法的实现不同,但可以使用同样的形式调用它。在 C 语言中,函数指针就是解决这个问题的“利器”。

函数指针的原型决定了调用方法,例如定义函数指针:

int (*student_self_introduction) (struct student *p_student);

无论该函数指针指向何处,都表示该函数指针指向的是 int 类型返回值,具有一个*p_student 参数的函数,其调用形式如下:

student_self_introduction(p_student);

函数指针的指向代表了函数的实现,指向不同的函数就代表了不同的实现。基于此,为了使每个学生对象可以有自己独特的介绍方式,在学生类的定义中,可以不实现自我介绍方法,但可以通过函数指针约定自我介绍方法的调用形式。更新学生类的定义:

student.h 文件```

```c
//微信公众号:嵌入式系统
#ifndef __STUDENT_H
#define __STUDENT_H

struct student
{
    int (*student_self_introduction)(struct student *p_student);  /* 新增个性化自我介绍 */
    char name[10];  /* 姓名 (假定最长 10 字符)*/
    unsigned int id; /* 学号 */
    char sex;   /* 性别:'M',男;'F' ,女 */
    float height;  /* 身高 */
    float weight;  /* 体重 */
};

int student_init(struct student *p_student,
                 char *p_name,
                 unsigned int id,
                 char sex,
                 float height,
                 float weight,
                 int (*student_self_introduction)(struct student *));

/* 学生类提供的自我介绍方法 */
static inline int student_self_introduction(struct student *p_student)
{
    return p_student->student_self_introduction(p_student);
}

#endif

此时,对于外界来讲,学生类“自我介绍方法”的调用形式并未发生任何改变,函数原型还是一样的(由于只有一行代码,因而以内联函数的形式存放到了头文件中)。基于此,“第一节课的内容”可以保持完全不变(for循环调用全部)。在这种方式下,每个对象在初始化时,需要指定自己特殊的自我介绍方,例如张三对象的创建过程为:

int student_zhangsan_introduction(struct student *p_student)
{
    const char *str = "亲爱的老师,同学们!我叫张三,来自湖北仙桃,是一个自信开朗,积极向上的人,我有着广泛的兴趣爱好,喜欢打篮球、看书、下棋、听音乐……";

    printf("%sn", str);
    return 0;
}

int main()
{
    struct student student[50];

    /* 根据每个学生的信息,依次创建各个学生对象 */
    student_init(&student[0], "zhangsan", 2024001, 'M', 173, 60, student_zhangsan_introduction);

    // ...

    /* 上第一节课 */
    first_class(student, 50);
}

多态的核心是:对于上层调用者,不同的对象可以使用完全相同的操作方法,但是每个对象可以有各自不同的实现方式。多态是面向对象编程非常重要的特性,C 语言依赖指针实现多态

(微信公众号【嵌入式系统】很多设计模式或硬件多型号适配都是基于这个基础,可以参考《嵌入式软件的设计模式(上)》)。

4.2 I/O 设备驱动

C 程序使用 printf()打印日志信息,在 PC 上运行时,日志信息可能输出到控制台,而在嵌入式系统中,信息可能通过某个串口输出。printf()函数的解释是输出信息至 STDOUT(标准输出)。显然printf()函数就具有多态性,对于用户来讲,其调用形式是确定的,但内部具体输出信息到哪里,却会随着 STDOUT 的不同而不同。

在一些操作系统中(如Linux),硬件设备(例串口、ADC 等)的操作方法都和文件操作方法类似(一切皆文件),都可以通过 open()、close()、read()、write()等几个标准函数进行操作。为统一 I/O 设备的使用方法,要求每个 I/O 设备都提供 open、close、read、write 这几个标准函数的实现,即每个 I/O设备的驱动程序,对这些标准函数的实现在函数调用上必须保持一致。这本质上就是一个多态问题,即以同样的方法使用不同的 I/O 设备。

通过函数指针解决这个问题,首先定义file_ops结构体,包含了相对应的函数指针,指向I/O 设备针对操作的实现函数。

file_ops.h 文件
//微信公众号:嵌入式系统
//代码片段只是原理性展示
struct file_ops
{
    void (*open)(char *name, int mode);
    void (*close)();
    int (*read)();
    void (*write)();
};

对于 I/O设备,其驱动程序提供这 4个函数的实现,并将 file_ops结构体的函数指针指向对应的函数。

#include "file_ops.h"

static void open(char *name, int mode)
{
    //...
}

static void close()
{
    //...
}

static int read()
{
    //...
}

static void write()
{
    //...
}

struct file_ops my_console = {open, close, read, write};

所有的函数都使用 static修饰符,避免与外部的函数产生命名冲突。对于该设备,仅对外提供了一个可以使用的 file_ops 对象 my_console。

上面展示了设备 I/O 的一般管理方法,其中的编程方法或技巧正是面向对象编程中多态的基础,也再一次展现了函数指针在多态中的重要地位,多态可以视为函数指针的一种典型应用。(微信公众号【嵌入式系统】类似使用是Linux设备驱动的基础)。

4.3 带检查功能的栈

前面范例实现了栈的核心逻辑(入栈和出栈),假设现在增加需求,实现“带检查功能的栈”,即在数据入栈之前,必须进行特定的检查,“检查通过”后才能压人栈中。检查方式有多种:

范围检查:必须在特定的范围之内,比如1 ~ 9,才视为检查通过;奇偶检查:必须是奇数或者偶数,才视为检查通过;变化检查:值必须增加(比上一次的值大),才视为检查通过。

4.3.1 基于继承实现“带范围检查功能”的栈

先不考虑多种检查方式,仅实现范围检查。参照“命名栈”的实现,使用继承方式,在普通栈的基础上实现一个新类,范例程序如下:

stack_with_range_check.h  带范围检查的栈
#ifndef __STACK_WITH_RANGE_CHECK_H
#define __STACK_WITH_RANGE_CHECK_H

#include "stack.h"  /* 包含基类头文件 */

struct stack_with_range_check
{
    struct stack super;  /* 基类(超类)*/
    int min;    /* 最小值 */
    int max;    /* 最大值 */
};

int stack_with_range_check_init(struct stack_with_range_check *p_stack,
                                int *p_buf,
                                int  size,
                                int min, int max);

/* 入栈 */
int stack_with_range_check_push(struct stack_with_range_check *p_stack, int val);

/* 出栈 */
int stack_with_range_check_pop(struct stack_with_range_check *p_stack, int *p_val);

#endif

带范围检查的栈 C 文件 stack_with_range_check.c

//微信公众号:嵌入式系统
#include "stack_with_range_check.h"

int stack_with_range_check_init(struct stack_with_range_check *p_stack,
                                int *p_buf,
                                int size,
                                int min, int max)
{
    /* 初始化基类 */
    stack_init(&p_stack->super, p_buf, size);

    /* 初始化子类成员 */
    p_stack->min = min;
    p_stack->max = max;
    return 0;
}

int stack_with_range_check_push(struct stack_with_range_check *p_stack, int val)
{
    if((val >= p_stack->min) && (val <= p_stack->max))  //差异点
    {
        return stack_push(&p_stack->super, val);
    }
    return -1;
}

int stack_with_range_check_pop(struct stack_with_range_check *p_stack, int *p_val)
{
    return stack_pop(&p_stack->super, p_val);
}

为了接口的简洁性,没有再展示解初始化等函数的定义。新增入栈时作检查,出栈和普通栈是完全相同的,但基于最小知识原则也封装了一个 pop 接口,使该类的用户完全不需要关心普通栈。

依照这个方法,可以实现其它检查方式的栈。核心是实现带检查功能的入栈函数,因而仅简单展示另外两种检查方式下入栈函数的实现,分别如下:

//奇偶检查入栈函数
int stack_with_oddeven_check_push(struct stack_with_oddeven_check *p_stack, int val)
{
    if(((p_stack->iseven) && ((val % 2) == 0)) || ((!p_stack->iseven) && ((val % 2) != 0)))
    {
        return stack_push(&p_stack->super, val); //检查通过:偶校验且为偶数,或奇校验且为奇数
    }
    return -1;
}

//变化检查入栈函数
int stack_with_change_check_push(struct stack_with_change_check *p_stack, int val)
{
    if(p_stack->pre_value < val)
    {
        p_stack->pre_value = val;
        return stack_push(&p_stack->super, val); //检查通过:本次入栈值大于上一次的值
    }
    return -1;
}

由此可见,这种实现方式存在一定的缺陷,不同检查方法对应的入栈函数不相同,对于用户来讲,使用不同的检查功能,就必须调用不同的入栈函数。即操作不同的栈使用不同的接口。但观察几个入栈函数,其入栈方法类似,示意代码如下:

int stack_XXX_push(struct stack_XXX *p_stack, int val)
{
    if(检查通过)  //不同栈的差异仅是检测条件不同
    {
        return stack_push(&p_stack->super, val);
    }
    return -1;
}

可使用多态思想,将“检查”函数的调用形式标准化编写一个通用的、与具体检查方式无关的入栈函数。

4.3.2 基于多态实现通用的“带检查功能的栈”

使用函数指针表示“检查功能”,指向不同的检查函数。可以定义一个包含函数指针的类:

struct stack_with_validate
{
    struct stack super;            /* 基类(超类)*/
    int (*validate)(struct stack_with_validate *p_this, int val);  /* 检查函数 */
};

和其它普通方法一样,类中抽象方法(函数指针)的第一个成员同样是指向该类对象的指针。此时,数据入栈前的检查工作交给 validate 指针所指向的函数实现。假定其指向的函数在检查数据时,返回 0 表示检查通过可入栈,其它值表示检查未通过。完整的带检查功能的栈实现范例如下:

带检查功能的栈 H 文件(stack_with_validate.h)

//微信公众号:嵌入式系统

#ifndef __STACK_WITH_VALIDATE_H
#define __STACK_WITH_VALIDATE_H

#include "stack.h"     /* 包含基类头文件 */
struct stack_with_validate
{
    struct stack  super;   /* 基类(超类)*/
    int (*validate)(struct stack_with_validate *p_this, int val); /* 检查函数 */
};


int stack_with_validate_init(struct stack_with_validate *p_stack,
                             int *p_buf,
                             int size,
                             int (*validate)(struct stack_with_validate *, int));

/* 入栈 */
int stack_with_validate_push(struct stack_with_validate *p_stack, int val);

/* 出栈 */
int stack_with_validate_pop(struct stack_with_validate *p_stack, int *p_val);

#endif

带检查功能的栈 C 文件(stack_with_validate.c)

#include "stack_with_validate.h"
#include "stdio.h"

int stack_with_validate_init(struct stack_with_validate *p_stack,
                             int *p_buf,
                             int size,
                             int (*validate)(struct stack_with_validate *, int))

{
    /* 初始化基类 */
    stack_init(&p_stack->super, p_buf, size);
    p_stack->validate = validate;  //检查条件,上层说了算
    return 0;
}

int stack_with_validate_push(struct stack_with_validate *p_stack, int val)
{
    if( (p_stack->validate == NULL) || 
        ((p_stack->validate != NULL) && (p_stack->validate(p_stack, val) == 0)) )
    {
        return stack_push(&p_stack->super, val);
    }
    return -1;
}

int stack_with_validate_pop(struct stack_with_validate *p_stack, int *p_val)
{
    return stack_pop(&p_stack->super, p_val);
}

带某种检查功能的栈,重点是实现其中的 validate 方法。基于带检查的栈,实现带范围检查的栈,程序详见如下:

带范围检查的栈 H 文件更新(stack_with_range_check.h)

#ifndef __STACK_WITH_RANGE_CHECK_H
#define __STACK_WITH_RANGE_CHECK_H

#include "stack_with_validate.h"  /* 包含基类头文件 */

struct stack_with_range_check
{
    struct stack_with_validate super;  /* 基类(超类)*/
    int min;        /* 最小值 */
    int max;        /* 最大值 */
};

struct stack_with_validate * stack_with_range_check_init(struct stack_with_range_check *p_stack,
        int *p_buf,
        int size,
        int min,
        int max);

#endif

带范围检查的栈 C 文件更新(stack_with_range_check.c)

#include "stack_with_range_check.h"

static int _validate(struct stack_with_validate *p_this, int val)
{
    struct stack_with_range_check *p_stack = (struct stack_with_range_check *)p_this;
 
    if((val >= p_stack->min) && (val <= p_stack->max))
    {
        return 0; /* 检查通过 */
    }

    return -1;
}

struct stack_with_validate * stack_with_range_check_init(struct stack_with_range_check *p_stack,
        int *p_buf,
        int size,
        int min,
        int max)
{
    /* 初始化基类 */
    stack_with_validate_init(&p_stack->super, p_buf, size, _validate);
 
    /* 初始化子类成员 */
    p_stack->min = min;
    p_stack->max = max;
    return 0;
}

带范围检查的栈,主要目的就是实现“检查功能”对应的函数:_validate,并将其作为 validate 函数指针(抽象方法)的值。

在面向对象编程中,包含抽象方法的类通常称之为抽象类,抽象类不能直接实例化(因为其还有方法未实现),抽象类只能被继承,且由子类实现其中定义的抽象方法。在 UML 类图中,抽象类的类名和其中的抽象方法均使用斜体表示,普通栈、带检查功能的栈和带范围检查的栈,它们之间的关系详见图。

带范围检查的栈,其主要作用是实现其父类中定义的抽象方法,进而创建一个真正的“带检查功能”的栈对象(此时的抽象方法已实现),该对象即可提交给外部使用。带范围检查的栈并没有其他特殊的方法,因而在其初始化完成后,通过初始化函数的返回值向外界提供了一个“带检查功能”的栈对象,后续用户即可使用 stack_with_validate.h 文件中的push 和 pop 方法操作该对象。

带范围检查的栈使用范例如下:

//微信公众号:嵌入式系统
#include "stack_with_range_check.h"
#include "stdio.h"

int main()
{
    int  val;
    int  buf[20];
    int  i;
    int  test_data[5] = {2, 4, 5, 3, 10};

    struct stack_with_range_check  stack;

    struct stack_with_validate *p_stack = stack_with_range_check_init(&stack, buf, 20, 1, 9);

    for(i = 0; i < 5; i++)
    {
        if(stack_with_validate_push(p_stack, test_data[i]) != 0)
        {
            printf("The data %d push failed!n", test_data[i]);
        }
    }

    printf("The pop data: ");
    while(1)  /* 弹出所有数据 */
    {
        if(stack_with_validate_pop(p_stack, &val) == 0)
        {
            printf("%d ", val);
        }
        else
        {
            break;
        }
    }
    return 0;
}

无论何种检查方式,其主要目的都是创建“带检查功能”的栈对象(完成抽象方法的实现)。创建完毕后,对于用户操作方法都是完全相同的 stack_with_validate_push 和 stack_with_validate_pop ,与检查方式无关。为避免赘述,这里不再实现另外两种检查功能的栈,仅展示出他们的类图。

在这里插入图片描述

在一些大型项目中,初始化过程往往和应用程序是分离的(即stack_with_range_check_init 内部封闭不可见),也就是说,对于用户来讲,其仅会获取到一个 struct stack_with_validate *类型的指针,其指向某个“带检查功能的栈”,实际检查什么,用户可能并不关心,应用程序基于该类型指针编程,将使应用程序与具体检查功能无关,即使后续更换为其它检查方式,应用程序也不需要做任何改动。

4.4 抽象分离

如果是硬件资源有限,功能单一或大概率无需扩展的嵌入式软件开发,进行到这基本可以满足需求;如果是复杂应用,且硬件资源充足还可继续优化。

4.4.1 检查功能抽象

前面的实现中,将检查功能视为栈的一种扩展(使用继承),检查逻辑直接在相应的扩展类中实现。这就使检查功能与栈绑定在一起,检查功能的实现无法独立复用。如果要实现一个“带检查功能的队列”,同样是上述的 3 种检查逻辑,期望能够复用检查逻辑相关的代码。显然,由于当前检查逻辑的实现与栈捆绑在一起,无法单独提取出来复用。

检查功能与栈的绑定,主要在“带检查功能的栈”中体现,该类的定义如下:

struct stack_with_validate
{
    struct stack  super;   /* 基类(超类)*/
    int (*validate)(struct stack_with_validate *p_this, int val); /* 检查函数 */
};

super 用于继承自普通栈,validate 表示一个抽象的数据检查方法,不同的检查方法,通过该指针所指向的函数体现。由于检查方法validate是该类的一个方法,检查逻辑与栈绑定。为了解绑分离,可以将检查逻辑放到独立的与栈无关的类中,额外定义一个抽象的校验器类,专门表示数据检查逻辑:

struct validator
{
    int (*validate)(struct validator *p_this, int val); /* 检查函数 */
};

虽然该类仅包含 validate 函数指针,但需注意该函数指针类型的变化,其第一个参数为指向校验器的指针,而在“带检查功能的栈”中,其第一个参数是指向“带检查功能的栈”的指针。通过该类的定义,明确的将检查逻辑封装到独立的校验器类中,与栈再无任何关联。不同的检查逻辑,可以在其子类中实现,校验器类和各个子类之间的关系如下:由于校验器类仅包含一个函数指针,因此其只需要在头文件中定义出类即可,程序如下:

校验器类定义(validator.h)

#ifndef __VALIDATOR_H
#define __VALIDATOR_H

struct validator
{
    int (*validate)(struct validator *p_this, int val);
};

static inline int validator_init(struct validator *p_validator,
                                 int (*validate)(struct validator *, int))
{
    p_validator->validate = validate;
    return 0;
}

static inline int validator_validate(struct validator *p_validator, int val)  /* 校验函数 */
{
    if(p_validator->validate == NULL)  /* 校验函数为空,视为无需校验 */
    {
        return 0;
    }
    return p_validator->validate(p_validator, val);
}

#endif

初始化函数负责为 validate 赋值,validator_validate 函数是校验器对外提供的校验函数,在其实现中仅调用了 validate 函数指针指向的函数。由于函数都比较简单,因而直接使用了内联函数的形式进行了定义。接下来以范围校验为例,实现一个范围校验器。

范围校验器 H 文件内容(validator_range_check.h)

#ifndef __VALIDATOR_RANGE_CHECK_H
#define __VALIDATOR_RANGE_CHECK_H

#include "validator.h"
struct validator_range_check
{
    struct validator super;
    int min;
    int max;
};

struct validator* validator_range_check_init(struct validator_range_check *p_validator, int min, int max);

#endif

范围校验器 C 文件内容(validator_range_check.c)

//微信公众号:嵌入式系统
#include "validator_range_check.h"

static int _validate(struct validator *p_this, int val)
{
    struct validator_range_check *p_stack = (struct validator_range_check *)p_this;
    if((val >= p_stack->min) && (val <= p_stack->max))
    {
        return 0;  /* 检查通过 */
    }
    return -1;
}

struct validator* validator_range_check_init(struct validator_range_check *p_validator, int min, int max)
{
    validator_init(&p_validator->super, _validate);
    p_validator->min = min;
    p_validator->max = max;
    return &p_validator->super;
}

由于 validator_range_check 类仅用于实现 validator 抽象类中定义的抽象方法,其初始化函数可以直接对外返回一个标准的校验器(其中的抽象方法已实现)。按照同样的方法,可以实现validator_oddeven_check 类和 validator_change_check 类。将检查功能从“带检查功能的栈”中分离出来之后,“带检查功能的栈”中就无需再维护检查功能对应的抽象方法。其可以通过依赖的方式使用检查功能,即依赖一个校验器。在类图中,依赖关系可以使用一个虚线箭头表示,箭头指向被依赖的类,示意图如下:“带检查功能的栈”类定义如下:

struct stack_with_validate
{
    struct stack super;    /* 基类(超类)*/
    struct validator *p_validator; /* 依赖的校验器 */
};

与先前相比,其核心变化是由一个 validate 函数指针(指向具体的检查方法)变更为 p_validator 指针(指向抽象的检查方法),变化虽小,但是两种截然不同的设计理念。之前的方式是定义了一个抽象方法,而现在的方式是依赖于一个校验器对象。

基于此更新“带检查功能的栈”类的实现如下:

带检查功能的栈 H 文件更新(stack_with_validate.h)

#ifndef __STACK_WITH_VALIDATE_H
#define __STACK_WITH_VALIDATE_H

#include "stack.h"   /* 包含基类头文件 */
#include "validator.h"

struct stack_with_validate
{
    struct stack super;  /* 基类(超类)*/
    struct validator *p_validator;
};

int stack_with_validate_init(struct stack_with_validate *p_stack,
                             int *p_buf,
                             int size,
                             struct validator *p_validator);

int stack_with_validate_push(struct stack_with_validate *p_stack, int val);
int stack_with_validate_pop(struct stack_with_validate *p_stack, int *p_val);

#endif

带检查功能的栈 C 文件更新(stack_with_validate.c)

//微信公众号:嵌入式系统
#include "stack_with_validate.h"
#include "stdio.h"

int stack_with_validate_init(struct stack_with_validate *p_stack,
                             int *p_buf,
                             int size,
                             struct validator *p_validator)
{
    stack_init(&p_stack->super, p_buf, size);
    p_stack->p_validator = p_validator;
    return 0;
}

int stack_with_validate_push(struct stack_with_validate *p_stack, int val)
{
    if((p_stack->p_validator == NULL) || (validator_validate(p_stack->p_validator, val) == 0)) //注意差别
    {
        return stack_push(&p_stack->super, val);
    }
    return -1;
}

int stack_with_validate_pop(struct stack_with_validate *p_stack, int *p_val)
{
    return stack_pop(&p_stack->super, p_val);
}

“带检查功能的栈”的应用接口(push 和 pop)并没有发生任何改变,应用程序可以被复用,测试更新后的带检查功能的栈:

#include "stack_with_validate.h"
#include "validator_range_check.h"
#include "stdio.h"
int main()
{
    int buf[20];
    struct stack_with_validate stack;
    struct validator_range_check validator_range_check;

    /* 获取范围检查校验器 */
    struct validator *p_validator = validator_range_check_init(&validator_range_check, 1, 9);
    stack_with_validate_init(&stack, buf, 20, p_validator);
 
    stack_validate_application(&stack);//使用和先前继承方式一样,实现忽略
 
    return 0;
}

4.4.2  定义抽象栈

定义校验器类后,整个系统实现了两种栈:普通栈和“带检查功能的栈”,无论什么栈,对于用户来讲都是实现入栈和出栈两个核心逻辑。两种栈提供两种入栈和出栈方法。

普通栈提供的方法为:

int stack_push(struct stack *p_stack, int val);  /* 入栈 */
int stack_pop(struct stack *p_stack, int *p_val); /* 出栈 */

“带检查功能的栈”提供的方法为:

int stack_with_validate_push(struct stack_with_validate *p_stack, int val);  /* 入栈 */
int stack_with_validate_pop(struct stack_with_validate *p_stack, int *p_val); /* 出栈 */

用户执行入栈和出栈操作,使用不同类的栈,调用的函数不同。通过多态思想,将入栈和出栈定义为抽象方法(函数指针),则可以达到这样的效果:无论使用何种栈,都可以使用相同的方法来实现入栈和出栈。基于此定义抽象栈。

抽象栈类定义(stack.h)

#ifndef __STACK_H
#define __STACK_H

struct stack
{
    int (*push)(struct stack *p_stack, int val);
    int (*pop)(struct stack *p_stack, int *p_val);
};

static inline int stack_init(struct stack *p_stack,
                             int (*push)(struct stack *, int),
                             int (*pop)(struct stack *, int *))
{
    p_stack->push = push;
    p_stack->pop = pop;
}

static inline int stack_push(struct stack *p_stack, int val)
{
    return p_stack->push(p_stack, val);
}

static inline int stack_pop(struct stack *p_stack, int *p_val)
{
    return p_stack->pop(p_stack, p_val);
}

#endif

基于抽象栈的定义,使用抽象栈提供的接口实现一个通用的应用程序,该应用程序与底层细节无关,任何栈都可以使用该应用程序进行测试。

基于抽象栈实现的应用程序:

#include "stack.h"
#include "stdio.h"
int stack_application(struct stack *p_stack)
{
    int i;
    int val;
    int test_data[5] = {2, 4, 5, 3, 10};

    for(i = 0; i < 5; i++)
    {
        if(stack_push(p_stack, test_data[i]) != 0)
        {
            printf("The data %d push failed!n", test_data[i]);
        }
    }

    printf("The pop data: ");
    while(1) 
    {
        if(stack_pop(p_stack, &val) == 0)
        {
            printf("%d", val);
        }
        else
        {
            break;
        }
    }
    return 0;
}

先有应用层代码再有底层代码。在实现具体栈之前,就可以开始编写应用程序(微信公众号【嵌入式系统】这就是依赖倒置原则,可参考《嵌入式软件设计原则随想》)。实现普通栈:

普通栈 H 文件内容(stack_normal.h)

#ifndef __STACK_NORMAL_H
#define __STACK_NORMAL_H

#include "stack.h"
struct stack_normal
{
    struct stack super;
    int top;   /* 栈顶 */
    int *p_buf;   /* 栈缓存 */
    unsigned int size; /* 栈缓存的大小 */
};

struct stack * stack_normal_init(struct stack_normal *p_stack, int *p_buf, int size);

#endif

普通栈 C 文件内容(stack_normal.c)

//微信公众号:嵌入式系统
#include "stack_normal.h"

static int _push(struct stack *p_this, int val)
{
    struct stack_normal *p_stack = (struct stack_normal *)p_this;
    if(p_stack->top != p_stack->size)
    {
        p_stack->p_buf[p_stack->top++] = val;
        return 0;
    }
    return -1;
}

static int _pop(struct stack *p_this, int *p_val)
{
    struct stack_normal *p_stack = (struct stack_normal *)p_this;
    if(p_stack->top != 0)
    {
        *p_val = p_stack->p_buf[--p_stack->top];
        return 0;
    }
    return -1;
}

struct stack * stack_normal_init(struct stack_normal *p_stack, int *p_buf, int size)
{
    p_stack->top = 0;
    p_stack->size = size;

    p_stack->p_buf = p_buf;
    stack_init(&p_stack->super, _push, _pop);
    return &p_stack->super;
}

基于普通类的实现,测试普通栈类:

#include "stack_normal.h"
int main()
{
    int buf[20];

    struct stack_normal stack;
    struct stack *p_stack = stack_normal_init(&stack, buf, 20);
    stack_application(p_stack);
    return 0;
}

“带检查功能的栈”是在普通栈的基础上,增加了检查功能,实现范例程序如下:

带检查功能的栈 H 文件更新(stack_with_validate.h)

#ifndef __STACK_WITH_VALIDATE_H
#define __STACK_WITH_VALIDATE_H

#include "stack.h"   /* 包含基类头文件 */
#include "validator.h"


struct stack_with_validate
{

    struct stack super;    /* 基类(超类)*/
    struct stack*p_normal_stack;  /* 依赖于普通栈的实现 */
    struct validator *p_validator;
};

struct stack * stack_with_validate_init(struct stack_with_validate *p_stack,
                                        struct stack  *p_normal_stack,
                                        struct validator *p_validator);

#endif

检查功能的栈 C 文件更新(stack_with_validate.c)

#include "stack_with_validate.h"
#include "stdio.h"
static int _push(struct stack *p_this, int val)
{
    struct stack_with_validate *p_stack = (struct stack_with_validate *)p_this;
    if((p_stack->p_validator == NULL) || (validator_validate(p_stack->p_validator, val) == 0))
    {
        return stack_push(p_stack->p_normal_stack, val);
    }
    return -1;
}

static int _pop(struct stack *p_this, int *p_val)
{
    struct stack_with_validate *p_stack = (struct stack_with_validate *)p_this;
    return stack_pop(p_stack->p_normal_stack, p_val);
}

struct stack * stack_with_validate_init(struct stack_with_validate *p_stack,
                                        struct stack *p_normal_stack,
                                        struct validator *p_validator)
{
    stack_init(&p_stack->super, _push, _pop);

    p_stack->p_validator = p_validator;
    p_stack->p_normal_stack = p_normal_stack;
    return &p_stack->super;
}

基于“带检查功能的栈”的实现,测试范例如下:

#include "stack_normal.h"
#include "stack_with_validate.h"
#include "validator_range_check.h"

int main()
{
    int buf[20];
    struct stack_normal stack;
    struct stack_with_validate stack_with_validate;
    struct validator_range_check validator_range_check;

    struct stack *p_stack_normal = stack_normal_init(&stack, buf, 20);

    struct validator *p_validator = validator_range_check_init(&validator_range_check, 1, 9);
    struct stack *p_stack = stack_with_validate_init(&stack_with_validate,
                              p_stack_normal,
                              p_validator);

    stack_application(p_stack);
    return 0;
}

由此可见,无论底层的各种栈如何实现,对于上层应用来讲,其可以使用同一套接口stack_application操作各种各样不同的栈。

多种多态示例的核心解决方案都是相同的,即:定义抽象方法(函数指针),使上层应用可以使用同一套接口访问不同的对象。从类的角度看,每个类中操作的规约都是相同的,而这些类可以用不同的方式实现这些同名的操作,从而使得拥有相同接口的对象可以在运行时相互替换。

同样的应用程序,可以在多个硬件平台上运行,更换硬件时应用程序无需作任何改动。在嵌入式系统中,相同功能芯片的更新替换,也是多态应用最多的场景,根据硬件差异多态封装,应用层无感使用相同接口。基于多态的思想实现“与硬件无关”的应用程序,还可以衍生出两个概念:抽象接口与依赖倒置,它们的核心都是多态。更多编码原则可参考《嵌入式软件设计原则随想》、《Unix哲学之编程原则》,分层架构《嵌入式软件分层隔离的典范》。

5 小节

学会了屠龙技,但是没有龙,怎么办?有些东西只是一种思维模式,作为日常开发工作中潜移默化的一种偏爱。所以嵌入式软件开发究竟有没对象呢?有但少。

推荐器件

更多器件
器件型号 数量 器件厂商 器件描述 数据手册 ECAD模型 风险等级 参考价格 更多信息
AD9517-4ABCPZ 1 Analog Devices Inc 12-Output Clock Generator with Integrated 1.6 GHz VCO
$17.35 查看
SN74LV595ARGYR 1 Texas Instruments Eight-bit shift registers with 3-state output registers 16-VQFN -40 to 125

ECAD模型

下载ECAD模型
$0.9 查看
ATS080BSM-1 1 CTS Corporation Parallel - Fundamental Quartz Crystal, 8MHz Nom, HC-49/US-SM, 2 PIN
$0.35 查看

相关推荐

电子产业图谱

嵌入式系统开发技术交流,软件开发的思路与方案共享,行业资讯的分享。