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文件

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文件。并配置成如下格式:

bash 复制代码
touch Makefile #创建文件
vim Makefile #编辑文件
 cat Makefile #查看Makefile内容
# 定义目标文件名
TARGET = test

# 编译规则:默认目标为运行程序
all: run

# 编译生成可执行文件
$(TARGET): $(TARGET).c
	gcc -o $(TARGET) $(TARGET).c

# 运行程序
run: $(TARGET)
	./$(TARGET)

# 清理编译生成的文件
clean:
	rm -f $(TARGET)

然后我们输入make run。test.c文件就被自动编译并运行了。如下:

bash 复制代码
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 定义目标文件名(定义变量)

bash 复制代码
# 定义目标文件名
TARGET = test

在 Makefile 里,TARGET = test 其实是给一个叫 TARGET 的"标签"起了个值叫 test

简单说,这个"标签"就像一个代号,后面在写编译规则的时候,不用反复写 test 这个具体名字,直接用 TARGET 这个代号就行。

比如要生成名为 test 的可执行文件,定义这个之后,后续所有涉及文件名的地方,都可以用 TARGET 代替。这样做的好处是,如果以后想把文件名改成别的(比如 app),只需要改这一行的 testapp,其他地方不用动,既方便又不容易出错,还能让整个文件的结构更清楚,一眼就知道 TARGET 代表的是最终要生成的那个文件。

类比一下:类似于C语言当中的宏替换

3.2 all: run 目标依赖规则

bash 复制代码
# 编译规则:默认目标为运行程序
all: run

在 Makefile 里,all: run 是一条"目标依赖"规则。

这里的 all 是一个特殊的目标(通常作为默认目标,即输入 make 命令时不指定目标就会执行它),而 run 是它所依赖的另一个目标。

意思就是:要完成 all 这个目标,必须先完成 run 这个目标。当执行 make all(或直接 make,因为 all 常作为默认目标)时,make 会先去检查并执行 run 对应的操作,等 run 完成后,all 才算完成。

这样设计的作用是把多个操作串起来,比如让 all 依赖 run,而 run 又依赖编译操作,就能实现"执行 make 就自动完成编译并运行程序"的效果,简化操作流程。

3.3 目标文件的依赖关系与依赖方法

bash 复制代码
# 编译生成可执行文件
$(TARGET): $(TARGET).c
	gcc -o $(TARGET) $(TARGET).c

这里引入新的名词: 依赖关系 依赖方法

  • 依赖关系:目标文件(如test)依赖于对应源文件(如test.c),源文件的存在或修改决定目标文件是否需要生成/更新。
  • 依赖方法:当依赖关系满足时,通过gcc编译器将源文件(如test.c)编译为目标文件(如test)的具体操作。
  • 自动执行:make工具会依据此规则,自动检查依赖并执行编译步骤。
  • 整个过程,简单来说,这行就是告诉 make 工具:想得到那个可执行文件,得先有对应的源文件;有了源文件后,就用 gcc 把它变成可执行文件。你运行 make 时,它会自动按这个逻辑来做。
    注:依赖关系和依赖方法必须具有合理性

解释:依赖关系和依赖方法必须具有合理性

比如你想做一碗番茄炒蛋(目标):

  • 依赖关系得合理:做番茄炒蛋,依赖的必须是番茄和鸡蛋(总不能依赖白菜和猪肉,那就不是番茄炒蛋了)。
  • 依赖方法也得合理:有了番茄和鸡蛋后,正确的做法是"番茄切块、鸡蛋打散,一起下锅翻炒"(总不能把番茄和鸡蛋直接丢进水里煮,那成了乱炖,不是番茄炒蛋的做法)。
  • 依赖关系和依赖方法必须对应起来:依赖的东西得是做这件事必需的,用这些东西的方法也得能真的做出目标结果------就像用番茄鸡蛋炒菜,才能得到番茄炒蛋,这就是合理性。

3.4 运行与清理操作的规则

bash 复制代码
# 运行程序
run: $(TARGET)
	./$(TARGET)

这行是说,要执行"run"这个操作,得先确保最终的可执行文件(比如test)已经生成好了。

等这个可执行文件准备好了,就会自动运行它(比如执行./test)。

简单讲,就是 "想运行程序,得先有程序;程序有了,就直接启动它" 。当你输入"make run"时,工具会按这个逻辑先检查程序是否存在,存在就马上运行。

bash 复制代码
# 清理编译生成的文件
clean:
	rm -f $(TARGET)

定义(类似C语言的宏定义)了一个叫"clean"的操作,作用是清理生成的文件。

意思是,当你执行"make clean"时,它会先确认要清理的目标(就是前面定义的那个可执行文件,比如test),然后用删除命令把这个文件删掉。

3.5 .PHONY关键字

在 Makefile 里,.PHONY 是一个特殊的关键字,用来声明"伪目标"。

作用

"伪目标"不是实际存在的文件,而是一个纯粹的操作指令(比如 allcleanrun 这些)。

.PHONY 声明后,Make 工具会明确:即便当前目录下有和伪目标同名的文件(比如叫 clean 的文件),执行 make clean 时也会忽略这个文件,直接执行 clean 对应的命令(比如删除文件)。

简单说
.PHONY 就是告诉 Make:"这些名字是操作指令,别当文件看待,不管有没有同名文件,都按我写的命令执行",避免因同名文件导致操作失效。

比如:

makefile 复制代码
.PHONY: all clean run

就是声明 allcleanrun 都是伪目标,确保 make allmake 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 这个命令来编译"。

这个过程就是自动推导:

  1. 看到目标是 .o 结尾的文件(如 test.o),自动假设它依赖同名的 .c 文件(test.c);
  2. 自动使用 C 编译器(默认 ccgcc)的标准命令来编译,不用手动写编译步骤。

这样一来,你在 Makefile 里只需写清楚目标和依赖(比如 test.o: test.c),不用写具体编译命令,Make 会自己"猜"出该怎么做,大大简化了配置。

不止 .c 生成 .o,对 .cpp 生成 .o.o 链接成可执行文件等常见场景,Make 都有内置的自动推导规则,这也是它能高效处理编译流程的原因之一。

5.makefile的常用语法

bash 复制代码
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

c 复制代码
#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

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

c 复制代码
#include "process.h"

// 测试函数:调用不同总量的进度条
void test() {
    process(256);   // 测试总量256
    process(1024);  // 测试总量1024
    process(50);    // 测试总量50
    process(5000);  // 测试总量5000
}

// 主函数:程序入口
int main() {
    test();
    return 0;
}

2.配置Makefile文件

bash 复制代码
# 编译器设置
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.结果显示

bash 复制代码
===================================================================================================== [ 256.0 / 256.0 ] 100.00 % ? 
下载完成!
===================================================================================================== [ 1024.0 / 1024.0 ] 100.00 % ? 
下载完成!
===================================================================================================== [ 50.0 / 50.0 ] 100.00 % ? 
下载完成!
===================================================================================================== [ 5000.0 / 5000.0 ] 100.00 % ? 
下载完成!
相关推荐
weixin_4370446417 小时前
Netbox批量添加设备——堆叠设备
linux·网络·python
hhy_smile17 小时前
Ubuntu24.04 环境配置自动脚本
linux·ubuntu·自动化·bash
宴之敖者、18 小时前
Linux——\r,\n和缓冲区
linux·运维·服务器
LuDvei18 小时前
LINUX错误提示函数
linux·运维·服务器
未来可期LJ18 小时前
【Linux 系统】进程间的通信方式
linux·服务器
Abona18 小时前
C语言嵌入式全栈Demo
linux·c语言·面试
Lenyiin18 小时前
Linux 基础IO
java·linux·服务器
The Chosen One98518 小时前
【Linux】深入理解Linux进程(一):PCB结构、Fork创建与状态切换详解
linux·运维·服务器
经年未远18 小时前
vue3中实现耳机和扬声器切换方案
javascript·学习·vue
Hill_HUIL18 小时前
学习日志22-静态路由
网络·学习