第4章:隐式规则与模式匹配:Makefile的自动推导能力,%模式匹配规则
说实话,我刚接触Makefile那会儿,最头疼的就是写一堆重复的编译规则。每个.cpp文件都要配一条规则,项目一大了,光复制粘贴就够呛。后来我才发现,Makefile其实有「自动推导」这个好东西,说白了就是它能自己猜出你想干嘛。
4.1 隐式规则:Makefile的「读心术」
Makefile内置了一套默认规则,叫隐式规则。你不需要告诉它怎么把.cpp变成.o,它自己就会用$(CXX) -c去编译。我刚开始用的时候还觉得不靠谱,心想「它真能猜对?」。试了一次,还真行。
举个例子,你只要写:
hello: hello.o
$(CXX) -o hello hello.o
Makefile会自动推导出hello.o需要从hello.cpp编译而来。它默认的编译命令是:
$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) -o $@ $<
嗯,这里要注意,隐式规则依赖的是文件名匹配。如果你的源文件叫hello.cpp,目标文件叫hello.o,它就能对上。否则,它就不灵了。
隐式规则的本质:Makefile内置了一套「文件名模式 → 命令」的映射表。你只要遵循命名约定,它就能自动干活。
4.2 常见的隐式规则有哪些?
我个人习惯把常用的隐式规则记在脑子里,这样写Makefile时心里有数。下面这张表是我经常用到的:
| 源文件后缀 | 目标文件后缀 | 默认命令 |
|---|---|---|
| .cpp | .o | $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) -o $@ $< |
| .c | .o | $(CC) -c $(CPPFLAGS) $(CFLAGS) -o $@ $< |
| .f | .o | $(FC) -c $(FFLAGS) -o $@ $< |
| .y | .c | $(YACC) $(YFLAGS) $< |
| .l | .c | $(LEX) $(LFLAGS) $< |
你看,C++、C、Fortran、Yacc、Lex都有对应的隐式规则。我在项目中遇到过一个问题:明明写了main.o的依赖,但Makefile就是报错。后来发现是我把.cpp文件命名成了main.cc,而隐式规则只认.cpp。所以,命名规范真的很重要。
4.3 模式匹配:% 通配符的威力
隐式规则虽然方便,但有时候我们需要自定义规则。比如,你想把.cpp文件编译到build/目录下,而不是当前目录。这时候,隐式规则就帮不上忙了。你需要用模式匹配。
模式匹配的核心就是%通配符。它匹配任意长度的字符串,类似于Shell里的*。但%只能用在Makefile的规则中,不能用在Shell命令里。
举个例子:
build/%.o: src/%.cpp
$(CXX) -c $(CXXFLAGS) -o $@ $<
这条规则的意思是:任何build/xxx.o都依赖于src/xxx.cpp。这里的%匹配了xxx这个部分。你想想看,如果项目里有100个.cpp文件,你只需要写这一条规则就够了。
小技巧:%匹配的是「文件名主干」,不包括目录和后缀。比如build/main.o匹配src/main.cpp,%的值就是main。
4.4 模式匹配的实战案例
我记得有一次做Qt项目,需要把.ui文件转换成.h文件。Qt的uic工具就是干这个的。用模式匹配写起来特别简洁:
UI_DIR = ui
GEN_DIR = generated
$(GEN_DIR)/%.h: $(UI_DIR)/%.ui
uic $< -o $@
这样,只要ui/目录下有.ui文件,Makefile就会自动生成对应的.h文件到generated/目录下。我曾经在项目里用这个模式管理了30多个UI文件,一条规则搞定,省了不少事。
再比如,处理Qt的.qrc资源文件:
$(GEN_DIR)/%.cpp: $(RES_DIR)/%.qrc
rcc $< -o $@
你看,模式匹配让Makefile变得特别灵活。你只需要定义好「输入→输出」的转换规则,剩下的交给Makefile自己去匹配。
4.5 隐式规则与模式匹配的协作
实际项目中,隐式规则和模式匹配经常一起用。比如,你定义了模式规则来编译.cpp到.o,然后隐式规则负责链接.o到可执行文件。这样分工明确,代码也干净。
下面我用一张图来展示它们的关系:
4.6 避坑指南:隐式规则的陷阱
隐式规则虽然方便,但也有一些坑。我曾经踩过一个:当隐式规则和你写的显式规则冲突时,Makefile会优先使用显式规则。但如果你不小心写错了显式规则,隐式规则也不会自动补上。
举个例子:
main.o: main.cpp
# 这里我忘了写编译命令
# 隐式规则不会自动生效!
这种情况下,Makefile会报错,因为它找不到命令。所以,要么你写完整的显式规则,要么干脆不写,让隐式规则去处理。
警告:不要混用隐式规则和显式规则对同一个目标。要么全交给隐式规则,要么自己写完整。否则容易出问题。
另一个坑是:隐式规则依赖的变量(比如CXX、CXXFLAGS)如果没有定义,它会使用默认值。默认的CXX是g++,但如果你在Windows上用MSVC,就得手动设置CXX=cl。我刚开始用的时候没注意,结果编译出来的东西链接不上,折腾了半天才发现是编译器不对。
4.7 如何查看隐式规则?
如果你想知道Makefile到底有哪些隐式规则,可以用这个命令:
make -p -f /dev/null 2>/dev/null | grep -A 10 "隐含规则"
或者更直接一点:
make --print-data-base -f /dev/null 2>/dev/null | grep -E "^[a-z%].*:"
我个人习惯用make -p来查看所有规则,包括隐式的和显式的。这样心里有数,知道哪些规则是自动生效的,哪些需要自己写。
4.8 小结
隐式规则和模式匹配是Makefile的「自动挡」和「手动挡」。隐式规则让你少写代码,模式匹配让你灵活控制。两者结合,写起Makefile来既省力又高效。
嗯,这一章就到这里。记住,用好%通配符,你的Makefile就能从「写死」变成「写活」。下次遇到大量源文件时,试试用模式匹配,你会发现世界清静很多。
核心要点:
- 隐式规则:Makefile内置的默认编译/链接规则
- 模式匹配:用
%通配符自定义规则,匹配文件名主干 - 协作使用:模式匹配负责编译,隐式规则负责链接
- 注意命名规范:文件名后缀要匹配隐式规则的约定
可可精品资料库