makefile在C++中的使用

Make工具

Make是unix上被用来简化为一个工程中的各个不同模块构建可执行程序的工具。在Makefile中有各种各样的规则来指定目标入口。make工具则读取所有的这些规则并依照行事。

例如,如果一个规则指定了某个依赖,那么make工具将在编译时包含这个依赖。make命令利用makefile来完成模块构建和文件清理的工作。

make的常用语法如下:

1
make target_label # target_label是在makefile中指定的目标

例如,如果我们想要执行清理文件的操作,那么我们可以这么做:

1
make clean # 注意,clean实际上是指定了rm作为执行命令

C++ Makefile

Makefile是一个由make命令来使用并用来构建目标的纯文本文件。一个Makefile也包含了一些源码级别的依赖信息以及构建顺序。

现在让我们看看Makefile的通用结构:

一个典型的Makefile通常以一个变量声明开头,后面紧跟着一串用来构建指定目标的目标入口。这些目标可能是.o或者是其他的可执行文件,比如说C或者C++或者java.class文件。

我们还可以有一串被目标标识(target label)所指定并被用来执行一系列命令的目标入口。

因此一个通用的makefile大致如下:

1
2
3
4
# comment
target: dependency1 dependency2 ... dependencyn
command
# 注意,command前面的tab缩进对于make来说是必要的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 指定编译器,g++代表C++编译器,gcc代表C编译器
CC = g++
# 编译器flags
# -g表示需要添加debug信息到可执行文件中
# -Wall表示打开大多数编译器警告
CFLAGS = -g -Wall
#
TARGET = main

all: $(TARGET)
$(TARGET): $(TARGET).cpp
$(CC) $(CFLAGS) -o $(TARGET) $(TARGET).cpp
clean:
$(RM) $(TARGET)

Make和Makefile的例子

假设我们的工程有如下几个文件:

  • Main.cpp:主程序
  • Point.h:point类的头文件
  • Point.cpp:Point类的实现类
  • Square.h:square的头文件
  • Square.cpp:square类的实现类

对于上面的几个文件,我们需要将这些文件各自编译并生成.o文件,然后将它们link成一个叫做main的可执行文件。

接下来我们将各自编译这些文件:

  • g++ -c main.cpp:生成main.o
  • g++ -c point.cpp:生成point.o
  • g++ -c square.cpp:生成square.o

接下来,我们需要将这些.o文件link成一个可执行文件:

1
g++ -o main main.o point.o square.o

接下来,我们需要确定当某个文件发生变更时需要重新编译和重新生成的其他关联文件。下图描述了这种依赖关系。

image-20211230234102091

在上面的依赖图中,我们可以看到main位于根部,而main又包含了对象文件main.opoint.osquare.o,它们分别由各自的cpp文件编译生成。

所有的cpp实现都使用了它们自己的头文件,而main.cpp又引用了point.hsquare.h

接下来point.cpp引用了point.hsquare.cpp引用了square.hpoint.h

如果我们的工程中只有少数几个文件,那么我们可以不用关系它们之间的依赖关系,但是,实际项目中,我们可能会有成百上千个文件,如果我们每次修改完一个文件之后都要手动编译和生成所有文件,那无疑是令人崩溃的。因此,我们需要使用make工具来帮我们简化这个流程。

注意,必须使用Makefile作为文件名,不区分大小写,且放在源代码的根目录。

1
2
3
4
5
CC = g++
CFLAGS = -wall -g
# 确定依赖关系
main: main.o Point.o Square.o
$(CC) $(CFLAGS) -o main main.o Point.o Square.o

上面的命令实际上等价于我们在终端中执行

g++ -wall -g -o main main.o point.o square.o

接下来我们需要生成对象文件:main.o、point.o、square.o

1
2
3
4
main.o: main.cpp point.h square.h
$(CC) $(CFLAGS) -c main.cpp
Point.o: Point.h
Square.o: Square.h Point.h

Makefile的优点

  • 对于大项目,可以让我们以系统性和更有效的方式呈现项目;
  • 可以让源代码更加有效和易读,并利于debug;
  • Makefile可以自动编译那些被修改过的文件,因此当发生文件变更时我们不需要手动重新生成整个项目。
  • Makefile允许我们一次编译多个文件,因此我们只需要一个步骤就可以完成整个项目的编译。