Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

Linux C++ 开发 系列的前面2篇文章,我们介绍了通过g++来编译C++代码。这对于HelloWorld程序或者简单的Demo程序来说没有问题,但对于包含多个.cpp和多个.h文件的复杂项目来说,直接用g++命令来编译的话,将会使编译的指令非常冗长且难于维护。这个时候我们可以考虑用makefile来构建我们的程序。

1. make 和 Makefile

1.1. 什么是make?

make是一个自动化构建工具,广泛应用于C/C++项目中,但也可以用于其他编程语言。它的主要功能是根据Makefile中的规则自动执行一系列命令,从而生成目标文件。make通过比较目标文件和依赖文件的时间戳来决定是否需要重新构建某个目标,从而避免了不必要的编译,提高了构建效率。

我们安装GCC后,应该默认就已经安装了make,没有没有安装,Ubuntu下可通过如下命令来安装:

1
2
sudo apt update
sudo apt install make

安装完成后,你可以通过以下命令来验证make是否安装成功:

1
make --version

1.2. 什么是Makefile?

Makefile 是一个文本文件,定义了构建项目的规则和指令。通常定义了多条包含 目标(target)、依赖(dependency)和命令(command) 的规则。

1.3. make 与 Makefile的关系

  • Makefile 你可以理解为是自动构建的脚本,里面通过 目标(target)、依赖(dependency)和命令(command) 定义了规则,告诉make工具要如何一步步构建我们的最终目标。
  • make 是一个命令工具,是一个解释并执行Makefile中指令的命令工具,按照Makefile制定的规则,构建最终的目标产物。

2. Makefile的语法

2.1. 基本语法

Makefile的基本语法如下:

1
2
目标: 依赖
命令
  • 目标: 通常是需要生成的文件名,也可以是某个操作(如clean)。
  • 依赖: 生成目标文件所依赖的其他文件或其他目标。
  • 命令: 生成目标所需执行的shell命令,必须以Tab键开头。

注意: 命令前面必须是tab键,表示命令的开始。不能用4个空格或者两个空格。

2.2. 变量

在Makefile中,可以使用变量来简化规则的编写。变量定义如下:

1
变量名 = 值

使用变量时,需要在变量名前加上$符号,并用括号括起来:

1
$(变量名)

2.3. 伪目标

伪目标是一种特殊的目标,它不代表具体的文件,通常用于执行某些操作。伪目标需要使用.PHONY声明:

1
2
3
.PHONY: clean
clean:
rm -f hello hello.o

2.4. 模式规则

模式规则允许定义通用的规则,适用于多个目标。例如:

1
2
%.o: %.c
$(CC) $(CFLAGS) -c $<

这条规则表示所有.o文件都依赖于对应的.c文件,并且使用相同的编译命令。

2.5. 自动变量

make提供了一些自动变量,用于简化命令的编写:

  • $@:表示目标文件名。
  • $<:表示第一个依赖文件名。
  • $^:表示所有依赖文件名。

2.6. 条件判断

Makefile支持条件判断,可以根据不同的条件执行不同的命令:

1
2
3
4
5
ifeq ($(DEBUG),1)
CFLAGS += -g
else
CFLAGS += -O2
endif

3. 示例演示

3.1. 编译HelloWorld程序

我们用Makefile来编译《Linux C++ 开发2 - 编写、编译、执行第一个程序》中的Hello world程序。

Makefile:

1
2
3
4
5
6
7
8
9
# 编译 demo01.cpp
demo01.out: demo01.cpp
g++ ./demo01.cpp -o demo01.out

# 申明clean为伪目标
.PHONY: clean
# 定义 clean 命令
clean:
rm -f demo01.out

编译和运行:

1
2
3
4
5
# 编译
make
# 运行
./demo01.out
Hello, world!

3.2. 编译多文件项目

3.2.1. 项目概述

C++之迭代器》一文中有一个例子是这样的:

一个公司有多个部门,每个部门有多个人组成,这些人中有开发人员,有测试人员,和与项目相关的其它人员,其结构如下图片。

现在要遍历这个公司的所有开发人员,遍历这个公司的所有测试人员。

在这篇文章中,我们用迭代器模式实现了这个需求,类的结构图是这样的:

详细代码参见: https://gitee.com/spencer_luo/iterator

现在我们就以这个项目为例,看看这个项目的makefile需要怎么写?

3.2.2. 需求分析

代码结构如下,有三个.cpp,两个.h,两个.hpp

依赖关系如下:

Iterator(client) –> Enumerator –> Company –> Department –> Person

1
2
3
4
5
6
7
8
9
./iterator
├── Company.cpp
├── Company.h
├── Department.hpp
├── Enumerator.hpp
├── Iterator.cpp
├── Person.cpp
├── Person.h
└── README.md

3.2.3. Makefile V1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 构建的最终目标 Iterator(可执行文件)
Iterator:Iterator.o Company.o Person.o
g++ -o Iterator Iterator.o Company.o Person.o
# 构建目标 Iterator.o
Iterator.o:Iterator.cpp
g++ -c Iterator.cpp
# 构建目标 Company.o
Company.o:Company.cpp
g++ -c Company.cpp
# 构建目标 Person.o
Person.o:Person.cpp
g++ -c Person.cpp

# 申明clean为伪目标
.PHONY: clean
clean:
rm -f *.o Iterator

执行make进行编译:

1
2
3
4
5
make      
g++ -c -o Iterator.o Iterator.cpp
g++ -c -o Company.o Company.cpp
g++ -c -o Person.o Person.cpp
g++ -o Iterator Iterator.o Company.o Person.o

上面的Makefile大家可能会有一些疑问,这里对可能存在的疑问做一些解答。

3.2.3.1. 问题一:为什么没有头文件的依赖?

问题描述:

如:编译Person.o时,Person.cpp是包含了Person.h的,为什么这条规则不写成:

1
2
Person.o:Person.cpp Person.h
g++ -c Person.cpp

问题解答:

这种写法也是没有问题的,对于makefile而言,没有语法错误。但是没有这个必要,《Linux C++ 开发3 - 你写的Hello world经过哪些过程才被计算机理解和执行?》一文中我们讲了在程序预处理阶段,预处理器会将所有通过#include包含的头文件替换成真正的内容,所以我们编译的时候只需要对.cpp进行编译即可。

3.2.3.2. 问题二:为什么没有对.hpp的规则定义?

问题描述:

为什么Department.hppEnumerator.hpp不需要编译。

问题解答:

正常,我们创建C++代码文件的时候,一般会创建两个文件:

  • 一个是头文件(如:abc.h),用来进行类、函数、常量等的声明。
  • 一个是源文件(如:abc.cpp),用来进行类、函数的定义。

但这样每次要创建两个文件,而且要在两个文件上分别进行声明和定义,挺麻烦的。于是为了偷懒,对于一些简单的,没有交叉引用的类,我们通常会把声明和定义都放在一个文件中,这个文件通常以.hpp作为后缀(如:abc.hpp)。

.hpp 本质上还是一个头文件,GCC在编译的时候,会把它当做头文件来处理。所以我们在Makefile中可以不用写对.hpp的编译规则。

3.2.4. Makefile V2.0

GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要在每一个.o文件后都写上编译的命令和规则,因为我们的make会自动识别,并自己推导命令。

于是我们的Makefile可以简化为:

1
2
3
4
5
6
7
8
# 构建的最终目标 Iterator(可执行文件)
Iterator:Iterator.o Company.o Person.o
g++ -o Iterator Iterator.o Company.o Person.o

# 申明clean为伪目标
.PHONY: clean
clean:
rm -f *.o Iterator

执行make命令,我们会看到它会自动先去编译.o, 然后再链接生成最终的二进制文件,编译的过程和V1.0是一样的。

1
2
3
4
5
make      
g++ -c -o Iterator.o Iterator.cpp
g++ -c -o Company.o Company.cpp
g++ -c -o Person.o Person.cpp
g++ -o Iterator Iterator.o Company.o Person.o
推荐阅读
Linux C++ 开发6 - GDB调试入门指南 Linux C++ 开发6 - GDB调试入门指南 Linux C++ 开发5 - 一文了解CMake构建 Linux C++ 开发5 - 一文了解CMake构建 Linux C++ 开发2 - 编写、编译、执行第一个程序 Linux C++ 开发2 - 编写、编译、执行第一个程序

评论