Linux项目构建工具--Makefile
@[toc]
零.前言
曾经听过这样一句话:会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。一个工程中的文件不计其数,makefile定义了一系列的规则来决定,哪些文件需要先进行编译,哪些文件要后编译从而进行项目的构建和开发。
1.什么是make/Makefile
我们已经学会了使用gcc或者g++来编译代码形成可执行程序,与VS中不同的是,似乎gcc和g++只能编译一个文件。如果我们想同时编译多个文件呢?就像VS中我们需要同时创建多个.cpp并同时进行编译。
这就需要用到Linux下的项目自动构建工具make和Makefile了。那么这俩货到底怎么用呢?
我们要明确make是一条指令,而Makefile是一个文件。
2.make/Makefile的使用
(1)准备工作
我们首先建立一个文件test.c,运行结果是打印"hello Makefile"。这里不多赘述。
我们要明白我们的目的,是要实现多文件编译。
我们先建立一个Makefile文件,然后打开它:
touch Makefile
vim Makefile
没错,和你想的一样,Makefile就是一个普通的文件,里面空荡荡。需要我们进行添加。
(2)依赖关系与依赖方法
在Makefile中可以调用多个文件,每一个文件必须要有两个条件,那就是依赖关系和依赖方法。
我们在Makefile中写入这样一段内容:
test:test.c //依赖关系
gcc test.c -o test //依赖方法
其中这两行指令分别代表的就是依赖关系和依赖方法。
其中,test是test.c所生成的可执行文件,因此test的生成依赖于test.c,因此称为依赖关系。
注意第二行在开头要空出一个tab的大小。依赖方法就是test.c生成test的过程。
两者缺一不可。
那这样达到的目的是什么呢?
(3)make
此时我们退出vim,并执行make指令,我们发现test已经被test.c执行生成了。
make指令的作用就是寻找名为makefile或者Makefile的文件,并执行其中的第一条依赖方法。
因此这里生成了test可执行程序。
但是当我们再次使用make的时候,会出现这样的说明:
它认为test已经是最新版本的了,只有修改test.c中的内容,才能重新进行make。
如果我们没有对应的依赖方法文件怎么办?make会自动向下寻找生成该依赖方法文件的内容,这样说比较抽象,我来举一个栗子:
比如我们要生成的编译期间的一系列文件,我们在Makefile中输入如下内容:
test:test.o
gcc test.o
test.o:test.s
gcc -c test.s -o test.o
test.s:test.i
gcc -S test.i -o test.s
test.i:test.c
gcc -E test.c -o test.i
执行make,make会在Makefile中执行第一条依赖方法,但是却发现我们要生成的test没有依赖的test.o文件,因此make会继续执行下一条依赖关系和依赖方法,发现要想生成.o文件,依然没有依赖其所依赖的test.s还会向下寻找,直到找到生成test.i的依赖关系test.c,再一层一层回溯。make会一层一层去找文件的依赖关系,直到生成第一个目标文件。
此时.i .s .o文件均被创建出来了:
(4)make clean
当我们使用make创建了test可执行程序后,我们是否有方法也通过make指令进行文件的删除呢?答案是肯定的。
我们还需要对Makefile中的文件进行一次填充:
test:test.o
gcc test.o -o test
test.o:test.s
gcc -c test.s -o test.o
test.s:test.i
gcc -S test.i -o test.s
test.i:test.c
gcc -E test.c -o test.i
.PHONY:clean
clean:
rm test.c test.i test.s test.o test
.PHONY修饰对应的符号,表示伪目标的概念,它修饰的内容总是可执行的。
比如上面的例子中make就不是总可以被执行,执行一次后如果没有完成文件刷新,就不会再执行了。
而这里定义的clean是可以执行多次的。同时,clean没有依赖方法。
我们执行make clean指令,发现执行的就是该依赖方法,文件都被删除了,同时我们可以执行多次:
这里成功执行了多次,只是没有找到已经被删除了的文件,发生了报错。
(5)依赖关系的别名
我们使用来
^代表所依赖的文件。
这样也是可以生成test可执行程序的。
3.进度条小项目
了解了make/Makefile,我们来做一个进度条的小项目,并通过make/Makefile来完成编译执行。
(1)sleep函数
注意sleep函数由于某种原因,在大部分win下的编译器是无法使用的,其中传入的参数代表秒:
#include<stdio.h>
int main()
{
printf("hello time\n");
sleep(5);
return 0;
}
这段代码运行之后会出现什么现象呢?
很显然,打印出hello time之后,程序等待5s后再结束。
如果我们将代码改为:
#include<stdio.h>
int main()
{
printf("hello time");
sleep(5);
return 0;
}
注意两者的区别在于hello time后是否有换行符。
此时程序运行发生了变化:程序等待5s之后再发生打印。
为什么会出现这样的变化呢?下面来分析一下这个问题:
其实字符串在显示到显示器之前,会先被存放在缓冲区中,而没有显示到显示器。只有当遇到\n或者程序执行结束的时候,才会刷新显示器。在第二个程序中,字符串hello time会被先存放到缓冲区,等待遇到\n或者程序执行结束的时候刷新到显示器。
printf函数已经被执行了,只不过还没将结果显示到显示器而已,程序结束后刷新显示器才会在屏幕上打印。
(2)刷新显示器
如果我们不想加\n也不想等程序结束之后再刷新显示器,我们可以使用fflush来进行手动刷新。
#include<stdio.h>
int main()
{
printf("hello time");
fflush(stdout);
sleep(5);
return 0;
}
我们使用man手册来查看一下fflush函数:
它的参数是一个流文件指针类型,其实C程序会默认打开三个输入输出流:stdin(键盘),stdout(显示器),stderr(错误显示器),在操作系统看来,其实它们都属于文件范围。我们要刷新显示器就需要向fflush中传入stdout。
此时这段代码和加入\n的那段代码的运行结果就相同了。
(3)倒计时
使用\n来刷新显示器的坏处在于,刷新后会发生换行操作。如果我们想设计一个倒计时的程序,执行结果可能就是这样的:
10
9
8
…
此时使用fflush(stdout)来刷新显示器的优势就被显示了出来。
#include<stdio.h>
int main()
{
int i=0;
for(i=10;i>=0;i--)
{
printf("%d\r",i);
fflush(stdout);
sleep(1);
}
return 0;
}
注意其中的\r表示回显,即将光标放回行初位置。但是我们发现打印倒计时的时候,10后面跟的0还一直在,9并没有将其进行覆盖。这是因为凡是显示在显示器或者在键盘读取的都是字符。我们的显示器和键盘都是字符设备,因此9只能覆盖一个字符,即10中的1。
我们可以通过预留空间来解决这一问题。
printf("%2d\r",i);
当打印的字符不足两个的时候,前面会使用空格来占位,刷新了10中的1。
(4)进度条
有了如上基础,我们就可以来实现一个简单的进度条了。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#define NUM 100
int main()
{
char bar[NUM+1];
memset(bar,'\0',sizeof(bar));
int i=0;
const char* label="|/-\\";
while(i<=100)
{
printf("[%-100s][%3d%%][%c]\r",bar,i,label[i%4]);
fflush(stdout);
bar[i]='#';
i++;
usleep(50000);
}
return 0;
}
我们可以根据个人的喜好来进行进度条内容的控制,这里我显示了进度条的进度的百分比,注意使用了usleep函数进行控制,与sleep不同的是,usleep传入的是微秒。
4.总结
本节我们了解了make和makefile的使用,偷偷说,博主刚开始也没有觉得make和makefile工具的实用之处,当我将test.c频繁地进行更改编译的时候,我发现make是真的香!如果没有make是不是还有些gcc那一长串?而在makefile的依赖方法中写一次就足够了。生成可执行的时候直接make。
我们还了解了进度条小程序,以及使用fflush函数来刷新显示器,在不想换行的情况下使用fflush来刷新缓冲区还是很实用的。
- 点赞
- 收藏
- 关注作者
评论(0)