模块化编程
1.为什么要模块化编程?
答:如果我是甲方,我向你提出我的需求,历经一个月你终于写出了我想要的代码。但是,你把所有函数都写在了一个c文件内,那么你觉得我还会找你后期维护吗?不可能,因为我看懂了你的整个程序框架,自己就会维护了----------------【以上纯属玩笑】
*****我们需要将函数拆分,把它模块化,这样一来我们的主函数里面没有多少东西,随着项目的增大,可能需要一个团队来完成开发,每个人负责不同的功能,比如我需要一个输入输出函数。那么A负责写输入函数,B负责写输出函数,最后经由主函数调用即可,这样做便捷高效。
2.怎么拆分已经写好的程序?
2.1程序的分解
*****将一个.c程序里面的n个子函数给拆分成一个一个的.c文件
这里先用昨晚写的程序来做示范:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 5
#define M 3
#define SIZE 3
//描述一个学生的:name age three scores
typedef struct student
{
char name[N];
int age;
float scores[M]i;
}Stu;
//函数功能:实现结构体的交换
//参数1:结构体1
//参数2:结构体2
//函数返回值:void
void swap(Stu *pa,Stu *pb)
{
Stu temp = {0};
temp = *pa;
*pa = *pb;
*pb = temp;
}
//函数功能:求总成绩
//参数1:数组的首地址(用指针)
//参数2:数组元素的个数
//函数返回值:总成绩
float sumScores(float pc[],int count)
{
float sum = 0;
int i;
for(i = 0;i < count;i++)
{
sum = sum + pc[i];
}
return sum;
}
//函数功能:封装子函数实现输出
//参数1:结构体首地址(利用指针)Stu *pc
//参数2:结构体个数int count
//函数返回值:void
void input(Stu *ps,int count)
{
int i,j;
for(i = 0;i < count;i++)
{
printf("please input the student name age scores:\n");
scanf("%s",ps->name);
scanf("%d",&ps->age);
for(j = 0;j < M;j++)
{
scanf("%f",&ps->scores[j]);
}
ps++;
}
}
//函数功能:输出
//参数1:结构体的首地址
//参数2:结构体个数
//函数返回值:void
void output(Stu *ps,int count)
{
int i,j;
for(i = 0;i < count;i++)
{
printf("%s ",ps->name);
printf("%d ",ps->age);
for(j = 0;j < M;j++)
{
printf("%.1f ",ps->scores[j]);
}
ps++;
printf("\n");
}
}
//函数功能:求年纪最大人的首地址
//参数1:结构体首地址Stu *ps
//参数2:结构体的个数int count
//函数返回值:Stu *
Stu * calPmaxAge(Stu *ps,int count)
{
//定义一个结构体指针来保存年级最大人的首地址
Stu *pMax = NULL;
//假设第一个为最大年纪人的首地址
pMax = ps;
//依次比较
int i;
for(i = 1;i < count;i++)
{
if(pMax->age < (ps+i)->age)
{
pMax = ps+i;
}
}
return pMax;
}
//函数功能:根据年纪进行排序
//参数1:结构体首地址Stu *ps
//参数2:结构体的个数int count
//返回值:void
void sortByAge(Stu *ps,int count)
{
int i,j;
for(i = 0;i < count-1;i++)
{
for(j = 0;j < count-1;j++)
{
if((ps+j)->age > (ps+j+1)->age)
{
//交换位置
swap(ps+j,ps+j+1);
}
}
}
}
//函数功能:根据年纪进行排序
//参数1:结构体首地址Stu *ps
//参数2:结构体的个数int count
//返回值:void
void sortByName(Stu *ps,int count)
{
int i,j;
for(i = 0;i < count-1;i++)
{
for(j = 0;j < count-1;j++)
{
if(strcmp((ps+j)->name,(ps+j+1)->name) > 0)
{
//交换位置
swap(ps+j,ps+j+1);
}
}
}
}
//函数功能:根据总成绩排序
//参数1:结构体首地址Stu *ps
//参数2:结构体的个数int count
//返回值:void
void sortBySumScores(Stu *ps,int count)
{
int i,j;
for(i = 0;i < count-1;i++)
{
for(j = 0;j < count-1-i;j++)
{
if(sumScores((ps+j)->scores,M) > sumScores((ps+j+1)->scores,M))
{
//交换位置
swap(ps+j,ps+j+1);
}
}
}
}
int main(void)
{
//申请SIZE个Stu那么大的空间
//void *malloc(size_t size)
//功能:申请空间
//参数:要申请空间的大小
//返回值:成功返回申请成功的首地址,失败返回NULL
//定义一个结构体指针变量,去接收malloc的返回值
int op;
Stu *ps = NULL;
Stu *pMax = NULL;
ps = (Stu *)malloc(sizeof(Stu) *SIZE);
//malloc申请到的空间没有名字,返回的只是首地址
if(NULL == ps)
{
perror("malloc error");
return -1;
}
while(1)
{
printf("请输入选项:\n");
printf("1----------input\n");
printf("2----------output\n");
printf("3----------calPmaxAge\n");
printf("4----------sortByAge\n");
printf("5----------sortByName\n");
printf("6----------sortBySumScores\n");
printf("-1---------exit\n");
scanf("%d",&op);
if(-1 == op)
{
break;
}
switch(op)
{
case 1:
input(ps,SIZE);
break;
case 2:
output(ps,SIZE);
break;
case 3:
pMax = calPmaxAge(ps,SIZE);
printf("年纪最大人的首地址为%p\n",pMax);
break;
case 4:
sortByAge(ps,SIZE);
break;
case 5:
sortByName(ps,SIZE);
break;
case 6:
sortBySumScores(ps,SIZE);
break;
}
}
//释放
//参数:申请到空间的首地址
free(ps);
ps = NULL;
//释放之后,空间被释放,但是值还在
return 0;
}
2.1.1 先看主函数:
int main(void)
{
//申请SIZE个Stu那么大的空间
//void *malloc(size_t size)
//功能:申请空间
//参数:要申请空间的大小
//返回值:成功返回申请成功的首地址,失败返回NULL
//定义一个结构体指针变量,去接收malloc的返回值
int op;
Stu *ps = NULL;
Stu *pMax = NULL;
ps = (Stu *)malloc(sizeof(Stu) *SIZE);
//malloc申请到的空间没有名字,返回的只是首地址
if(NULL == ps)
{
perror("malloc error");
return -1;
}
while(1)
{
printf("请输入选项:\n");
printf("1----------input\n");
printf("2----------output\n");
printf("3----------calPmaxAge\n");
printf("4----------sortByAge\n");
printf("5----------sortByName\n");
printf("6----------sortBySumScores\n");
printf("-1---------exit\n");
scanf("%d",&op);
if(-1 == op)
{
break;
}
switch(op)
{
case 1:
input(ps,SIZE); //输入函数
break;
case 2:
output(ps,SIZE);//输出函数
break;
case 3:
pMax = calPmaxAge(ps,SIZE);//存放年龄最大学生的结构体首地址
printf("年纪最大人的首地址为%p\n",pMax);
break;
case 4:
sortByAge(ps,SIZE);//按照学生的年龄进行排序
break;
case 5:
sortByName(ps,SIZE);//按照学生的姓名(字符串排序)
break;
case 6:
sortBySumScores(ps,SIZE);//按照三门总成绩进行排序
break;
}
}
//释放
//参数:申请到空间的首地址
free(ps);
ps = NULL;
//释放之后,空间被释放,但是值还在
return 0;
}
通过仔细观察会发现,主函数里面一直在死循环判断swatch语句,子函数一共有6个,接下来我们就将这个长长的代码拆分为一个个独立的.c文件。
2.1.2 拆分第一步: 自定义头文件
1.创建.h文件
我的整个程序名叫malloc.c,所以我要在当前目录下创建一个malloc.h文件。
.h文件包括5大块:头文件、宏定义、结构体类型的定义、枚举类型的定义、函数声明
命令:vi molloc.h
按照上图所示,把原来函数里面的头文件、宏定义、结构体类型定义、枚举以及子函数声明全都剪切过来,需要注意的一点是,子函数声明粘贴过来之后需要在最后加分号’;’
为避免以后会出现.h文件重复调用而产生错误,所以需要在定义.h文件之初先声明:如果没有定义.h文件,那么就定义.h文件,在最后结束定义
#ifndef _MALLOC_H // 如果没有定义malloc.h文件
#define _MALLOC_H //那么就定义malloc.h文件
.....
#endif //结束定义
这三个语句中的.h函数名需大写,其中的点’.‘由下划线’_'代替。
#ifndef _MALLOC_H
#define _MALLOC_H
//1.引入库函数头文件
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//2.宏定义
#define N 20 //定义姓名数组
#define M 3 //定义存放三门成绩的数组
#define SIZE 3 //定义结构体个数
//3.结构体类型定义
typedef struct person //定义一个学生信息结构体
{
char name[N]; //姓名
int age; //年龄
float scores[M]; //存放成绩数的数组
}Stu; //结尾加上Stu,重命名结构提为:Stu
//4.枚举类型定义
//5.函数声明
void sortByName(Stu *ps , int count); //姓名交换
void sortByAge(Stu *ps , int count); //按照年龄排序
void sortBySumScores(Stu *ps , int count);//按照总分排序
float Sum(float score[] , int count); //求和函数
Stu * pMaxAge(Stu *ps , int count); //取年龄最大的结构体首地址
void Input(Stu *ps , int count); //输入函数,定义一个struct person 类型的结构体指针p
void Output(Stu *p , int count); //输出函数,同上
#endif
至此,.h文件就创建完成了。
2.1.3 链接
由于主函数内目前没有各种声明了,所以需引用刚才定义的.h文件
这里需要注意的一点是:自定义的.h文件与库函数自带的.h文件的区别
一般引用头文件都是:#include<stdio.h> #include<string.h> 等等,都是以尖括号包含这些.h文件的,但是自定义的.h文件在被引用时,不能用尖括号,需要采用英文双引号"malloc.h ",这种形式。
#include<stido.h> //标注库函数头文件
#include<string.h>
#include<math.h>
//-----------//
#include"malloc.h" //自定义头文件
#include"sum.h"
2.1.4 gedit文本编译器
- 我们之前拆分的函数步骤是,把五种类型的文件给存放到一个malloc.h文件中【见2.1.2】,然后再在malloc.c函数中通过#include"malloc.h"来引用这个.h文件,最后gcc编译也是没有问题的。
- 但是这样做只是把各种声明。定义给拆分出去了,我们在malloc.c这个函数中定义的很多子函数依然还于主函数处于同一个.c文件中,为了以后项目的高效、分工,一个庞大的函数需要由多个.c文件来组成,通常我们见到许多大佬写的程序都是由多个.c文件组成的,每一个.c文件都代表不同的功能,最后经由一个主函数来调用。
- 所以我们在当前分离函数的基础上,需要再把mallo.c函数给继续拆分,把里面的每一个子函数都给封装成一个独立的.c文件,由此引入—>gedit文本编译器
我们一共有 个子函数:
void sortByName(Stu *ps , int count); //姓名交换
void sortByAge(Stu *ps , int count); //按照年龄排序
void sortBySumScores(Stu *ps , int count);//按照总分排序
float Sum(float score[] , int count); //求和函数
Stu * pMaxAge(Stu *ps , int count); //取年龄最大的结构体首地址
void Input(Stu *ps , int count); //输入函数,定义一个struct person 类型的结构体指针p
void Output(Stu *p , int count); //输出函数,同上
接下来就是用gedit:
gedit io.c pMaxAge.c sort.c sum.c malloc.c输入完后回车,你会发现一个这样的界面:
他会一次性打开多个.c文件,若有的.c文件不存在,则会新建并打开,打开以后我们按照不同函数功能,将malloc.c程序里面的子函数给剪切到对性的.c文件中去,然后每一个.c文件首行都要引用我们定义了的.h文件(malloc.h),最后保存关闭即可。
重新ls,刷新当前目录文件,这样在我们存放malloc.c的相同路径下就多了几个.c文件。
接下来介绍最重要的一个步骤:Make,首先需要弄清楚gcc的工作流程:
3、gcc工作原理
gcc编译分为四个阶段 :预处理、编译、汇编、链接
1> 预处理:
gcc -E test.c -o test.i 首先是将我们写好的.c文件编译为.i文件,这一步处理的是以#号开头的文件,也就是头文件、宏定义等等
2> 编译:
gcc -S tesi.i -o test.s 然后是将我们预处理过后的.i文件编译,生成汇编语言。【需要注意的是:汇编代码是在执行汇编前就已经产生了的】
3> 汇编:
这一步主要是将编译后形成的汇编代码翻译为机器语言----->二进代码,此时电脑已经可以识别这个程序了,但目前还没有运行权限,也就是【可执行权限】
4> 链接:
gcc test.o -o test 经过前三步的操作,我们的.c文件已经变成了.o文件,链接就是将.o文件赋予执行权限,生成tese文件,./test即可直接运行。
gcc编译的两种方式:
法一:将前三个阶段合三为一
**
gcc -c test.c -o test.o
**,生成.o文件后再运行gcc test.o -o test生成可执行文件test。
法二:直接一步生成可执行文件
gcc test.c -o test
4、多文件编译
eg:
gcc -c a.c -o a.o
gcc -c b.c -o b.o
gcc -c sum.c -o sum.o
gcc -c tmd.c -o tmd.o
可多文件同时编译,一并生成可执行文件:
gcc a.o b.o sum.o tmd.o -o test,生成可执行文件test。
- 一开始在拆分我的malloc.c函数时,我遇到了一个困扰我整个晚自习的一个问题,就是当我vi创建了一个malloc.h文件后,我把malloc.c里面的头文件、宏定义……等等【详情2.1.2】,放到malloc.h文件后,在malloc.c文件中引用.h文件,可以通过编译,但是我接着继续拆分子函数,切分为一个个.c文件后,虽然在每个.c文件中都引用了malloc.h声明,但是依然报错。
- 最后经过我仔细观看上课的回放,终于发现问题所在,原来我拆分完子函数后,并没有进行gcc编译的前三步【不清楚的可回到标题3】,由于没有进行前三步,所以这些个.c文件并没有生成.o文件,故计算机不会识别他们。
- 既然发现了问题所在,那么该怎么解决就很清楚了:我们只需要把每个.c文件都编译为.o文件呀!
还是两种方法,一步一步来,或者三步合一。我相信稍微比我聪明点的同学都知道使用第二种方法,你们万万没想到吧,我把第二种方法忘了,就在今晚更新博客时翻看昨天的笔记才发现竟有如此简单之办法。------言归正传,经过编译后每一个都声称了.o文件,这时再./malloc, 他就能正常运行了
如下图:(请自觉忽略那些个.i .s文件^ _ ^)
5、Make工程管理器
首先你要明白为什么会诞生这个命令,对于我这种初学者来说,目前我的工程还未必会用得到Makefile,但是老师多次强调,日后在工作中Makefile是一个重要工具。
对于以后的工程来说,会包含多个.c文件,如上图所示若最底层的a.c被修改了,那么可能整个工程的所有.c文件都要重新gcc编译,若你的.c文件足够多,而且他们之间都有逻辑关系,必须先编译a.c,才能继续编译b.c,再继续往上编译,如此一来,实在是太费脑细胞了,所以为了避免每次修改.c文件,不用再手动一个一个编译生成.o文件,才引入了Makefile。
- make命令会在当前路径下寻找一个叫做Makefile/makefile的文件
注意:大写的M和小写的m都可以。
这里引用一位博主的文章:原文链接:https://blog.csdn.net/qq_43687652/article/details/119713711
由成百上千个文件构成的项目,如果其中只有一个或少数几个文件进行了修改,按照之前所学的gcc编译工具,就不得不把这所有的文件重新编译一遍,因为编译器并不知道哪些文件是最近更新的
所以人们就希望有一个工程管理器能够自动识别更新了的文件代码
实际上,make工程管理器也就是个“自动编译管理器”,这里的自动是指它能够根据文件时间戳自动发现更新过的文件而减少编译的工作量,同时,它通过读入makefile文件的内容来执行大量的编译工作。用户只需编写一次简单的编译语句就可以了。
5.1Makefile规范
首先在malloc.c函数的存放路径下[vi Makefile],新建一个Makefile文件,然后开始编写Makefile:
目标文件1:依赖文件1
(Tab键)依赖文件1如何生成目标文件1
(如果你的编译器不会自动空格,需要手动按Tab建空格,再继续编写:依赖文件n如何生成目标文件n)
目标文件2:依赖文件2
(Tab键)依赖文件2如何生成目标文件2
注意:
1、 两个目标文件之间没有任何关系,默认只执行目标文件1
2、 一个Makefile中可以有多个目标
3、 调用make时,需要告诉它目标是什么,如果没有指定目标,那么make以Makefile中第一个目标作为执行目标
5.2 Makefile文件的编写方式
前几日做了一个小项目,用到了makefile,直接就拿项目来举例吧!
可以看到,一个标准的项目包括四大部分:include、src、obj、bin。他们各自存放不同的文件:
include里面用来存放自定义的头文件【.h文件】
src里面存放的是主函数和各种功能的子函数文件【.c文件】
obj存放的是汇编后的机器代码文件,没有可执行权限【.o文件】
bin目录存放的是可执行文件,默认为a.out
6.如何自定义头文件(.h文件)
6.1 头文件的创建
假如我们要为我们编写的项目创建一个名为logistics.h的头文件,第一步cd include进入到include文件夹,然后vi logistics.h,创建并打开这个.h文件。
为了避免重复引用.h函数,其定义格式为:
#ifndef _LOGISTICS_H
#define _LOGISTICS_H
//中间包括:
/*1.头文件*/
/*2.宏定义*/
/*3.结构体类型定义*/
/*4.枚举类型定义*/
/*5.函数声明*/
/*等等...*/
#endif
头文件里面要注意的是函数的声明:其格式为函数名+分号
数据类型 函数名 (形参);
6.2 src里面.c文件的编写
src里面包括一个main函数和一些功能子函数,需要注意的是:
一个程序有且只有一个main函数。
模块化编程的好处是:程序的空间复杂度会大大降低,调用完某个子函数,运行结束后空间会自动释放。并且程序的可读性、可移植性高。
我的项目一共创建了7个.c文件,有6个子函数和一个main函数。如图下所示:
子函数的编写规范:
头文件的引用方式。
<> 尖括号,表示系统自带库函数
" " 英文双引号,表示自定义库函数
我们在include文件夹里定义的logistics.h头文件该如何在另一个目录下的.c文件中引用呢?
很简单,之前我们写程序都是 #include<stdio.h>, 在另一个文件中引用的方式为:#include"…/include/logistics.h"。
…/表示上一级目录,意思是告诉它我要在上一级目录下的include文件夹中引用名字为logistics.h这个头文件。
附上一个完整.c文件:
#include"../include/logistics.h"
//删除物流订单
//参数1:Hash表的首地址
//参数2:要删除的订单编号
//返回值:成功返回OK,失败返回失败原因
int deleteHash(Hash *pHash , int id)
{
//1.入参判断
if(NULL == pHash)
{
return HASHNULL;
}
//2.判断单号是否正确
if(id < 0 || id > pHash->last_id)
{
return POSERROR;
}
//3.通过Hash函数计算链表的数组下标
int pos;
pos = HashFun(id);
//4.定义一个链表指针指向该链表的首地址
Link *pHead = NULL;
pHead = pHash->pArr[pos];
//4.1判断是否为空
if(NULL == pHead)
{
return POSERROR;
}
//5.删除
while(NULL != pHead)//横向遍历
{
//id是否匹配
if(id == (pHead->data).post_id)
{
//修改状态为:作废
pHash->count--; //订单减少一个
strcpy((pHead->data).status,"作废");
pHead = pHead->pNext; //指针后移
}
}
return OK;
}
6.3 src里面的Makefile编写
- 名词解释
CC: C编译器的名称,默认为CC
CFLAGS: C编译器的选项,无默认值
RM: 文件删除程序的名称,默认为 rm -rf - 自定义变量
OBJ: 用于存放可执行文件的名字
OBJS:用于存放所有的目标文件 - 自动变量
$@ : 表示目标文件
$<: 表示第一个依赖文件
$^: 表示所有的依赖文件
举个栗子
【横线表示Tab,横线处必须空格】
我们的Makefile文件为:
stu:stu.o io.o
stu.o:stu.c
____gcc -c stu.c -o stu.o
io.o:io.c
____gcc -c io.c -o io.o
优化为:
CC:=gcc
CFLAGS:=-c
(接下来把所有的gcc换为CC,把-c换为CFLAGS)
stu:stu.o io.o
stu.o:stu.c
____$(CC) KaTeX parse error: Expected group after '_' at position 40: … io.o:io.c _̲___(CC) $(CFLAGS) io.c -o io.o
再优化:
CC:=gcc
CFLAGS:=-c
#(接下来把所有的gcc换为CC,把-c换为CFLAGS)
stu:stu.o io.o
stu.o:stu.c
$(CC) $(CFLAGS) $< -o $@
io.o:io.c
$(CC) $(CFLAGS) $< -o $@
7 嵌套Makefie编写的流程
分为三部分:编写总控Makefile、编写src里面的Makefile、编写obj里面的Makefile
- 编写总控Makefile,书写自定义变量或预定义变量
APP:=自定义的可执行程序名称
CC:=gcc
CFLAGS:=-c
OBJS:=main.o io.o ipput.o...(src里面生成的.o文件)
export CC CFLAGS OBJS APP
ALL:
make -C src
make -C obj
.PHONY:clean #解决假目标
clean:
$(RM) bin/*
$(RM) obj/*.o
若clean:前面不加.PHONY:clean,若
- 编写
恰好在主控Makefile存在的路径下有一个名为clean的文件夹,此时运行make clean则会报错:make:"clean is up to date.意思是:clean已是最新
加上这句话后则不会报错!
注意:.PHONY必须大写 - 编写src里面的Makefile
ALL:$(OBJS)
mv $^ ../obj
____.o:____.c
$(CC) $(CFLAGS) $< -o $@
- 编写obj里面的Makefile
ALL:$(APP)
mv $(APP) ../bin
$(APP):$(OBJS)
$(CC) $^ -o $@