Linux:make自动化和实战演练
Hello,小伙伴们!又到了咱们一起捣鼓代码的时间啦!💪 把生活调成热情模式,带着满满的能量钻进编程的奇妙世界吧——今天也要写出超酷的代码,冲鸭!🚀
我的博客主页:喜欢吃燃面
我的专栏:《C语言》,《C语言之数据结构》,《C++》,《Linux学习笔记》
感谢你点开这篇博客呀!真心希望这些内容能给你带来实实在在的帮助~ 如果你有任何想法或疑问,非常欢迎一起交流探讨,咱们互相学习、共同进步,在编程路上结伴成长呀!
一.make与makefile
1. make和makefile的概念
- 核心关联:能否编写Makefile,侧面反映是否具备完成大型工程的能力。
- Makefile作用:定义规则,指定文件编译顺序、重编译需求及复杂操作,实现“自动化编译”,写好后仅需make命令即可完成整个工程编译,大幅提升开发效率。
- 相关工具:make是解释Makefile指令的命令工具,多数IDE均支持(如Delphi的make、Visual C++的nmake、Linux下GNU的make)。
- 关系与目标:make是命令,Makefile是文件,二者搭配实现项目自动化构建,成为工程编译的通用方法。
注:习惯上我们将make命令执行的文件命名为Makefile。
2. make和Makefile的使用
我们先准备一个“”查找100以内素数“”的test.c文件
#include <stdio.h>int main() {printf("100以内的素数:\n");// 遍历2到100的所有数(1不是素数)for (int n=2; n<=100; n++) {int f=1; // 1:默认是素数,0:不是// 用2到n/2的数试除,判断是否能整除for (int i=2; i<=n/2; i++)if (n%i == 0) {f=0; break;} // 能整除则标记非素数并退出if (f) printf("%d ", n); // 是素数则打印}putchar('\n');return 0;
}
然后我们创建Makefile文件。并配置成如下格式:
touch Makefile #创建文件
vim Makefile #编辑文件cat Makefile #查看Makefile内容
# 定义目标文件名
TARGET = test# 编译规则:默认目标为运行程序
all: run# 编译生成可执行文件
$(TARGET): $(TARGET).cgcc -o $(TARGET) $(TARGET).c# 运行程序
run: $(TARGET)./$(TARGET)# 清理编译生成的文件
clean:rm -f $(TARGET)
然后我们输入make run。test.c文件就被自动编译并运行了。如下:
make run
./test
100以内的素数:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
3.配置Makefile文件的语法格式
我们以运行test.c的makefile文件为例,来分析语法:
3.1 定义目标文件名(定义变量)
# 定义目标文件名
TARGET = test
在 Makefile 里,TARGET = test 其实是给一个叫 TARGET 的“标签”起了个值叫 test。
简单说,这个“标签”就像一个代号,后面在写编译规则的时候,不用反复写 test 这个具体名字,直接用 TARGET 这个代号就行。
比如要生成名为 test 的可执行文件,定义这个之后,后续所有涉及文件名的地方,都可以用 TARGET 代替。这样做的好处是,如果以后想把文件名改成别的(比如 app),只需要改这一行的 test 为 app,其他地方不用动,既方便又不容易出错,还能让整个文件的结构更清楚,一眼就知道 TARGET 代表的是最终要生成的那个文件。
类比一下:类似于C语言当中的宏替换。
3.2 all: run 目标依赖规则
# 编译规则:默认目标为运行程序
all: run
在 Makefile 里,all: run 是一条“目标依赖”规则。
这里的 all 是一个特殊的目标(通常作为默认目标,即输入 make 命令时不指定目标就会执行它),而 run 是它所依赖的另一个目标。
意思就是:要完成 all 这个目标,必须先完成 run 这个目标。当执行 make all(或直接 make,因为 all 常作为默认目标)时,make 会先去检查并执行 run 对应的操作,等 run 完成后,all 才算完成。
这样设计的作用是把多个操作串起来,比如让 all 依赖 run,而 run 又依赖编译操作,就能实现“执行 make 就自动完成编译并运行程序”的效果,简化操作流程。
3.3 目标文件的依赖关系与依赖方法
# 编译生成可执行文件
$(TARGET): $(TARGET).cgcc -o $(TARGET) $(TARGET).c
这里引入新的名词: 依赖关系 和 依赖方法
- 依赖关系:目标文件(如test)依赖于对应源文件(如test.c),源文件的存在或修改决定目标文件是否需要生成/更新。
- 依赖方法:当依赖关系满足时,通过gcc编译器将源文件(如test.c)编译为目标文件(如test)的具体操作。
- 自动执行:make工具会依据此规则,自动检查依赖并执行编译步骤。
- 整个过程,简单来说,这行就是告诉 make 工具:想得到那个可执行文件,得先有对应的源文件;有了源文件后,就用 gcc 把它变成可执行文件。你运行 make 时,它会自动按这个逻辑来做。
注:依赖关系和依赖方法必须具有合理性。
解释:依赖关系和依赖方法必须具有合理性
比如你想做一碗番茄炒蛋(目标):
- 依赖关系得合理:做番茄炒蛋,依赖的必须是番茄和鸡蛋(总不能依赖白菜和猪肉,那就不是番茄炒蛋了)。
- 依赖方法也得合理:有了番茄和鸡蛋后,正确的做法是“番茄切块、鸡蛋打散,一起下锅翻炒”(总不能把番茄和鸡蛋直接丢进水里煮,那成了乱炖,不是番茄炒蛋的做法)。
- 依赖关系和依赖方法必须对应起来:依赖的东西得是做这件事必需的,用这些东西的方法也得能真的做出目标结果——就像用番茄鸡蛋炒菜,才能得到番茄炒蛋,这就是合理性。
3.4 运行与清理操作的规则
# 运行程序
run: $(TARGET)./$(TARGET)
这行是说,要执行“run”这个操作,得先确保最终的可执行文件(比如test)已经生成好了。
等这个可执行文件准备好了,就会自动运行它(比如执行./test)。
简单讲,就是 “想运行程序,得先有程序;程序有了,就直接启动它” 。当你输入“make run”时,工具会按这个逻辑先检查程序是否存在,存在就马上运行。
# 清理编译生成的文件
clean:rm -f $(TARGET)
定义(类似C语言的宏定义)了一个叫“clean”的操作,作用是清理生成的文件。
意思是,当你执行“make clean”时,它会先确认要清理的目标(就是前面定义的那个可执行文件,比如test),然后用删除命令把这个文件删掉。
3.5 .PHONY关键字
在 Makefile 里,.PHONY 是一个特殊的关键字,用来声明“伪目标”。
作用
“伪目标”不是实际存在的文件,而是一个纯粹的操作指令(比如 all、clean、run 这些)。
用 .PHONY 声明后,Make 工具会明确:即便当前目录下有和伪目标同名的文件(比如叫 clean 的文件),执行 make clean 时也会忽略这个文件,直接执行 clean 对应的命令(比如删除文件)。
简单说
.PHONY 就是告诉 Make:“这些名字是操作指令,别当文件看待,不管有没有同名文件,都按我写的命令执行”,避免因同名文件导致操作失效。
比如:
.PHONY: all clean run
就是声明 all、clean、run 都是伪目标,确保 make all、make clean 等命令总能正常执行。
不推荐用 .PHONY 声明“实际生成文件的目标”。
但“纯操作型目标”(如 clean、all、run)必须用 .
PHONY 声明——关键是区分目标的类型。核心原则:.PHONY 只用于“伪目标”,不用于“文件目标”
- 文件目标:指最终会生成具体文件的目标(比如之前的 $(TARGET),会生成 test 可执行文件)。
这类目标不能用 .PHONY 声明,因为 Make 的核心逻辑是“检查文件是否存在/更新”:如果声明为 .PHONY,会忽略文件的实际状态,每次执行都重新生成,一个文件如果反复被编译发出浪费时间,违背了 Make“增量编译”的初衷。- 伪目标:指不生成文件、只执行操作的目标(比如 clean、all、run)。
这类目标必须用 .PHONY 声明,避免目录下有同名文件时,Make 误判“目标已完成”而跳过操作。简单总结
- 像 $(TARGET)(生成 test 文件)这样的“文件目标”:不声明 .PHONY(默认就是文件目标)。
- 像 clean、all、run 这样的“操作型目标”:必须声明 .PHONY,这是规范且可靠的写法。
4.make的自动推导过程
Make 的“自动推导”(也叫“隐式规则”)是它的核心特性之一,简单说就是:Make 会默认知道一些常见的编译规则,不用你写完整命令,它能自动推导怎么生成目标文件。
比如,当你要生成 test.o 这个目标文件时,即使你没写具体编译命令,Make 会自动判断:
“既然要生成 test.o,那很可能依赖 test.c 源文件,而且应该用 gcc -c test.c -o test.o 这个命令来编译”。
这个过程就是自动推导:
- 看到目标是
.o结尾的文件(如test.o),自动假设它依赖同名的.c文件(test.c); - 自动使用 C 编译器(默认
cc或gcc)的标准命令来编译,不用手动写编译步骤。
这样一来,你在 Makefile 里只需写清楚目标和依赖(比如 test.o: test.c),不用写具体编译命令,Make 会自己“猜”出该怎么做,大大简化了配置。
不止 .c 生成 .o,对 .cpp 生成 .o、.o 链接成可执行文件等常见场景,Make 都有内置的自动推导规则,这也是它能高效处理编译流程的原因之一。
5.makefile的常用语法
BIN=proc.exe # 定义变量
CC=gcc
#SRC=$(shell ls *.c) # 采⽤shell命令⾏⽅式,获取当前所有.c⽂件名
SRC=$(wildcard *.c) # 或者使⽤ wildcard 函数,获取当前所有.c⽂件名
OBJ=$(SRC:.c=.o) # 将SRC的所有同名.c 替换 成为.o 形成⽬标⽂件列表
LFLAGS=-o # 链接选项
FLAGS=-c # 编译选项
RM=rm -f # 引⼊命令
$(BIN):$(OBJ) @$(CC) $(LFLAGS) $@ $^ # $@:代表⽬标⽂件名。 $^: 代表依赖⽂件列表 @echo "linking ... $^ to $@"
%.o:%.c # %.c 展开当前⽬录下所有的.c。 %.o: 同时展开同名.o@$(CC) $(FLAGS) $< # $<: 对展开的依赖.c⽂件,⼀个⼀个的交给gcc。 @echo "compling ... $< to $@" # @:不回显命令
.PHONY:clean
clean:$(RM) $(OBJ) $(BIN) # $(RM): 替换,⽤变量内容替换它 .PHONY:test
test: @echo $(SRC) @echo $(OBJ)
5.1 变量定义
BIN=proc.exe:指定最终生成的可执行文件名。CC=gcc:指定使用的编译器为gcc。SRC=$(wildcard *.c):通过wildcard函数获取当前目录下所有.c源文件。OBJ=$(SRC:.c=.o):将所有.c源文件对应的目标文件名(.o文件)整理成列表。LFLAGS=-o:链接阶段的选项,用于指定输出文件。FLAGS=-c:编译阶段的选项,用于生成目标文件(不进行链接)。RM=rm -f:定义删除文件的命令,-f表示强制删除,忽略不存在的文件。
5.2 编译与链接规则
- 可执行文件生成(
$(BIN):$(OBJ)):
依赖所有.o目标文件,通过gcc链接这些目标文件生成可执行文件proc.exe,并输出链接提示信息。 - 目标文件生成(
%.o:%.c):
利用模式匹配,为每个.c源文件生成对应的.o目标文件,编译时输出提示信息。
5.3 伪目标与辅助操作
clean目标:
用于清理编译生成的所有.o目标文件和可执行文件proc.exe,确保开发环境的整洁。test目标:
用于打印当前目录下的.c源文件列表和对应的.o目标文件列表,方便开发者检查文件匹配情况。
5.4 特殊符号说明
$@:代表目标文件名(如proc.exe或某个.o文件)。$^:代表所有依赖文件的列表。$<:代表第一个依赖文件(在模式匹配中用于逐个处理.c源文件)。@:加在命令前,可隐藏命令本身的回显,仅显示自定义提示信息。
二.实战演练——进度条process
预备知识
first.回车与换行的区别
核心结论是:回车(CR)是光标回到行首,换行(LF)是光标下移一行,两者是独立操作,不同系统对“换行”的实现组合不同。
###1.核心概念区分
- 回车(Carriage Return,CR):对应ASCII码的
\r,作用是让光标从当前位置回到本行开头,不改变行的位置。- 换行(Line Feed,LF):对应ASCII码的
\n,作用是让光标从当前位置下移一行,不改变列的位置2.不同系统的换行实现
- Windows系统:用
\r\n(回车+换行)组合表示“换行”,先回行首再下移。- Unix/Linux/Mac(OS X及以后):用
\n(仅换行)表示“换行”,系统会自动补全回车动作。
1.代码准备
1.1 process.h
#ifndef PROCESS_H
#define PROCESS_H#include <unistd.h> // 提供usleep函数声明
#include <stdio.h> // 提供printf等I/O函数声明extern char s[102]; // 声明进度条字符数组(全局变量)
void process(double _total); // 声明进度条处理函数#endif
1.2 process.c
#include "process.h"
#include <stdlib.h> // 提供rand、srand函数
#include <time.h> // 提供time函数(用于随机数种子)// 定义全局变量(与头文件声明对应)
char s[102] = {0};
// 旋转光标序列(局部常量,仅在当前文件使用)
const char spinner[] = "|/-\\";// 进度条实现函数:模拟下载过程,显示进度
void process(double _total) {srand((unsigned int)time(NULL)); // 初始化随机数种子(按时间戳)double total = _total; // 总下载量double current = 0.0; // 当前已下载量double percent = 0.0; // 下载百分比(保留两位小数)int spin_idx = 0; // 旋转光标索引// 循环模拟下载过程,直到完成while (current < total) {// 随机生成每次下载的增量(1~5的整数,转为double)double step = (double)(rand() % 5 + 1);current += step;// 避免超过总量(防止进度超过100%)if (current > total) {current = total;}// 计算百分比(精确到两位小数)percent = (current / total) * 100.0;// 更新进度条字符数组(用'='填充已完成部分)int bar_length = (int)percent; // 进度条长度 = 百分比整数部分// 清空之前的进度条(避免残留字符)for (int i = 0; i < 100; i++) {s[i] = ' ';}// 填充已完成部分for (int i = 0; i < bar_length; i++) {s[i] = '=';}s[100] = '\0'; // 确保字符串结束符(避免越界)// 输出进度信息(\r表示回到行首,实现覆盖刷新)printf("[%-100s] [ %.1f / %.1f ] %.2f%% %c\r",s, current, total, percent, spinner[spin_idx]);fflush(stdout); // 强制刷新缓冲区(确保实时显示)// 更新旋转光标索引(循环切换4个字符)spin_idx = (spin_idx + 1) % 4;usleep(20000); // 休眠20ms(控制进度更新速度)}// 下载完成后,输出最终状态(换行避免覆盖)printf("[%-100s] [ %.1f / %.1f ] 100.00%% \n", s, total, total);printf("下载完成!\n\n");
}
1.3 main.c
#include "process.h"// 测试函数:调用不同总量的进度条
void test() {process(256); // 测试总量256process(1024); // 测试总量1024process(50); // 测试总量50process(5000); // 测试总量5000
}// 主函数:程序入口
int main() {test();return 0;
}
2.配置Makefile文件
# 编译器设置
CC = gcc
# 编译选项:启用警告,链接必要库(此处无需额外库)
CFLAGS = -Wall -Wextra# 目标可执行文件名
TARGET = progress_bar# 源文件列表
SRCS = main.c process.c# 生成目标
all: $(TARGET)# 编译可执行文件
$(TARGET): $(SRCS)$(CC) $(CFLAGS) $(SRCS) -o $(TARGET)# 运行程序
run: $(TARGET)./$(TARGET)# 清理编译产物
clean:rm -f $(TARGET)# 伪目标(避免与同名文件冲突)
.PHONY: all run clean
简单说明:
CC = gcc:指定使用gcc作为编译器。CFLAGS = -Wall -Wextra:启用额外的警告信息(-Wall开启基本警告,-Wextra开启更多警告),有助于发现代码中的潜在问题。TARGET = progress_bar:指定最终生成的可执行文件名为progress_bar。SRCS = main.c process.c:列出需要编译的源文件。all: $(TARGET):默认目标,执行make时会编译生成可执行文件。run: $(TARGET):自定义目标,执行make run可直接编译(若未编译)并运行程序。clean: rm -f $(TARGET):清理目标,执行make clean会删除生成的可执行文件。.PHONY: all run clean:声明伪目标,避免目录中存在同名文件时干扰Makefile的执行。
3.结果显示
===================================================================================================== [ 256.0 / 256.0 ] 100.00 % ?
下载完成!
===================================================================================================== [ 1024.0 / 1024.0 ] 100.00 % ?
下载完成!
===================================================================================================== [ 50.0 / 50.0 ] 100.00 % ?
下载完成!
===================================================================================================== [ 5000.0 / 5000.0 ] 100.00 % ?
下载完成!
