当前位置: 首页 > news >正文

【Linux】自动化构建工具make和Makefile和第一个系统程序—进度条

文章目录

  • 前言
  • 一. 什么是自动化构建工具-make/Makefile
  • 二. make/Makefile的基本使用
    • 2.1 依赖关系和依赖方法
      • 依赖关系
      • 依赖方法
      • 依赖关系与依赖方法的协同作用
    • 2.2 伪目标(.PHONY)
    • 2.3 stat 指令
    • 2.4 推导过程
    • 2.5 扩展语法——更加通用的Makefile
      • 编译当前目录下的多个文件
    • 2.6 测试和优化Makefile
  • 三. 第一个系统程序——进度条
    • 3.1 回车和换行
    • 3.2 倒计时程序
    • 3.3 进度条程序
  • 最后


前言

在上一篇文章中,我们详细介绍了编辑器vim的使用和理解gcc编译器的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解make和Makefile自动化构建工具和第一个系统程序—进度条的内容,接下来一起看看吧!


一. 什么是自动化构建工具-make/Makefile

在 Linux(以及整个类 Unix 开发环境)里,make 是一个“任务编排器”,Makefile 是它默认读取的“任务说明书”。

make

make是一个命令工具,是一个解释Makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,可执行文件通常是 /usr/bin/make功能:读取“任务规则”,比较文件时间戳,只执行需要重新做的步骤——最常用来增量编译,但也能跑任意shell命令。

Makefile

Makefile是一个纯文本文件,默认文件名就叫 Makefile(或 makefile)。内容:一条条“规则”,告诉 make

Makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

总结make 是“调度器”,Makefile 是“说明书”,两者配合,把手工敲的长串命令变成一条 make 就能完成的自动化流程。

二. make/Makefile的基本使用

创建了一个test.c文件和Makefile文本文件
在这里插入图片描述

使用make命令可以自动编译test.c文件产生test可执行程序,使用make clean命令可以自动清理test可执行程序。
在这里插入图片描述

那么Makefile中的内容有什么含义呢?

test:test.c    gcc test.c -o test    .PHONY:clean    
clean:    rm -f test

2.1 依赖关系和依赖方法

在这里插入图片描述

依赖关系

依赖关系定义了 “目标(target)” 与 “它所依赖的文件或其他目标” 之间的关联。简单来说:要生成目标,必须先确保其依赖的文件 / 目标已存在且是最新的

核心逻辑

  • 目标(target):通常是最终要生成的文件(如可执行程序、目标文件.o),也可以是伪目标(如clean)。
  • 依赖(prerequisites):生成目标所需要的前置文件或其他目标(如源文件.c、头文件.h、其他.o文件)。
  • make工具会比较目标与依赖的修改时间:如果依赖的修改时间比目标更新(或目标不存在),则需要通过 “依赖方法” 重新生成目标。

依赖方法

依赖方法(也称为 “命令”)定义了 “当依赖满足更新条件时,如何生成或更新目标” 的具体操作(通常是编译、链接等命令)。

依赖规则

  • 依赖方法必须紧跟在 “目标:依赖” 这一行的下方,且每一行命令必须以 Tab 键开头(不能用空格,否则make会报错)。
  • 命令按顺序执行,若某条命令执行失败(返回非 0 状态),make会终止后续操作。

依赖关系与依赖方法的协同作用

make工具的工作流程正是基于二者的配合:

  1. 检查目标是否存在:若不存在,直接执行依赖方法生成。
  2. 若目标存在,比较目标与所有依赖的修改时间:

    若所有依赖的修改时间都早于目标(依赖未更新),则目标无须重建
    若任一依赖的修改时间晚于目标(依赖已更新),则执行依赖方法重新生成目标

如果依赖未更新,则不会重复创建目标:
在这里插入图片描述

2.2 伪目标(.PHONY)

伪目标(如clean)本身不是实际文件,而是用于执行特定操作(如清理中间文件)。其依赖关系和方法通常用于定义 “无文件依赖的操作”。

在这里插入图片描述

# 声明clean为伪目标(避免与同名文件冲突)
.PHONY: clean  # 依赖关系:无实际依赖(可省略)
# 依赖方法:删除中间文件和可执行程序
clean:  rm -f test  # 清理命令

为什么要有伪目标?

伪目标既不产生目标文件,也不依赖于任何文件,只执行依赖方法,并且可以重复执行常用于删除文件,做清理工作

make clean命令可以重复执行:
在这里插入图片描述

2.3 stat 指令

在 Linux 中,stat指令用于显示文件或文件系统的详细状态信息(元数据),包括文件的权限、大小、inode 信息、时间戳(访问 / 修改 / 变更时间)等。

三个关键时间戳

  • Access(访问时间):文件内容最后被读取的时间
  • Modify(修改时间):文件内容最后被修改的时间
  • Change(变更时间):文件属性最后被修改的时间

在这里插入图片描述

可执行程序test的Modify时间比test.c文件的Modify时间晚,说明依赖未更新。
在这里插入图片描述

可执行程序test的Modify时间比test.c文件的Modify时间早,说明依赖已更新,可以执行make指令重新生成目标文件。
在这里插入图片描述

通过修改test.c文件的权限从而修改test.c文件的Change时间:
在这里插入图片描述

此时test.c的Change时间比test可执行程序的Change时间晚,但还是无法执行make指令,更加证明了只有当依赖文件的Modify时间晚于目标文件的Modify时间时,才可以执行make指令从而重新生成目标文件
在这里插入图片描述

.PHONY定义的伪目标clean之所以能够一直被执行,其原因就是它忽略了依赖文件与目标文件的修改时间的对比。

验证:就算写了伪目标依赖test.c文件,但还是可以重复执行make clean,所以伪目标通常忽略依赖关系。

在这里插入图片描述
在这里插入图片描述

2.4 推导过程

在上述过程中,我们写的依赖文件都是在当前目录中直接存在的;如果依赖文件并没有直接存在,而是依赖于其他依赖文件呢?

这里我们就根据编译过程,生成编译过程中所有的临时文件,来写makefile

myproc:myproc.o    gcc myproc.o -o myproc    
myproc.o:myproc.s    gcc -c myproc.s -o myproc.o    
myproc.s:myproc.i    gcc -S myproc.i -o myproc.s    
myproc.i:myproc.c    gcc -E myproc.c -o myproc.i    .PHONY:clean                                                                                                                                                                        
clean:    rm -f *.i *.s *.o myproc

myproc依赖于myproc.o、myproc.o依赖于myproc.s、myproc.s依赖于myproc.i、myproc.i依赖于myproc.c。myproc.c则直接存在于当前目录下。

在这里插入图片描述

在这里插入图片描述

2.5 扩展语法——更加通用的Makefile

上面的Makefile只适用于一个指定文件的情况下,如果换个指定文件或多个文件,那么Makefile就要重写了,接下来我们要学习更多Makefile的语法,使Makefile变得更加通用。

BIN = test    
SRC = test.c    
OBJ = test.o    
CC = gcc    
RM = rm -f    $(BIN):$(OBJ)    $(CC) $(OBJ) -o $(BIN)    
$(OBJ):$(SRC)    $(CC) -c $(SRC) -o $(OBJ)    .PHONY:clean    
clean:    $(RM) $(OBJ) $(BIN)

我们可以通过创建变量来表示我们编译要用到的文件和选项,当需要访问这些变量的时候可以使用 $()来访问(使用$(BIN)来访问BIN变量的值)。

在这里插入图片描述

我们还可以使用$^$@ 来表示依赖关系中的依赖文件目标文件

BIN = test    
SRC = test.c    
OBJ = test.o    
CC = gcc    
RM = rm -f    $(BIN):$(OBJ)    $(CC) $^ -o $@    
$(OBJ):$(SRC)    $(CC) -c $^ -o $@                                                                                                 .PHONY:clean    
clean:    $(RM) $(OBJ) $(BIN)

编译当前目录下的多个文件

上面的Makefile还是只能操作一个.c源文件,如果当前目录下有多个.c源文件,有什么办法可以同时编译当前目录下的所有.c源文件吗?

获取当前目录下的所有.c文件
有两种方法:

  • $(shell ls *.c) :采用shell命令行方式,获取当前所有.c文件名
  • $(wildcard *.c) :使用wildcard函数,获取当前所有.c文件名

将所有的.c修改为.o
对于多个.c文件,我们需要对它们进行编译从而产生对应的.o文件,最后再链接成可执行程序test;所以对于变量OBJ来说,我们需要获取所有.c文件对应的.o文件。

OBJ=$(SRC:.c=.o)

这个语法就是将SRC所有的同名.c替换成为.o,形成目标文件列表。

通配符%和逐个执行$<

我们经常写的*.c表示所有以>.c为结尾的文件;在Makefile的使用中,通常需要用到通配符,而Makefile的通配符为%

我们在写由所有的.c文件生成对应的.o文件时,就需要用到通配符%,这样就可以自动匹配了。

在匹配结束之后,多个.o目标文件分别依次依赖.c文件;这样就不能使用$^直接取依赖文件列表了,而是使用%<将依赖文件列表中的多个文件一个一个执行。

BIN = test    
SRC = $(wildcard *.c)    
OBJ = $(SRC:.c=.o)    
CC = gcc    
RM = rm -f    $(BIN):$(OBJ)    $(CC) $^ -o $@    
%.o:%.c    $(CC) -c $<    .PHONY:clean    
clean:    $(RM) $(OBJ) $(BIN)

2.6 测试和优化Makefile

BIN = test    
SRC = $(wildcard *.c)    
OBJ = $(SRC:.c=.o)    
CC = gcc    
RM = rm -f    
LFLAGS = -o    
FLAGS = -c    $(BIN):$(OBJ)    $(CC) $^ $(LFLAGS) $@    
%.o:%.c    $(CC) $(FLAGS) $<    .PHONY:clean                                                                             
clean:    $(RM) $(OBJ) $(BIN)

这里优化,新增加了编译选项FLAGS和链接选项LFLAGS

我们新建20个文件进行测试:

touch test{1..20}.c

在这里插入图片描述
通过make的运行结果,我们可以知道:在执行依赖方法的每条语句时,都会回显每条语句的内容

如果我们不想要回显每条语句的内容,可以在每条语句前面加个@

BIN = test    
SRC = $(wildcard *.c)    
OBJ = $(SRC:.c=.o)    
CC = gcc    
RM = rm -f    
LFLAGS = -o    
FLAGS = -c    $(BIN):$(OBJ)    @$(CC) $^ $(LFLAGS) $@    
%.o:%.c    @$(CC) $(FLAGS) $<    .PHONY:clean    
clean:    @$(RM) $(OBJ) $(BIN)

在这里插入图片描述

但是这样的话,我们就不知道编译和链接的过程了,所以我们可以手动添加。

BIN = test    
SRC = $(wildcard *.c)    
OBJ = $(SRC:.c=.o)    
CC = gcc    
RM = rm -f    
LFLAGS = -o    
FLAGS = -c    $(BIN):$(OBJ)    @$(CC) $^ $(LFLAGS) $@    @echo "链 接... $^ 成 $@"     
%.o:%.c    @$(CC) $(FLAGS) $<    @echo "编 译... $< 成 $@"    .PHONY:clean    
clean:    @$(RM) $(OBJ) $(BIN)

注意:这里在echo语句前也要加@不让它回显,否则就会回显出来echo语句的内容,从而打印两遍一模一样的内容,不符合预期。

在这里插入图片描述

三. 第一个系统程序——进度条

3.1 回车和换行

Windows下,我们通常认为回车和换行是一个概念,但事实上,换行是换到下一行的当前位置,而回车是回到当前行的起始位置。我们之所以会认为回车和换行是一个概念,是因为\n它做了回车和换行两个操作。

下面来看两段代码:

#include <stdio.h>    int main()    
{    printf("Hello Linux\n");    return 0;    
}
#include <stdio.h>    int main()    
{    printf("Hello Linux\r");    return 0;    
}

在这里插入图片描述
为什么回车\r之后什么都没输出?

因为回车是回到当前行的起始位置,当程序结束时确实是打印了Hello Linux,但是光标确在当前行的起始位置,因为shell会在命令结束时,在光标位置打印提示信息,刚好把Hello Linux给覆盖了。

这里我们再看两段代码:

#include <stdio.h>      
#include <unistd.h>      int main()    
{    printf("Hello Linux\n");    sleep(3);    return 0;                                                                                                                                                                       
}
#include <stdio.h>    
#include <unistd.h>    int main()    
{    printf("Hello Linux");    sleep(3);    return 0;    
}

在这里插入图片描述

运行结果

  • 第一个代码先输出Hello Linnux,再睡眠了三秒
  • 第二个代码先睡眠了三秒,再输出Hello Linux

这里是为什么呢?这里就要了解缓冲区这个概念了。

在计算机中,缓冲区(Buffer) 是一块用于暂时存储数据的内存区域,其核心作用是协调数据产生速度与处理速度不匹配的问题(例如 CPU 与外设、程序与 I/O 设备之间的速度差异),通过批量传输数据减少实际 I/O 操作次数,从而提高效率。
基本作用
当程序进行输入 / 输出(I/O)操作时,数据不会直接实时传输到目标设备(如磁盘、终端)或从源设备读取,而是先暂存在缓冲区中。只有当缓冲区满足特定条件时(如满了、或遇到特殊字符),才会将数据批量传输。

行缓冲区是一种按 “行” 划分的缓冲机制,其刷新(数据传输)的触发条件主要有两个:

  • 遇到换行符(‘\n’) 时,立即将缓冲区中当前行的数据传输出去;
  • 当缓冲区存储的数据量达到其最大容量时,即使未遇到换行符,也会触发刷新。

所以第一个代码遇到换行符就立即把Hello Linux输出出去了,第二个代码则先把Hello Linux放在缓冲区,等程序结束时才把它输出出去。

如果我们想要让缓冲区强制刷新也是可以的,可以使用fflush函数来刷新缓冲区(stdout)的内容。

在这里插入图片描述

这样就是先输出Hello Linux,再休眠三秒。

3.2 倒计时程序

了解了回车和换行之后,现在来简单实现一个倒计时程序

#include<stdio.h>    
#include<unistd.h>    int main()    
{    int cnt = 10;    while(cnt>=0)                                                                                                                                                                   {    printf("%-2d\r",cnt);    fflush(stdout);    sleep(1);    cnt--;    }    printf("\n");    return 0;    
}

注意

  • 输出时记得要用%-2d,让数字占两格,不够的用空格替代,并且向左对齐
  • 输出每个数字后,都要使用fflush强制刷新缓冲区

3.3 进度条程序

在这里插入图片描述
首先要准备四个文件

  • Makefile文件
  • main.c:程序主函数main所在的文件,也用于测试进度条
  • process.h:进度条程序的头文件
  • process.c:进度条程序的源文件
#include "process.h"    
#include <string.h>    
#include <unistd.h>    #define SIZE 101    
#define STYLE '#'// v1:展示进度条基本功能    
void process()    
{    int rate=0;    char buffer[SIZE];    memset(buffer,0,sizeof(buffer));    const char* label = "|/-\\";    int len = strlen(label);    while(rate<=100){printf("[%-100s][%d%%][%c]\r",buffer,rate,label[rate%len]);fflush(stdout);buffer[rate]=STYLE;++rate;usleep(50000);}printf("\n");
}

在这里插入图片描述
这个函数已经实现了进度条的基本功能,但是进度条每次累加的大小是相同的,这有点不太符合实际,因为现实中的网速不是固定的,是随时变化的,所以我们还需要再进行修改。

为了控制速度,我们可以控制current累加的值,也可以控制休眠时间usleep的大小。

#include "process.h"        
#include <unistd.h>        
#include <time.h>        
#include <stdlib.h>        // 函数指针类型        
typedef void (*call_t)(const char*,double,double);        double total = 1024.0;        
//double speed = 1.0;        
double speed[] = {1.0, 0.5, 0.3, 0.2, 0.1, 0.01};        // 回调函数        
void download(double total,call_t cb)        
{        srand(time(NULL));        double current = 0.0;        while(current<=total)        {        cb("下 载 中",total,current);    if(current==total) break;    int random = rand()%6;        usleep(5000);        current += speed[random];    if(current>=total){    current = total;    }    }            printf("\n");        
}
// v2:根据进度,动态刷新一次进度条    
void FlushProcess(const char* tips,double total,double current)    
{    const char* label = "|/-\\";    int len = strlen(label);    static int index = 0;    char buffer[SIZE];    memset(buffer,0,sizeof(buffer));    double rate = (current*100.0)/total;    int num = (int)rate;    int i = 0;    for(; i < num; i++)    {    buffer[i]=STYLE;    }    printf("%s...[%-100s][%.1lf%%][%c]\r",tips,buffer,rate,label[index++]);    fflush(stdout);    index%=len;    
}

我们可以使用随机数来实现current每次加的值都是随机的,从而控制网速。

将每次刷新进度条的过程封装成一个函数,再利用函数指针去接受该函数的地址,实现出回调函数。

process.h

#pragma once    #include <stdio.h>    //v2    
void FlushProcess(const char* tips,double total,double current);                         // v1    
void process();

process.c

#include "process.h"    
#include <string.h>    
#include <unistd.h>    #define SIZE 101    
#define STYLE '#'    // v2:根据进度,动态刷新一次进度条    
void FlushProcess(const char* tips,double total,double current)    
{    const char* label = "|/-\\";    int len = strlen(label);    static int index = 0;    char buffer[SIZE];    memset(buffer,0,sizeof(buffer));    double rate = (current*100.0)/total;    int num = (int)rate;    int i = 0;    for(; i < num; i++)    {    buffer[i]=STYLE;    }    printf("%s...[%-100s][%.1lf%%][%c]\r",tips,buffer,rate,label[index++]);    fflush(stdout);    index%=len;    
}
// v1:展示进度条基本功能    
void process()                                                                                                                                                                      
{    int rate=0;    char buffer[SIZE];    memset(buffer,0,sizeof(buffer));    const char* label = "|/-\\";    int len = strlen(label);    while(rate<=100){printf("[%-100s][%d%%][%c]\r",buffer,rate,label[rate%len]);fflush(stdout);buffer[rate]=STYLE;++rate;usleep(50000);}printf("\n");
}

main.c

#include "process.h"    
#include <unistd.h>    
#include <time.h>    
#include <stdlib.h>    // 函数指针类型        
typedef void (*call_t)(const char*,double,double);    double total = 1024.0;    
//double speed = 1.0;        
double speed[] = {1.0, 0.5, 0.3, 0.2, 0.1, 0.01};    // 回调函数        
void download(double total,call_t cb)    
{    srand(time(NULL));    double current = 0.0;    while(current<=total)    {    cb("下 载 中",total,current);    if(current==total) break;    int random = rand()%6;    usleep(5000);    current += speed[random];    if(current>=total){    current = total;    }    }    printf("\n");    
}    void upload(double total,call_t cb)    
{    srand(time(NULL));    double current = 0.0;                                                                                                                                                           while(current<=total)    {    cb("上 传 中",total,current);    if(current==total) break;    int random = rand()%6;usleep(5000);    current += speed[random];if(current>=total){current = total;}}printf("\n");
}
int main()
{download(1024.0,FlushProcess);printf("download 1024.0MB done\n");download(512.0,FlushProcess);printf("download 512.0MB done\n");download(256.0,FlushProcess);printf("download 256.0MB done\n");download(128.0,FlushProcess);printf("download 128.0MB done\n");download(64.0,FlushProcess);printf("download 64.0MB done\n");upload(500.0,FlushProcess);printf("upload 500.0MB done\n");//process();return 0;
}

最后

本篇关于自动化构建工具make和Makefile和第一个系统程序—进度条到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!

http://www.dtcms.com/a/553848.html

相关文章:

  • YOLO python 实现多种物体识别(时钟,水杯,小熊,路人,车辆)
  • wordpress如何自动采集网站图片建模培训
  • 建设银行网站会员怎么用装修设计公司快餐店
  • 华宇亮相2025丽泽法务论坛,共探法治前沿与法商融合新路径
  • 嫦娥号地月轨道、环月(一个月)MATLAB仿真
  • Spring AI Alibaba 【六】
  • .NET10 - 尝试一下Blazor Web Assembly Standalone App的fingerprint新特性
  • 浏览器使用 <embed> 标签预览 PDF 的原理
  • 未来之窗昭和仙君(四十七)开发商品进销存——东方仙盟筑基期
  • 交流平台网站架构怎么做wordpress客户端登录界面
  • 网站附件下载表格怎么做营销是做什么
  • Xcode26升级以后重要
  • Mac电脑解决sudo密码问题方法
  • 仓颉语言宏(Cangjie Macros)详细介绍及强大使用
  • Python异常、模块、包
  • flink 流式窗口
  • 仿京东电商的网站开发智慧团建网站什么时候维护好
  • 23.NAT之Easy-IP
  • Godot游戏开发——C# (一)
  • PyTorch实战:从零搭建CV模型技术文章大纲
  • 高效的DDC可编程控制器风机水泵空调节能控制器公司
  • PIL (Python Imaging Library) 相关方法详解1
  • U-Net 家族全解析
  • 德语网站制作网页设计建网站
  • Windows Linux 子系统 (WSL) 上的 Ubuntu
  • ftp工具下载网站源码教程手机视频wordpress
  • LeetCode 二叉树 437. 路径总和 III
  • 【GoLang】【框架学习】【GORM】4. 使用 BeforeUpdate hook 操作时,出现反射报错
  • 有哪些好点的单页网站公司管理制度完整版
  • 庭田科技亮相成都复材盛会,以仿真技术赋能产业革新