在 C++ 中,从编写第一个 C++ 程序开始,我们就一直在代码开头使用 #include 指令,比如 #include <iostream>。
其实,#include 是一条预处理指令,专门用来把指定头文件的内容引入到当前源文件中。为什么写一行 #include 就能直接使用别的文件里的代码呢?下面我们就来看看它的工作原理与具体用法。
C++ #include 的工作原理
在 C++ 中,凡是以井号(#)开头的命令都叫做 “预处理指令”。很多初学的小伙伴会误以为,只有当程序运行到 #include 这一行时,才会去打开对应的头文件。实际上,这样理解是不正确的。
在 C++ 的编译与运行过程中,#include 是在正式编译开始前的 “预处理阶段” 执行的。它的工作方式非常直接,说白了就是:在编译开始前,把指定头文件里的全部内容直接 “复制粘贴” 到 #include 所在的那一行。
举个简单的例子,假设我们在头文件 point.h 中定义了一个表示坐标的结构体,代码如下:
struct Point {
int x;
int y;
};接着,我们在主程序 main.cpp 的开头使用 #include "point.h" 引入这个头文件:
#include "point.h"
int main() {
Point p = {3, 5};
return 0;
}当预处理器处理完 #include "point.h" 这一行后,point.h 里的代码就会原地展开到 main.cpp 中。也就是说,上面这段 main.cpp 交给编译器正式编译时,实际等价于下面这段代码。
// 展开 point.h 后的内容
struct Point {
int x;
int y;
};
int main() {
Point p = {3, 5};
return 0;
}这样一来,编译器在编译 main() 函数里的 Point p = {3, 5}; 时,上方已经有了 Point 的完整定义,自然就能顺利通过语法检查。
尖括号与双引号的区别
平时写代码时,我们有时写 #include <iostream> 使用尖括号,有时写 #include "point.h" 使用双引号。这两种写法有什么区别呢?
语法:
#include <文件名>
#include "文件名"说明:
#include 指令需要独占一行,并且行尾不要加英文分号(;)。尖括号与双引号最主要的区别在于预处理器去哪里查找头文件,具体对比如下表所示。
| 语法格式 | 查找文件顺序 | 适用场景 |
|---|---|---|
| #include <文件名> | 直接去系统标准库目录中查找 | 引入 C++ 标准库头文件,如 <iostream> |
| #include "文件名" | 先在当前源文件所在目录查找,找不到再去系统目录查找 | 引入自己编写的项目头文件,如 "point.h" |
既然双引号在当前目录找不到时也会去系统目录查找,那平时能不能全都写成双引号呢?其实并不推荐这样做,主要有以下 2 个原因:
- 查找更直接且不易冲突:使用尖括号时,预处理器会直接去系统目录查找,不用先在项目目录里白找一趟。就算项目文件夹下碰巧有一个同名文件,也不会发生误引入。
- 代码意图更清晰:严格区分尖括号和双引号,我们在阅读代码时就能清楚分辨哪些是 C++ 标准库头文件,哪些是项目自定义的头文件。
下面我们先来看一个引入标准库头文件的例子。
示例 1:引入标准库头文件
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> sites = {"绿叶网", "www.lvyenet.com"};
std::cout << sites[0] << ":" << sites[1] << '\n';
return 0;
}运行结果如下。
绿叶网:www.lvyenet.com分析:
在这个例子中,我们在文件顶部按字母顺序引入了 <iostream>、<string> 和 <vector> 这 3 个标准库头文件。这样一来,main() 函数中就能正常使用 std::cout、std::string 以及 std::vector 了。
提示 在编写 C++ 代码时,我们用到了哪个标准库工具,就应该显式 #include 对应的头文件。不要因为它在当前电脑上碰巧跟着其他头文件间接包含进来了就省略不写,否则换个环境编译时很容易报错。
接下来,我们把尖括号和双引号结合起来看一个完整示例。先在本地新建一个文件夹,放入第 1 节写好的 point.h,再在同一个文件夹下新建 main.cpp。
示例 2:引入自定义头文件
#include <iostream>
#include "point.h"
int main() {
Point p = {3, 5};
std::cout << p.x + p.y << '\n';
return 0;
}在终端进入该文件夹,执行 g++ main.cpp -o app 编译并运行后,运行结果如下。
8分析:
#include <iostream> 表示使用尖括号引入系统自带的标准输入输出头文件,#include "point.h" 则表示使用双引号引入当前目录下的自定义头文件。由于 point.h 里只定义了结构体,没有单独拆出 .cpp 实现文件,所以编译时只需要传入 main.cpp 即可。
如果项目里的文件比较多,我们通常会把自定义头文件整理到子文件夹中,比如存放在 include/point.h。引入时只需要在双引号里按相对路径写清目录层级,代码如下:
#include "include/point.h" // 引入子目录下的头文件小伙伴们需要注意,这里的路径分隔符建议统一使用正斜杠(/),不要写成反斜杠(\),这样代码在 Windows、Linux 和 macOS 系统下都能通用。
使用 #include 的注意事项
弄清楚 #include “复制粘贴文本” 的本质后,我们在实际开发中还需要注意以下两个常见问题。
1. 头文件不包含具体实现
很多初学的小伙伴在接触多文件编程时容易产生一个误解,以为只要在 main.cpp 里写了 #include "math_utils.h",对应的 math_utils.cpp 里的函数实现代码也会自动加进来。
实际上并非如此。假设 math_utils.h 中只有下面这一行函数声明:
int add(int a, int b);当 main.cpp 执行 #include "math_utils.h" 时,预处理器只是把这一行函数声明复制到了 main.cpp 里,帮它通过编译阶段的语法检查。
如果想要让整个程序真正跑起来,我们在编译时仍然必须把提供函数实现的 math_utils.cpp 一起交给编译器,比如在终端执行 g++ main.cpp math_utils.cpp -o app。
2. 避免重复包含同一个头文件
既然 #include 会把头文件的内容完整复制过来,那么如果同一个 .cpp 源文件把没有加保护的头文件 #include 了两次,会发生什么呢?下面我们来看一个例子。
示例 3:重复包含引发报错
#include <iostream>
#include "point.h"
#include "point.h" // 重复包含 point.h
int main() {
Point p = {3, 5};
std::cout << p.x + p.y << '\n';
return 0;
}运行结果如下。
(报错)error: redefinition of 'struct Point'分析:
从运行结果可以看出,编译器直接报错了,其中的 redefinition 表示 “重复定义”。这是因为经过预处理展开后,main.cpp 里同时出现了两份 struct Point 的定义。而在同一个源文件中,同一个结构体或类是不允许重复定义的。
需要注意的是,如果头文件里只有普通函数声明,重复包含通常不会报错,因为函数声明允许出现多次。但只要头文件里定义了结构体或类,一旦重复包含就会立刻报错。为了避免这个问题,我们在编写头文件时都需要加上保护指令,具体做法我们会在下一节 “头文件保护” 中展开介绍。
常见问题
1. 为什么 <iostream> 不写 .h 后缀,而自定义头文件要写 .h 后缀?
这是 C++ 标准为了区分标准库与普通项目文件而制定的命名规则:
C++ 原生标准库头文件:统一省略 .h 后缀,比如 <iostream>、<vector>、<string>。
继承自 C 语言的标准库头文件:在 C++ 中推荐去掉末尾的 .h,并在名字最前面加上字母 c,比如原 C 语言的 <math.h> 在 C++ 中写为 <cmath>,<stdio.h> 写为 <cstdio>。
自己编写的项目头文件:属于本地磁盘上的具体文件,我们引入时必须写全带有 .h 或 .hpp 后缀的完整文件名,比如 "point.h"。
