介绍
这里以一个例子来演示利用Makfile进行多文件编译
一共有四个源程序:main.cpp,printhello.cpp,factorial.cpp,functions.h
首先是main.cpp内容
#include<iostream>
#include"functionals.h"
using namespace std;
int main()
{
printhello();
cout << "This is main:" << endl;
cout << "The factorial of 5 is: " << factorial(5) << endl;
return 0;
}
调用两个函数
printhello.cpp内容
#include<iostream>
#include"functionals.h"
using namespace std;
void printhello()
{
int i;
cout << "Hello World!" << endl;
}
定义了一个函数,输出hello
factorial.cpp内容
#include"functionals.h"
int factorial(int n)
{
if(n == 0 || n == 1){
return 1;
}
else{
return n * factorial(n - 1);
}
}
定义了一个函数,用于计算阶乘
functions.h内容
#ifndef _FUNCTIONALS_H_
#define _FUNCITIONALS_H_
void printhello();
int factorial(int n);
#endif
在这个文件里写了函数的声明
首先是可以在命令行直接用g++编译:
g++ -o main .\factorial.cpp .\printhello.cpp .\main.cpp
.\main.exe
Hello World!
This is main:
The factorial of 5 is: 120
但如果源文件很多,这样编译要花费很多时间
一个省时间的方式就是我们可以逐个编译,修改了哪个文件就编译哪个文件(只编译不链接),其他文件不编译
g++ .\main.cpp -c
g++ .\factorial.cpp -c
g++ .\printhello.cpp -c
这样生成了三个.o文件,然后用命令g++ *.o -o main
链接在一起。
不知道为什么,Windows这样写会报错g++: error: *.o: Invalid argumentg++: error: *.o: Invalid argument
,不能用通配符,所以我每个都写出来的g++ .\factorial.o .\printhello.o .\main.o -o main
(Windows不能用通配符这种,需要单独写出,Linux可以)
那么这样会带来一个问题,当文件特别多的时候,每次手动去输入这个命令会特别麻烦,所以在命令行输入g++编译的这些命令,完全可以写到一个脚本文件里,这个脚本文件有个固定的格式,叫Makefile,注释用 #。
version 1
## version 1
hello: main.cpp printhello.cpp factorial.cpp
g++ -o hello main.cpp printhello.cpp factorial.cpp
前面hello表示生产的可执行程序叫hello,冒号后面的文件表示:hello的生成依赖于后面那些文件
g++前面有个tab,不能用空格,表示使用后面这个命令生成上面的hello文件
使用的方法就是在命令行输入make
,make命令会自动去找当前目录下Makefile文件,如果名字不是Makefile,可以用命令make -f Makefile
生成后,如果再make一次,会提示:make: 'hello' is up to date
(make: “hello”已是最新。
)表示hello已经很新了,就是hello比那三个cpp文件都新,是在三个文件后生成的,没必要再生成了,除非对其中一个文件做了修改,那么被修改的文件日期新于hello,这时候再make,它会查规则,发现它所依赖的文件在hello生成后被修改了,就会用第二行命令重新生成。(Windows不会检查,Linux会检查)
缺点:如果文件特别多,每次编译时间很长
version 2
CXX指定编译器
TARGET指定目标
TARGET这个变量依赖于OBJ这些文件,如果OBJ相比TARGET更新的话,那么就会用下面命令
make后,Makefile去读第一个碰到的目标TARGET,就是要生成的目标,因为hello不存在,就要去生成,生成要依赖OBJ文件,而OBJ文件又去找依赖的cpp文件
如果某个文件修改了它只会去编译更改了的cpp文件
# version 2
CXX = g++
TARGET = hello
OBJ = main.o printhello.o factorial.o
$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(OBJ)
main.o: main.cpp
$(CXX) -c main.cpp
printhello.o: printhello.cpp
$(CXX) -c printhello.cpp
factorial.o: factorial.cpp
$(CXX) -c factorial.cpp
version 3
加了一个编译选项CXXFLAGS = -c -Wall
(Warning all)就是所有的warning都显示出来
不同的一个地方是加了个$@
,其实就是冒号前面的东西,就是$(TARGET)
,$^
是冒号后面的东西,就是$(OBJ)
这样又少写了一些变量
刚才每个cpp都写了规则生成.o,这里写了个统一的规则%.cpp
就是每个cpp文件,$<
是指所依赖的,^
往上指是指所有的,<
往左指是指第一个,在这里只有一个,所以一样。
clean那两行,有个目标叫clean,要生成的话,不管依赖是什么,删除所有.o文件和目标文件。
当文件夹下有一堆.o文件和目标文件,可以用make clean
命令,make命令就回去Makefile中找clean目标,然后执行命令。在很多集成开发环境中有个clean,做的就是这件事。
那为什么要有个隐藏的PHONY呢?就是为了防止有个文件叫clean。因为当恰好有个叫clean的文件时,会产生歧义,文件已经有了,就不会产生这个文件了,会提示已经是最新了。这时候就要用.PHONY: clean
,.PHONY
文件永远不会存在,它依赖clean,clean就会删除所有那些文件。
这个版本更加模块化,后面如果有新的文件,只要在OBJ那行写上即可,例如加上file.o
# version 3
CXX = g++
TARGET = hello
OBJ = main.o printhello.o factorial.o
CXXFLAGS = -c -Wall
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@
.PHONY: clean
clean:
rm -f *.o $(TARGET)
会出现一条警告,因为加了-Wall,可以在CXXFLAGS加一些选项,修改起来很方便
version 4
前面还有个麻烦的,要写文件名,这个版本就不用写了
SRC那行是所有当前目录下cpp都放到SRC变量里,就像ls一样,把所有cpp扩展名的文件,满足这个条件的都放到里面。
做一个path路径的替换,把cpp全换成o,就得到了OBJ列表。
# version 4
CXX = g++
TARGET = hello
SRC = $(wildcard *.cpp)
OBJ = $(patsubst %.cpp, %.o, $(SRC))
CXXFLAGS = -c -Wall
$(TARGET): $(OBJ)
$(CXX) -o $@ $^
%.o: %.cpp
$(CXX) $(CXXFLAGS) $< -o $@
.PHONY: clean
clean:
rm -f *.o $(TARGET)