04. 远古时期:手写 g++ 和 Makefile 编译 C++ 项目
在CMake出现之前,C++项目是如何构建呢?
本节以一个简单的计算程序来演示
1. CPP项目结构
01_Make/
├── main.cpp # 主程序
├── calc.cpp # 计算函数实现
├── calc.h # 计算函数声明
└── Makefile # make的构建脚本
1.1 calc.h
#ifndef CALC_H
#define CALC_H
// 加法函数
int add(int a, int b);
// 减法函数
int subtract(int a, int b);
#endif
1.2 calc.cpp
#include "calc.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
1.3 main.cpp
#include <iostream>
#include "calc.h"
int main() {
int x = 10, y = 5;
std::cout << x << " + " << y << " = " << add(x, y) << std::endl;
std::cout << x << " - " << y << " = " << subtract(x, y) << std::endl;
return 0;
}
2. 直接g++编译
上一节,使用task来编译项目,实际上是手动写g++命令,指定参数,来编译出可执行文件。
这种方式是最原始的,效率也是低下的。
2.1 一次性编译所有文件
缺点:只修改一个文件也需要重新编译所有文件
# 一次性编译链接
g++ -o calculator main.cpp calc.cpp -I.
# 运行
./calculator
2.2 分步编译
每个源文件单独编译出目标文件,再将所有的目标文件链接成可执行文件:
# 编译单个源文件,不链接,生成目标文件
g++ -c calc.cpp -o calc.o -I.
g++ -c main.cpp -o main.o -I.
# 链接所有目标文件,生成可执行文件
g++ main.o calc.o -o calculator
# 运行
./calculator
假设只修改了main.cpp:
# 只编译被修改的文件
g++ -c main.cpp -o main.o -I.
# 链接所有目标文件,生成可执行文件
g++ main.o calc.o -o calculator
# 运行
./calculator
3. 使用make构建
3.1 Makefile实操
关于
Makefile,有非常多的语法规则,不是本课重点,不再展开~
直接手敲g++命令,无法自动管理依赖,也不方便增量编译(只编译修改的文件)
可以把编译/链接/安装步骤写入到名为Makefile的脚本中
有了Makefile,你不用每次手动敲一堆g++命令,就能高效、可维护地构建项目
创建了一个 Makefile文件,内容如下:
# 内置变量:表示 C++ 编译器
# 将变量 CXX 赋值为字符串 "g++"
# 如果要切换到clang++,只需改为 CXX = clang++
CXX = g++
# 内置变量:C++编译标志
# -I.:包含当前目录作为头文件搜索路径
# -Wall:启用所有警告
# -std=c++11:使用C++11标准
CXXFLAGS = -I. -Wall -std=c++11
# 自定义变量:存储最终生成的可执行文件名
TARGET = calculator
# 自定义变量:列出所有C++源文件
SRCS = main.cpp calc.cpp
# 自定义变量:目标文件列表
# 模式替换,语法:$(VAR:pattern=replacement)
# 展开过程:
# 1. SRCS = "main.cpp calc.cpp"
# 2. 将 .cpp 替换为 .o
# 3. 结果:OBJS = "main.o calc.o"
OBJS = $(SRCS:.cpp=.o)
# 最后一行中 all 被.PHONY 声明为伪目标
# 它是默认目标,因为在 Makefile 中,第一个出现的非特殊目标就是默认目标
# 当只输入 make 而不指定目标时,make 会自动选择第一个目标进行构建
# :冒号是依赖关系分隔符,all 这个目标依赖 calculator
all: $(TARGET)
# 自动推导规则:.cpp -> .o
# %:通配符,匹配任意字符串
# 匹配任何.o文件依赖于同名的.cpp文件
# $(CXX):展开为g++
# $(CXXFLAGS):展开为-I. -Wall -std=c++11
# -c:编译但不链接
# $<:第一个依赖文件,对于main.o: main.cpp,$< = main.cpp
# $@:目标文件,对于main.o: main.cpp,$@ = main.o
# 完整展开示例:
# main.o: main.cpp
# g++ -I. -Wall -std=c++11 -c main.cpp -o main.o
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# 链接
# 展开:
# calculator: main.o calc.o
# g++ main.o calc.o -o calculator
$(TARGET): $(OBJS)
$(CXX) $(OBJS) -o $(TARGET)
# 指定目标文件依赖的头文件
# 作用:当calc.h修改时,main.o和calc.o都需要重新编译
main.o: calc.h
calc.o: calc.h
# 清理
# 最后一行中 clean 被.PHONY 声明为伪目标
# make clean 会执行该目标
# 展开:rm -f main.o calc.o calculator
clean:
rm -f $(OBJS) $(TARGET)
# .PHONY:声明伪目标
# 伪目标不是真实文件,只是一个标签,用于防止与同名文件冲突
# 示例:
# 如果有文件叫clean,执行make clean会失败(除非声明为.PHONY)
# 声明后,make知道clean是动作,不是文件
.PHONY: all clean
执行 make 时的流程:
1. 默认执行第一个目标:all
2. 检查all的依赖:需要calculator
3. 检查calculator的依赖:需要main.o和calc.o
4. 检查main.o的依赖:
匹配%.o: %.cpp模式规则 → main.o: main.cpp
检查是否有main.cpp(有)
检查显式依赖:main.o: calc.h
如果main.cpp或calc.h比main.o新,则执行编译
5. 检查calc.o的依赖:类似过程
6. 所有.o文件就绪后:执行链接命令生成calculator

3.2 Makefile 的优点
使用 Makefile,有以下优点:
- 自动化与可复现
把编译命令、编译选项、链接、生成文件、安装、清理等写成规则,任何人运行make都能得到相同结果。 - 增量构建
make根据时间戳和依赖只重新编译发生变化的源文件,极大节省时间(而不是每次全量编译)。 - 依赖管理
规则里指定哪些文件依赖哪些头文件,make能正确处理编译顺序与重建条件。 - 并行构建
使用make -jN并行执行多个独立编译任务,显著加速构建。 - 封装复杂流程
支持自定义目标(如test,install,distclean),可以把preprocess、codegen、测试、打包等流程集成进来。
3. CMake
Makefile存在以下问题:
- 需要学习
Makefile语法 - 跨平台问题(需要条件判断)
- 大型项目
Makefile复杂 - 依赖检测不够智能
- 不支持安装、打包等高级功能
有没有更简单的方法呢?
答:CMake
本文是博主原创文章,转载请注明来源 明王讲QT







