前言
C++ 标准库由几十个头文件组成,每个头文件声明一组相关的设施。写代码时#include哪几个,看起来是小事,实际上直接影响可移植性、编译速度和代码的可维护性。
最常见的坏习惯是#include <bits/stdc++.h>。GCC 提供了这样一个扩展头,能一次引入几乎所有标准头,写竞赛代码很方便,但它是 GCC 的扩展,不是标准头文件;MSVC 不提供,Clang 搭配 libc++ 也不提供。把它带进正式项目,等于把代码绑死在某一个编译器上。
另一个普遍问题是「靠传递包含碰运气」:用了std::sort却不包含<algorithm>,因为<vector>在某套实现里恰好间接包含了它。标准不保证这种间接关系,换个标准库版本就可能编译失败。
还有一类混淆来自 C 头文件:<string.h>是 C 的字符串函数(strlen、memcpy),<string>是 C++ 的std::string,<cstring>是把前者放进std命名空间的版本。三者名字相近,用途完全不同。
本文讲标准头文件的分类、C 与 C++ 头文件的区别、include what you use原则、头文件保护写法,以及常见的包含顺序与循环依赖问题。
一、C 头文件与 C++ 头文件
C++ 标准库保留了 C 标准库的功能,提供两种形式:
| C 形式 | C++ 形式 | 内容 |
|---|
<stdio.h> | <cstdio> | printf、fopen、FILE |
<stdlib.h> | <cstdlib> | malloc、free、atoi |
<string.h> | <cstring> | strlen、memcpy、strcmp |
<math.h> | <cmath> | sqrt、sin、floor |
<ctype.h> | <cctype> | isdigit、toupper |
<time.h> | <ctime> | time、clock |
<stdint.h> | <cstdint> | int32_t、uint64_t |
区别在于命名空间:
<cstdio>保证名字在std命名空间中(如std::printf),是否同时放进全局命名空间由实现决定。<stdio.h>保证名字在全局命名空间,是否同时放进std由实现决定。
新代码统一用<c...>形式,并写std::前缀。这样不会污染全局命名空间,也不会因为实现差异导致名字找不到。
注意<string>和<cstring>不是一回事:前者是std::string类,后者是 C 风格字符串函数。只写#include <string>却调用strlen,在多数实现上会编译失败(因为strlen声明在<cstring>里)。
二、include what you use 与头文件保护
原则很简单:代码里直接使用了哪个设施的哪个名字,就包含声明它的头文件,不依赖其他头文件的间接包含。
例如:
// 适用于 C++17
#include <algorithm> // std::sort
#include <iostream> // std::cout
#include <string> // std::string
#include <vector> // std::vector不能因为<iostream>在某个实现里包含了<string>,就省掉<string>。标准只规定每个头文件必须提供哪些声明,不规定它不能额外包含什么,也不规定它必须包含什么。不同标准库实现(libstdc++、libc++、MSVC STL)的包含关系差别很大,依赖它就是把可移植性交给运气。
这条原则还有一个好处:减少不必要的编译依赖。一个源文件只包含真正需要的头,改动无关头文件时不会触发大面积重编译。
2.1 前向声明
如果只需要指针或引用,不需要完整类型,可以用前向声明代替包含:
// 适用于 C++17
class Widget; // 前向声明
void process(const Widget& w); // 参数是引用,不需要完整定义但以下情况必须包含完整定义:
- 按值传递或返回对象。
- 继承自该类。
- 访问成员。
- 在类中定义该类型的成员对象。
- 使用
sizeof、static_cast到该类型(部分转换除外)。
标准库为此提供了<iosfwd>,其中前向声明了std::ostream、std::istream等类型,适合只在头文件里声明operator<<的场景:
// 适用于 C++17
#include <iosfwd>
std::ostream& operator<<(std::ostream& os, const Widget& w);实现文件里再包含<ostream>。
2.2 头文件保护
自定义头文件必须防止重复包含,否则同一个翻译单元里可能出现重复定义。两种写法:
// 适用于 C++17
#ifndef MY_WIDGET_H
#define MY_WIDGET_H
class Widget { /* ... */ };
#endif // MY_WIDGET_H或者:
// 适用于 C++17
#pragma once
class Widget { /* ... */ };#pragma once更简洁,几乎所有主流编译器都支持,但它不是标准的一部分,行为细节(比如符号链接、大小写不敏感文件系统)由实现决定。包含保护(include guard)是标准做法,宏名要唯一,通常用「项目名_文件名_H」的形式。
无论用哪种,都要保证头文件可以独立包含:单独写一个只包含该头文件的源文件,不应当出现「找不到声明」的错误。这要求头文件自己包含它需要的所有依赖,而不是要求调用方先包含某些东西。
三、头文件里不该放什么
3.1 不要放定义
在头文件里定义普通函数或全局变量,被多个源文件包含时会产生重复定义(multiple definition)。正确做法是:
- 函数声明放头文件,定义放源文件。
- 需要头文件内定义时,用
inline(函数)或inline变量(C++17)。 - 模板和
constexpr函数可以放头文件,它们有特殊的链接规则。
// 适用于 C++17
// header.h
inline int square(int x) { return x * x; } // 可以放头文件
inline constexpr int kMaxSize = 1024; // C++17 内联变量3.2 不要写 using namespace
// 适用于 C++17
// 错误:头文件里的 using 指令会污染所有包含者
using namespace std;头文件被谁包含是不可控的,一条using namespace std;可能让别人的count、distance、data全部产生二义性。即使在源文件里,也建议限制在函数作用域内。
3.3 不要在头文件里写匿名命名空间
匿名命名空间的成员在每个翻译单元中各有一份。放在头文件里,每个包含者都得到独立副本,容易出现「改了一个,另一个没变」的问题,也会增大二进制体积。头文件里的常量用inline constexpr。
四、包含顺序与头文件速查
推荐的头文件包含顺序(从特殊到一般):
- 本文件对应的头文件(如果是
.cpp)。 - C 系统头文件。
- C++ 标准库头文件。
- 第三方库头文件。
- 本项目其他头文件。
把「自己的头文件」放第一位,可以验证它是否能独立包含:如果它漏了某个依赖,会立刻暴露,而不是被后面包含的头文件掩盖。
循环依赖(A 包含 B,B 又包含 A)通常意味着设计有问题。常见的破解方式:
- 用前向声明替代包含。
- 把共同依赖抽到第三个头文件。
- 用 pimpl 隐藏实现细节。
包含保护只能防止无限递归,不能解决「A 和 B 互相需要完整定义」的语义问题。
4.1 常用头文件速查
| 设施 | 头文件 |
|---|
std::vector、std::array | <vector>、<array> |
std::string | <string> |
std::string_view(C++17) | <string_view> |
std::sort、std::find | <algorithm> |
std::cout、std::cin | <iostream> |
std::ostringstream | <sstream> |
std::unique_ptr、std::shared_ptr | <memory> |
std::optional、std::variant(C++17) | <optional>、<variant> |
std::function | <functional> |
std::thread、std::mutex | <thread>、<mutex> |
std::atomic | <atomic> |
std::size_t、std::ptrdiff_t | <cstddef> |
std::int32_t | <cstdint> |
std::filesystem(C++17) | <filesystem> |
std::memcpy、std::strlen | <cstring> |
std::sqrt、std::floor | <cmath> |
std::numeric_limits | <limits> |
std::runtime_error | <stdexcept> |
完整示例
// 适用于 C++17
// widget.h
#ifndef MY_WIDGET_H
#define MY_WIDGET_H
#include <iosfwd>
#include <string>
class Widget {
public:
explicit Widget(std::string name);
const std::string& name() const;
private:
std::string name_;
};
std::ostream& operator<<(std::ostream& os, const Widget& w);
#endif // MY_WIDGET_H// 适用于 C++17
// widget.cpp
#include "widget.h"
#include <ostream>
#include <utility>
Widget::Widget(std::string name) : name_(std::move(name)) {}
const std::string& Widget::name() const { return name_; }
std::ostream& operator<<(std::ostream& os, const Widget& w) {
return os << w.name();
}头文件里std::ostream只以引用形式出现,所以用<iosfwd>前向声明即可;源文件里真正要写输出,才包含<ostream>。std::string是成员类型,必须包含<string>。这就是「按需包含」的具体应用。
常见坑点
- ❌ 用
<bits/stdc++.h>:
``cpp #include <bits/stdc++.h> // GCC 扩展,不可移植`` ✅ 显式包含用到的标准头。
- ❌ 依赖传递包含:
``cpp #include <vector> std::sort(v.begin(), v.end()); // 没有包含 <algorithm>`✅ 直接包含<algorithm>`。
- ❌ 把
<string.h>当成std::string:
``cpp #include <string.h> std::string s; // 编译错误`✅ C++ 字符串用<string>,C 字符串函数用<cstring>`。
- ❌ 头文件里写
using namespace std;。
✅ 写std::前缀,或只做using std::string;且放在源文件。
- ❌ 头文件缺少包含保护,被重复包含导致重定义。
✅ 用 include guard 或#pragma once,并保证头文件可独立包含。
- ❌ 在头文件里定义非 inline 函数或全局变量。
✅ 声明放头文件、定义放源文件;需要头文件内定义时用inline。
- ❌ 用
<iostream>只为声明std::ostream参数。
✅ 用<iosfwd>前向声明,实现文件再包含<ostream>。
- ❌ 头文件里放匿名命名空间变量。
✅ 用inline constexpr定义常量,避免每个翻译单元一份副本。
总结
| 原则 | 做法 |
|---|
| 用 C++ 形式 | <cstdio>而不是<stdio.h> |
| 按需包含 | 用什么包含什么,不靠传递 |
| 可独立包含 | 头文件自己带上依赖 |
| 防重复 | include guard 或#pragma once |
| 不污染 | 头文件里不写using namespace |
| 减少依赖 | 只需声明时用前向声明 /<iosfwd> |
| 定义归位 | 非 inline 定义放源文件 |
标准库头文件的用法,核心只有一句「include what you use」。它同时解决了三个问题:可移植性(不依赖某套实现的包含关系)、编译速度(不引入无关依赖)、可读性(一眼看出这个文件用了什么)。把<bits/stdc++.h>留在竞赛里,把明确的包含带进项目,是成本最低的工程习惯之一。