C++标准库头文件正确用法 前言C 标准库由几十个头文件组成每个头文件声明一组相关的设施。写代码时#include哪几个看起来是小事实际上直接影响可移植性、编译速度和代码的可维护性。最常见的坏习惯是#include bits/stdc.h。GCC 提供了这样一个扩展头能一次引入几乎所有标准头写竞赛代码很方便但它是 GCC 的扩展不是标准头文件MSVC 不提供Clang 搭配 libc 也不提供。把它带进正式项目等于把代码绑死在某一个编译器上。另一个普遍问题是「靠传递包含碰运气」用了std::sort却不包含algorithm因为vector在某套实现里恰好间接包含了它。标准不保证这种间接关系换个标准库版本就可能编译失败。还有一类混淆来自 C 头文件string.h是 C 的字符串函数strlen、memcpystring是 C 的std::stringcstring是把前者放进std命名空间的版本。三者名字相近用途完全不同。本文讲标准头文件的分类、C 与 C 头文件的区别、include what you use原则、头文件保护写法以及常见的包含顺序与循环依赖问题。一、C 头文件与 C 头文件C 标准库保留了 C 标准库的功能提供两种形式C 形式C 形式内容stdio.hcstdioprintf、fopen、FILEstdlib.hcstdlibmalloc、free、atoistring.hcstringstrlen、memcpy、strcmpmath.hcmathsqrt、sin、floorctype.hcctypeisdigit、touppertime.hctimetime、clockstdint.hcstdintint32_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 与头文件保护原则很简单代码里直接使用了哪个设施的哪个名字就包含声明它的头文件不依赖其他头文件的间接包含。例如// 适用于 C17#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 前向声明如果只需要指针或引用不需要完整类型可以用前向声明代替包含// 适用于 C17class Widget; // 前向声明void process(const Widget w); // 参数是引用不需要完整定义但以下情况必须包含完整定义按值传递或返回对象。继承自该类。访问成员。在类中定义该类型的成员对象。使用sizeof、static_cast到该类型部分转换除外。标准库为此提供了iosfwd其中前向声明了std::ostream、std::istream等类型适合只在头文件里声明operator的场景// 适用于 C17#include iosfwdstd::ostream operator(std::ostream os, const Widget w);实现文件里再包含ostream。2.2 头文件保护自定义头文件必须防止重复包含否则同一个翻译单元里可能出现重复定义。两种写法// 适用于 C17#ifndef MY_WIDGET_H#define MY_WIDGET_Hclass Widget { /* ... */ };#endif // MY_WIDGET_H或者// 适用于 C17#pragma onceclass Widget { /* ... */ };#pragma once更简洁几乎所有主流编译器都支持但它不是标准的一部分行为细节比如符号链接、大小写不敏感文件系统由实现决定。包含保护include guard是标准做法宏名要唯一通常用「项目名_文件名_H」的形式。无论用哪种都要保证头文件可以独立包含单独写一个只包含该头文件的源文件不应当出现「找不到声明」的错误。这要求头文件自己包含它需要的所有依赖而不是要求调用方先包含某些东西。三、头文件里不该放什么3.1 不要放定义在头文件里定义普通函数或全局变量被多个源文件包含时会产生重复定义multiple definition。正确做法是函数声明放头文件定义放源文件。需要头文件内定义时用inline函数或inline变量C17。模板和constexpr函数可以放头文件它们有特殊的链接规则。// 适用于 C17// header.hinline int square(int x) { return x * x; } // 可以放头文件inline constexpr int kMaxSize 1024; // C17 内联变量3.2 不要写 using namespace// 适用于 C17// 错误头文件里的 using 指令会污染所有包含者using namespace std;头文件被谁包含是不可控的一条using namespace std;可能让别人的count、distance、data全部产生二义性。即使在源文件里也建议限制在函数作用域内。3.3 不要在头文件里写匿名命名空间匿名命名空间的成员在每个翻译单元中各有一份。放在头文件里每个包含者都得到独立副本容易出现「改了一个另一个没变」的问题也会增大二进制体积。头文件里的常量用inline constexpr。四、包含顺序与头文件速查推荐的头文件包含顺序从特殊到一般本文件对应的头文件如果是.cpp。C 系统头文件。C 标准库头文件。第三方库头文件。本项目其他头文件。把「自己的头文件」放第一位可以验证它是否能独立包含如果它漏了某个依赖会立刻暴露而不是被后面包含的头文件掩盖。循环依赖A 包含 BB 又包含 A通常意味着设计有问题。常见的破解方式用前向声明替代包含。把共同依赖抽到第三个头文件。用 pimpl 隐藏实现细节。包含保护只能防止无限递归不能解决「A 和 B 互相需要完整定义」的语义问题。4.1 常用头文件速查设施头文件std::vector、std::arrayvector、arraystd::stringstringstd::string_viewC17string_viewstd::sort、std::findalgorithmstd::cout、std::ciniostreamstd::ostringstreamsstreamstd::unique_ptr、std::shared_ptrmemorystd::optional、std::variantC17optional、variantstd::functionfunctionalstd::thread、std::mutexthread、mutexstd::atomicatomicstd::size_t、std::ptrdiff_tcstddefstd::int32_tcstdintstd::filesystemC17filesystemstd::memcpy、std::strlencstringstd::sqrt、std::floorcmathstd::numeric_limitslimitsstd::runtime_errorstdexcept完整示例// 适用于 C17// widget.h#ifndef MY_WIDGET_H#define MY_WIDGET_H#include iosfwd#include stringclass 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// 适用于 C17// widget.cpp#include widget.h#include ostream#include utilityWidget::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.hcpp #include bits/stdc.h // GCC 扩展不可移植 ✅ 显式包含用到的标准头。❌ 依赖传递包含cpp #include vector std::sort(v.begin(), v.end()); // 没有包含 algorithm✅ 直接包含algorithm。❌ 把string.h当成std::stringcpp #include string.h std::string s; // 编译错误✅ C 字符串用stringC 字符串函数用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留在竞赛里把明确的包含带进项目是成本最低的工程习惯之一。