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 % ? 
下载完成!
相关推荐
顾安r3 小时前
11.8 脚本网页 推箱子
linux·前端·javascript·flask
___波子 Pro Max.3 小时前
Linux source命令详解与应用场景
linux
橘子134 小时前
Linux网络基础(一)
linux·网络·arm开发
你想考研啊6 小时前
linux安装jdk和tomcat和并自启动
java·linux·tomcat
习惯就好zz8 小时前
WSL2 安装Ubuntu卡在安装进度0%无响应问题解决
linux·windows·ubuntu·wsl·wsl2
循环过三天8 小时前
3.4、Python-集合
开发语言·笔记·python·学习·算法
躲猫猫的喵喵9 小时前
Ubuntu2204降内核版本
linux·运维·服务器·ubuntu
昌sit!9 小时前
Linux系统性基础学习笔记
linux·笔记·学习
zdslovezy10 小时前
CentOS 系统升级 OpenSSH 和 OpenSSL 的完整方案
linux·运维·centos