Linux下的Makefile与进度条程序
目录
Linux下的Makefile与进度条程序
Makefile与make
Makefile与make介绍
创建第一个Makefile并使用make
Makefile文件基本格式介绍
Makefile依赖方法执行过程
Makefile通用写法
进度条程序
实现效果
前置知识
回车(\r)与换行(\n)
输出缓冲区
实现进度条
Linux下的Makefile与进度条程序
Makefile
与make
Makefile
与make
介绍
在Linux中,Makefile
是一个文件,make
是一个指令,当使用make
指令时,该指令会在当前目录下找Makefile
文件从而执行内部的内容
创建第一个Makefile
并使用make
首先,在当前目录下创建一个Makefile
文件(也可以写成makefile
),例如:
接下来在同级目录下创建一个code.c
文件
使用vim编辑器输入下面的内容:
#include <stdio.h>
int main()
{
printf("hello linux\n");
return 0;
}
保存code.c
文件后退出当前vim,使用vim打开Makefile
文件,输入下面的内容:
code:code.c
gcc -o code code.c
.PHONY:clean
clean:
rm -rf code
需要注意,gcc -o code code.c
和rm -rf code
前方是一个Tab键的大小,而不是4个或者8个空格
保存Makefile
文件后退出当前vim,在当前目录下输入make
指令即可在当前目录下创建code.c
对应的可执行文件(具有可执行权限并且文件本身可执行)code
,例如下图:
通过常规方式运行该可执行文件./code
即可看到打印输出的内容:
接着使用make clean
指令清理刚才生成的可执行文件code
:
Makefile
文件基本格式介绍
以前面例子中的Makefile
为例:
code:code.c
gcc -o code code.c
.PHONY:clean
clean:
rm -rf code
- 第一行中的
code:code.c
代表依赖关系,code
表示目标文件,code.c
表示依赖文件列表中的文件,第二行的gcc -o code code.c
代表依赖方法(指令) - 第三行中的
.PHONY
表示生成一个伪目标,clean
表示伪目标的名字(可以类比变量名) - 第四行及第五行与第一行及第二行含义一致,表示依赖关系和依赖方法,而因为
clean
没有需要依赖的文件,所以clean:
后没有任何依赖文件列表文件
依赖关系:表示两个文件之间构成的一定关系,比如父子关系
依赖方法:通过依赖方法可以执行的对应的指令
依赖文件列表:code.c
所处的位置即为依赖文件列表,为了生成目标文件code
而需要的文件称为依赖文件,依赖文件列表可以含有不止一个文件
注意:理论上来说,依赖文件列表中的code.c
在当前情况下可以不写,但是如果不写,在第一次执行make
指令后,不论之后code.c
是否修改,再执行make
指令都无法执行对应的依赖方法,因为code
文件已经存在,所以为了保证可以修改,需要加上code.c
从上面的运行结果可以看出,每一次执行make时都会在控制台回显出对应的依赖方法,如果将编译指令改为echo "测试"
,则效果如下:
可以看到先回显了对应的依赖方法,再执行依赖方法,如果不希望出现这种情况,可以在执行的指令前加上@
使指令不再回显,所以上面的Makefile
可以修改为:
code:code.c
@echo "测试"
运行结果如下:
所以原始的Makefile
可以修改为:
code:code.c
@echo "Start Compiling..."
@gcc -o code code.c
@echo "End Compiling..."
.PHONY:clean
clean:
@echo "Cleaning code..."
@rm -rf code
@echo "End Cleaning..."
一个依赖集中可以有多个依赖方法
此时正常运行结果如下:
如果代码出现错误,则gcc
会中断编译,所以此时运行结果如下:
使用.PHONY
可以生成一个指定名字的伪目标,伪目标的作用是:清除依赖方法执行时进行的文件时间对比,下面是具体介绍:
首先,在Linux中可以使用stat+文件名
查看文件当前的属性,对于code.c
有:
执行结果中,主要关注三个部分:Access
、Modify
和Change
,这三个部分分别表示文件最近一次的访问时间、文件内容被修改的时间和文件属性被修改的时间
Access
时间:一般不是特别精确,因为如果一个文件访问一次就需要更新一次访问时间,那么对于多个文件来说,这种操作的消耗对于CPU来说是很大的Modify
时间:Modify
时间只表示文件内容被修改的时间,如果文件属性时间修改,则不影响Modify
时间,但是需要注意,Modify时间一旦改变一般伴随着Change
时间改变,因为修改文件内容有时会影响到文件的相关属性(例如文件大小等)Change
时间:Change
时间只表示文件属性被修改的时间,修改文件属性时间不会影响Modify
时间
接着,观察对于没有添加伪目标的Makefile
第一部分依赖集,如果code
文件已经存在,再一次进行make
的效果:
code:code.c
@echo "Start Compiling..."
@gcc -o code code.c
@echo "End Compiling..."
如果此时对code.c
文件进行修改,那么执行结果会有所不同:
那么指令是如何知道文件是否被修改呢?就是通过前面提到的Modify
时间和Change
时间,过程如下图所示:
因为code.c
创建的时间早于code.c
编译的时间,所以开始时不存在code
文件,所以第一次执行make
指令时正常执行。
当code.c
文件未修改时,第二次执行make
指令会发现code.c
的Modify
时间和Change
时间依旧在make
之前,因为第一次已经满足了code.c
的两个时间在code
文件的两个时间之前,所以gcc
就不会再进行一次编译。
当修改code.c
文件后,code.c
的Modify
时间和Change
时间改变,导致code.c
的两个时间在code
文件的两个时间之后,此时gcc
就可以正常执行,从而make
指令不受影响
而如果再Makefile
中为这一部分添加一个伪目标,则可以清除指令中文件时间的对比过程:
.PHONY:code
code:code.c
@echo "Start Compiling..."
@gcc -o code code.c
@echo "End Compiling..."
此时无论执行多少次make
指令,都不会出现make
指令中gcc
因为文件时间对比而导致执行结果不同:
make
指令虽然结果完全相同,但是不代表依赖方法没有执行,即文件确实每一次都重新编译
执行完编译部分的make
指令,想要执行删除code
文件对应的make
指令需要在make
后加上clean
,这个clean
代表伪目标名,之所以前面直接使用make
就可以执行编译指令,是因为make
指令在读取Makefile
文件时是从上至下顺序查找,而直接使用make
,就会执行第一个依赖集对应的依赖方法,执行完毕后就不会再继续往下读;而对于删除code
文件的指令来说,其所在位置时Makefile
中的第二个依赖集,所以需要告诉make
指令找哪一部分
所以,此处可以看出.PHONY
的第二个作用就是声明一个伪目标,通过该伪目标帮助make
指令快速定位需要执行的依赖集
如果细心可以发现,对于clean
依赖集来说,不论是否有.PHONY
都可以无限制执行rm -rf
依赖方法,所以可以推断出rm -rf
指令本身不会考虑文件的时间属性,但是为什么此处还需要加.PHONY
?一方面是为了声明伪目标,另一方面是为了当前依赖集中的其他指令会有时间对比
Makefile
依赖方法执行过程
前面学习到,当执行gcc -o code code.c实际上是分成了四步,即:
code.c
文件编译生成code.i
文件code.i
文件编译生成code.s
文件code.s
文件编译生成code.o
文件code.o
文件编译生成code
可执行文件
将对应的指令写入Makefile
中,代码如下:
code:code.o
gcc -o code code.o
code.o:code.s
gcc -c code.s -o code.o
code.s:code.i
gcc -S code.i -o code.s
code.i:code.c
gcc -E code.c -o code.i
.PHONY:clean
clean:
@rm -rf code
根据make
从上至下的运行顺序,首先执行gcc -o code code.o
,但是,因为code.o不存在,并且code.o
文件依赖于code.s
文件,所以继续执行code.o:code.s
对应的依赖方法,以此类推直到最后一条依赖方法gcc -E code.c -o code.i
执行向上返回执行前面未执行的依赖方法。整个过程可以理解为在一个栈中操作:
假设此处执行的依赖方法同样进栈
所以执行的结果如下图所示:
实际上,在真正开发中,只需要用到两个部分,如下:
code:code.o
gcc -o code code.o
code.o:code.c
gcc -c code.c -o code.o
此时运行结果如下:
Makefile
通用写法
在前面的Makefile中,每一个依赖方法都需要在前面的依赖关系部分的文件重新写一遍,为了简化过程,可以使用下面的写法:
TARGET=code
SRC=code.o
$(TARGET):$(SRC)
$(CC) -o $@ $<
%.o:%.c
$(CC) -c $< -o $@
.PHONY:clean
clean:
@rm -rf $(TARGET) $(SRC)
上面的代码中,首先创建了两个变量分别代表生成的目标文件code
以及第一个依赖集中的依赖文件列表中的文件,在依赖方法中使用了两个自动变量(一般建议大写),分别是$@
和$<
在Makefile
中,$@
表示生成的目标文件,$<
表示从依赖文件列表中取出一个文件,对应的还有$^
表示依赖文件列表中的所有文件
而对于gcc
来说,在Makefile
中可以使用内置变量CC
(表示C编译器的名字)代替
如果涉及到多个文件编译,则在SRC
和%.c
处使用空格分隔每一个文件
至此,一个基本的Makefile文件编写语法就这么多,如果需要更详细了解Makefile文件,请自行搜索
进度条程序
实现效果
前置知识
回车(\r
)与换行(\n
)
在C语言或者其他高级语言中,换行(\n
)表示回到下一行的开始处,实际上换行的效果并不如此
回车(\r
):回到当前光标所在行的开始
换行(\n
):前往光标所在行的下一行,但是光标是平行向下移动
输出缓冲区
观察下面程序运行的结果:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello linux\n");
sleep(2);
return 0;
}
如果在Linux终端运行该程序,可以看到程序先打印了hello linux
,然后等待了2秒才显示prompt
提示
这里的sleep
函数不是Windows下的Sleep
函数,但是效果基本一致
将上面的程序修改为下面的程序,再观察效果:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello linux");
sleep(2);
return 0;
}
可以看到程序先等待了2秒,然后才打印hello linux
C语言程序默认从上往下顺序执行代码,所以不可能是先执行了sleep(2)
才执行printf("hello linux");
,出现这种现象的原因就是因为缓冲区的存在,程序在输出时并不会直接将内容输出到显示器上,而是先输出到输出缓冲区,再通过刷新/结束缓冲区将内容打印到屏幕上,而之所以在有\n
时会显示再等待就是因为\n
刷新了缓冲区,导致内容打印到了屏幕上
如果使用将\n
替换为\r
则同样会先等待再打印,因为\r
也不具备刷新缓冲区的效果,如果在当前情况下想刷新缓冲区但又不想使用\n
,则可以使用fflush()
函数,传递参数为标准输出stdout
,代码如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello linux");
fflush(stdout);
sleep(2);
return 0;
}
实现进度条
基础版进度条
思路:首先创建3个文件,分别是测试文件main.c
、头文件process.h
和实现文件process.c
。对于进度条,实际上就是先打印原数组内容,再填充数组然后刷新缓冲区,对于百分比,只需要使用循环变量控制即可,对于最右侧闪烁的符号,实际上就是四个动画帧符号,每一次循环加载一个动画帧即可,但是为了循环加载,需要使下标在指定范围内循环,可以考虑循环队列(数组版)下标轮回的思路,所以基本代码如下:
// 实现文件
#include "process.h"
void process()
{
char bar[NUM] = {0};// 进度条数组
int count = 0;
const char *label = "|/-\\";//控制进度条最右侧的闪烁符号(| 顺时针旋转)
size_t len = strlen(label);
while(count <= 100)
{
// [%-100s]预留100个字符的位置,每一次打印数组bar中的字符,因为初始化为0,所以数组中全是'\0',当遇到第一个'\0'就停止,加上-表示从右往左(默认从左往右)打印
// 使用count控制进度条百分比,百分号%需要额外转义
// label闪烁符号,使用count%4使下标循环在0-3,思路可以联想循环队列数组版控制下标轮回的方式
printf("[%-100s][%d%%][%c]\r", bar, count, label[count%len]);
fflush(stdout); // 先刷新缓冲区,打印出停留在缓冲区的内容
bar[count++] = STYLE; // 向数组中添加字符
usleep(20000);// 睡眠时间单位为微秒,1秒 = 1000毫秒=1000000微秒
}
printf("\n");
}
// 头文件
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define NUM 101 // 定义进度条字符的个数
#define STYLE '#' // 定义进度条的样式
void process();
// 测试文件
#include "process.h"
int main()
{
process();
return 0;
}
对应的Makefile
如下:
TARGET=process
SRC=process.c main.c
$(TARGET):$(SRC)
$(CC) $^ -o $@
.PHONY:clean
clean:
rm -rf $(TARGET)