【Linux】Linux项目自动化构建工具——make/Makefile

1.背景

  • 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力
  • 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的 规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂 的功能操作
  • makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编 译,极大的提高了软件开发的效率。
  • make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命 令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一 种在工程方面的编译方法。
  • make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建

2.make/Makefile 

2.1.见一见make/Makefile是怎么工作的

make是一个命令,Makefile是一个文件,当前目录下的文件

可以写makefile也可以写Makeile

 

我们在里面写下

第一行的意思就是mycode.c编译成名字叫mycode的可执行程序,这个叫依赖关系

第二行是怎么编译,这个是依赖方法

完成了以后,我们再也不用使用gcc命令来编译了

直接生成了一个可执行程序

后面我们不想要可执行程序mycode了,我们直接打开Makefile

这样子我们就删掉了那个可执行程序

我们写代码就不用gcc了

我们系统自动存在make命令

它和Makefile一起配对使用的

我们现在了解make/Makefile是怎么使用的了

那么问题来了

2.2.什么是依赖关系?什么是依赖方法?

我们看个例子就明白了

假如你是个在校大学生,快要到月底了,这时候你可能就要打电话给你爸要生活费了。你打电话给你爸爸,说 "爸,我是你儿子。",这就是表明依赖关系。你打电话告诉你爸你是他儿子的时候,实际上你的潜台词就是 "我要依赖你"。

你给你爸打电话说:"爸我是你儿子",说完就把电话一挂,对于你爸来说会一脸懵逼 —— "这孩子今天怎么了,这是被绑架了?",你爸就不太清楚了。也就是说,你在打电话时只是表明了依赖关系,但你并没有达到你想要做的目的(要下个月的生活费),所以正确的方法应该是:"爸,我是你儿子,我要下个月的生活费。",你表达了你是谁,并且要求给你打钱。

我是你儿子 —— 表明了 "依赖关系",因为依赖关系的存在,所以才能给你打钱。

打钱 —— 就是 "依赖方法",当你把依赖关系和依赖方法表明时,你就能达到要钱的目的。

依赖关系不对,依赖方法再对也没有用,比如你的舍友给你爸打电话,说:"我是你儿子的舍友,给我打钱!",你爸绝对不会打钱的。

依赖方法表明了,依赖方法不正确同样没有用,比如你打电话给你爸:说:"我是你儿子,给我打钱我要充游戏!",你爸也不会给你打钱的!

通过上面的比喻,相信你已经知道什么是依赖关系和依赖方法了,他们必须都为真。

依赖关系和依赖方法都要为真,才能达成要钱的目的!

依赖关系:我们上面mycode的形成需要依赖于mycode.c

依赖方法:只有依赖关系可不够,还需要指明怎么依赖

上面那个依赖关系是简写的,下面我们故意写点繁琐的 

 

我们把上面那个修改成下面这个更繁琐的

我们报存,去使用它

这个好像没有什么问题

我们看看啊,我们书写的时候是从上往下写的,但是它是从下面往上执行的,为什么呢?

make在扫描makefile时是从上往下的,但是当前目录没有提供mycode.o,所以往下先生成mycode.o,而形成mycode.o又要形成mycode.s,依次类推,所以先生成mycode.i,刚好生成mycode.i需要mycode.c,刚好当前目录

这个过程特别像栈

这个是makefile依赖文件的自动化推导

有人说,它既然能推导,我们要是把它变乱序还能不能推导了?

事实证明,完全没有任何问题!!!!!

我要是故意漏掉一行会怎么样!

结果是不能正常使用

由此,我们得到一个结论:make会自动推导Makefile中的依赖结构,栈式结构

2.3.如何清理make生成的临时文件

我们使用make生成的东西里面有很多临时文件

比如下面的mycode.i,mycode.o,mycode.s文件

我们需要引入一个东西来清理这些临时文件

我们打开Makefile

clean没有依赖关系,只执行删除功能 

很好,全删除了

新的问题又来了,我们看下面

我们可以使用make clean,那么是不是意味着我们可以使用make mycode ,make mycode.o,make mycode.i , make mycode .s?我们验证一下

事实证明确实如此

我们接着看啊,我们直接把clean放到前面

 

我们此时再执行make指令会有什么反应?

我靠,怎么变删除了!!!

所以make会自顶向下查找第一个依赖关系,并执行该依赖关系的依赖方法

所以我们不要把清理工作放在最前面

2.4.为什么我们只可以make一次 ,后面make多次就不行?怎么做到的?

我们看看下面这个情况

我们make使用一次之后就不让我编译了

为了方便,我们把Makefile全改回来简单版本

只让我们make一次

我们将代码修改,再make一下,发现又可以了,再make又不行了

为什么不让我们编译了?

因为源文件没有更新的话,没有必要,提高编译效率

那它是怎么做到的?

  1. 先有源文件,再有可执行程序——源文件一定比可执行文件先进行修改
  2. 如果我们更改了源文件,历史上曾经还有可执行,那么源文件的最近修改时间,一定比可执行程序要新!
  3. 一般而言,源文件的最近修改时间会不会和可执行程序最近修改时间是不会一样的,除非我们去修改了设置

基于上面两个常识,我们就能知道为什么了

我们只需比较可执行程序的最近修改时间和源文件最近修改时间,

  1. 如果可执行程序的修改时间比源文件的修改时间新,那么说明源文件是旧的,不需要重新编译;
  2. 如果可执行程序的修改时间比源文件的修改时间旧,那么说明源文件是新的,需要重新编译;

我们来验证一下上面的猜想

我们先补充一下知识

linux有一条指令stat,专门用来查看文件的生成时间

对于文件有3个时间(简称Acm)

  • Access(进入):最近被访问时间
  • Modify(改变):最近文件内容被修改时间
  • Change(更改): 最近文件属性被修改时间

 文件=文件内容+文件属性

 在linux中,我们把文件内容改了,文件属性也改了(大小)

我们很容易知道Access更改的频率是非常高的,但是文件是存在在外部磁盘里,当用户一多,更改频率太大容易影响性能,所以Access不会每次都更改

我们去修改一下mycode.c

我们发现 全变了

 我们发现上面的Change改变了

 我们回头去验证

我们怎么判断可执行程序和源文件的新旧啊?

我们一般比较Modify

其实最简单的方法就是把它们各自的时间转换为时间戳进行对比

mycode.c的Access更改的原因是因为gcc编译时读取了mycode.c

我们比较可执行程序和源文件的Modify时间,显然可执行程序的更新

 

这里不让我们make了

我们直接创建一个新的mycode.c

时间全更新了

这个新的mycode.c会覆盖旧的mycode.c

 

这个时候啊,源文件的Modify时间比可执行程序的新,肯定可以执行make

我们看看可执行程序的Modify时间和源文件的

可执行程序的时间更新啊

 不能执行make

依次类推

我们得出结论:make会根据源文件和目标文件的新旧,判断是否需要重新执行依赖关系!它不一定总是执行的

今天我就是想对应的依赖关系被执行呢?

那么就引入了新的语法

我们打开Makefile

改成下面这样子

 

这样子就能总是执行mycode的依赖关系啦

但是我们一般不会把这个语法用在这里

我们常常把这个语法用到清理工作,因为清理工作需要总是被执行

这样子清理工作就能多次被执行了

2.5.特殊符号

我们在上面的Makefile文件里面写的是

我们可以将其修改成下面这样子

  • $@代表依赖关系的目标文件,冒号的左侧
  • $^代表冒号的右侧

 也能正常运转,我们发现使用make就会回显,我们不想回显,怎么做呢?

这样子即可

3.Linux第一个项目——进度条

制作进度条,我们需要一些储备知识

3.1.回车换行

我们对回车换行可能有的误解

真正的回车换行应该是下面这样子的

回车(Carriage Return):在打字机时代,回车指的是将打字机的打印头(称为"carrier")移回到行首的操作。在计算机时代,回车通常表示将光标移动到当前行的开头,而不会换到下一行。在ASCII字符集中,回车通常用"\r"表示。

换行(Line Feed)::换行是指将光标移动到下一行的操作,使得文本在纵向上向下移动一个行高。在ASCII字符集中,换行通常用"\n"表示。

  • 在Unix和类Unix系统(如Linux和macOS)中:通常使用换行字符(“\n”)来表示换行。
  • 在Windows系统中:,通常使用回车和换行的组合来表示换行,即"\r\n"。

 3.2.缓冲区

缓冲区(Buffer)是计算机内存中的一块特定区域,用于临时存储数据。它在许多计算机系统和应用程序中发挥着重要作用,通常用于临时存储输入数据、输出数据或在内存和其他设备之间进行数据传输。


输入缓冲区:用于暂时存储从输入设备(如键盘、鼠标、网络接口等)接收到的数据,直到程序能够处理它们。输入缓冲区使得程序可以按需处理输入,而不必担心输入数据的速度与程序处理速度不匹配的问题。

输出缓冲区:用于暂时存储将要发送到输出设备(如显示器、打印机、网络接口等)的数据,直到设备准备好接收它们。输出缓冲区可以提高数据传输的效率,因为程序不必等待设备就绪就可以继续执行

3.2.1.缓冲区何时被清理

拿C语言举个例子:

在C语言中,标准库函数printf()用于将格式化的数据打印到标准输出流(通常是终端)。但是,printf()函数并不会立即将数据显示到终端上。相反,它会将数据写入到输出缓冲区中。输出缓冲区是一个临时存储区域,用于存放printf()函数打印的数据,直到满足一定条件时才将其刷新(即将数据发送到终端并显示出来)。

这些条件包括:

  1. 遇到换行符 \n:当printf()函数遇到换行符时,输出缓冲区会被自动刷新,将缓冲区中的数据输出到终端并显示出来。
  2. 缓冲区满:当输出缓冲区满了,它也会被自动刷新。
  3. 调用fflush()函数:显式调用fflush(stdout)函数可以强制刷新输出缓冲区,将其中的数据输出到终端。
  4. 程序结束:当程序正常终止时,所有的缓冲区都会被刷新。

3.2.2.验证缓冲区存在

我们先写出基本的代码结构

processBar.h

processBar.c我们暂时不写

main.c

我们新建一个makefile,写成下面这个

我们为什么只写两个源文件,不写头文件呢?

这个是因为头文件在makefile的同一目录里,我们的main.c包含了这个头文件,make会自动寻找头文件

没有任何问题 

我们基于这个结构来验证一下缓冲区的问题

我们可以查找sleep函数

我们编译运行上面那个代码

发现hello world直接出来了,没有等待3ms 

 

我们更新一下代码

helloworld居然没有先出来出来

在等待的这段时间里,helloworld是在输出缓冲区里

后出来的helloworld

我们再更新 一下代码

fflush函数可以去man3号手册查

直接输出了

3.3.代码 

有了以上的知识储备,咱们就可以尝试编写一下简单的倒计时程序了,思路如下:

  • 首先新建一个time.c文件,然后再用我们之前讲的makefile工具来实现time.c文件的自动构建:

#include <stdio.h>
#include <unistd.h>
int main()
{int cnt = 10;while(cnt >= 0){// 打印的时候每次覆盖上一次出现的数字printf("倒计时:%2d\r",cnt);// 强制冲刷缓冲区fflush(stdout);--cnt;sleep(1);}printf("\n");return 0;
}

make命令进行编译: 

  • 这里有个小拓展,如果我们要覆盖上次的数字是4位,这次是三次(比如1000到999),可以用%4d这个输出形式来解决,也可以用下面这种方法:
#include <stdio.h>
#include <unistd.h>
int main()
{int cnt = 1000;int tmp = cnt;int num = 0;while (tmp){++num;tmp /= 10;}while(cnt >= 0){// 主要就是这里的变化,用最大数字的位数来做占位符printf("倒计时:%*d\r",num, cnt);fflush(stdout);--cnt;sleep(1);}printf("\n");return 0;
}

 

总共有三个部分:
 

1. 我们要实现的进度条用#来进行加载;

2. 后面要有数据来表示现在加载的进度是多少(百分数);

3. 最后用一个动态旋转的类来表示程序还在继续加载

1. 动态加载的过程

动态加和之前的倒计时差不多,每次都要覆盖上次出现的#,具体思路如下:


1. 定义一个字符类型数组char *str,用memset()函数进行初始化(‘\0’);

2. 循环100次,每次循环都在数组中加一个#,并打印str('\r’进行覆盖);

3. 强制冲刷缓冲区;

2. 进度加载

我们可以用每次循环的次数来当作是当前加载的进度,当然还要进行覆盖,具体思路如下:


1. 每次循环都以当前的循环次数作为加载进度;

2. 每次覆盖上一次的进度;

3. 强制冲刷缓冲区。

4. 程序休眠(可以用usleep()函数,单位是微秒)

3. 动态旋转

定义一个数组,并初始化为-\\/-,覆盖的方法和之前类似,就不详细说了。

#include "process_bar.h"
#include <memory.h>
#include <unistd.h>
#define style '#'
#define round "-\\/-"
void test()
{int i = 0;char str[100];memset(str,'\0',sizeof(str));while (i <= 100){str[i] = style;printf("[%-100s][%d%%][%c]\r",str,i,round[i % 4]);fflush(stdout);++i;usleep(10000);}printf("\n");
}

 3.4.第二版本

我们正常用进度条肯定不是单独使用的,会结合其他的场景,例如下载界面,登陆界面

对于要下载的文件,肯定有文件大小,下载的时候网络也有它的带宽,所以在下载的时候,每次下载的大小都是一个带宽,我们可以先写一个下载的函数:

void download()
{double bandwidth = 1024 * 1024 * 1.0;double filesize = 1024 * 1024 * 10.0;double cur = 0.0;while (cur <= filesize){// 调用进度条函数test(filesize, cur);// 每次增加带宽cur += bandwidth;usleep(20000);}printf("\n");printf("this file has been downloaded\n");
}
void test(double total, double current)
{char str[101];memset(str,'\0',sizeof(str));int i = 0;// 这次的比率double rate = (current * 100) / total;// 循环次数int loop_count = (int)rate;while (i <= loop_count){str[i++] = style; }printf("[%-100s][%.1lf%%][%c]\r",str,rate,round[loop_count % 4]);fflush(stdout);
}
// 头文件 process_bar.h
#include <stdio.h>typedef void(*callback_t)(double, double);// 函数指针(回调函数)void test(double total, double current);// 函数实现文件 process_bar.c
#include "process_bar.h"
#include <memory.h>
#include <unistd.h>
#define style '#'
#define round "-\\/-"void test(double total, double current)
{char str[101];memset(str,'\0',sizeof(str));int i = 0;double rate = (current * 100) / total;int loop_count = (int)rate;while (i <= loop_count){str[i++] = style; }printf("[%-100s][%.1lf%%][%c]\r",str,rate,round[loop_count % 4]);fflush(stdout);
}// main.c 主函数和 download 函数
#include "process_bar.h"
#include <unistd.h>double bandwidth = 1024 * 1024 * 1.0;
void download(double filesize, callback_t cb)
{double cur = 0.0;while (cur <= filesize){cb(filesize, cur);cur += bandwidth;usleep(20000);}printf("\n");printf("this file has been downloaded\n");
}int main()
{download(1024*1024*100.0,test);download(1024*1024*20.0,test);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/21980.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java集合思维导图

详细内容请看链接内容 Java集合面试题集——2024最新大厂面试

【图像识别系统】表情识别Python+人工智能深度学习+TensorFlow+卷积算法网络模型+图像识别

表情识别系统&#xff0c;本系统使用Python作为主要编程语言&#xff0c;通过TensorFlow搭建ResNet50卷积神经算法网络模型&#xff0c;通过对7种表情图片数据集&#xff08;‘Neutral’, ‘Anger’, ‘Disgust’, ‘Fear’, ‘Happy’, ‘Sad’, ‘Surprise’&#xff09;进行…

RabbitMQ学习笔记(一)RabbitMQ部署、5种队列模型

文章目录 1 认识MQ1.1 同步和异步通讯1.1.1 同步通讯1.1.2 异步通讯 1.2 技术对比 2 RabbitMQ入门2.1 RabbitMQ单机部署2.2 RabbitMQ基本结构2.3 RabbitMQ队列模型2.3.1 简单队列模型&#xff08;Simple Queue Model&#xff09;2.3.2 工作队列模型&#xff08;Work Queue Mode…

visual studio打包qt算子时,只生成dll没有生成lib等文件

问题&#xff1a;在visual studio配置了qt项目&#xff0c;并打包成dll&#xff0c;原则上会生成一堆文件&#xff0c;包括dll,lib等文件。 解决办法&#xff1a; 挨个右击源代码的所有头文件-》属性-》项类型。改成qt头文件形式&#xff0c;如下。

事务详讲(本地及分布式)

本地事务在分布式的问题: 因为在分布式服务中,难免一个接口中会有很多调用远程服务的情况,这个就非常容易出现问题,以下是一个详细的例子: 例如,你为了保证事物的一致性等要求,所以,你方法上只写了Transactional,但你的业务中又需要调用其他微服务的方法(Feign),这时就容易出现…

【机器学习】Qwen1.5-14B-Chat大模型训练与推理实战

目录 一、引言 二、模型简介 2.1 Qwen1.5 模型概述 2.2 Qwen1.5 模型架构 三、训练与推理 3.1 Qwen1.5 模型训练 3.2 Qwen1.5 模型推理 四、总结 一、引言 Qwen是阿里巴巴集团Qwen团队的大语言模型和多模态大模型系列。现在&#xff0c;大语言模型已升级到Qwen1.5&…

使用 Scapy 库编写 ICMP 重定向攻击脚本

一、介绍 ICMP重定向攻击&#xff08;ICMP Redirect Attack&#xff09;是一种网络攻击&#xff0c;攻击者通过发送伪造的ICMP重定向消息&#xff0c;诱使目标主机更新其路由表&#xff0c;以便将数据包发送到攻击者控制的路由器或其他不可信任的设备上。该攻击利用了ICMP协议…

springboot配置集成RedisTemplate和Redisson,使用分布式锁案例

文章要点 自定义配置属性类集成配置RedisTemplate集成配置分布式锁Redisson使用分布式锁简单实现超卖方案 1. 项目结构 2. 集成RedisTemplate和Redisson 添加依赖 依赖的版本与继承的spring-boot-starter-parent工程相对应&#xff0c;可写可不写 <!--spring data redis…

Spring boot 集成mybatis-plus

Spring boot 集成mybatis-plus 背景 Spring boot集成mybatis后&#xff0c;我们可以使用mybatis来操作数据。然后&#xff0c;我们还是需要写许多重复的代码和sql语句&#xff0c;比如增删改查。这时候&#xff0c;我们就可以使用 mybatis-plus了&#xff0c;它可以极大解放我…

沐风老师3DMAX顶点切线控制插件VertexTangants安装使用方法

3DMAX顶点切线控制插件VertexTangants安装使用方法 3DMAX顶点切线控制插件VertexTangants&#xff0c;用于轻松控制图形顶点切线的工具。 【主要功能】 -脚本具有获取选定顶点的自动检测功能&#xff0c;您可以随时使用“获取按钮”获取选定顶点。 -有一个用于激活撤消的ON按…

项目资源管理

目录 1.概述 2.六个过程 2.1. 规划资源管理 2.2. 估算活动资源 2.3. 获取资源 2.4. 建设团队 2.5. 管理团队 2.6. 控制资源 3.应用场景 3.1.十个应用场景 3.2.软件开发项目 3.2.1. 资源规划 3.2.2. 资源分配 3.2.3. 资源获取 3.2.4. 资源优化 3.2.5. 资源监控与…

如何在外网http访问内网邮件server?

不少公司选择用winmail搭建部署内部邮箱服务器&#xff0c;对于邮件管理员&#xff0c;不但需要在局域网内&#xff0c;常常需要在外网也能访问到邮箱服务管理。winmail本身系统功能可以开启http访问管理&#xff0c;但当需要在外网http访问内网邮箱服务时&#xff0c;需要用到…

vue3通过Vite实现工程化

1. vue3简介 Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0c;Vue 都可以胜任。官网为:Vue.js - 渐进…

秋招突击——算法打卡——6/3——复习{最低通行费、(状态压缩DP)小国王}——新做:{罗马数字转整数、最长公共前缀}

文章目录 复习背包模型——最低通行费题目内容实现代码 &#xff08;状态压缩DP&#xff09;小国王检查状态本身是否存在两个连续的1计算所有的合法状态已经所有合法状态之间的转移动态规划过程 新作罗马数字转整数个人实现实现代码 参考做法实现代码 最长公共前缀个人实现参考…

Docker无法stop或者rm指定容器

Docker无法stop或者rm指定容器 今日准备重启一下docker 容器部署的 Nginx 时&#xff0c;使用的命令是 docker exec -it ir-nginx nginx -s reload 结果发现无法重启报错 然后想着关闭再启动&#xff0c;结果发现 docker restart 、docker stop 、docker kill 、docker exec 都…

【科学文献计量】使用Endnote软件打开中国知网导出的文献期刊解析不正确问题解决

使用Endnote软件打开中国知网导出的文献期刊解析不正确问题解决 问题解决问题 新建一个Endnote的材料库,然后把下载好的中国知网文献数据(知网数据导出的是Endnote格式样式)导入进来。找到文件所在路径,导入的类型选择是“Endnote import”,然后点击确定,界面结果如下 …

汇编:数据定义数据填充

数组的定义 在32位汇编语言中&#xff0c;定义数组时&#xff0c;通常使用定义数据指令&#xff08;如 DB, DW, DD,DQ &#xff09;和标签来指定数组的名称和内容。DB定义字节数组&#xff08;每个元素占1字节&#xff09;、DW定义字数组&#xff08;每个元素占2字节&#xff…

CAD 文件(DXF / DWG)转换为(DXF / PDF / PNG / SVG)

方法一Github 这个是ezdxf出品的&#xff0c;可以使用命令行的方式进行转换 ezdxf draw -o file.<png|svg|pdf> <file.dxf>也可以自己改动代码 examples/addons/drawing/pdf_export.py 但是直接运行会有误&#xff0c;以下是我改动后的代码&#xff1a; from ez…

#13前端后花园周刊-10个现代 Node.js 运行时新特性、Nextjs15、Astro4.9、CSS压缩

⚡️行业动态 JavaScript 的创建者 Brendan Eich 在 Twitter/X 上出现&#xff0c;反驳了 JS 是“最邋遢的”的说法&#xff0c;称其只有 50% 。 &#x1f4c6;发布 Next.js 15 RC 流行的 React 元框架已经准备好迎接一个主要的新版本&#xff0c;它有一个 RC&#xff0c;让…

VS2015 +Qt 新建单元测试工程报错error LNK2019,error LNK2001: 无法解析的外部符号 WinMain

项目场景&#xff1a; 使用Qt5.9.9和vs2015进行单元测试工程的创建 问题描述 创建完成后&#xff0c;编译项目&#xff0c;报错&#xff1a; error LNK2019&#xff0c;error LNK2001: 无法解析的外部符号 WinMain 原因分析&#xff1a; 原因是笔者创建工程的时候&#xf…