一、代码分层

我们以单片机为例来介绍分层,在单片机程序中,代码分为两类,分别是:

  • 驱动代码:读写寄存器,直接控制硬件工作的代码就是驱动代码
  • 应用代码:与读写寄存器无关,只是会间接的调用“驱动代码”

1)51单片机

由于51单片机比较简单,所以在51单片机程序中,驱动代码和应用代码一般混在一起的,没有明显的区分,
比如将“驱动代码”和“应用代码”写在一个函数中,或者同一个文件中,这就是混在一起,没有明显的区分,
对于简单的单片机来说,区不区分“驱动代码”和“应用代码”问题不大

2)stm32单片机

stm32单片机相比51来说复杂了很多,如果驱动代码和应用代码再不做区分的话,
不管是对于程序的开发、还是代码的维护,都会带来很多的麻烦

推出stm32单片机的ST公司帮我们进行了分层,ST公司专门封装出了“驱动函数库”,这些库函数只做一件事,
那就是读写寄存器、控制硬件,我们自己写的代码则负责调用“驱动库函数”,与读写寄存器的硬件控制没有直接关系,
我们自己写的调用“驱动库函数”的代码就是“应用代码”


如此一来就有了明显的分层:

 我们写的应用代码  (调用驱动库函数)
            |
            |
            V
ST驱动库函数 (读写寄存器,控制硬件)

如果我们在中间再加一层OS,就变成了非常经典的“三层结构”

 我们写的应用代码
    |
    |
    V
 OS(操作系统)
    |
    |
    V
ST驱动库函数 (读写寄存器,控制硬件)

以上只是一个大的分层,而且并不是所有的分层都是这样的,
在这里我们只是想通过这个经典分层结构来介绍什么是分层思想

实际上在应用代码、OS、驱动代码内部还会进行更详细的代码逻辑分层,
不管是那种分层,分层的思想和目的都是相似的


不同层之间如何传递信息?

显然是通过函数参数来传递信息的,当要传递的信息非常丰富时,
往往会使用结构体来封装,而且有时还会封装函数指针

C的结构体中是不能直接定义函数的,但是可以存放函数指针,
因此这种有函数指针的结构体与c++java的可以在内部定义函数的“类”有一定的相似性,
因此我们把这种结构体内有函数指针情况,称为是在模拟“面向对象”思想,当然这个说法并不准确


二、结构体封装函数指针

前面而介绍分层时,应用代码、OS、驱动之间这种大的分层,我们使用“结构体封装函数指针”,
与这种大的分层之间没什么关系,而是往往与代码内部的逻辑分层有关,一个好的C程序,
在功能实现上一定有逻辑分层,在前面就说过,这样的话不仅方便开发,也方便维护升级


操作学生老师的例子

功能:
有两个结构体数组,分别放了的学生和老师的信息,然后对这些数据进行如下两种操作:

  • 打印显示数据
  • 修改数据

由于操作学生和老师的代码具有很高的相似性,所以就从里面提取出“公共代码”,操作学生和老师的
具体代码就为差异性代码,这么做的好处就是,如果以后还需要操作什么其它的人员数据,公共代码部分不
需要改动,都是共用的,只需要添加具体的差异性代码,然后对接公共代码即可,非常的方便。

                                      main函数
                                        |
                                        |调用公共代码的“显示函数”
                                        |调用公共代码的“修改函数”
                                        |

                                     公共代码
                                        |
                                        |显示数据函数   (对接差异代码的显示函数) 
                                        |修改数据函数   (对接差异代码的修改函数) 
                                        |

      差异代码——学生                     差异代码——老师                 差异代码——其它人员
        |                                  |                               ...
        |对接公共显示                      |对接公共显示                    ...
        |显示学生数据                      |显示老师数据的函数
        |                                  |
        |对接公共修改                      |对接公共修改
        |修改学生数据                      |修改老师数据的函数
        |                                  |

公共代码与差异代码之间,明显是两个分界线很明显的逻辑分层

问题是公共代码与差异代码之间,如何进行对接呢?

那么这个就需要用到“结构体封装函数指针”的做法,当然,不仅需要封装函数指针,还需要封装备操作的数据


三、例子:操作学生教师信息

代码的逻辑结构

公共代码通过“回调函数”来调用差异代码的函数时,只需要使用函数指针即可调用,
不需要直接使用函数名来调用,避免了“公共代码”也被差异化

大家试想下,如果在公共代码中,直接通过Stu_print函数名来调用“显示学生数据函数”,
那么调用老师的Tea_print函数时,公共代码就没法兼容了,
如此一来公共代码就不再是“公共代码”了,也是一个有差异性的代码

我们将公共代码独立出来后,添加操作新人员信息的差异性代码时,只要使用registerFun函数,
向“公共代码”登记操作信息即可,然后公共代码就可以回调差异代码的函数了

代码 :
stu.hstu.c: 差异性代码——操作学生
tea.htea.c: 差异性代码——操作老师
public.hpublic.c:公共代码——对接差异性代码
main.c:主函数所在.c


1. 公共代码

文件:public.h

#ifndef STU_PUBLIC_H
#define STU_PUBLIC_H

/* 用于想公共代码注册操作信息的结构体 */
struct Register
{
    void *buf;      //数组第一个元素的地址
    int n;          //数组元素个数

    void (*print)(); //打印函数的指针
    void (*alter)(); //修改函数的指针
};

extern void registerFun(struct Register *reginfo);
extern void printFun(void);
extern void alterFun(void);


#endif //STU_PUBLIC_H

文件:public.c

#include <stdio.h>
#include "public.h"


//保存差异代码注册的"reg"结构体,里面等封装了操作信息
struct Register *regp = NULL;


/* 注册函数,注册的过程就是记录下"差异代码"传递过来的reg的指针 */
void registerFun(struct Register *reginfo)
{
    regp = reginfo;
}


/* 显示所有信息 */
void printFun(void)
{
    /* 通过注册的reg信息, 通过函数指针, 调用差异代码的"显示函数",
     * 如果注册的是"学生操作信息",调用的就是学生的"显示函数",否者就是老师的"显示函数"
     *
     * 调用时, 将"数组元素个数"和"数组首元素指针"传递给下层的差异代码,
     * 差异代码拿到"数组"后,就可以操作数组中的数据了
     */
    regp->print(regp->n, regp->buf);
}


void alterFun(void)
{
    /* 修改数据,操作原理是一样的 */
    regp->alter(regp->n, regp->buf);
}

2. 学生代码

文件:stu.h

#ifndef STU_STU_H
#define STU_STU_H

/* 学生结构体 */
typedef struct Student
{
    char name[20];  // 名字
    int stuNum;     // 学号
    float score;    // 分数
}Stu;

/* 学生函数的声明 */
extern void Stu_initFun(int n, Stu buf[]);  //全局函数

//本地函数
//被毁掉的函数,是通过函数指针来调用的,此时完全可以定
//义为本地函数,定义为本地函数,可以更好的提高函数的安全性
static void Stu_alter(int n, void *buf);
static void Stu_print(int n, void *buf);


#endif //STU_STU_H

文件:stu.c

#include <stdio.h>
#include "public.h"
#include "stu.h"

//用于封装操作学生的基本信息
static struct Register reg = {}; //初始化为0


/* 功能:初始化操作,向"公共代码"注册操作学生的信息
 *      注册的信息为"学生结构体数组" 和 "操作学生数据的函数"
 * 参数:
 *   n: 学生结构体数组的元素个数
 *   buf: 数组首元素指针
 */
void Stu_initFun(int n, Stu buf[])
{
    /* 使用struct Register结构体封装操作学生的信息 */
    reg.n = n;                      // 学生结构体数组的大小
    reg.buf = (void *)buf;          // 学生数组的首元素指针,公共代码不区分具体的类型
    reg.print = Stu_print;          // 打印学生数据的函数指针,让公共代码回调
    reg.alter = Stu_alter;          // 修改学生数据的函数指针,让公共代码回调

    /* 向公共代码注册,所谓注册就
     * 是将封装的结构体传递公共代码,
     * 让公共代码通过这些信息来操作学生数据 */
    registerFun( &reg );
}


/* 功能: 通过学号找打某个学生,然后修改学生数据
 *       这个函数由"公共代码"回调,公共代码会讲数组的n和buf传递回来
 *
 * 参数:
 *  n:数组元素个数
 *  buf: 数组的首元素指针
 *       公共代码只记录存储的地址,不区分具体的类型,所以为void *
*/
static void Stu_alter(int n, void *buf)
{
    int i = 0;
    int stuNum;
    Stu *stu = (Stu *)buf; //强制转为具体Stu *, 如此才能进行具体的操作

    printf("输入学号: ");
    scanf("%d", &stuNum);

    for(i=0; i<n; i++)
    {
        if(stu[i].stuNum == stuNum)
        {
            printf("%s %d %f\n", stu[i].name, stu[i].stuNum, stu[i].score);

            printf("输入新信息\n");
            scanf("%s %d %f", stu[i].name, &stu[i].stuNum, &stu[i].score);
            break;
        }
    }

    if(i == n) printf("无此学生\n");

}


/* 功能: 显示所有学生的信息, 由"公共代码"回调
 * 参数: 同Stu_alter
*/
static void Stu_print(int n, void *buf)
{
    int i = 0;
    Stu *stu = (Stu *)buf;

    for(i=0; i<n; i++)
    {
        printf("%s %d %f分\n", stu[i].name, stu[i].stuNum, stu[i].score);
    }
}

3. 老师代码:

文件:tea.h

#ifndef STU_TEA_H
#define STU_TEA_H

/* 教师结构体 */
typedef struct Teacher
{
    char name[20];  //名字
    int teaNum;     //教师编号
    int grade;      //级别
}Tea;

extern void Tea_initFun(int n, Tea buf[]);
static void Tea_alter(int n, void *buf);
static void Tea_print(int n, void *buf);

#endif //STU_TEA_H

文件:tea.c

//
// Created by Administrator on 2019/8/11.
//

#include <stdio.h>
#include "public.h"
#include "tea.h"

static struct Register reg = {0};

void Tea_initFun(int n, Tea buf[])
{
    reg.n = n;
    reg.buf = (void *)buf;
    reg.print = Tea_print;
    reg.alter = Tea_alter;

    registerFun( &reg );
}

static void Tea_alter(int n, void *buf)
{
    int i = 0;
    int teaNum;
    Tea *tea = (Tea *)buf;

    printf("输入教师编号: \n");
    scanf("%d", &teaNum);

    for(i=0; i<n; i++)
    {
        if(tea[i].teaNum == teaNum)
        {
            printf("%s %d %d\n", tea[i].name, tea[i].teaNum, tea[i].grade);
            printf("输入新信息\n");
            scanf("%s %d %d", tea[i].name, &tea[i].teaNum, &tea[i].grade);
            break;
        }
    }
}

static void Tea_print(int n, void *buf)
{
    int i = 0;
    Tea *tea = (Tea *)buf;

    for(i=0; i<n; i++)
    {
        printf("%s %d %d级\n", tea[i].name, tea[i].teaNum, tea[i].grade);
    }
}

4. 入口文件

文件:main.c

#include <stdio.h>
#include "public.h"     //公共代码的.h
#include "stu.h"        //差异代码---操作学生信息代码的.h
#include "tea.h"        //差异代码---操作老师信息代码的.h

#define STUNUM 6        //学生结构体数组的元素个数
#define TEANUM 7        //老师结构体数组的元素个数

/* 存放"学生"数据的结构体数组  */
Stu stu[STUNUM] = {
        {"张  三", 1, 65.0},
        {"李  四", 2, 34.5},
        {"李  雪", 3, 99.5},
        {"宋  朋", 4, 75.0},
        {"黄明明", 5, 60.5},
        {"周  华", 6, 85.5},
};

/* 存放"老师"数据的结构体数组  */
Tea tea[TEANUM] = {
        {"黄  蓉", 1, 1},
        {"杨  康", 2, 4},
        {"周伯通", 3, 6},
        {"郭  靖", 4, 6},
        {"欧阳锋", 5, 8},
        {"杨  过", 6, 2},
        {"吴  邪", 6, 2},1
};

/* 主函数 */
int main(void)
{
    int select = 0;  //操作选择

    while(1)
    {
        printf("1. 操作学生\n");
        printf("2. 操作老师\n");
        scanf("%d", &select);

        if(1 == select)
            Stu_initFun(STUNUM, stu); //初始化学生的操作
        else if(2 == select)
            Tea_initFun(TEANUM, tea); //初始化老师的操作

        /* 选择初始化的谁,具体操作的就是谁 */
        while(1)
        {
            printf("1. 显示信息\n");
            printf("2. 修改信息\n");
            printf("3. 重新选择操作对象\n");
            scanf("%d", &select);

            if(1 == select) printFun();      //打印信息
            else if(2 == select) alterFun(); //修改信息
            else if(3 == select) break;
            else printf("无此选项\n");
        }

    }

    return 0;
}