我正在学习This制作教程。在这里,他们在先决条件中提到了头文件。这真的有必要吗?我运行了没有头的相同代码,它起作用了。例如,我有以下代码
funcs.h
#ifndef FUNCS_H
#define FUNCS_H
int add(int a, int b);
#endif
funcs.cpp
#include "funcs.h"
int add(int a, int b){
return a+b;
}
main.cpp
#include<iostream>
#include "funcs.h"
int main(){
std::cout << add(1,2) << "\n";
}
Makefile
add : main.o funcs.o
g++ -o add main.o funcs.o
main.o : main.cpp funcs.h
g++ -c main.cpp
funcs.o : funcs.cpp funcs.h
g++ -c funcs.cpp
clean :
rm add main.o funcs.o
即使我从funcs.o
和main.o
目标中删除头文件,它仍然有效。那么,还有其他原因为什么要放置头文件?
makefile中冒号后面的文件名是目标的依赖项。 make检查它们,如果有任何依赖关系比目标更新,则使用下一行的命令重新构建目标。
因此,当从依赖项中删除头文件时,您当然仍然可以构建并运行,但是如果仅更改头文件中的内容,make将无法正确地重新构建所有目标。
@@ Rene对您的问题有正确的答案,但是我想指出,最佳实践是自动生成标头依赖项。否则,中型和大型构建系统将变得很难维护。例如,如果第三方在您的main.cpp中添加了行#include "foo.h"
,则很有可能无法更新Makefile使其匹配。如果foo.h依赖于另一个头,它将变得更加复杂。更好的做法是让makefile和编译器自动生成依赖项。下面的示例将更清晰地构建您的示例:
SRCS := main.cpp funcs.cpp
OBJS := $(SRCS:%.cpp=%.o)
DEPS := $(OBJS:%.o=%.d)
DEPFLAGS = -MMD -MP
add: $(OBJS)
$(CC) -o $@ $^
$(OBJS) : %.o : %.cpp
$(CXX) $(DEPFLAGS) -c $<
-include $(DEPS)
clean:
rm -f $(OBJS) $(DEPS) add
由于您正在运行教程,所以我假设您是新手,因此我将逐步介绍一下:
首先,将变量SRCS
设置为所有必需的源。然后使用模式替换(描述为OBJS
)基于SRCS
设置here
一旦完成,目标add
就会依赖于所有OBJS
。我在链接配方中使用了automatic variables $@
和$^
来表示目标以及所有依赖关系的列表。再次这样做是为了避免有人更新一件事,而忘记更新其他地方。
A static pattern rule然后用于生成$(OBJS)
中的任何文件。这就是乐趣所在。在编译器中传递了一些额外的标志(假定使用GCC编译器),这些标志将导致编译器与.o文件一起生成.d文件。这些都涉及(您可以在here中找到确切的说明...).d文件类似于:
main.o: main.cpp funcs.h
funcs.h:
注意,这是一个微型makefile,仅列出依赖项。这样就可以从主makefile中将其包含进来,这是下一行发生的情况。您得到:
-include $(DEPS)
此处的-
在这里很重要-这意味着如果.d文件不存在(第一次迭代中将不存在),则不要包含它。如果确实存在,则将其读入,就像它是内联键入一样。请注意,在第一次迭代中,.o文件也将不存在,因此无论如何都将构建目标,因此列出依赖关系是没有意义的。在随后的迭代中,您的目标将读取当前构建版本所依赖的依赖项,如果这些依赖项发生任何更改,它将知道当前对象已过时,然后重新构建它。
传入的标志是-MMD
,它告诉make构建依赖关系,而-MP
可以保护您是否在两次构建之间删除头文件。
最后,您需要清除所有生成的依赖项以及生成的对象。
如果您想更深入一点,可以参考Auto-Dependency Generation,其中有一些我没有提到的技巧(例如将deps保留在自己的目录中,等等。