云计算百科
云计算领域专业知识百科平台

Linux实战(三):makefile的编写

 Makefile的编写

1. Makefile 的核心规则

Makefile 的语法非常固定,由三部分组成:

 目标(Target): 依赖(Dependencies)
 <Tab键> 命令(Command)

注意:命令前面必须是一个 Tab 键,不能是空格,否则会报错。


2. Makefile 的分步编写

我们可以把 Makefile 看作是一份给 make 工具阅读的“配方说明书” 📝。它告诉工具:最终产品是什么,需要哪些原材料,以及加工的步骤是什么。

让我们通过下面三个维度来拆解 Makefile:

1. 变量定义 (Variables) 📦

Makefile 的开头通常会定义变量。这就像是在程序里定义常量,方便统一修改。

变量名含义作用
CXX C++ eXecutable 指定编译器。这里是 g++。
CXXFLAGS CXX FLAGS 编译参数。-fPIC 生成位置无关代码,-Iheader 告诉编译器去哪里找头文件。
LDFLAGS Link Der FLAGS 链接参数。-Llib 寻找库路径,-lMymath 链接具体库,-Wl,-rpath 记录运行时搜索路径。

2. 核心规则结构 (The Rules) 🏗️

每一条规则都遵循这个逻辑:

 目标文件: 依赖文件
  执行命令

  • 依赖检查:make 会检查“依赖文件”的修改时间。如果依赖文件比“目标文件”新,它才会执行下面的命令。这就是增量编译的核心。


3. 逐行逻辑拆解 🔍

我们将 Makefile 分成四个“加工环节”来看:

  • 环节一:最终产物 (math)

    math: src/main.o lib/libMymath.so
    $(CXX) src/main.o $(LDFLAGS) -o math

    它说:要生成可执行文件 math,必须先有 main.o 和动态库 .so。

  • 环节二:中间零件 (main.o)

    src/main.o: src/main.cpp
    $(CXX) $(CXXFLAGS) -c src/main.cpp -o src/main.o

    它说:要把源码变成二进制目标文件。注意 -c 参数表示只编译,不链接。

  • 环节三:共享仓库/动态库 (libMymath.so)

    lib/libMymath.so: src/Mymath.o
    $(CXX) -shared src/Mymath.o -o lib/libMymath.so

    它说:用 -shared 参数把 .o 包装成动态库。

  • 环节四:大扫除 (clean)

    clean:
    rm -f math src/*.o lib/*.so

    这是一个“伪目标”,用来快速删除所有生成的二进制文件,还原纯净代码。


3. Makefile 为什么“智能”?

当你运行 make 命令时,它会检查:

  • 是否存在目标文件? 如果没有 math,就执行命令生成它。

  • 文件的修改时间:如果 src/main.cpp 的修改时间晚于 src/main.o,它会重新编译这个 .o;如果时间没变,它就会跳过这一步。

  • 这就是所谓的增量编译。


    4. 自动化变量(让代码更简洁)

    上面的 Makefile 还是有点啰嗦,我们可以使用“黑话”来简化它:

    • $@:代表“目标文件”

    • $^:代表“所有的依赖文件”

    • $<:代表“第一个依赖文件”

    简化后的版本:

     math: src/main.o lib/libMymath.so
      $(CXX) $< $(LDFLAGS) -o $@
     ​
     src/main.o: src/main.cpp
      $(CXX) $(CXXFLAGS) -c $< -o $@


    5. 常见指令

    • make:执行 all 目标,开始智能编译。

    • make clean:执行清理工作,把生成的二进制文件全删掉。

    • make -j 4:利用多核并行编译(如果你有 4 核,速度飞快)。


    为了确保你已经掌握了 Makefile 的逻辑,我有一个小问题考考你:

    如果在我的 Makefile 中,math 目标后面写的依赖只有 src/main.o,而没写 lib/libMymath.so。当我修改了动态库的源码 Mymath.cpp 并重新运行 make 时,你觉得 math 这个可执行文件会发生什么变化?

  • make 会自动发现 Mymath.cpp 变了,从而重新生成动态库并重新链接 math。

  • make 只会更新动态库,但不会重新链接 math,因为 math 觉得自己的依赖项(main.o)没变。

  • 你直觉选哪一个?🤔


    Makefile进阶

    你的直觉非常敏锐,而且在大多数初学者编写的 Makefile 中,这确实是最容易发生的错误!🎯

    你的判断是正确的:如果 math 的依赖列表只写了 src/main.o,那么 make 确实只会检查 main.o 的时间。即使你修改了库的代码并生成了新的 libMymath.so,math 程序也不会意识到需要重新链接

    为了避免这种“感知迟钝”,我们需要在 Makefile 的规则中显式地把 .so 文件也列为 math 的依赖项


    🔍 观察依赖关系

    让我们看看修正后的逻辑:

    # 这样写,math 就会时刻盯着这两个文件
    math: src/main.o lib/libMymath.so
    $(CXX) src/main.o $(LDFLAGS) -o math

    这样,一旦 lib/libMymath.so 的时间戳更新(因为它被重新编译了),make 就会发现 math 的其中一个依赖变新了,从而触发重新链接命令。


    模式规则(Pattern Rules)

    既然我们已经聊到了依赖,你之前问过如果有很多文件怎么办。在 Makefile 中,我们不需要为每个 .cpp 手写规则,可以使用 % 统配符。

    这就是模式规则:

    # 这是一条通用的“生产线”
    # 它说:任何 .o 文件都依赖于同名的 .cpp 文件
    %.o: %.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

    • %:匹配任何字符串(比如 main 或 Mymath)。

    • $<:自动化变量,指代第一个依赖文件(即 .cpp)。

    • $@:自动化变量,指代目标文件(即 .o)。


    ❓ 一个关于“头文件”的小谜题

    现在想象一下,如果你修改了 header/Mymath.h 里的一个函数声明,但没有动任何 .cpp 文件。

    根据我们现在的 Makefile 规则:

  • src/main.o 依赖于 src/main.cpp

  • src/Mymath.o 依赖于 src/Mymath.cpp

  • 在这种情况下,你觉得运行 make 会发生什么?

    • A) make 会发现头文件变了,自动重新编译相关的 .o 文件。

    • B) make 什么都不会做,因为它没看到 .cpp 文件发生变化。

    我们可以根据这个问题的答案,聊聊 Makefile 中最进阶的“自动生成依赖”技术。你选哪一个?🤔


    你的判断完全正确!🎯

    如果不做特殊处理,make 确实会表现得非常“迟钝”。这是因为在 Makefile 的规则里,我们只告诉了它 .o 文件依赖于 .cpp 文件。make 并不具备“透视眼”,它不会自动钻进源代码里去查看 #include 了哪些头文件。

    这就导致了一个经典的开发痛点:你修改了头文件里的类定义或函数声明,运行 make,它却告诉你 up to date。结果你运行程序时,可能会因为二进制不兼容而莫名其妙地崩溃。💥


    让依赖关系“透明化”

    为了解决这个问题,程序员们通常有三种进化方案:

    1. 手动添加法(最原始)

    在规则里手动把头文件加上去:

    src/main.o: src/main.cpp header/Mymath.h
    $(CXX) $(CXXFLAGS) -c $< -o $@

    • 缺点:如果有 100 个头文件,手动维护简直是噩梦。

    2. 利用编译器自动生成(最专业)

    实际上,g++ 本身就有一项黑科技参数:-MMD。

    当你编译时加上这个参数,g++ 会在生成 .o 的同时,顺便生成一个 .d 文件(依赖文件),里面记录了该 .cpp 用到的所有头文件。

    3. 终极版 Makefile 模板

    我们可以通过在 Makefile 中包含(include)这些 .d 文件,让 make 自动感知头文件的变化:

    # 1. 变量定义:配置编译器和路径
    CXX = g++
    CXXFLAGS = -fPIC -Iheader -MMD # -MMD 会自动生成 .d 依赖文件
    LDFLAGS = -Llib -lMymath -Wl,-rpath,./lib

    # 2. 确定所有的目标文件 (.o) 和对应的依赖文件 (.d)
    OBJS = src/main.o src/Mymath.o
    DEPS = $(OBJS:.o=.d)

    # 3. 默认目标:执行 'make' 时运行的任务
    all: math

    # 4. 链接规则:将 .o 文件链接成可执行程序
    math: src/main.o lib/libMymath.so
    $(CXX) src/main.o $(LDFLAGS) -o $@

    # 5. 动态库规则:将 Mymath.o 包装成 .so 文件
    lib/libMymath.so: src/Mymath.o
    @mkdir -p lib # 确保 lib 目录存在
    $(CXX) -shared $< -o $@

    # 6. 通用的编译规则:将所有的 .cpp 编译为 .o
    %.o: %.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

    # 7. 引入隐藏的依赖规则:让 make 知道头文件的变化
    -include $(DEPS)

    # 8. 清理任务
    .PHONY: clean
    clean:
    rm -f math src/*.o src/*.d lib/*.so

    这一块的内容确实是 Makefile 进阶的难点,因为涉及到 make 的自动化“黑科技”。我来逐一为你拆解,带你看看这些代码背后发生了什么。

    1. 变量转换的魔法 🪄

    OBJS = src/main.o src/Mymath.o
    DEPS = $(OBJS:.o=.d)

    这两行的作用是自动生成文件名列表:

    • OBJS:你手动定义的两个 .o 目标文件。

    • DEPS = $(OBJS:.o=.d):这是一种后缀替换语法。它的意思是:把 OBJS 变量中所有以 .o 结尾的部分都换成 .d。

    • 结果:现在 DEPS 变量的内容就变成了 src/main.d src/Mymath.d。


    2. 这里的 include 是什么意思? 📂

    在 C++ 代码里,#include 是包含头文件;而在 Makefile 里,include 是包含另一个 Makefile 片段。

    • 还记得我们给 CXXFLAGS 加上了 MMD 吗?

    • 当编译器执行 g++ -c main.cpp -MMD 时,它会产生两个文件:main.o 和一个纯文本的 main.d。

    • 这个 main.d 里的内容大概长这样:src/main.o: src/main.cpp header/Mymath.h。

    • include $(DEPS) 的作用就是把这些生成的 .d 文件像“拼图”一样拼进当前的 Makefile 里。这样 make 就知道:原来 main.o 不仅依赖 .cpp,还依赖 Mymath.h!

    为什么用 -include 而不是 include?

    加个横杠 – 是为了告诉 make:“如果这些 .d 文件还没生成(比如第一次编译时),请忽略错误继续跑,别罢工。”


    3. .PHONY:防止同名“撞车” 🏷️

    .PHONY: clean

    这是一个非常重要的防御性指令。

    在 Makefile 中,规则的左边通常是一个真实的文件名。如果你在文件夹里真的创建了一个叫 clean 的空文件,make 就会陷入困惑。

    由于 make 的核心逻辑是对比“目标文件”和“依赖文件”的时间戳,如果目录下真的有一个叫 clean 的文件,而你在 Makefile 里写的 clean: 后面没有任何依赖项,make 就会认为:

  • clean 这个文件已经存在了。

  • 它没有任何依赖项,所以它永远不会“过期”。

  • 结论:不需要执行任何操作。

  • 这就是为什么我们要使用 .PHONY(意为“伪造的”或“虚名的”)。它告诉 make:“不要把 clean 当成一个文件,它只是一个命令的名字,每次我输入 make clean 时,你尽管执行就对了!” 🏷️


    感谢你能看到这里 如果本文对你编写makefile有一点点帮助 那就是我最大的心愿 下篇再见。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux实战(三):makefile的编写
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!