C语言嵌入式开发系统架构篇:模块划分

模块化编程


你有没有拼过乐高积木?乐高积木有各种各样的小块,例如长方形、正方形、三角形等等。每个小块都有特定的形状和功能,当你想要搭建一个城堡时,就会把不同形状的积木按照一定的规则组合起来,最终形成一个完整的城堡。这其中,每一块积木就像是一个模块,它们各自独立又相互配合,共同完成了搭建城堡的任务。
在 C 语言编程里,模块化编程也是类似的概念。当开发一个较为复杂的软件项目时,一般会把整个项目按照功能划分成一个个相对独立的模块。这里的模块划分,重点在 “划” 字,也就是怎样合理地将一个复杂的软件系统分解为一系列功能独立的部分,让它们相互协作,共同完成整个系统的需求。​
C语言的一个模块通常是由一个.c 源文件和一个.h 头文件组成。.c 文件里写的是模块的具体实现代码,例如函数的定义、变量的定义等等;.h 文件则主要用于声明该模块对外提供的接口,如函数原型、全局变量声明等。假设写了一个用于数学计算的模块,在.c 文件里实现加法、减法、乘法等具体的计算函数,在.h 文件里声明这些函数,让其他模块知道有哪些函数可以调用以及这些函数的参数和返回值类型 。​

为什么要划分模块


提升代码的可读性

先来看一段没有进行模块化的 C 语言代码,假设要实现一个简单的学生信息管理系统,包含添加学生信息、查询学生信息和显示学生信息的功能,如果把所有代码都写在一起,可能会这样:​

#include <stdio.h>​
#include <string.h>​
​
// 定义学生结构体​
struct Student {​
char name[50];​
int age;​
float score;​
};​
​
int main() {​
struct Student students[100];​
int studentCount = 0;​
​
// 添加学生信息功能​
struct Student newStudent;​
printf("请输入学生姓名:");​
scanf("%s", newStudent.name);​
printf("请输入学生年龄:");​
scanf("%d", &newStudent.age);​
printf("请输入学生成绩:");​
scanf("%f", &newStudent.score);​
students[studentCount] = newStudent;​
studentCount++;​
​
// 查询学生信息功能​
char searchName[50];​
printf("请输入要查询的学生姓名:");​
scanf("%s", searchName);​
int found = 0;​
for (int i = 0; i < studentCount; i++) {​
if (strcmp(students[i].name, searchName) == 0) {​
printf("找到学生:姓名 %s,年龄 %d,成绩 %.2f\n", students[i].name, students[i].age, students[i].score);​
found = 1;​
break;​
}​
}​
if (!found) {​
printf("未找到该学生\n");​
}​
​
// 显示所有学生信息功能​
printf("所有学生信息如下:\n");​
for (int i = 0; i < studentCount; i++) {​
printf("姓名 %s,年龄 %d,成绩 %.2f\n", students[i].name, students[i].age, students[i].score);​
}​
​
return 0;​
}​
​

这段代码虽然实现了基本功能,但是随着功能的增多,main函数会变得越来越冗长和复杂,代码的可读性会非常差,别人阅读和理解这段代码时会比较费劲 。​
现在对其进行模块化处理,将添加、查询和显示功能分别封装到不同的函数中,再分别放到不同的源文件和头文件里。例如创建一个student_operations.c源文件和student_operations.h头文件。​

// student_operations.h​
#ifndef STUDENT_OPERATIONS_H​
#define STUDENT_OPERATIONS_H​
​
#include <stdio.h>​
#include <string.h>​
​
// 定义学生结构体​
struct Student {​
char name[50];​
int age;​
float score;​
};​
​
// 添加学生信息函数声明​
void addStudent(struct Student students[], int *studentCount, struct Student newStudent);​
// 查询学生信息函数声明​
void searchStudent(struct Student students[], int studentCount, char searchName[]);​
// 显示所有学生信息函数声明​
void displayStudents(struct Student students[], int studentCount);​
​
#endif​
​
// student_operations.c​
#include "student_operations.h"​
​
// 添加学生信息函数定义​
void addStudent(struct Student students[], int *studentCount, struct Student newStudent) {​
students[*studentCount] = newStudent;​
(*studentCount)++;​
}​
​
// 查询学生信息函数定义​
void searchStudent(struct Student students[], int studentCount, char searchName[]) {​
int found = 0;​
for (int i = 0; i < studentCount; i++) {​
if (strcmp(students[i].name, searchName) == 0) {​
printf("找到学生:姓名 %s,年龄 %d,成绩 %.2f\n", students[i].name, students[i].age, students[i].score);​
found = 1;​
break;​
}​
}​
if (!found) {​
printf("未找到该学生\n");​
}​
}​
​
// 显示所有学生信息函数定义​
void displayStudents(struct Student students[], int studentCount) {​
printf("所有学生信息如下:\n");​
for (int i = 0; i < studentCount; i++) {​
printf("姓名 %s,年龄 %d,成绩 %.2f\n", students[i].name, students[i].age, students[i].score);​
}​
}​
​
然后在main.c中调用这些函数:​
​
// main.c​
#include "student_operations.h"​
​
int main() {​
struct Student students[100];​
int studentCount = 0;​
​
// 添加学生信息​
struct Student newStudent;​
printf("请输入学生姓名:");​
scanf("%s", newStudent.name);​
printf("请输入学生年龄:");​
scanf("%d", &newStudent.age);​
printf("请输入学生成绩:");​
scanf("%f", &newStudent.score);​
addStudent(students, &studentCount, newStudent);​
​
// 查询学生信息​
char searchName[50];​
printf("请输入要查询的学生姓名:");​
scanf("%s", searchName);​
searchStudent(students, studentCount, searchName);​
​
// 显示所有学生信息​
displayStudents(students, studentCount);​
​
return 0;​
}​


通过模块化,代码结构变得清晰,每个函数的功能一目了然,main函数也只负责调用各个功能模块,逻辑分明,提升了代码的可读性。​

增强代码可维护性

还是以上面的学生信息管理系统为例,如果后续需要修改查询学生信息的逻辑,例如按照成绩范围查询学生。在未模块化的代码中,需要在冗长的main函数中找到查询相关的代码部分进行修改,这不仅容易出错,而且如果还有其他地方也用到了类似的查询逻辑,很难保证修改的一致性。​
在模块化的代码中,只需要在student_operations.c文件中修改searchStudent函数即可,其他模块的代码不受影响。例如修改后的searchStudent函数可以如下:​

// 修改后的查询学生信息函数定义​
void searchStudent(struct Student students[], int studentCount, float minScore, float maxScore) {​
int found = 0;​
for (int i = 0; i < studentCount; i++) {​
if (students[i].score >= minScore && students[i].score <= maxScore) {​
printf("找到学生:姓名 %s,年龄 %d,成绩 %.2f\n", students[i].name, students[i].age, students[i].score);​
found = 1;​
}​
}​
if (!found) {​
printf("未找到在该成绩范围内的学生\n");​
}​
}​


这样一处修改不影响其他模块,降低了维护成本,使得代码的可维护性大大增强。​

实现代码复用​

假设又需要开发一个新的项目,也需要进行学生信息管理相关的操作。在模块化编程的情况下,可以直接复用之前写好的student_operations.c和student_operations.h模块。只需要在新的项目中包含student_operations.h头文件,然后调用其中的函数,就可以快速实现学生信息管理功能,而不需要重新编写这些功能的代码。​
例如下面编写了一个数学计算模块,包含加法、减法、乘法、除法等函数,当在不同的项目中需要进行数学计算时,都可以复用这个模块。​

// math_operations.h​
#ifndef MATH_OPERATIONS_H​
#define MATH_OPERATIONS_H​
​
// 加法函数声明​
int add(int a, int b);​
// 减法函数声明​
int subtract(int a, int b);​
// 乘法函数声明​
int multiply(int a, int b);​
// 除法函数声明​
float divide(int a, int b);​
​
#endif​
​
​
// math_operations.c​
#include "math_operations.h"​
​
// 加法函数定义​
int add(int a, int b) {​
return a + b;​
}​
​
// 减法函数定义​
int subtract(int a, int b) {​
return a - b;​
}​
​
// 乘法函数定义​
int multiply(int a, int b) {​
return a * b;​
}​
​
// 除法函数定义​
float divide(int a, int b) {​
if (b != 0) {​
return (float)a / b;​
}​
return 0;​
}​


在其他项目中使用时,只需:​

// main.c​
#include "math_operations.h"​
#include <stdio.h>​
​
int main() {​
int result1 = add(3, 5);​
int result2 = subtract(10, 4);​
int result3 = multiply(6, 7);​
float result4 = divide(15, 3);​
​
printf("3 + 5 = %d\n", result1);​
printf("10 - 4 = %d\n", result2);​
printf("6 * 7 = %d\n", result3);​
printf("15 / 3 = %.2f\n", result4);​
​
return 0;​
}​


通过模块化,把常用功能封装起来,在不同软件项目中复用,提高了开发效率。

如何进行模块划分

依据功能划分

在 C 语言中,依据功能进行模块划分是一种常用的方法。例如要开发一个学生管理系统,这个系统涉及到对学生信息的一系列操作。我们可以根据不同的功能,将其划分为多个模块。添加学生信息模块:负责接收用户输入的学生信息,如姓名、年龄、学号、成绩等,并将这些信息存储到系统中。可以定义一个函数来实现这个功能:​​

#include <stdio.h>​
#include <string.h>​
​
// 定义学生结构体​
struct Student {​
char name[50];​
int age;​
int id;​
float score;​
};​
​
// 添加学生信息函数​
void addStudent(struct Student students[], int *count, struct Student newStudent) {​
students[*count] = newStudent;​
(*count)++;​
}​


删除学生信息模块:根据用户指定的条件,例如学号,从系统中删除对应的学生信息。实现这个功能的函数可能如下:​​

// 删除学生信息函数​
void deleteStudent(struct Student students[], int *count, int idToDelete) {​
int i, j;​
for (i = 0, j = 0; i < *count; i++) {​
if (students[i].id != idToDelete) {​
students[j++] = students[i];​
}​
}​
*count = j;​
}​


查询学生信息模块:能够按照不同的查询条件,如姓名、学号、成绩范围等,从系统中检索出符合条件的学生信息并展示给用户。以按学号查询为例,函数实现如下:​​

// 查询学生信息函数​
void searchStudent(struct Student students[], int count, int idToSearch) {​
int i;​
for (i = 0; i < count; i++) {​
if (students[i].id == idToSearch) {​
printf("姓名: %s, 年龄: %d, 学号: %d, 成绩: %.2f\n", students[i].name, students[i].age, students[i].id, students[i].score);​
return;​
}​
}​
printf("未找到学号为 %d 的学生\n", idToSearch);​
}​


修改学生信息模块:允许用户对已存在的学生信息进行修改,比如修改学生的成绩、年龄等。下面是一个修改学生成绩的函数示例:​

// 修改学生信息函数​
void modifyStudentScore(struct Student students[], int count, int idToModify, float newScore) {​
int i;​
for (i = 0; i < count; i++) {​
if (students[i].id == idToModify) {​
students[i].score = newScore;​
printf("学生 %s 的成绩已修改为 %.2f\n", students[i].name, newScore);​
return;​
}​
}​
printf("未找到学号为 %d 的学生\n", idToModify);​
}​


通过功能划,让每个模块专注于一项特定的功能,使得代码结构清晰,易于理解和维护。当需要对某个功能进行修改或扩展时,只需要在对应的模块中进行操作,而不会影响到其他模块的功能。

遵循高内聚低耦合原则

高内聚是指一个模块内部的各个函数和数据之间联系紧密,这些函数和数据共同完成一个相对独立且单一的功能。例如,在一个图形绘制模块中,所有的函数和数据结构都紧密围绕图形绘制的相关操作,如绘制直线的函数、绘制圆形的函数,它们都在这个模块中协同工作。这样的模块具有很强的功能内聚性,代码的可读性和可维护性高。当需要对图形绘制功能进行优化或修改时,只需要修改这个模块内部的代码,而不用担心对其他不相关的功能产生影响。​
低耦合要求模块之间的联系要尽可能松散。模块之间应该通过简单和确定的接口进行交互,不能直接进行相互操作(如操作其它模块的函数或数据)。例如在一个游戏开发项目中,角色模块和地图模块是两个独立的模块。角色模块负责管理角色的属性、行为等,地图模块负责管理游戏地图的布局、地形等。它们之间通过一些简单的接口进行交互,角色模块可以通过地图模块提供的接口获取角色当前所在位置的地形信息,以决定角色的移动速度是否受到影响,但角色模块并不需要了解地图模块内部是如何存储和管理地图数据的。这样的低耦合设计使得每个模块都具有较高的独立性,当需要替换或修改某个模块时,对其他模块的影响较小,有利于软件的扩展和维护。如果模块之间耦合度高,一个模块的修改可能会引发一系列其他模块的连锁反应,增加了软件开发和维护的难度。​
模块之间可以通过设计合理的函数接口进行交互、尽量避免采用全局变量共享等方式来实现交互。在不同的模块中尽量减少使用相同的全局变量,而是通过函数参数传递数据,这样可以降低模块之间的耦合度。

模块划分的具体步骤

需求分析


在进行模块划分之前,需要明确项目要实现哪些功能,以及这些功能的具体要求和约束条件。以图书管理系统为例,通过与图书馆管理人员和用户沟通,分析业务流程,得出以下需求:用户能够进行图书的借阅、归还、查询操作;管理员可以对图书信息进行添加、修改、删除,以及对用户信息进行管理等。只有对这些需求有了清晰的认知,我们才能准确地划分出各个功能模块,为后续的开发工作奠定基础。

功能分解


需求明确后,下一步工作是将系统的整体功能逐步分解为一个个小的功能模块。还是以图书管理系统为例,可以将其主要功能分解为以下几个模块:​
借阅模块:负责处理用户借阅图书的业务逻辑,包括检查用户借阅资格、更新图书库存、记录借阅信息等。可以通过定义一系列函数来实现这些功能,例如borrowBook函数用于执行借阅操作,它接收用户 ID 和图书 ID 作为参数,在函数内部进行相关的数据库操作和业务逻辑判断。​
归还模块:处理用户归还图书的流程,包括检查图书是否逾期、计算逾期罚款(如果有)、更新图书状态和借阅记录等。可以编写returnBook函数来实现这个模块的功能。​
查询模块:提供多种查询方式,如按书名查询、按作者查询、按 ISBN 查询等,方便用户快速找到所需图书。可以定义不同的查询函数,如searchBookByName、searchBookByAuthor等。​
图书管理模块(管理员功能):管理员可以使用这个模块添加新图书到系统中,包括录入图书的各种详细信息;修改现有图书的信息,比如更新版本、修改库存数量等;删除已损坏或不再需要的图书。可以分别编写addBook、modifyBook、deleteBook等函数来实现这些功能。​
用户管理模块(管理员功能):管理员可以对用户信息进行管理,如添加新用户、修改用户信息(如联系方式、借阅权限等)、删除用户(在特定情况下)。同样可以通过编写相应的函数来实现这些功能,如addUser、modifyUser、deleteUser。​
通过上面的功能分解,将一个复杂的系统功能细化为多个简单、独立的功能模块,每个模块只负责一项具体的任务,便于后续的开发、测试和维护。

模块定义


模块划分完成后,还需要对模块进行定义,确定模块的功能、接口、内部数据结构和算法。
模块功能的确定:用简洁、准确的语言描述模块的功能。例如,前面提到的借阅模块,其功能就是处理用户借阅图书的完整流程,包括验证用户身份、检查图书可用性、更新借阅记录和库存等。
接口的定义:模块接口是该模块与其他模块进行交互的通道,它定义了其他模块如何调用该模块提供的函数和数据。例如借阅模块,可以定义一个borrowBook函数作为接口,其函数原型可能如下:​
int borrowBook(int userId, int bookId);​
其中userId表示借阅用户的 ID,bookId表示要借阅图书的 ID,函数返回值可以用来表示借阅操作的结果,如 0 表示借阅成功,-1 表示用户不存在,-2 表示图书不存在,-3 表示用户借阅次数已达上限等。​
数据结构的定义:根据模块的功能需求,设计合适的内部数据结构来存储和管理模块所需的数据。在图书管理系统中,可能会定义结构体来表示图书信息和用户信息,如:

// 定义图书结构体​
struct Book {​
int id;​
char title[100];​
char author[50];​
int availableCopies;​
};​
​
// 定义用户结构体​
struct User {​
int id;​
char name[50];​
int borrowedCount;​
};​

选择算法:针对模块的功能,选择合适的算法来实现。例如如在查询模块中,若要实现高效的图书查询功能,可以根据不同的查询条件选择不同的算法。按书名查询时,可以使用字符串匹配算法;若数据量较大,为了提高查询效率,还可以采用一些数据结构优化查询,如哈希表来存储图书信息,以加快按 ISBN 查询的速度。​

模块实现​

模块定义完成后,就可以开始编写C语言代码了。在编码过程中,要遵循 C 语言的编程规范,确保代码的可读性、可维护性和高效性。以图书管理系统的借阅模块为例,假设使用 MySQL 数据库来存储图书和用户信息,使用 C 语言的 MySQL C API 来进行数据库操作。下面是一个简单的borrowBook函数实现示例:​

#include <mysql/mysql.h>​
#include <stdio.h>​
#include <stdlib.h>​
​
// 假设已经定义了数据库连接相关的宏和函数​
#define DB_HOST "localhost"​
#define DB_USER "root"​
#define DB_PASS "password"​
#define DB_NAME "library"​
​
// 连接数据库函数​
MYSQL* connectDB() {​
MYSQL* conn = mysql_init(NULL);​
if (!mysql_real_connect(conn, DB_HOST, DB_USER, DB_PASS, DB_NAME, 0, NULL, 0)) {​
fprintf(stderr, "连接数据库失败: %s\n", mysql_error(conn));​
exit(1);​
}​
return conn;​
}​
​
// 借阅图书函数​
int borrowBook(int userId, int bookId) {​
MYSQL* conn = connectDB();​
char query[256];​
​
// 检查用户是否存在​
sprintf(query, "SELECT * FROM users WHERE id = %d", userId);​
if (mysql_query(conn, query) == 0) {​
MYSQL_RES* res = mysql_store_result(conn);​
if (mysql_num_rows(res) == 0) {​
mysql_free_result(res);​
mysql_close(conn);​
return -1; // 用户不存在​
}​
mysql_free_result(res);​
} else {​
fprintf(stderr, "查询用户失败: %s\n", mysql_error(conn));​
mysql_close(conn);​
return -1;​
}​
​
// 检查图书是否存在​
sprintf(query, "SELECT * FROM books WHERE id = %d", bookId);​
if (mysql_query(conn, query) == 0) {​
MYSQL_RES* res = mysql_store_result(conn);​
if (mysql_num_rows(res) == 0) {​
mysql_free_result(res);​
mysql_close(conn);​
return -2; // 图书不存在​
}​
mysql_free_result(res);​
} else {​
fprintf(stderr, "查询图书失败: %s\n", mysql_error(conn));​
mysql_close(conn);​
return -2;​
}​
​
// 检查用户借阅次数是否已达上限​
sprintf(query, "SELECT borrowed_count FROM users WHERE id = %d", userId);​
if (mysql_query(conn, query) == 0) {​
MYSQL_RES* res = mysql_store_result(conn);​
MYSQL_ROW row = mysql_fetch_row(res);​
int borrowedCount = atoi(row[0]);​
if (borrowedCount >= 5) { // 假设用户最多可借阅5本书​
mysql_free_result(res);​
mysql_close(conn);​
return -3; // 用户借阅次数已达上限​
}​
mysql_free_result(res);​
} else {​
fprintf(stderr, "查询用户借阅次数失败: %s\n", mysql_error(conn));​
mysql_close(conn);​
return -3;​
}​
​
// 更新图书库存和用户借阅记录​
mysql_autocommit(conn, 0); // 开启事务​
sprintf(query, "UPDATE books SET available_copies = available_copies - 1 WHERE id = %d", bookId);​
if (mysql_query(conn, query) != 0) {​
fprintf(stderr, "更新图书库存失败: %s\n", mysql_error(conn));​
mysql_rollback(conn);​
mysql_close(conn);​
return -4;​
}​
​
sprintf(query, "UPDATE users SET borrowed_count = borrowed_count + 1 WHERE id = %d", userId);​
if (mysql_query(conn, query) != 0) {​
fprintf(stderr, "更新用户借阅记录失败: %s\n", mysql_error(conn));​
mysql_rollback(conn);​
mysql_close(conn);​
return -4;​
}​
​
mysql_commit(conn); // 提交事务​
mysql_close(conn);​
return 0; // 借阅成功​
}​
​

模块测试​

模块编写完成后,还需要对每个模块进行单元测试,以确保其功能的正确性和稳定性。测试模块时,可以通过编写测试用例来验证模块在不同输入情况下的输出是否符合预期。​
下面以借阅模块的borrowBook函数为例,可以编写如下测试用例:​

#include <check.h>​
​
// 引入要测试的函数​
int borrowBook(int userId, int bookId);​
​
// 测试用例:测试正常借阅情况​
START_TEST(test_borrowBook_normal) {​
int result = borrowBook(1, 1); // 假设用户ID为1,图书ID为1​
ck_assert_int_eq(result, 0); // 预期借阅成功,返回0​
}​
END_TEST​
​
// 测试用例:测试用户不存在的情况​
START_TEST(test_borrowBook_user_not_exist) {​
int result = borrowBook(100, 1); // 假设不存在ID为100的用户​
ck_assert_int_eq(result, -1); // 预期返回-1,表示用户不存在​
}​
END_TEST​
​
// 测试用例:测试图书不存在的情况​
START_TEST(test_borrowBook_book_not_exist) {​
int result = borrowBook(1, 100); // 假设不存在ID为100的图书​
ck_assert_int_eq(result, -2); // 预期返回-2,表示图书不存在​
}​
END_TEST​
​
// 测试用例:测试用户借阅次数达上限的情况​
START_TEST(test_borrowBook_borrow_limit_reached) {​
// 假设用户ID为1已经借阅了5本书​
int result = borrowBook(1, 2);​
ck_assert_int_eq(result, -3); // 预期返回-3,表示用户借阅次数已达上限​
}​
END_TEST​
​
// 测试套件​
Suite* borrowBook_suite(void) {​
Suite* s;​
TCase* tc_core;​
​
s = suite_create("BorrowBook Suite");​
tc_core = tcase_create("Core");​
​
tcase_add_test(tc_core, test_borrowBook_normal);​
tcase_add_test(tc_core, test_borrowBook_user_not_exist);​
tcase_add_test(tc_core, test_borrowBook_book_not_exist);​
tcase_add_test(tc_core, test_borrowBook_borrow_limit_reached);​
​
suite_add_tcase(s, tc_core);​
​
return s;​
}​
​
int main(void) {​
int number_failed;​
Suite* s;​
SRunner* sr;​
​
s = borrowBook_suite();​
sr = srunner_create(s);​
​
srunner_run_all(sr, CK_NORMAL);​
number_failed = srunner_ntests_failed(sr);​
srunner_free(sr);​
​
return (number_failed == 0)? EXIT_SUCCESS : EXIT_FAILURE;​
}​


上面的测试用例,覆盖了正常情况和各种异常情况,可以有效地验证borrowBook函数的正确性。