Linux项目自动化构建工具 - make/Makefile 练习 进度条 倒计时
make/Makefile的重要性
- 会不会写Makefile,从侧面说明了一个人是否具备完成大型工程的能力。
- 一个工程的源文件不计其数,按照其类型、功能、模块分别放在若干个目录当中,Makefile定义了一系列的规则来指定:哪些文件需要先编译,哪些文件需要后编译,甚至于进行更复杂的功能操作。
- Makefile带来的好处就是“自动化编译”,一旦写好,只需一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
- mak是一个命令工具,是一个解释Makefile当中指令的命令工具,一般来说,大多数的IDE都有这个命令,例如:Delphi的make,Visual
- C++的nmake,Linux下GNU的make。可见,Makefile都成为了一种在工程方面的编译方法。
- make是一条命令,Makefile是一个文件,两个搭配使用,完成项目自动化构建。
依赖关系和依赖方法
在使用make/Makefile前我们首先应该理解各个文件之间的依赖关系以及它们之间的依赖方法。
依赖关系: 文件A的变更会影响到文件B,那么就称文件B依赖于文件A。
例如,test.o文件是由test.c文件通过预处理、编译以及汇编之后生成的文件,所以test.c文件的改变会影响test.o,所以说test.o文件依赖于test.c文件。
依赖方法: 如果文件B依赖于文件A,那么通过文件A得到文件B的方法,就是文件B依赖于文件A的依赖方法。
例如,test.o依赖于test.c,而test.c通过gcc -c test.c -o
test.o指令就可以得到test.o,那么test.o依赖于test.c的依赖方法就是gcc -c test.c -o test.o。
就相当于月末没钱,找父母要,其中依赖关系是我是你孩子,依赖方法就是要钱,缺一不可。
多文件编译
当你的工程当中有源文件的时候,应该如何进行编译生成可执行程序呢?
gcc
首先,我们可以直接使用gcc指令对源文件进行编译,进而生成可执行程序。

单个文件:使用源文件直接生成可执行程序

但进行多文件编译的时候一般不使用源文件直接生成可执行程序,而是先用每个源文件各自生成自己的二进制文件,然后再将这些二进制文件通过链接生成可执行程序。

1.先各自生成二进制文件
2.将二进制文件通过链接生成可执行程序
原因:
若是直接使用源文件生成可执行程序,那么其中一个源文件进行了修改,再生成可执行程序的时候就需要将所以的源文件重新进行编译链接。
而若是先用每个源文件各自生成自己的二进制文件,那么其中一个源文件进行了修改,就只需重新编译生成该源文件的二进制文件,然后再将这些二进制文件通过链接生成可执行程序即可。
make 和Makefile
但是随着源文件个数的增加,我们每次重新生成可执行程序时,所需输入的gcc指令的长度与个数也会随之增加。这时我们就需要使用make和Makefile(m可以大写或小写)了,这将大大减少我们的工作量
第一步:在源文件所在目录下创建一个名为Makefile/makefile的文件。

第二步 :用vim编写格式
第一种

黄色的为依赖关系白色的为方法

make是如何⼯作的,在默认的⽅式下,也就是我们只输⼊make命令。
那么: 1. make会在当前⽬录下找名字叫“Makefile”或“makefile”的⽂件。
2. 如果找到,它会找⽂件中的第⼀个⽬标⽂件(target),在上⾯的例⼦中,他会找到 myproc 这 个⽂件,并把这个⽂件作为最终的⽬标⽂件。
3. 如果 myproc ⽂件不存在,或是 myproc 所依赖的后⾯的 myproc.o ⽂件的⽂件修改时间要 ⽐ myproc 这个⽂件新(可以⽤ touch 测试),那么,他就会执⾏后⾯所定义的命令来⽣成 myproc 这个⽂件。
4. 如果 myproc 所依赖的 myproc.o ⽂件不存在,那么 make 会在当前⽂件中找⽬标为 myproc.o ⽂件的依赖性,如果找到则再根据那⼀个规则⽣成 myproc.o ⽂件。(这有点像⼀ 个堆栈的过程) 5. 当然,你的C⽂件和H⽂件是存在的啦,于是 make 会⽣成myproc.o ⽂件,然后再⽤myproc.o ⽂件声明 make 的终极任务,也就是执⾏⽂件 hello 了。
6. 这就是整个make的依赖性,make会⼀层⼜⼀层地去找⽂件的依赖关系,直到最终编译出第⼀个 ⽬标⽂件。
7. 在找寻的过程中,如果出现错误,⽐如最后被依赖的⽂件找不到,那么make就会直接退出,并 报错,⽽对于所定义的命令的错误,或是编译不成功,make根本不理。
第二种

Makefile文件的简写方式:

变量定义部分
BIN=test:定义变量BIN,代表最终生成的可执行文件名,这里设置为test。SRC=$(wildcard *.c):使用wildcard函数获取当前目录下所有扩展名为.c的源文件,将结果赋值给变量SRC。OBJ=$(SRC:.c=.o):利用变量替换功能,把SRC中所有.c后缀的文件名替换为.o后缀,得到对应的目标文件列表,赋值给变量OBJ。CC=gcc:定义变量CC,指定使用gcc作为编译器。RM=rm -f:定义变量RM,表示删除文件的命令,-f选项表示强制删除,不询问。
编译规则部分
$(BIN):$(OBJ):这是一个依赖规则,表明可执行文件$(BIN)(即test)依赖于所有的目标文件$(OBJ)(由.c文件编译生成的.o文件)。$(CC) $^ -o $@:当满足上一条依赖规则需要重新生成目标时,执行此命令。$(CC)是前面定义的gcc编译器;$^是自动化变量,代表所有依赖文件,即$(OBJ)中的目标文件;-o是gcc选项,用于指定输出文件名;$@是自动化变量,代表当前规则的目标文件,即$(BIN)(test)。该命令整体意思是用gcc将所有目标文件链接成可执行文件test。echo "链接 $^ 成 $@":执行链接命令后,输出提示信息,说明正在将哪些依赖文件链接成哪个目标文件。
模式规则部分
%.o:%.c:这是一个模式规则,%是通配符,表示所有.o文件依赖于对应的.c文件,比如main.o依赖于main.c。$(CC) -c $<:针对上述模式规则,当.c文件发生变化需要重新编译时执行此命令。$(CC)是gcc编译器;-c选项表示只进行编译,不链接;$<是自动化变量,代表当前规则的第一个依赖文件,也就是对应的.c源文件。该命令会将.c文件编译成.o目标文件。echo "编译... $< 成 $@":编译完成后,输出提示信息,显示正在将哪个.c文件编译成哪个.o文件。
伪目标和清理规则部分
.PHONY:clean:声明clean为伪目标。这意味着即使当前目录下存在名为clean的文件,make也会执行clean规则,而不是认为它已经是最新的而不执行。clean::clean规则的开始。@$(RM) $(OBJ) $(BIN):执行此命令,@表示执行命令时不显示命令本身;$(RM)是前面定义的删除命令;该命令会删除所有目标文件$(OBJ)和可执行文件$(BIN)(test) ,起到清理编译生成文件的作用。
Linux小练习 ———倒计时


printf("%-2d\r", a);:使用printf函数输出变量a的值。%-2d是格式化字符串,-表示左对齐,2表示输出宽度为 2 个字符;\r是回车符,在 Linux 终端中,它会将光标移到当前行的开头。fflush(stdout);:刷新标准输出缓冲区。在 Linux 系统中,为了提高 I/O 效率,输出操作往往不是立即执行的,而是先将数据存储在缓冲区中。调用fflush(stdout)可以强制将缓冲区中的数据立即输出到终端。
小练习——进度条
1. process.h 和 process.c 文件
宏定义
#define NUM 101
#define STYLE '='
NUM定义了进度条字符数组的长度,101用于存储进度条字符和字符串结束符'\0'。STYLE定义了进度条使用的字符,这里使用=来表示进度。
process_v1 函数
void process_v1()
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable="|/-\\";
int len = strlen(lable);
int cnt = 0;
while(cnt <= 100)
{
printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt%len]);
fflush(stdout);
buffer[cnt]= STYLE;
cnt++;
usleep(50000);
}
printf("\n");
}
- 初始化部分:
char buffer[NUM];:定义一个字符数组buffer用于存储进度条的字符。memset(buffer, 0, sizeof(buffer));:将buffer数组的所有元素初始化为0,即空字符'\0'。const char *lable="|/-\\";:定义一个常量字符串lable,用于显示旋转的进度指示符。int len = strlen(lable);:计算lable字符串的长度。int cnt = 0;:初始化计数器cnt为0,表示进度从0%开始。
- 循环部分:
while(cnt <= 100):循环条件,当进度小于等于100%时继续循环。printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt%len]);:输出进度条信息,%-100s表示左对齐输出长度为100的字符串,%d%%输出当前进度百分比,%c输出旋转指示符。\r用于将光标移动到行首,实现覆盖输出。fflush(stdout);:强制刷新标准输出缓冲区,确保进度条信息立即显示。buffer[cnt]= STYLE;:将buffer数组中当前进度位置的字符设置为STYLE(即=)。cnt++;:计数器加1,表示进度增加1%。usleep(50000);:暂停50000微秒(即50毫秒),模拟进度更新的延迟。
- 结束部分:
printf("\n");:进度达到100%后,输出换行符,结束进度条显示。
FlushProcess 函数
void FlushProcess(double total, double current)
{
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable="|/-\\";
int len = strlen(lable);
static int cnt = 0;
int num = (int)(current*100/total);
int i = 0;
for(; i < num; i++)
{
buffer[i] = STYLE;
}
double rate = current/total;
cnt %= len;
printf("[%-100s][%.1f%%][%c]\r", buffer, rate*100, lable[cnt]);
cnt++;
fflush(stdout);
}
- 初始化部分:
- 与
process_v1函数类似,定义并初始化buffer数组和lable字符串。 static int cnt = 0;:定义一个静态变量cnt,用于控制旋转指示符的显示。静态变量在函数调用结束后会保留其值。int num = (int)(current*100/total);:根据当前完成量current和总量total计算当前进度的百分比对应的字符数量。
- 与
- 填充进度条部分:
for(; i < num; i++):循环将buffer数组中前num个元素设置为STYLE(即=)。
- 计算进度和输出部分:
double rate = current/total;:计算当前进度的比例。cnt %= len;:对cnt取模,确保cnt的值在0到len - 1之间。printf("[%-100s][%.1f%%][%c]\r", buffer, rate*100, lable[cnt]);:输出进度条信息,%.1f%%表示输出一位小数的百分比。cnt++;:cnt加1,用于更新旋转指示符。fflush(stdout);:强制刷新标准输出缓冲区。
2. main.c 文件
全局变量
double total = 1024.0;
double speed = 1.0;
total:定义文件的总大小为1024.0,单位可以理解为MB。speed:定义下载速度为1.0,单位可以理解为MB/s。
DownLoad 函数
void DownLoad()
{
double current = 0;
while(current <= total)
{
FlushProcess(total, current);
usleep(3000);
current += speed;
}
printf("\ndownload %.2lfMB Done\n", current);
}
- 初始化部分:
double current = 0;:初始化当前下载量current为0。
- 循环部分:
while(current <= total):循环条件,当当前下载量小于等于总大小时继续循环。FlushProcess(total, current);:调用FlushProcess函数更新进度条显示。usleep(3000);:暂停3000微秒(即3毫秒),模拟下载数据的延迟。current += speed;:当前下载量增加speed的值。
- 结束部分:
printf("\ndownload %.2lfMB Done\n", current);:下载完成后,输出下载完成的信息。
main 函数
int main()
{
DownLoad();
DownLoad();
DownLoad();
DownLoad();
DownLoad();
DownLoad();
DownLoad();
DownLoad();
return 0;
}
多次调用 DownLoad 函数,模拟多次文件下载过程。
完整代码
process.h
#pragma once
#include <stdio.h>
// 声明进度条函数
void process_v1();
void FlushProcess(double total, double current);
process.c
#include "process.h"
#include <string.h>
#include <unistd.h>
#define NUM 101
#define STYLE '='
// 版本1:简单的进度条实现
void process_v1() {
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable = "|/-\\";
int len = strlen(lable);
int cnt = 0;
while (cnt <= 100) {
printf("[%-100s][%d%%][%c]\r", buffer, cnt, lable[cnt % len]);
fflush(stdout);
buffer[cnt] = STYLE;
cnt++;
usleep(50000);
}
printf("\n");
}
// 版本2:根据总进度和当前进度更新进度条
void FlushProcess(double total, double current) {
char buffer[NUM];
memset(buffer, 0, sizeof(buffer));
const char *lable = "|/-\\";
int len = strlen(lable);
static int cnt = 0;
int num = (int)(current * 100 / total);
int i = 0;
for (; i < num; i++) {
buffer[i] = STYLE;
}
double rate = current / total;
cnt %= len;
printf("[%-100s][%.1f%%][%c]\r", buffer, rate * 100, lable[cnt]);
cnt++;
fflush(stdout);
}
main.c
#include "process.h"
#include <stdio.h>
#include <unistd.h>
double total = 1024.0;
double speed = 1.0;
// 模拟下载函数,调用进度条更新函数
void DownLoad() {
double current = 0;
while (current <= total) {
FlushProcess(total, current);
usleep(3000);
current += speed;
}
printf("\ndownload %.2lfMB Done\n", current);
}
int main() {
// 多次调用下载函数
for (int i = 0; i < 8; i++) {
DownLoad();
}
return 0;
}
