前言
接上篇,接着学习基本工具。
博主水平有限,不足之处望请斧正。
三、make和makefile
是什么
makefile(Makefile):用来写入 依赖关系和依赖方法 的文件。
make:用来执行 makefile 的命令。
为什么
允许我们自动化构建项目,方便。
怎么用
写出makefile ==> make执行。
先见见猪跑:
[bacon@VM-12-5-centos 3-make]$ touch makefile
[bacon@VM-12-5-centos 3-make]$ vim makefile
[bacon@VM-12-5-centos 3-make]$ ls
makefile
[bacon@VM-12-5-centos 3-make]$ touch test.c
[bacon@VM-12-5-centos 3-make]$ vim test.c
[bacon@VM-12-5-centos 3-make]$ make
gcc test.c -o test
[bacon@VM-12-5-centos 3-make]$ ls
makefile test test.c
[bacon@VM-12-5-centos 3-make]$ ./test
hello makefile
test.c:
#include <stdio.h>
int main()
{
printf("hello makefile\n");
return 0;
}
makefile:
test:test.c
gcc test.c -o test
.PHONY:clean
clean:
rm -f test
“test”:目标文件(要得到的文件)。
“test.c”:依赖文件(得到目标文件的基础)。
.PHONY(adj. <口>假的;欺骗的):伪目标修饰(被修饰的目标成为伪目标,每次都生成)
“clean”:没有依赖文件的伪目标,每次都会执行。
"gcc test.c -o test ":得到目标文件test的方法。
"rm -f test ":得到目标文件clean的方法。
以上就构成了makefile的全部要素:依赖关系和依赖方法。
怎么理解呢?
又月初了,大学生张三的生活费也差不多了,需要打个电话给老爸要钱。做成这件事必要的两个要素,
张三打电话给他老爸:“老爸,我是你儿子。”——依赖关系:张三依赖他老爸。
张三接着说:“该打点生活费了哈。”——依赖方法:这件事上,张三通过要钱来对他老爸产生依赖。
为什么说是必要的呢?
张三打电话给李四的老爸:“打点生活费哈。”——依赖关系不成立:张三不依赖李四老爸。
张三打电话给他老爸:“老爸,我是你儿子。”(随即挂断电话)——没表明依赖方法:张三老爸一头雾水。
到这里,我们再看makefile:
test:test.c#依赖关系
gcc test.c -o test #依赖方法
.PHONY:clean#指定clean为伪目标(总是生成)
clean:#依赖关系
rm -f test#依赖方法
“test” 通过 "gcc test.c -o test "的方法 依赖"test.c"而生。
“.PHONY clean” 总是通过 "rm -f test " 的方法 不依赖任何文件而生。
再看,
当我们将已经编译且未修改过的文件再次编译:
[bacon@VM-12-5-centos 3-make]$ make
make: `test' is up to date.
【make是怎么知道不需要再编译的?】
[bacon@VM-12-5-centos 3-make]$ stat test.c
File: ‘test.c’
Size: 80 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 921768 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1003/ bacon) Gid: ( 1003/ bacon)
Access: 2022-12-01 19:27:01.871065494 +0800
Modify: 2022-12-01 19:27:01.515064249 +0800
Change: 2022-12-01 19:27:01.515064249 +0800
Birth: -
[bacon@VM-12-5-centos 3-make]$ stat test
File: ‘test’
Size: 8360 Blocks: 24 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 921769 Links: 1
Access: (0775/-rwxrwxr-x) Uid: ( 1003/ bacon) Gid: ( 1003/ bacon)
Access: 2022-12-01 19:28:21.868345278 +0800
Modify: 2022-12-01 19:28:21.485343938 +0800
Change: 2022-12-01 19:28:21.485343938 +0800
Birth: -
-
Access: 指最后一次读取的时间。
(一定时间/次数后才更新,读取操作太频繁,总是更新要进行更多IO,慢,也因为这个时间没那么重要,没必要随时更新)
-
Modify: 指最后一次修改内容的时间。
-
Change: 指最后一次修改属性的时间。
Access和Modify的验证:
[bacon@VM-12-5-centos 3-make]$ stat test.c
File: ‘test.c’
Size: 144 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 921768 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1003/ bacon) Gid: ( 1003/ bacon)
Access: 2022-12-01 19:33:55.887513525 +0800
Modify: 2022-12-01 19:33:55.520512242 +0800
Change: 2022-12-01 19:33:55.520512242 +0800
Birth: -
[bacon@VM-12-5-centos 3-make]$ vim test.c
[bacon@VM-12-5-centos 3-make]$ stat test.c
File: ‘test.c’
Size: 240 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 921768 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1003/ bacon) Gid: ( 1003/ bacon)
Access: 2022-12-01 19:34:17.867590404 +0800
Modify: 2022-12-01 19:34:17.290588387 +0800
Change: 2022-12-01 19:34:17.290588387 +0800
Birth: -
我们vim进行读取了,access变,也改变了内容,modigy变,
【但是为什么change也变??】
:内容改变了,文件大小也改变,文件大小也是属性呀!
Change的验证:
[bacon@VM-12-5-centos 3-make]$ chmod a+r test.c
[bacon@VM-12-5-centos 3-make]$ stat test.c
File: ‘test.c’
Size: 240 Blocks: 8 IO Block: 4096 regular file
Device: fd01h/64769d Inode: 921768 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1003/ bacon) Gid: ( 1003/ bacon)
Access: 2022-12-01 19:34:17.867590404 +0800
Modify: 2022-12-01 19:34:17.290588387 +0800
Change: 2022-12-01 19:38:12.245410193 +0800
Birth: -
好像,通过源文件和可执行程序的Modify时间就可以确定:
源文件更加地新:可执行程序已经落后了,需要重新编译。
源文件更加地新:可执行程序还没修改,不需要再编译了。
(想验证的话可以通过touch某个已存在文件来更新全部时间)
所以.PHONY的本质就是不略过这个“对比时间”的规则。
如果我们用.PHONY将test这个目标文件也变成伪目标,又能强制重复编译:
.PHONY:test
test:test.c
gcc test.c -o test
.PHONY:clean
clean:
rm -f test
[bacon@VM-12-5-centos 3-make]$ make
gcc test.c -o test
[bacon@VM-12-5-centos 3-make]$ make
gcc test.c -o test
[bacon@VM-12-5-centos 3-make]$ make
gcc test.c -o test
[bacon@VM-12-5-centos 3-make]$ make
gcc test.c -o test
makefile了解得差不多了,但make不只是单纯直接执行就万事大吉了。
- make默认生成第一个目标文件——所以我们直接make就可以生成,要是想也可以指定生成第一个
- make默认只生成一个目标文件
[bacon@VM-12-5-centos 3-make]$ make test
gcc test.c -o test
[bacon@VM-12-5-centos 3-make]$ make clean
rm -f test
[bacon@VM-12-5-centos 3-make]$ make test; make clean
gcc test.c -o test
rm -f test
makefile的推导规则
想了解它的推导规则,我们得把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 -f test.i test.s test.o test
这样其实才是真正过程:
.c ==预处理==> .i ==编译==> .s ==汇编==> .o ==链接.o==> 可执行
那我得先生成下面的.i才能一步步往后走啊?
对的,当make在当前目录找不到test依赖的test.o,就会往下走;又看到当前目录下没有test.o依赖的test.s,就又往下走;直到走到要生成test.i的时候,当前目录下有test.c,就一步步往回执行。
makefile的推导规则就像栈一样。
*并不建议这样写,直接用.c一步到位就很好,上面只是为了帮助理解。
第一个小程序:进度条
预备知识
1. 缓冲区问题
我们先来通过一个简单程序看一个现象:
#include <stdio.h>
#include <unistd.h>//休眠函数头文件
int main()
{
printf("i'm here!\n");//带\n
sleep(3);//休眠函数
return 0;
}
当我们去掉/n:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("i'm here!");
sleep(3);
return 0;
}
【明明先执行printf,才执行sleep,为什么先sleep才能看到数据被打印?】
我们的代码是顺序结构,先prinf后sleep,为什么数据休眠完才打印?
这涉及到缓冲区的概念:执行sleep前,printf一定执行完了。不过printf打印的数据而没有立即刷新到显示器,而是打印到缓冲区。而sleep执行完,程序退出,才再刷新缓冲区。就看到了“先sleep,后printf”的现象,其实只是看不见printf先执行,而不是它后执行。
【为什么不立即刷新缓冲区或是直接打印到显示器?】
因为显示器是外设,速度太慢,频繁的访问会导致效率降低——所以我们按需刷新(显示器默认一行刷新一次)。
*今天不过多解释缓冲区的概念,以免提高学习成本。
我们也可以手动刷新缓冲区:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("i'm here!");
fflush(stdout);//刷新缓冲区的函数
sleep(3);
return 0;
}
不通过显示器默认的行刷新来刷新,而是手动刷新,也能达到效果。
2. 回车换行的概念
回车和换行其实不是一个概念:
- 回车:光标移动到行首
- 换行:光标移动到下一个行同一位置
只不过我们想要的效果是 回车 + 换行,所以键盘上的ENTER键就直接是 回车+换行 的效果
(较早期的键盘,键帽形状都很形象)
一般 \r 是回车,\n是换行,不过语言给我们做了处理,\n直接就是回车换行。
进度条
我们要实现的效果大概是:
先来个倒数感受一下:
makefile:
process:main.c process.c
gcc main.c process.c -o process
.PHONY:clean
clean:
rm -f process
//process.h
#pragma once
#include <stdio.h>
void ProcessOn();
//process.c
#include "process.h"
void ProcessOn()
{
printf("test\n");
}
//main.c
include "process.h"
int main()
{
ProcessOn();
return 0;
}
[bacon@VM-12-5-centos process]$ ./process
test
makefile中不需要写.h相关的操作,因为.h就在当前目录下能找到。
再来实现进度的倒数(百分比):
倒计时当然不能用\n换行,不是我们要的效果。所以换行,打印完一个数字后换行到行首再次覆盖式打印,就能原地倒数。
void ProcessOn()
{
int cnt = 9;
while(cnt)
{
printf("On: %d\r", cnt--);
sleep(1);//休眠函数,让程序休眠1秒
}
}
[bacon@VM-12-5-centos process]$ ./process
[bacon@VM-12-5-centos process]$
程序没问题啊,为什么不打印信息?
还是缓冲区的问题,显示器默认行缓冲,咱这里换行无法刷新,所以手动刷新。
void ProcessOn()
{
int cnt = 9;
while(cnt)
{
printf("On: %d\r", cnt--);
fflush(stdout);
sleep(1);
}
}
再试试10秒:
int cnt = 10;
这可咋办?格式控制
printf("On: %2d\r", cnt--);
倒数的道理其实和进度条差不多,主要就是缓冲区问题和回车的概念,再来看看进度条吧!
//process.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#define SYMBOL '#'
#define BARSIZE 101 //100个填充符号 + \0
void ProcessOn();
//process.c
#include "process.h"
void ProcessOn()
{
char bar[BARSIZE] = "";
int cnt = 0;
while(cnt <= 100) // [0, 100]
{
//打印 1/2/.../100个SYMBOL
//-100控制右对齐
//%将%转义,让它不是格式控制前缀,只是正常的字符
printf("[%-100s] [%d%%]\r", bar, cnt);
bar[cnt++] = SYMBOL;
fflush(stdout);
//sleep(1); //每1%停1秒太慢,咱们这里让他5秒跑完:可以用usleep(),单位微秒(1微秒 = 1秒/1百万
usleep(50000); //1秒打印20次,1次需要 1/20 秒, 再乘1百万,1百万/20 = 5万
}
printf("\n完成!\n");
}
//main.c
#include "process.h"
int main()
{
ProcessOn();
return 0;
}
但,当进度不推进(卡住)的时候,我们也没法看出来呀,搞个动态的标识就好了!最终代码和效果如下:
//process.h
#pragma once
#include <stdio.h>
#include <unistd.h>
#define SYMBOL '#'
#define BARSIZE 101 //100个填充符号 + \0
void ProcessOn();
//process.c
#include "process.h"
void ProcessOn()
{
char bar[BARSIZE] = "";
char rotate[] = {'|', '/', '-', '\\'};
int cnt = 0;
while(cnt <= 100) // [0, 100]
{
printf("[%-100s] [%-3d%%][%c]\r", bar, cnt, rotate[cnt % sizeof(rotate)]); //打印 1/2/.../100个SYMBOL
bar[cnt++] = SYMBOL;
fflush(stdout);
//sleep(1); //每1%停1秒太慢,咱们这里让他5秒跑完:可以用usleep(),单位微秒(1微秒 = 1秒/1百万
usleep(50000); //1秒打印20次,1次需要 1/20 秒, 再乘1百万,1百万/20 = 5万
}
printf("\n完成!\n");
}
//main.c
#include "process.h"
int main()
{
ProcessOn();
return 0;
}
今天的分享就到这里了,感谢观看!
这里是培根的blog,期待与你共同进步,
下期见~