百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 文章教程 > 正文

C语言进阶教程:Makefile 的编写与使用

yund56 2025-07-16 08:56 3 浏览

在C语言(以及其他许多编程语言)的项目开发中,当项目包含多个源文件、需要特定的编译选项、或者有复杂的依赖关系时,手动执行编译命令会变得繁琐且容易出错。make 是一个强大的构建自动化工具,它通过读取名为 Makefile(或 makefile)的文件来管理和自动化编译过程。

一、为什么需要 Makefile?

  • 自动化构建:只需一个 make 命令,就可以完成整个项目的编译、链接等步骤。
  • 增量编译make 工具能够判断哪些文件被修改过,只重新编译那些受影响的文件及其依赖项,从而大大节省编译时间,尤其是在大型项目中。
  • 管理依赖关系Makefile 可以清晰地定义文件之间的依赖关系,确保文件以正确的顺序被编译。
  • 定义通用操作:除了编译,还可以定义如清理项目(删除中间文件和可执行文件)、安装程序、运行测试等常用操作。
  • 跨平台性make 工具在各种操作系统(Linux, macOS, Windows via MinGW/Cygwin)上都可用,使得构建脚本具有一定的可移植性。

二、Makefile 基本结构

一个 Makefile 主要由一系列的规则 (Rules) 组成。每条规则定义了一个或多个目标(通常是文件名)、这些目标的依赖项以及生成这些目标所需的命令。

基本规则格式:

 target: prerequisites
     command
     command
     ...
  • target (目标):通常是需要生成的文件名,如可执行文件或目标文件。也可以是一个“伪目标”(Phony Target),代表一个动作,如 clean
  • prerequisites (依赖项):是生成 target 所需要的文件或其他的 target。如果任何一个依赖项比 target 更新(或者 target 不存在),那么 command 就会被执行。
  • command (命令):是生成 target 的 Shell 命令。重要:每条命令必须以一个制表符 (Tab) 开头,而不是空格。

示例:一个简单的 Makefile

假设我们有 main.c, utils.cutils.h

utils.h:

 // utils.h
 #ifndef UTILS_H
 #define UTILS_H
 
 void helper_function();
 
 #endif

utils.c:

 // utils.c
 #include <stdio.h>
 #include "utils.h"
 
 void helper_function() {
     printf("This is a helper function.\n");
 }

main.c:

 // main.c
 #include <stdio.h>
 #include "utils.h"
 
 int main() {
     printf("Main program started.\n");
     helper_function();
     return 0;
 }

Makefile:

 # 定义编译器
 CC = gcc
 # 定义编译选项
 CFLAGS = -Wall -g
 
 # 默认目标 (第一个目标)
 program: main.o utils.o
     $(CC) $(CFLAGS) -o program main.o utils.o
 
 main.o: main.c utils.h
     $(CC) $(CFLAGS) -c main.c -o main.o
 
 utils.o: utils.c utils.h
     $(CC) $(CFLAGS) -c utils.c -o utils.o
 
 # 清理命令 (伪目标)
 .PHONY: clean
 clean:
     rm -f program main.o utils.o

解释:

  • CC = gccCFLAGS = -Wall -g 是变量定义。在规则的命令部分,通过 $(CC)$(CFLAGS) 来引用它们。
  • program: main.o utils.o:目标 program 依赖于 main.outils.o
  • $(CC) $(CFLAGS) -o program main.o utils.o:生成 program 的命令。
  • main.o: main.c utils.h:目标 main.o 依赖于 main.cutils.h。如果 main.cutils.h 被修改,main.o 将被重新生成。
  • .PHONY: clean:声明 clean 是一个伪目标。伪目标不代表一个实际的文件名,而是代表一个动作。这样做可以防止目录下恰好有一个名为 clean 的文件时 make clean 命令不执行。
  • rm -f program main.o utils.oclean 目标的命令,用于删除生成的文件。

使用:

  • makemake program:构建 program 可执行文件。
  • make main.o:只生成 main.o
  • make clean:执行清理操作。

三、Makefile 中的常用特性

1. 变量 (Variables)

变量可以使 Makefile 更易于维护和配置。

  • 定义变量
  • VARIABLE_NAME = value
    # 或者
    VARIABLE_NAME := value # 立即展开变量,推荐使用
    VARIABLE_NAME ?= value
    # 如果变量未定义,则赋值
    VARIABLE_NAME += value
    # 追加值
  • 引用变量$(VARIABLE_NAME)${VARIABLE_NAME}

示例:

 CC = gcc
 CFLAGS = -Wall -g
 TARGET = myapp
 SRCS = main.c foo.c bar.c
 OBJS = $(SRCS:.c=.o) # 将 .c 后缀替换为 .o
 
 $(TARGET): $(OBJS)
     $(CC) $(CFLAGS) -o $(TARGET) $(OBJS)
 
 %.o: %.c
     $(CC) $(CFLAGS) -c lt; -o $@

2. 自动变量 (Automatic Variables)

make 提供了一些特殊的自动变量,它们在规则的命令部分中非常有用:

  • $@:规则中的目标文件名。
  • lt;:规则中的第一个依赖文件名。
  • $^:规则中所有依赖文件的列表(以空格分隔,不含重复)。
  • $?:所有比目标新的依赖文件的列表。
  • $*:对于模式规则,表示匹配到的“茎”(stem)。

示例 (使用自动变量改写之前的规则):

 # ... (变量定义 CC, CFLAGS)
 
 program: main.o utils.o
     $(CC) $(CFLAGS) -o $@ $^
 
 main.o: main.c utils.h
     $(CC) $(CFLAGS) -c lt; -o $@
 
 utils.o: utils.c utils.h
     $(CC) $(CFLAGS) -c lt; -o $@

3. 模式规则 (Pattern Rules)

模式规则允许你为一类文件定义通用的规则,通常使用 % 作为通配符。

示例:通用的 .o 文件生成规则

%.o: %.c
    $(CC) $(CFLAGS) -c lt; -o $@

这条规则表示:任何一个 .o 文件(如 main.o)依赖于同名的 .c 文件(如 main.c)。 lt; 会是 .c 文件名,$@ 会是 .o 文件名。

使用模式规则可以大大简化 Makefile:

CC = gcc
CFLAGS = -Wall -g
TARGET = program
SRCS = main.c utils.c
OBJS = $(SRCS:.c=.o) # main.o utils.o
DEPS = utils.h # 假设所有 .c 都可能依赖 utils.h

$(TARGET): $(OBJS)
    $(CC) $(CFLAGS) -o $@ $^

# 模式规则,适用于所有 .o 文件
%.o: %.c $(DEPS)
    $(CC) $(CFLAGS) -c lt; -o $@

.PHONY: clean
clean:
    rm -f $(TARGET) $(OBJS)

注意:在 %.o: %.c $(DEPS) 中,如果 $(DEPS) 包含多个头文件,并且某个 .c 文件实际上并不依赖所有这些头文件,这仍然是安全的,只是可能会导致不必要的重编译。更精确的依赖关系管理见下文。

4. 函数 (Functions)

make 提供了一些内置函数,用于处理文件名、文本等。

  • $(wildcard pattern):查找匹配 pattern 的文件列表。例如 $(wildcard *.c) 会列出当前目录下所有 .c 文件。
  • $(patsubst pattern,replacement,text):模式替换。例如 $(patsubst %.c,%.o,foo.c bar.c) 结果是 foo.o bar.o
  • $(addprefix prefix,names...):给文件名添加前缀。
  • $(addsuffix suffix,names...):给文件名添加后缀。

示例:自动获取源文件列表

SRCS = $(wildcard *.c) # 获取所有 .c 文件
OBJS = $(SRCS:.c=.o)
# 或者 OBJS = $(patsubst %.c,%.o,$(SRCS))

5. 条件语句 (Conditional Syntax)

Makefile 支持条件判断,可以根据变量的值或系统环境来改变构建行为。

ifeq (condition_value, test_value)
    # commands or variable assignments if true
else
    # commands or variable assignments if false
endif

ifdef VARIABLE_NAME
    # if VARIABLE_NAME is defined
endif

ifndef VARIABLE_NAME
    # if VARIABLE_NAME is not defined
endif

示例:根据 DEBUG 变量设置不同编译选项

CFLAGS_COMMON = -Wall

ifeq ($(DEBUG), 1)
    CFLAGS = $(CFLAGS_COMMON) -g -DDEBUG_MODE
else
    CFLAGS = $(CFLAGS_COMMON) -O2
endif

# ... rest of Makefile ...

调用时:make DEBUG=1make

6. 包含其他 Makefile (Include Directive)

可以使用 include 指令将其他 Makefile 文件包含进来,这有助于组织大型项目。

include common_settings.mk
include module1/module1.mk

四、自动生成依赖关系

手动维护 .c 文件对 .h 文件的依赖关系(如 main.o: main.c utils.h)是很繁琐的,尤其当头文件包含关系复杂时。GCC 编译器可以帮助我们自动生成这些依赖关系。

GCC 选项:

  • -MMD-MD:生成依赖文件(通常是 .d 文件),同时编译源文件。
    • -MMD:只包含用户头文件(不包括系统头文件如 <stdio.h>)的依赖。
    • -MD:包含所有头文件的依赖。
  • -MP:在生成的依赖文件中为每个头文件创建一个伪目标,这样即使头文件被删除,make 也不会因为找不到依赖而报错。

Makefile 示例 (自动依赖生成):

CC = gcc
CFLAGS = -Wall -g -MMD -MP # 添加 -MMD -MP
TARGET = program
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(SRCS:.c=.d) # 依赖文件名列表

# 默认目标
all: $(TARGET)

$(TARGET): $(OBJS)
    $(CC) $(CFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c lt; -o $@

# 包含所有 .d 文件
# 如果 .d 文件不存在,make 会尝试通过其他规则生成它们,但这里我们不希望这样
# -include 会忽略不存在的文件或无法生成的文件的错误
-include $(DEPS)

.PHONY: clean all
clean:
    rm -f $(TARGET) $(OBJS) $(DEPS)

工作原理:

  1. 当编译 main.c 时(例如 gcc -Wall -g -MMD -MP -c main.c -o main.o),GCC 会额外生成一个 main.d 文件,内容类似:
  2. main.o: main.c utils.h some_other_header.h
  3. (如果使用了 -MP,还会包含 utils.h:some_other_header.h: 这样的伪目标)
  4. -include $(DEPS) 指令会将所有这些 .d 文件包含到主 Makefile 中。
  5. 这样,make 就能知道 main.o 不仅依赖 main.c,还依赖 utils.h 等。如果这些头文件发生变化,main.o 会被重新编译。

五、Makefile 进阶技巧

  • 递归 Make (Recursive Make):在大型项目中,可以将项目划分为多个子目录,每个子目录有自己的 Makefile。顶层 Makefile 可以调用子目录中的 make 命令。
  • SUBDIRS = module1 module2

    .PHONY: all $(SUBDIRS)
    all: $(SUBDIRS)

    $(SUBDIRS):
    $(MAKE) -C $@ # -C dir: 切换到 dir 目录执行 make

    clean:
    for dir in $(SUBDIRS); do \
    $(MAKE) -C $dir clean; \
    done
  • 递归 Make 有时会因其复杂性和难以优化而受到批评,但对于组织结构清晰的项目仍然常用。
  • 非递归 Make (Non-Recursive Make):使用更复杂的 Makefile 技巧,在顶层 Makefile 中管理所有源文件和依赖,避免递归调用 make。这通常需要更高级的 make 函数和模式规则知识。
  • 使用 make -jN 并行编译make -j4 会尝试同时运行最多 4 个编译任务,可以显著加快大型项目的编译速度(前提是 Makefile 写得好,依赖关系正确)。

六、总结

Makefile 是一个非常强大的工具,虽然初看起来语法有些奇特,但掌握它可以极大地提高C语言(及其他语言)项目的构建效率和可管理性。

关键点:

  • 理解目标、依赖、命令的基本规则结构。
  • 善用变量自动变量简化 Makefile。
  • 使用模式规则处理同类文件的编译。
  • 通过编译器选项(如 GCC 的 -MMD -MP)和 include 指令实现自动依赖管理
  • 伪目标 (.PHONY) 用于定义动作而非文件。

编写好的 Makefile 需要实践,但其带来的便利性是值得投入时间学习的。对于更复杂的项目,可以考虑使用更高级的构建系统,如 CMake, Meson, Bazel 等,它们通常会自动生成 Makefile 或 Ninja 文件。

相关推荐

什么是JavaScript,它能做什么(javascript干啥的)

一个页面分成三个部分,结构,样式,行为。HTML代表了页面的结构(骨架),CSS代表了页面的样式(皮肤),JavaScript代表了页面的行为(这种行为是被动的)。主动的行为需要一个大脑,后端作为我们...

一款自定义字幕内容的截屏生成器:fake-screenshot!

这是一个可以伪造任何网站界面截图的工具。但本工具的目的其实不是破坏,而是为了警告:不要轻易相信网上看到的“截图”!本工具的目的是传递(如上的)信息,而不是破坏。因此所有经过本工具制作出来的截图都被打...

JavaScript-JavaScript 219

1)JavaScript简介JavaScript:是一种脚本语言(程序),脚本是一条条的文字命令,执行时由系统的一个解释器将其一条条的翻译成机器可识别的指令然后执行,脚本语言是不经编译而是解释执行的,...

Vue3 神级工具:终于可以实现打字的动画效果了!

Typed.js是一个轻量级的JavaScript库,用于在网页上实现打字机动画效果。它支持自定义打字速度、循环模式、回调函数等,非常适合用于动态展示标语、代码片段或交互式文本效果。核心特性打字...

好用的JavaScript客户端PDF插件——jsPDF

介绍和往常一样,jsPDF是一个开源的客户端的PDF解决方案,在之前的文章中已经介绍过几个Web端和PDF相关的库,jsPDF同样是一个不错的客户端PDF引SDK,你可以通过jsPDF在客户端完成相...

历时10个多月,学习了这132 个CSS 特效,还不来学习

这132个特效,是我历时10个多月在油管一个一个跟着敲出来的,为了加强记忆,每个练习,我都录制了视频,在这里分享出来给大家。大家可能又会调侃了,你是工作不饱和吧,有时间做这些。其实,我目前工作还是挺饱...

Flux.1 Kontext:用文字编辑图像(flux.1.kontext)

FLUX.1Kontext是来自BlackForestLabs的一款新图像编辑模型。它是用于通过文本提示编辑图像的最佳模型之一,并且是FLUX.1家族的最新成员。在我们的测试中,我们发...

采用Stylus 扩展让你的浏览器字体变得更美观

今天锋哥带大家来玩一个有意思的操作。我对字体有着很高的敏感度,我对网页默认的字体,不是很满意。突如其来的疯狂念头,我能不能把我们网页的所有字体,就是默认的字体,强制改为我喜欢的这个霞鹜文楷字体呢?答案...

JavaScript奇技淫巧:隐形字符(javascript字符型转数值型方法)

JavaScript奇技淫巧:隐形字符本文,分享一种奇特的JS编程技巧,功能是:可以使字符串“隐形”、不可见!效果展示如下图所示,一个字符串经物别的操作之后,其长度有621字节,但内容却是“隐形”不可...

Axure9原型设计:能增删改数据的动态饼图(2)

在本篇中,我们将延续上篇的设计思路,进一步探索如何在Axure9中实现“可增删改数据”的动态饼图效果。最近无聊,在网上闲逛,看到一篇教程《能增删改数据的动态饼图》,故仿照实践。因信息量较大,分三篇...

JavaScript奇淫技巧:命令行语法高亮

JavaScript奇淫技巧:命令行语法高亮本文,将实现命令行输出带有语法高亮、带行号的JS代码。效果如下图所示:对于JS程序员而言,这个效果是有些惊喜的。而实现起来,却似乎是出乎意料的简单。直接上源...

JS如何判断文字被ellipsis了?(js判断字符是否存在)

原文来源于:程序员成长指北;作者:嘉琪coder如有侵权,联系删除前言如果想要文本超出宽度后用省略号省略,只需要加上以下的css就行了。ellipsis{overflow:hidden;...

前端资源-实用的JS插件(前端js工具)

现在前端资源越来越多,有创意十足的,有实用性高的,这些对于设计师和前端人员来说都是不错的灵感和资源,所以我们可多关注这些信息,对自己的专业技术有也会帮助的。今天设计达人网为大家分享有:页面进度条、图像...

p5.js 中文入门教程(p5js编辑器不能用)

本文简介点赞+关注+收藏=学会了本文的目标是和各位工友一起有序的快速上手p5.js,会讲解p5.js的基础用法。本文会涉及到的内容包括:项目搭建p5.js基础2D图形文字图形样式...

创建酷炫动画效果的10个JavaScript库

Dynamics.js是设计基于物理规律的动画的重要JavaScript库。它可以赋予生命给所有包含CSS和SVG属性的DOM(文本对象模型)元素,换句话说,Dynamics.js适用于所有Java...