【C++进阶】Makefile基础(一)

news2024/11/24 5:40:42

文章目录

    • 1. 环境
    • 2. 规则
    • 3. 原理
    • 4. 伪目标

Makefile 其实只是一个指示 make 程序(后面简称 make 或有时称之为 make 命令)如何为我们工作的命令文件,我们说 Makefile 其实是在说 make,这一点要有很清晰的认识。而对于我们的项目来说,Makefile 是指软件项目的编译环境。软件产品开发在编码阶段最常见的工作内容大致是:

  • 开发人员根据概要设计进行编码
  • 开发人员编译所设计的源代码以生成可执行文件
  • 开发人员对软件产品进行测试来验证其功能的正确性

上面的三个步骤是一个迭代过程,如果最终验证设计的正确性完全达到要求,那么就完成了编码阶段的开发,如果没有那还得重复这三个步骤,直到达到设计要求为止。

在上面的几步中,与 Makefile 关系最大的是第二步,那 Makefile 的好坏对于项目开发有些什么影响呢?设计得好的 Makefile,当我们重新编译时,只需编译那些上次编译成功后修改过的文件,也就是说编译的是一个 delta,而不是整个项目。反之,如果一个不好的 Makefile 环境,可能对于每一次的编译先要 clean,然后再重新编译整个项目。两种情况的差异是显然的,后者将耗费开发人员大量的时间用于编译,也就意味着低效率。对于小型项目,低效问题可能表现得并不明显,但对于规模相对大的项目,那就非常的明显了。开发人员可能一天做个十次编译(甚至更少)就没有时间用于编码和测试(调试)了。这就是为什么通常大型项目都会有一个专门维护 Makefile 的一个小团
队,来支撑产品的开发。

最为重要的是掌握二个概念,一个是目标(target),另一个就是依赖(dependency)。目标就是指要干什么,或说运行 make 后生成什么,而依赖是告诉 make 如何去做以实现目标。在 Makefile 中,目标和依赖是通过规则(rule)来表达的。我们最为熟悉的是采用 make 来进行软件产品的代码编译,但它可以被用来做很多很多的事情,后面我们会给出一些不是用 make 来进行代码编译的例子。驾驭 Makefile,最为重要的是要学会采用目标和依赖关系来思考所需解决的问题。


在这里插入图片描述

  • 目标(Targets): Makefile中的目标是指需要生成的文件或者是需要执行的操作。目标可以是一个文件、一个命令或者是一个操作序列。
  • 依赖(Dependencies): Makefile中的依赖是指目标所依赖的文件或者是命令。如果依赖文件发生了变化,那么目标也需要重新生成。
  • 命令(Commands): Makefile中的命令是指生成目标所需要执行的操作序列。这些操作可以是编译、链接、复制、打包等等。命令必须以一个制表符或者是多个空格开头,否则会被当成注释。

在这里插入图片描述
Makefile 是一个文本文件,其中包含一些规则和指令,用于描述如何编译和链接一个或多个源代码文件,生成可执行程序或库文件。

Makefile 的工作原理如下:

  1. Makefile 中定义了目标文件、依赖文件和命令。目标文件通常是可执行程序或库文件,依赖文件是源代码文件、头文件或其他依赖项,命令是编译、链接和生成目标文件的操作。

  2. 当执行 make 命令时,Makefile 中的规则会被解析,根据依赖关系生成一个依赖图,确定哪些文件需要重新编译。

  3. Make 程序根据依赖图和规则,递归地执行编译、链接和生成目标文件的操作,确保所有依赖项都被编译和链接,生成最终的目标文件。

  4. 如果某些依赖项没有改变,则不需要重新编译和链接,从而提高了编译效率。

  5. Makefile 还支持变量、条件语句、循环语句等高级特性,可以根据不同的条件进行编译和链接,生成不同的目标文件。

1. 环境

使用makefile的环境要求如下:

  1. 操作系统:makefile可以在大多数操作系统上使用,包括Linux、Unix、Mac OS X、Windows等。
  2. 编译器:makefile需要一个支持GNU make语法的编译器,例如GNU make、BSD make等。
  3. 目标文件:makefile需要可编译的源代码文件,例如C、C++、Java等。
  4. 环境变量:makefile需要一些环境变量来指定编译器、编译选项等,例如CC、CFLAGS、LDFLAGS等。
  5. 编辑器:makefile需要一个编辑器来编写和编辑makefile文件,例如Vim、Emacs等。
  6. make工具:makefile需要一个make工具来执行makefile文件,例如GNU make。

使用步骤:

  1. 安装GNU Make工具:Make是一个命令行工具,用于自动化构建软件的过程。可以从GNU官网下载并安装Make工具。
  2. 创建Makefile文件:Makefile是一个文本文件,其中包含了一系列规则和指令,用于描述如何构建软件。可以在项目根目录下创建一个名为Makefile的文件。
  3. 编写Makefile规则:Makefile规则由目标、依赖和命令组成。目标是指要生成的文件或者执行的操作;依赖是指生成目标的前提条件;命令是指生成目标的具体操作。
  4. 运行Make命令:在命令行中进入项目根目录下,输入make命令即可执行Makefile中定义的规则,生成目标文件或者执行操作。

命令行输入make -v,如果出现类似于下图的版本信息,那么说明make在你的环境中已经可用:
在这里插入图片描述

注意事项:

  1. 在编写Makefile时,应该尽可能使用变量和函数,以便提高代码的可读性和可维护性。
  2. Makefile中的命令必须以Tab键开始,而不是空格键。
  3. Makefile中的依赖关系应该尽可能明确,以便正确地判断哪些规则需要执行。
  4. 在执行Make命令之前,应该确保所有依赖文件都已经存在,否则会导致构建失败。

2. 规则

我们使用Hello World来开始Makefile规则的学习,编写一个如下的 Makefile 文件,文件的存放目录可以是任意的:

all:
	echo "Hello World"

在这里插入图片描述

需要注意的是 echo 前面必须只有 TAB,且至少有一个 TAB,而不能用空格代替。

Makefile 中第一个很重要的概念就是目标(target),上面代码中的 all 就是我们的目标,目标放在 : 的前面,其名字可以是由字母和下划线组成。echo “Hello World”就是生成目标的命令,这些命令可以是任何在你的环境中运行的命令以及 make 所定义的函数等等,这里的 echo 是 BASH Shell 中的一个命令,其功能是打印字符串到终端上。在这里的 all 目标是在终端上打印出“Hello World”,有时目标会是一个比较抽象的概念。all 目标的定义,其实是定义了如何生成 all 目标,这称之为规则,即上面的 Makefile 定义了一个生成 all 目标的规则。

下面的示例展示了三种不同的运行方式以及每种方式的运行结果:

  • 第一种方式:只要在 Makefile 所在的目录下运行make命令,终端上就会输出两行,第一行实际上是我们在 Makefile 中所写的命令,而第二行则是运行命令的结果
  • 第二种方式:运行make all命令,这告诉 make 工具,我要生成目标 all,其结果跟第一种方式一样
  • 第三种方式:运行make test,指示 make 为我们生成 test 目标。由于我们根本没有定义 test 目标,所以运行结果是可想而知的,make 的确报告了不能找到 test 目标

在这里插入图片描述
现在对上面的 Makefile 做一点小小的改动,如下面所示,增加了 test 规则用于构建 test 目标,实现在终端上打印出“Just for test!”:

all:
	echo "Hello World"
test:
	echo "Just for test!"

在这里插入图片描述
从如上输出我们可以发现:

  • 一个 Makefile 中可以定义多个目标
  • 调用make命令时,我们得告诉它我们的目标是什么,即要它干什么。当没有指明具体的目标是什么时,那么 make 以 Makefile 文件中定义的第一个目标作为这次运行的目标。这第一个目标也称之为默认目标(和是不是all没有关系)
  • 当 make 得到目标后,先找到定义目标的规则,然后运行规则中的命令来达到构建目标的目的。现在所示例的 Makefile 中,每一个规则中都只有一条命令,而实际的 Makefile,每一个规则可以包含很多条命令

对于前面的示例,当运行make时,在终端上还打印出了 Makefile 文件中的命令。有时并不希望它这样,因为这样可能使得输出的信息看起来有些混乱。要使make不打印出命令,只要做一点小小的修改,改过的 Makefile 如下所示,就是在命令前加了一个@。 这一符号告诉make,在运行时不要将这一行命令显示出来:

all:
	@echo "Hello World"
test:
	@echo "Just for test!

在这里插入图片描述
对上述代码再做一点点小改动,在 all 目标的:后加上 test 目标,如下所示

all: test
        @echo "Hello World"
test:
        @echo "Just for test!"

在这里插入图片描述


下面讲解一下 Makefile 中的依赖关系

上面的代码中,all 目标后的 test 告诉 make,all 目标依赖 test 目标,这一依赖目标在 Makefile 中又被称之为先决条件。出现这种目标依赖关系时,make 工具会按从左到右的先后顺序先构建规则中所依赖的每一个目标。如果希望构建 all 目标,那么 make 会在构建它之前得先构建 test 目标,这就是为什么称之为先决条件。下面用类图表达了 all 目标的依赖关系:

在这里插入图片描述

至此,我们了解 Makefile 中规则,下面是规则的文字和 UML。一个规则是由目标(targets)、先决条件(prerequisites)以及命令(commands)所组成的。需要指出的是,目标和先决条件之间表达的就是依赖关系(dependency),这种依赖关系指明在构建目标之前,必须保证先决条件先满足(或构建);而先决条件可以是其它的目标,当先决条件是目标时,其必须先被构建出来。

targets : prerequisites
	command

在这里插入图片描述
规则中目标可以有多个,当存在多个目标,且这一规则是 Makefile 中的第一个规则时,如果我们运行 make 命令不带任何目标,那么规则中的第一个目标将被视为是缺省目标,如下所示:

all test:
	@echo "Hello World"

在这里插入图片描述
make 处理一个规则的活动图如下图所示,当中的构建依赖目标(build dependent target(s))这一活动(注意是活动,而不是动作)就是重复图下图所示的同样的活动,你可以看作是对下面活动图的递归调用。而运行命令构建目标(run command to build target)则是一个动作,是由命令所组成的动作。活动与动作的区别是,动作是只做一件事(但是可以有多个命令),而活动可以包括多个动作。
在这里插入图片描述

3. 原理

接下来我们试着将规则运用到程序编译当中去,下面我们假设有用于创建 simple 可执行文件的两个源程序文件,我们需要写一个用于创建simple 可执行程序的 Makefile,这个 Makefile 需要如何去写?
foo.c

#include <stdio.h>
void foo ()
{
	printf ("This is foo()\n");
}

main.c

extern void foo();
int main ()
{
	foo();
	
	return 0;
}

写一个 Makefile 文件的第一步不是一个猛子扎进去试着写一个规则,而是先用面向依赖关系的方法想清楚,所要写的 Makefile 需要表达什么样的依赖关系,这一点非常的重要。通过不断的练习,我们最终能达到很自然的运用依赖关系去思考问题。到那时,你再写 Makefile 时,头脑会非常的清楚自己在写什么,以及后面要写什么。现在抛开 Makefile,我们先看一看 simple 程序的依赖关系是什么。

第一个跃入我们脑海中的依赖关系图,其中 simple 可执行文件显然是通过 main.c 和 foo.c 最后编译并连接生成的。通过这个依赖图,其实就可以写出一个 Makefile 来了。这样的依赖关系所写出来的 Makefile,在现实中不是很可行,就是你得将所有的源程序都放在一行中让 GCC 为我们编译:
在这里插入图片描述
下图是 simple 程序的依赖关系更为精确的表达,其中加入了目标文件。对于 simple 可执行程序来说,下图表示的就是它的“依赖树”。接下来需要做的是将其中的每一个依赖关系,即其中的每一个带箭头的虚线,用 Makefile 中的规则来表示:
在这里插入图片描述

all: main.o foo.o
        gcc main.o foo.o -o simple
main.o: main.c
        gcc main.c -c
foo.o: foo.c
        gcc foo.c -c

.PHONY:clean
clean:
        rm -f main.o foo.o simple

在这个 Makefile 中,我还增加了一个伪目标用于删除生成的文件,包括目标文件和 simple 可执行程序,这在现实的项目中很常见。
在这里插入图片描述
如果我们在不改变代码的清况下再编译会出现什么现象呢?下图给出了结果,注意到第二次编译并没有构建目标文件的动作,但为什么有构建simple可执行程序的动作呢?
在这里插入图片描述
Makefile会根据文件的时间戳(即最后修改时间)来判断文件是否需要重新构建。如果某个文件的时间戳比依赖它的文件要旧,那么该文件就需要重新构建。因此,如果你多次执行make命令,即使源文件和头文件没有变化,可执行文件的时间戳也会更新,从而导致重新构建。如果想避免这种情况,可以使用make的增量构建功能,这样只会重新构建必要的文件。

下面验证一下如果对 foo.c 进行改动,是否会重新构建。对于 make 工具,一个文件是否改动不是看文件大小,而是其时间戳。Linux下只需用 touch 命令来改变文件的时间戳,这相当于模拟了对文件进行了一次编辑,而不需真正对其进行编辑,如图所示,make 发现了 foo.c 的改变,并对其进行了重新编译:
在这里插入图片描述

4. 伪目标

在 Makefile 中,伪目标是一种特殊的目标,它并不代表一个实际的文件,而是用于完成特定的任务或者组织其他目标的执行顺序。

假设我们有一个C语言项目,包含以下几个文件:main.c, foo.c, bar.c, foo.h, bar.h。我们需要编译这个项目生成一个可执行文件my_program。一个简单的 Makefile 可能如下所示:

my_program: main.o foo.o bar.o
	gcc -Wall -g -o my_program main.o foo.o bar.o

main.o: main.c foo.h bar.h
	gcc -Wall -g -c main.c

foo.o: foo.c foo.h
	gcc -Wall -g -c foo.c

bar.o: bar.c bar.h
	gcc -Wall -g -c bar.c

clean:
	rm -f *.o my_program

在这个 Makefile 中,我们有一个名为clean的目标。它不依赖于其他目标,也不代表一个实际的文件。它的作用是删除所有的中间文件(.o文件)和生成的可执行文件(my_program),这就是一个典型的伪目标。

伪目标的主要特点和用途:

  • 不代表实际的文件:伪目标并不对应任何实际存在的文件,它只是为了完成特定任务而存在
  • 避免名称冲突:由于伪目标不代表实际的文件,我们可以避免因文件和目标名称相同而导致的错误
  • 更好地组织Makefile:通过伪目标,我们可以把不同的任务和操作分开,使Makefile更加清晰易读
  • 强制执行:使用伪目标,我们可以强制执行某个任务,而不受文件是否存在或已经更新的影响

在 Makefile 中,我们可以使用`.PHONY``声明一个伪目标,以明确地告诉 make 这个目标不是一个实际的文件。例如,我们可以在上面的例子中添加如下声明:

.PHONY: clean

这样做的好处是,即使当前目录下存在一个名为clean的文件,make 也会知道clean是一个伪目标,而不是一个实际的文件。

当然,除了上述clean伪目标之外,还有其他常见的伪目标。以下是一些在Makefile中经常使用的伪目标:

  1. all:这个伪目标通常用于编译整个项目。当用户执行make或make all时,它将自动编译并生成所有需要的目标。
    .PHONY: all
    all: my_program
    
  2. install:这个伪目标用于安装编译好的程序到系统指定的目录。通常,这需要管理员权限,因为它涉及到在系统目录中创建或修改文件。
    .PHONY: install
    install: my_program
    	cp my_program /usr/local/bin
    
  3. uninstall:这个伪目标用于从系统中删除已安装的程序。和install一样,它通常也需要管理员权限。
    .PHONY: uninstall
    uninstall:
    	rm -f /usr/local/bin/my_program
    
  4. test:这个伪目标用于运行项目的测试用例,确保项目的各个部分正常工作。
    .PHONY: test
    test: my_program
    	./test_script.sh
    
  5. help:这个伪目标用于显示Makefile的使用说明,帮助用户了解如何使用Makefile。
    .PHONY: help
    help:
    	@echo "Usage:"
    	@echo "  make all      - Compile the project"
    	@echo "  make clean    - Remove compiled files and binaries"
    	@echo "  make install  - Install the program"
    	@echo "  make test     - Run tests"
    

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

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

相关文章

21-CSS

目录 1.CSS是什么&#xff1f; 2.CSS基本语法 3.CSS类型 3.1.行内样式&#xff08;适用范围最小&#xff09; 3.2.内部样式&#xff08;适用范围适中&#xff09; 3.3.外部样式&#xff08;适用范围最大&#xff09; PS&#xff1a;关于缓存 3.4.多种样式优先级 4.代码…

浅析Linux Socket套接字编程中的 5 个隐患

隐患 1&#xff0e;忽略返回状态 第一个隐患很明显&#xff0c;但它是开发新手最容易犯的一个错误。如果您忽略函数的返回状态&#xff0c;当它们失败或部分成功的时候&#xff0c;您也许会迷失。反过来&#xff0c;这可能传播错误&#xff0c;使定位问题的源头变得困难。 捕…

XTDrone PX4 仿真平台|Ubuntu20.04 环境搭建失败记录

XTDrone PX4 仿真平台|Ubuntu20.04 环境搭建失败记录仿真环境搭建依赖安装ROS安装Gazebo安装MAVROS安装PX4配置仿真环境搭建 依赖安装 在使用apt安装的过程中&#xff08;包括之后ROS的安装&#xff09;&#xff0c;如果出现难以解决的依赖问题&#xff0c;可以使用aptitude …

Revit净高分析的三种方法

关于净高分析&#xff0c;就是在revit绘制模型到一定程度后&#xff0c;需要对构件的空间位置进行分析&#xff0c;如结构层高、管线净高、地下室是否满足人员车辆进出、地上部分是否满足正常的人员设备活动空间、净高是否满足设备进场安装等&#xff0c;它可以让我们提前发现不…

Codepipeline 跨账号访问 Codecommit

背景 大型企业中代码仓库通常存放在各部门开发账户中&#xff0c;而流水线则位于独立 DevOps 账户中。 本文我们将介绍如何创建跨账号访问 Codecommit 代码仓库的 Codepipeline 流水线&#xff0c;即 CodePipeline 调用另一个账号中的 Codecommit 代码仓库。 亚马逊云科技开发…

【Python_Scrapy学习笔记(十二)】基于Scrapy框架实现POST请求爬虫

基于Scrapy框架实现POST请求爬虫 前言 本文中介绍 如何基于 Scrapy 框架实现 POST 请求爬虫&#xff0c;并以抓取指定城市的 KFC 门店信息为例进行展示 正文 1、Scrapy框架处理POST请求方法 Scrapy框架 提供了 FormRequest() 方法来发送 POST 请求&#xff1b; FormReques…

阿里云ECS大测评【免费领阿里云套餐~】

送福利&#xff01;阿里云热门产品免费领&#xff08;含ECS&#xff09;&#xff0c;点击进入&#xff1a;https://click.aliyun.com/m/1000370359/ 本篇博客参与阿里云 ECS实例测评&#xff0c;这里采用阿里的C5.large计算型实例 本次对云服务器ECS产品能力的体验和建议&…

【2023最新】超详细图文保姆级教程:App开发新手入门(4)

之前章节我们已经完成了一个应用项目的导入、代码更新和代码提交和应用打包编译&#xff0c;本章继续讲述一下&#xff0c;如何在开发过程中进行代码的同步联机调试。 7 代码真机调试 7.1 纯静态CSS页面样式查看 代码调试有多种方式&#xff0c;如果是查看纯粹的静态样式&am…

价值迭代求解马尔可夫决策过程

Value Iteration Algorithm 其算法思想是: 在每一个状态s下&#xff0c; 之迭代算法流程如下&#xff1a; 初始化状态价值state value&#xff0c;即对每个状态的价值都赋一个初始值&#xff0c;一般是0 计算每一个状态-动作对的 动作价值函数&#xff0c;通常通过创建一个二维…

项目管理(PMP)考试:易混淆知识点汇总

请点击↑关注、收藏&#xff0c;本博客免费为你获取精彩知识分享&#xff01;有惊喜哟&#xff01;&#xff01; PMP考试中经常出现&#xff0c;但容易混淆的一些内容&#xff0c;包含15个会议、40个分析、6个矩阵、5个清单、5个审计、5个报告、4个分解结构、4个评估、3个日志…

谷歌人机验证Google reCAPTCHA

reCAPTCHA是Google公司推出的一项验证服务&#xff0c;使用十分方便快捷&#xff0c;在国外许多网站上均有使用。它与许多其他的人机验证方式不同&#xff0c;它极少需要用户进行各种识图验证。 它的使用方式如下如所示&#xff0c;只需勾选复选框即可通过人机验证。 虽然简单…

Python项目之中国数据可视化

文章目录关键词一、做什么二、怎么做1、获取数据&&处理数据2、数据库设计&&存储数据3、开发后端接口4、前端页面编写三、效果展示四、总结关键词 PythonDjangoPython网络爬虫echarts可视化阅读者&#xff08;Python基础、Django基础、H5基础&#xff09; 一、…

Linux学习笔记——UDP协议

文章目录UDP协议端格式UDP的特点面向数据报UDP的缓冲区基于UDP的应用层协议UDP使用注意事项16位的UDP校验和的校验原理UDP协议端格式 16位源端口号&#xff1a;数据从哪一个端口发出来的&#xff0c;也就是数据从哪一个进程发送出来的。在编写应用层代码的时候&#xff0c;用ui…

Git安装使用教程

Git - Downloadshttps://git-scm.com/downloads 安装 Git。可以在 Git 官网上下载安装包并按照提示进行安装。 GitHub或Gitee官网上注册一个账号&#xff0c;注册好后&#xff0c;桌面右键选择Git Bash&#xff0c;进行账号配置&#xff0c;命令如下&#xff1a; # 配置用户名…

spark第八章:Pyspark

系列文章目录 spark第一章&#xff1a;环境安装 spark第二章&#xff1a;sparkcore实例 spark第三章&#xff1a;工程化代码 spark第四章&#xff1a;SparkSQL基本操作 spark第五章&#xff1a;SparkSQL实例 spark第六章&#xff1a;SparkStreaming基本操作 spark第七章&#…

带隙基准基本原理

目录 负温度系数电压 正温度系数电压 带隙基准 小结 如何产生一个不受温度变化&#xff0c;保持恒定的电压基准呢&#xff1f; 我们假设&#xff0c;如果将两个具有相反温度系数&#xff08;TC&#xff09;的量以适当的权重相加&#xff0c;那么结果就会成为零温度系数&am…

MIT6.824 lab2C2D实验总结

2C 就是持久化一些变量&#xff0c;日志&#xff0c;任期&#xff0c;投票给谁&#xff0c;2D(lastincludeterm, lastincludeindex, snapshot)。同时最难受的是Figure8Unreliable这个测试点&#xff0c;总是几百次出现一两个错误。最后发现是对论文一句话的歧义。这里讲解一下…

Linux(DHCP原理与配置)

文章目录一 、什么是DHCP1.1DHCP定义1.2DHCP好处1.3DHCP的分配方式二 、DHCP 的工作过程三 、DHCP中的设置3.1 DHCP参数3.2 相关操作步骤一 、什么是DHCP 1.1DHCP定义 DHCP&#xff08;动态主机配置协议&#xff09;是一个局域网的网络协议。指的是由服务器控制一段IP地址范围…

swagger文件上传接口没有选择文件按钮问题解决 使用@RequestPart注解

在使用文件上传API时&#xff0c;swagger没有选择文件按钮 在MultipartFile前面加上RequestPart注解 PostMapping("/importFile")ApiOperation(value "文件上传API")public Object importFile(ApiParam(value "文件流", required true) Reque…

超越ChatGpt,最近爆火的AutoGPT 究竟是什么

一、AutoGPT是什么 最近几天&#xff0c;一款基于GPT-4的最强语言模型AutoGPT火遍了整个AI圈。众所周知&#xff0c;此前爆火AI圈的ChatGPT&#xff0c;唯一不太好用的地方就是需要人类不断的prompt。因此&#xff0c;如果你想要ChatGPT帮你去做一件复杂的事情&#xff0c;那么…