>> >> >> Reference << << << <<<<<<Ref>>>>>>
>> >> >> Indexer << << << <<<<<<Idx>>>>>>
Matched: 0

Tags

    Categories

      Types

        Top Results

          Make and Makefile
          M: 2025-12-31 - ljf12825
          1. make:是一个命令行工具它根据一个名为Makefile的脚本文件中的指令,自动地构建和管理项目
          2. Makefile:是一个文本文件,它定义了源文件之间的依赖关系以及构建这些文件的命令

          Make是一个构建工具(build tool),最早在Unix上诞生,用来自动化编译和构建项目
          它的核心思想是:

          • 目标(target):想要创建的东西(比如main.o或者app.exe或者一个自定义动作如clean
          • 依赖(dependencies):生成目标需要依赖哪些文件(比如源文件.c、头文件.h)。如果任何一个依赖文件比目标文件新,make就知道目标过期了,需要重新构建
          • 规则(rule):告诉Make如何从依赖生成目标(通常是编译命令)

          Make根据文件的修改时间,自动决定需要重新编译哪些文件,从而避免全量编译

          存在意义

          想象一个由多个文件组成的C++项目:

          • main.cpp
          • utils.cpp
          • helper.cpp
          • myapp(最终的可执行文件)

          没有make,每次修改后都需要手动输入一长串命令来编译

          g++ -c main.cpp -o main.o
          g++ -c utils.cpp -o utils.o
          g++ -c helper.cpp -o helper.o
          g++ main.o utils.o helper.o -o myapp
          

          繁琐且低效。如果只修改了helper.cpp,重新编译所有文件会浪费大量时间
          有了Makefile,只需要输入makemake工具就会:

          1. 读取Makefile
          2. 检查依赖关系和时间戳
          3. 只重新编译哪些被修改的文件以及依赖于这些文件的目标
          4. 最后链接可执行文件

          这极大地提高了开发效率,实现了增量编译
          在C/C++项目中,头文件依赖是最大的麻烦:修改了一个头文件,所有包含它的.c都要重新编译
          常见的做法是让编译器生成依赖

          g++ -MM main.cpp
          

          这会输出类似:

          main.o: main.cpp helper.h utils.h
          

          通常会配合Makefile里的-include来自动引入依赖文件(如.d文件),保证依赖关系正确,而不用手写

          Make的核心机制与哲学

          • 依赖检查:Make会比较目标文件和依赖文件的时间戳
            • 如果目标比依赖新 -> 不需要重新生成
            • 如果依赖比目标新 -> 需要重新执行命令
          • 声明式:在Makefile中,你声明目标和依赖,至于如何决定是否执行,由make根据时间戳和规则自动推导。这和命令式脚本不同,后者是需要一步一步写明要做什么
          • 递归构建:Make会递归地检查依赖关系
          • 最小重建:只编译修改过的部分(增量编译),节省时间
          • 自动化 + 增量化:它的核心价值不在于“能编译”,而在于“避免重复劳动”,只编译需要更新的部分

          Makefile语法

          target: dependencies
          <TAB> command
          
          • target:目标文件,最终要生成的东西
          • dependencies:依赖文件,如果依赖比目标新,Make就会执行规则
          • command:生成目标的命令,一行或多行shell命令(注意必须以Tab开头,不能用空格代替,这是make的历史遗留语法,但必须遵守)

          示例 假设有一个项目:main.chelper.c,最终形成可执行文件hello
          Makefile如下

          # 最终目标:生成可执行文件 hello
          hello: main.o helper.o
              gcc main.o helper.o -o hello
          
          # 目标文件 main.o 依赖于源文件 main.c和头文件 helper.h
          main.o: main.c helper.h
              gcc -c main.c -o main.o
          
          # 目标文件 helper.o 依赖于源文件 helper.c和头文件 helper.h
          helper.o: helper.c helper.h
              gcc -c helper.c -o helper.o
          
          # 自定义目标:清理编译生成的文件
          clean:
              rm -f *.o hello
          
          # 告诉 make: 'clean' 不是一个文件,而是一个动作名称
          .PHONY: clean
          
          • 当运行make(不加参数)时,make会默认执行Makefile中的第一个目标(这里是hello
          • 为了构建hello,它需要main.ohelper.o。如果这些.o文件不存在或比hello新,make会先去执行生成它们的规则
          • 在生成main.o时,它会检查main.chelper.h是否比main.o新。如果只修改了helper.h,那么main.ohelper.o(因为它们都依赖helper.h)都会被重新编译,最重新链接hello。这就是依赖关系的强大之处
          • clean目标没有依赖。它用于删除所有编译生成的文件。可以通过make clean来执行
          • .PHONY: clean告诉makeclean是一个“伪目标”,并不是要生成一个名为clean的文件。这是一个好习惯,可以避免如果当前目录下恰好有一个叫clean的文件时,make clean命令失效的问题

          伪目标(phony targets) .PHONY不只是clean,可以用它组织构建过程

          .PHONY: all debug release
          
          all: debug
          
          debug:
              $(MAKE) CFLAGS="-g -Wall"
          
          release:
              $(MAKE) CFLAGS="-O2 -DNDEBUG"
          

          这体现了Make还能作为一个任务调度器,不只是编译器的前端

          条件语句

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

          这样就可以make DEBUG=1控制编译模式

          递归Make
          在大型项目里,通常每个子目录都有自己的Makefile,顶层Makefile统一调度

          SUBDIRS = src utils tests
          
          all:
              for dir in $(SUBDIRS); do $(MAKE) -C $$dir; done
          

          变量和隐含规则

          变量
          可以定义变量来保存编译器名称、编译选项等

          # 定义变量
          CC = gcc
          CFLAGS = -Wall -g
          TARGET = hello
          OBJS = main.o helper.o
          
          # 使用变量 $(VAR_NAME)
          $(TARGET): $(OBJS)
              $(CC) $(OBJS) -o $(TARGET)
          
          main.o: main.c helper.h
              $(CC) $(CFLAGS) -c main.c
          
          helper.o: helper.c helper.h
              $(CC) $(CFLAGS) -c helper.c
          
          clean: 
              rm -f $(OBJS) $(TARGET)
          
          .PHONY: clean
          

          自动变量
          make提供了一些特殊的“自动变量”,在规则的命令中非常有用

          • $@:当前规则中的目标文件名
          • $<:第一个依赖项的文件名
          • $^:所有依赖项的文件列表

          使用自动变量可以进一步简化

          CC = gcc
          CFLAGS = -Wall -g
          TARGET = hello
          OBJS = main.o helper.o
          
          $(TARGET): $(OBJS)
              $(CC) $^ -o $@
          
          # 使用模式规则:如何从 .c 文件构建 .o 文件
          %.o: %.c
              $(CC) $(CFLAGS) -c $< -o $@
          
          clean:
              rm -f $(OBJS) $(TARGET)
          
          .PHONY: clean
          

          这里的%.o: %.c是一个模式规则,它告诉make:任何.o文件都依赖于同名的.c文件,并使用下面的命令来构建。这使得Makefile非常简洁,无需为每个.o文件写重复的规则
          GNU Make内建了大量的默认规则,比如

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

          很多时候,甚至不用写.o的生成规则,Make会自动推导出来。这就是为什么很多简单的Makefile看起来“缺失了一些规则,但依旧能工作

          make的使用

          在终端中,进入包含Makefile的目录

          1. 最基本的用法 在项目目录下直接运行
          make
          

          默认会执行Makefile中的第一个目标
          如果第一个目标是all,那就会先编译所有程序

          1. 指定目标
          make target_name
          

          例如

          make main
          make clean
          

          那就会执行mainclean目标对应的命令

          1. 指定Makefile文件 默认情况下,make会去找
          • GNUmakefile
          • makefile
          • Makefile 如果文件名不是这些,可以手动指定
          make -f MyMakefile
          
          1. 多线程编译(加速)
          make -j
          
          • -j:让make并行执行任务(依赖允许的情况下)
          • 可以指定线程数
          make -j4 # 开 4 个线程
          make -j8 # 开 8 个线程
          

          对大型C++项目编译速度提升非常大

          1. 查看执行的命令 有时候想知道make实际执行了哪些命令,可以加
          make VERBOSE=1
          make -n # 只打印命令,不执行
          
          1. 强制重新编译 有时候源文件没变,但想强制重新执行
          make -B
          

          忽略时间戳,强制执行所有命令

          1. 清理构建 一般Makefile里会有
          .PHONY: clean
          clean:
              rm -f *.o main
          

          使用:

          make clean
          

          删除中间文件,保证下次编译干净

          1. 指定变量 在命令行传递变量覆盖Makefile内定义的
          make CC=gcc
          make CFLAGS="-02 -Wall"
          

          非常适合调试或切换编译器

          1. 只执行某一步 有时候只想编译某个.o文件
          make main.o
          

          它会自动执行生成main.o的规则,不会编译整个项目