一文手把手教你写一个 Makefile 文件

news2024/12/24 6:15:46

如果我们是在Linux下开发,那Makefile肯定要知道,不懂Makefile,面对较大的工程项目的时候就会比较麻烦,懂得利用开发工具将会大大提高我们的开发效率,也可以说Makefile是必须掌握的一项技能。

一、了解什么是 Makefile

一个大型工程中的源文件不计其数,各个功能或者模块分别放在不同的目录下,手动敲命令去编译就带来很大的麻烦,那么Makefile可以定义一系列的编译规则,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至进行更复杂的功能操作,Makefile带来的好处就是——“自动化编译 ”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高软件开发的效率。

make ** 是一个命令工具,是一个解释Makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Linux下GNU的make、Visual C++的nmake、Delphi的make。可见,Makefile都成为了一种在工程方面的编译方法。当然,不同产商的make各不相同,也有不同的语法,但其本质都是在 “文件依赖性” 上做文章** 。

二、明白编译链接过程

在编写Makefile之前,还是要先了解清楚程序编译链接过程,无论是c、c++,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,Unix/Linux下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。

链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在Unix/Linux下是Archive File,也就是 .a 文件,也叫静态库文件。

总结一下,编译链接的过程如下:

源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。
在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。
在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那就会报链接错误码(Linker Error),在VC下,这种错误一般是:Link 2001错误,意思是说,链接器未能找到函数的实现。你需要指定函数的Object File。

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈

三、编写一个简单的 Makefile

1. Makefile 的基本语法规则:

目标 ... : 依赖 ...
        实现目标的具体表达式(命令)
        ...
        ...
  • 目标 (target):就是一个目标文件,可以是Object 文件,也可以是执行文件,还可以是一个标签(Label);
  • 依赖 (prerequisites):就是要生成那个target所需要的文件或是目标;
  • 命令 (command):Shell命令,也就是make工具需要执行的命令。
总结 】:通过依赖(prerequisites)中的一些文件生成目标(target)文件,目标文件要按照命令(command)中定义的规则来生成。

2. 来看一个简单的示例代码

简单写三个方法文件(openFile.c、readFile.c、writeFile.c)、一个头文件(operateFile.h)和一个主函数文件(main.c),代码如下:

// openFile.c
#include "operateFile.h"

void openFile()
{
    printf("open file...........\n");
}
// readFile.c
#include "operateFile.h"

void readFile()
{
    printf("read file...........\n");
}
// writeFile.c
#include "operateFile.h"

void writeFile()
{
    printf("write file...........\n");
}
// operateFile.h
#ifndef __OPERATEFILE_H__
#define __OPERATEFILE_H__

#include <stdio.h>

void openFile(void);
void readFile(void);
void writeFile(void);

#endif
// main.c
#include <stdio.h>
#include "operateFile.h"

int main()
{
    openFile();
    readFile();
    writeFile();
  
    return 0;
}

3. 根据上面的语法规则及编译链接过程编写一个Makefile文件

main:main.o openFile.o readFile.o writeFile.o  # main生成所需要的.o文件
    gcc -o main main.o openFile.o readFile.o writeFile.o  # 生成main的规则


main.o:main.c  # mian.o文件生成所需要的mian.c文件
    gcc -c main.c
openFile.o:openFile.c
    gcc -c openFile.c
readFile.o:readFile.c
    gcc -c readFile.c
writeFile.o:writeFile.c
    gcc -c writeFile.c


clean:    # 需要手动调用
    rm *.o main

注意:Makefile的注释符号是 ‘#’。

4. 编写完成后,执行make命令,make会在当前目录下找到名字为Makefile或makefile的文件,程序就会自动运行,产生相应的中间文件和可执行文件

a. 如果执行make出现如下信息,那就是命令行(makefile中的gcc或者rm)前面没有用tab键缩进,不能用空格:

b. 如果执行make出现如下信息,那就是你的代码没有修改过,Makefile拒绝你的请求:

这里还会有一种情况就是如果只修改过其中一个文件,那么重新编译就可以看到只编译修改的那个文件,没有编译其他未修改的文件,避免了重复编译。这里可以想象在一个大型源码的工程或者一个内核源码,里面的源文件上千或上万个,如果只修改了一个小问题,就要全部重新编译,就会花费大量编译的过程,Makefile就可以避免这个问题,而且支持多线程并发操作,可以减少很多编译的时间,提高工作效率。

那么Makefile是如何判断文件是否有修改过呢?

Makefile是通过对比时间戳 ,当我们生成中间文件或可执行文件之后,他们的创建时间肯定要比 .c文件最后修改的时间晚,如果某个 .c文件有新修改过,它的时间戳肯定会比原来生成中间文件或可执行文件的时间戳晚,这样就判断这个 .c文件有被更新过,就会重新编译它。

5. 正常运行后,执行可执行文件输入 ./main 即可,就能看到代码执行的结果

6. 在makefile文件的最后可以看到有个clean,这个clean就是前面所说的标签,它不是一个文件,所以make无法生成它的依赖关系和决定它是否要执行,只能通过显示指定这个目标才可以 ,通过make clean的指令就可以执行clean下面的命令。

到这里,一个基础版的Makefile就完成了。

四、Makefile的优化

学会了编写基础版的Makefile后,就可以对刚刚写的Makefile进行优化。

优化1:省略命令

我们将上面写的基础版Makefile改成下面这样的省略版:

main:main.o openFile.o readFile.o writeFile.o   
    gcc -o main main.o openFile.o readFile.o writeFile.o  

clean:   
    rm *.o main

执行make后的结果:

可以看到,这些文件都在同一目录下的时候,省略版和基础版的结果是一样的,省略版的makefile中去掉了生成main.o、openFile.o、readFile.o和writeFile.o这些目标的依赖和生成命令,这就是make的隐含规则,make会试图去自动推导产生这些目标的依赖和生成命令,这个行为就是隐含规则的自动推导。

优化2:引入变量

这里引入变量的意思有点像使用宏替换,改成(变量名),是格式:

TARGET = main
OBJS = main.o openFile.o readFile.o writeFile.o
CC = gcc

$(TARGET):$(OBJS)
    $(CC) -o $(TARGET) $(OBJS)

clean:   
    rm $(OBJS) $(TARGET)

优化3:引入函数

格式:$(函数名 实参列表)

# 函数1
$(wildcard  *.c)    # 表示当前路径下的所有的 .c
# 函数2
$(patsubst %.c, %.o, 所有的.c文件)    # 生成中间文件 .o
# 函数3
$(notdir xxx)   # 去除xxx文件的绝对路径,只保留文件名

引入函数后的Makefile版本可以改写成:

TARGET = main 
SOURCE = $(wildcard *.c)
OBJS = $(patsubst %.c, %.o, $(SOURCE))
CC = gcc

$(TARGET):$(OBJS)
    $(CC) -o $(TARGET) $(OBJS)
  
clean:   
    rm $(OBJS) $(TARGET)

优化4:对文件进行分类管理

在一个实际工程项目中程序文件比较多,我们就会对文件按照文件类型进行分类,分为头文件、源文件、目标文件和可执行文件,分别放在不同的目录中,由Makefile统一管理这些文件,将生产的目标文件放在目标目录下,可执行文件放到可执行目录下,分类目录如下图:

  • bin目录 :放可执行文件
  • include目录 :放头文件
  • obj目录 :放中间目标文件
  • src目录 :放源文件

可见原来那些文件都不在同一目录下了,那么这时候如果还用之前的Makefile,make就没法处理了,自动推导也会无法进行,就需要改成如下:

INC_DIR = ./include
BIN_DIR = ./bin
SRC_DIR = ./src
OBJ_DIR = ./obj
 
SRC = $(wildcard $(SRC_DIR)/*.c)      # /*/
OBJ = $(patsubst %.c, $(OBJ_DIR)/%.o, $(notdir $(SRC)))
 
TARGET = main
BIN_TARGET = $(BIN_DIR)/$(TARGET)
 
CC = gcc
 
$(BIN_TARGET):$(OBJ)
    $(CC) $(OBJ) -o $@
 
$(OBJ_DIR)/%.o:$(SRC_DIR)/%.c
    $(CC) -I$(INC_DIR) -c $< -o $@
 
clean:
    find $(OBJ_DIR) -name *.o -exec rm -rf {} \;  # 删除 .o 文件
    rm $(BIN_TARGET)   # 删除可执行文件main

在Makefile中,最终要生成可执行文件main我们把它叫做终极目标,其它所有的 .o 文件本身也是一个目标,也需要编译生成,工程里面许多的 .c 就会生成许多的 .o,每一个 .c 都写一遍目标依赖命令显然是不可行的,于是就有了类似for循环的东西,把所有目标变成一个集合,但不是真正用for循环,而是使用一些抽象的符号表示,解释如下:

  • %.o :所有 .o 结尾的文件
  • %.c :所有 .c 结尾的文件
  • $@ :表示目标文件
  • $< :表示第一个依赖文件,也叫初级依赖
  • $^ :表示所有的依赖文件,也叫终极依赖

当然,不止只有这些符号,只是列举了上面出现的或者常见的。

执行make后的结果:

make执行后bin目录里面已经生成了可执行文件main,obj目录里面已经生成了中间目标文件 main.o、openFile.o、readFile.o、writeFile.o,最后执行main后的结果也是和前面基础版的Makefile的结果是一样的。

 

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

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

相关文章

java集合框架内容整理

主要内容集合框架体系ArrayListLinkedListHashSetTreeSetLinkedHashSet内部比较器和外部比较器哈希表的原理List集合List集合的主要实现类有ArrayList和LinkedList&#xff0c;分别是数据结构中顺序表和链表的实现。另外还包括栈和队列的实现类&#xff1a;Deque和Queue。• Li…

steam/csgo搬砖项目真能月入过万吗?到底真的假的?

提前说明&#xff0c;这是正经、真实、官方渠道的兼职赚钱方式&#xff0c;不需要学历、不需要人脉&#xff0c;只要你认真学&#xff0c;每天花1-2个小时空闲时间动动手指&#xff0c;日赚300-1000问题不大&#xff01; 搬砖是从国外steam市场置办游戏装备回来&#xff0c;在…

javassm运列车物资进销存及领用管理系统

主要对个人中心、生产部管理、财务部管理、库房部管理、用户管理、车型分类管理、线路信息管理、列车信息管理、物资分类管理、物资信息管理、储备定额管理、物资入库管理、物资申请管理、入库记录管理的实现。 本系统设计的现状和趋势&#xff0c;从需求、结构、数据库等方面的…

五、Git本地仓库基本操作——分支管理

1. 什么是分支&#xff1f; master分支 我们在初始化git仓库的时候&#xff0c;会默认创建一个master分支&#xff0c;HEAD指针这时就会默认执行master分支。当我们在master分支提交&#xff08;commit&#xff09;了更新之后&#xff0c;master分支就会指向当前当前最新的co…

微搭中如何实现弹性布局

我们在实际开发中经常可能会有一些社交的场景&#xff0c;比如开发一个类似朋友圈九宫格图片展示的功能。因为图片的数量不确定&#xff0c;所以需要实现图片的从左到右顺序排列。 在微搭中可以以可视化的方式设置样式。但是对于我们这类特殊需求&#xff0c;只用可视化设置显…

【SSL/TLS】准备工作:HTTPS服务器部署:Nginx部署

HTTPS服务器部署&#xff1a;Nginx部署1. 准备工作2. Nginx服务器YUM部署2.1 直接安装2.2 验证3. Nginx服务器源码部署3.1 下载源码包3.2 部署过程4. Nginx基本操作4.1 nginx常用命令行4.2 nginx重要目录1. 准备工作 1. Linux版本 [rootlocalhost ~]# cat /proc/version Li…

【宝塔部署PHP项目】含域名访问部署、IP访问部署、数据库、端口号、Nginx等知识

一定要弄懂项目部署的方方面面。当服务器上部署的项目过多时&#xff0c;端口号什么时候该放行、什么时候才会发生冲突&#xff1f;多个项目使用redis怎么防止覆盖&#xff1f;Nginx的配置会不会产生站点冲突&#xff1f;二级域名如何合理配置&#xff1f;空闲的时候要自己用服…

Day886.MySQL的“饮鸩止渴”提高性能的方法 -MySQL实战

MySQL的“饮鸩止渴”提高性能的方法 HI&#xff0c;我是阿昌&#xff0c;今天学习记录的是关于MySQL的“饮鸩止渴”提高性能的方法的内容。 不知道在实际运维过程中有没有碰到这样的情景&#xff1a; 业务高峰期&#xff0c;生产环境的 MySQL 压力太大&#xff0c;没法正常响…

力扣SQL刷题8

目录1212. 查询球队积分1270. 向公司CEO汇报工作的所有人1285. 找到连续区间的开始和结束数字1321. 餐馆营业额变化增长--重要窗口函数知识点补充1212. 查询球队积分 题型&#xff1a;读题 解答&#xff1a;先考虑做主方时&#xff0c;再考虑客方时 SELECT t.team_id, t.te…

人工智能轨道交通行业周刊-第33期(2023.2.6-2.12)

本期关键词&#xff1a;高铁激光清洗、高铁确认列车、无线通信系统、推理服务优化、量子信息技术 1 整理涉及公众号名单 1.1 行业类 RT轨道交通中关村轨道交通产业服务平台人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟V…

大数据系列之:安装pulsar详细步骤

大数据系列之&#xff1a;安装pulsar详细步骤一、Pulsar版本和jdk对应关系二、安装JDK三、设置和激活jdk环境变量四、下载和解压Pulsar五、查看Pulsar目录六、启动Pulsar standalone cluster七、创建Kafka Topic八、往Topic写入数据九、消费pulsar的Topic一、Pulsar版本和jdk对…

c#小笔记本-基础

c#基本知识一.基础操作1.打印-writeline,write2.输入-readline,readkey二.变量1.折叠代码-#region&#xff0c;#endregion2.变量类型&#xff08;在c语言变量类型上新增的&#xff09;三.常量-const四.转义字符五.显示转换1.括号强转-低精度装高精度2.parse法-作用于字符串3.co…

实践指南|如何在 Jina 中使用 OpenTelemetry 进行应用程序的监控和跟踪

随着软件和云技术的普及&#xff0c;越来越多的企业开始采用微服务架构、容器化、多云部署和持续部署模式&#xff0c;这增加了因系统失败而给运维/ SRE / DevOps 团队带来的压力&#xff0c;从而增加了开发团队和他们之间的摩擦&#xff0c;因为开发团队总是想尽快部署新功能&…

28岁才转行软件测试,目前32了,我的一些经历跟感受

我是92年的&#xff0c;算是最早的90后&#xff0c;现在跟你介绍的时候还恬不知耻的说我是90后&#xff0c;哈哈&#xff0c;计算机专业普通本科毕业。在一个二线城市&#xff0c;毕业后因为自身能力问题、认知水平问题&#xff0c;再加上运气不好&#xff0c;换过多份工作&…

关于@hide的理解

在上一篇文章《学习HandlerThread》我们提到虽然HandlerThread类里有getThreadHandler()方法得到Handler&#xff0c;但是我们不可能调用到它。因为这个方法用hide注释了 /*** return a shared {link Handler} associated with this thread* hide*/NonNullpublic Handler getT…

数据一致性

目录一、AOP 动态代理切入方法(1) Aspect Oriented Programming(2) 切入点表达式二、SpringBoot 项目扫描类(1) ResourceLoader 扫描类(2) Map 的 computeIfAbsent 方法(3) 反射几个常用 api① 创建一个测试注解② 创建测试 PO 类③ 反射 api 获取指定类的指定注解信息(4) 返回…

基于Golang哈希算法监控配置文件变化

SHA(secure hashing algorithm)表示安全哈希算法.SHA是MD5的修正版本&#xff0c;用于数据摘要和认证。哈希和加密类似&#xff0c;唯一区别是哈希是单项的&#xff0c;即哈希后的数据无法解密。SHA有不同的算法&#xff0c;主要包括SHA-1, SHA-2, SHA-256, SHA-512, SHA-224, …

1.2.6存储结构-磁盘管理:单缓冲区与双缓冲区读取、流水线周期、计算流水线执行时间

1.2.6存储结构-磁盘管理&#xff1a;单缓冲区与双缓冲区读取、流水线周期、计算流水线执行时间流水线周期计算流水线执行时间微秒&#xff0c;时间单位&#xff0c;符号μs&#xff08;英语&#xff1a;microsecond &#xff09;&#xff0c;1微秒等于百万分之一秒&#xff08;…

SpringSecurity的使用与步骤

1、SpringSecurity流程图2、导入坐标<!-- spring-boot-starter-security --> <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- jjwt…

生物素-磺基-活性酯,Sulfo-NHS Biotin科研用试剂简介;CAS:119616-38-5

生物素-磺基-活性酯,Sulfo-NHS Biotin 结构式&#xff1a; ​ 编辑 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 英文名称&#xff1a;Sulfo-NHS-Biotin Sulfosuccinimidyl biotin 中文名称&#xff1a;磺酸基-Biotin-N-琥珀酰亚胺基酯 CAS&…