有文章将这几个之间的关系理清了,这里直接参考:原文链接。
下面来学习一下常见的Makefile文件的编写。参考来源【b站于仕琪】
下面的例子是用来介绍基本的Makefile文件的编写:
hello/
├──main.cpp
├──factorial.cpp
├──printhello.cpp
└──functions.h
main.cpp的定义如下:
#include
#include "functions.h"
using namespace std;
int main () {
printhello();
cout << "This is main:" << endl;
cout << "The result of factorial(5) is :" << factorial(5) << endl;
return 0;
}
factorial.cpp定义:
#include "functions.h"
int factorial(int n) {
if (n == 1) return 1;
else return n * factorial(n - 1);
}
printhello.cpp定义:
#include
#include "functions.h"
using namespace std;
void printhello() {
int i;
cout << "Hello World!" << endl;
}
functions.h定义:
#ifndef _FUNCTIONS_H_
#define _FUNCTIONS_H_
void printhello();
int factorial(int n);
#endif
上面的代码中,使用了宏定义防止头文件重复包含,以前没有系统总结过,下面总结一下。
上面的例子中,我们使用了宏定义方法防止头文件重复包含,具体而言:
在main.cpp中我们首先定义了#define _FUNCTIONS_H_
,然后在functions.h中有如下语句:
#ifndef _FUNCTIONS_H_
#define _FUNCTIONS_H_
...
#endif
这是宏定义避免头文件的模板,其中ifndef
的意思是:
如果这个宏定义了,就不会执行下面的语句。
这样的话,如果因为某种原因,第二次调用了头文件,那么由于第一次已经定义了这个宏,第二次判断这个宏已经存在了,所以就不会执行下面的语句了,也就避免了头文件的重复包含。
宏定于有时候可能会出现变量名冲突,另一种方案就是在头文件中使用#pargma once
//.h文件
#pargma once
...
这个方法更简洁,但是老的编译器可能不支持。
对于上述三个文件,我们也可以不使用Makefile进行编译,编译的方法如下:
g++ main.cpp factorial.cpp printhello.cpp -o main
结果:
使用手动的方法是完全可以的,但是当项目的体量很大,文件很多的时候,每次改动一个文件,就需要对所有的文件编译,非常耗时。手动编译的方法还有一种:先只编译不链接,然后再将所有的编译文件链接到一起:
g++ main.cpp -c
g++ factorial.cpp -c
g++ printhello.cpp -c
g++ *.o -o main
这个语句就是把所有的.o文件链接到一起,生成编译文件main。以上就是两种手动编译的方法,虽然有缺点,但是使用Makefile进行编译本质上就是把上面的语句写成了脚本而已,所以上面的两种方法就是基础。
首先需要创建一个Makefile文件,使用下面的指令
touch Makefile
创建的文件名字最好就是Makefile,这样就可以直接使用make指令,如果是其他的名字,使用make的指令的时候还要指定文件名。下面就是使用Makefile的第一个版本:
main: main.cpp factorial.cpp printhello.cpp
g++ -o main main.cpp printhello.cpp factorial.cpp
有几个需要注意的地方:
写好了Makefile之后,就可以编译了。
make
// 如果文件的名字不是Makefile,就需要指定文件的名字
make -f 文件名
到这里看来,版本一的Makefile除了一开始就把几个文件的名字写好了,与手动的并没有什么区别。
其实不然,还是有一点区别的。我们使用Makefile进行编译,如果目标生成的时间比依赖的源文件生成的时间都新的话(就是说文件没有更改过),此时我们make的话,就会提示我们,这是最新的目标,就不会重复编译了。但是这个版本并没有很只能,如果文件太多了,更改一点点,每次都需要重新编译所有,很费时间。
CXX = g++
TARGET = main
OBJ = main.o factorial.o printhello.o
$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(OBJ)
main.o: main.cpp
$(CXX) -c main.cpp
factorial.o: factorial.cpp
$(CXX) -c factorial.cpp
printhello.o: printhello.cpp
$(CXX) -c printhello.cpp
这个其实看着复杂,整体的思路就是使用手动编译的第二个思路,先编译不链接,生成.o文件,然后链接。
首先CXX
就是编译的方法,这里使用的g++,OBJ
就是依赖,跟版本1不同的是,这里的依赖使用的是.o文件,而不是.cpp文件。
紧接着:
$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(OBJ)
这一句其实拆解开来,就是版本1的小小变动的版本了,这里的依赖就变成了.o文件。所以我们接下来就要得到.o文件,这里对应的就是手动编译的第二种方法了。
使用版本二可以继承版本一的优点,同时如果只有1个文件修改了,我们不用再编译所有的文件了,只需要编译修改的文件,大大提高了编译的效率。但面临很多文件的时候,还是需要写很多语句,非常麻烦,下面的版本三就用来解决这个问题。
CXX = g++
TARGET = main
OBJ = main.o factorial.o printhello.o
CXXFLAGS = -c -Wall
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@
.PHONY: clean
clean:
rm -f *.o $(TARGET)
版本三前面和版本二是一样的,我们定义了一个变量CXXFLAGS
表示g++之后的选项,这里我们增加了一个-Wall
,表示在编译的时候把所有的警告也写出来。编译的代码:
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
的第一行其实就是main: main.o factorial.o printhello.o
,第二行出现了$@
以及$^
,在Makefile文件中,$@
就代表目标,也就是分号之前的内容,$^
代表依赖列表中所有,具体而言,这里就是三个.o文件,下面还将介绍一个$<
,这个表示依赖的第一个。很明显,如果依赖只有一个的话,二者是等价的。
所以上面的代码翻译古来就是下面的意思:
main: main.o factorial.o printhello.o
g++ -o main.o factorial.o printhello.o
其实就非常好理解了。
下面解释:
%.o: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@
这个就是将每个cpp文件作为依赖,生成对应名称的.o文件,使用%
来替代文件名。下面的$<
在上面已经解释了,其实就是%.cpp
。所以这句话翻译出来就是:g++ -c -Wall main.cpp -o main.o
,此时就可以生成.o文件了。但是根据之前的经验,生成.o文件只需要gcc -c main.cpp
即可,后面的要不要都无所谓,所以上面的内容可以简化为:
%.o: %.cpp
$(CXX) $(CXXFLAGS) $<
最后介绍:
.PHONY: clean
clean:
rm -f *.o $(TARGET)
我们先不关注第一句话,直接看第二句,第二句的目标是clean,没有依赖,执行的任务是删除全部的.o文件以及之前生成的目标。但是如果文件中正好有clean这个文件,会出现什么问题呢?我们在之前讲过,如果目标文件比依赖都晚的话,就不会再编译目标文件了,所以就会提示:clean已经是最新
,这里就达不到我们的要求了。针对这个问题,我们使用了.PHONY: clean
,其中.PHONY
是伪目标,这个目标永远不会存在,即使你这一次编译了,下一次他还是不存在。我们这里可以这么理解:.PHONY将clean标记为伪目标,此时make clean的时候就不管有没有文件存在了,每一次都会执行
。如果我们要使用make对某个目标编译的话,可以直接make 目标
。
目录里面存在clean文件,我们此时使用make clean:
可以看到,所有的.o文件以及main目标都被删除了。
到这里,我们已经完成的差不多了,编译也比较简单。但是再思考一下,如果我们换一个工程,岂不是还需要依赖,还是有一点麻烦的,我们可以再改进一下。
CXX = g++
TARGET = main
SRC = $(wildcard *.cpp)
OBJ = $(patsubst %.cpp, %.o, $(SRC))
CXXFLAGS = -c -Wall
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXFLAGS) $^
.PHONY: clean
clean:
rm -f *.o $(TARGET)
版本四就是我们的终极版本,SRC就是当前路径下所有的cpp文件名,OBJ就是将cpp文件名改为.o文件,后面的就和版本三是一样的了。有个这个版本,即使我们的代码换了个工程,照样也是可以编译的。
以上就是使用Makefile文件对cpp工程进行编译的方法,如果要对c工程进行编译,上面的g++换成gcc即可。