%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # 头文件引入 头文件扩展名为 `.h`,引入头文件的语法有两种: | | 编译器查找头文件的路径 | | ---------------- | ----------------------------------------------------------- | | `#include <文件名>` | 仅在 "**==系统头文件目录==**" 中查找该头文件(例如 `/usr/include`,为标准库头文件所在路径) | | `#include "文件名"` | 首先在 **==当前源文件所在的目录==** 中查找,若未找到则再去 **系统头文件目录** 中查找 | #### 指定额外的头文件搜索路径 ![[05-工具/GNU 工具/gcc 编译器#^1fmyfx]] <br><br> # 防止头文件重复引入 进行 **防卫式声明**,两种等效方式: - 方式一:使用**预处理指令** `#ifndef` 进行条件编译 - 方式二:使用**预处理指令** `#pragma once` (gcc、clang、msvc 编译器支持) ###### 方式一 ```c++ #ifndef __HEADER_H #define __HEADER_H // 头文件内容 ... #endif ``` 其中,宏的名称可以随意取。 ###### 方式二 使用 `#pragma once` 后,无论这个头文件被包含多少次,**它的内容都只会在每个编译单元中被处理一次**,防止头文件内容被重复包含。 ```c++ #pragma once // 头文件内容 ... ``` <br><br><br> # 防止头文件循环引用 "**头文件循环引用**" 出现在**两个类==互相包含==(其中一方必然以 "==指针==" 形式)** 的情况,两个头文件**互相 `#include` 对方**,导致循环无解。 解决办法: - 对于 "**持有对方==指针==**" 的一方(如下例的 `class A`): - 在其**头文件中仅对 `class B` 做==前向声明==**,而在其**源文件(`A.cpp` )中才引入对方头文件 `B.h`** - (源文件中的函数里会操作 B,故需要 B 的完整定义) - 对于 "**持有对方==实例==**" 的一方(如下例的 `class B`): - 在其头文件中**正常引入对方头文件**, `#include "A.h"` ### 示例 ```cpp title=A.h #pragma once class B; // 仅做前向声明 class A { public: B* b; // 无需完整定义 void func(); }; ``` ```cpp title=A.cpp #include "A.h" #include "B.h" // 故引入B.h头文件 void A::func() { b->func(); // 需要B类型的完整定义 } ``` ```cpp title=B.h #pragma once #include <iostream> #include "A.h" // 故引入A的头文件 class B { public: A a; // 持有A类实例, 需要A的完整定义 }; ``` ```cpp title=B.cpp #include "B.h" using namespace std; void B::func() { cout << "Done" << endl; } ``` ```cpp title=main.cpp #include "A.h" #include "B.h" int main() { A a; B b; a.func(); b.func(); } ``` <br><br><br> # 分离式编译 分离式编译: - 对于 "**类**":在头文件(`.h`)中**定义类**,而**在源文件(`.cpp`)中实现其成员函数**; - 对于 "**模版**" (类模版、函数模版): "**声明**" 和 "**定义/实现**" 在编译时必须位于同一个 "**翻译单元**" 中。 > [!note] 类的辅助函数的放置位置 > > 围绕着一个类通常包含一些**辅助函数**,如 `add`、`read`、`print` 、`swap` 等,其定义的操作从**概念上来说属于类的接口的组成部分**,但**实际上并不属于类本身**。这些**函数原型的声明**通常应该与**类声明**放在同一个头文件中,从而类的使用者只需要引入一个头文件。而**函数定义则与类成员函数的实现**放在同一个源文件中 。 > [!NOTE] 若某个头文件的内容 **==仅在类的实现文件(.cpp)中使用==**,则应当仅在 `.cpp` 源文件文件中 `#include`,而不要放在头文件中引入,从而**减少编译依赖,提高编译速度**。 <br><br> ## "模版声明" 与 "模版定义" 的分离 > [!important] 模板的 "**声明**" 和 "**定义**" 必须位于同一个翻译单元中 > > 原因在于:编译器在 **==实例化一个模版==** 时 **==必须要能看到模版的确切定义==**,从而**根据模版定义代入 "模版参数" 完成替换**,**生成实例化代码**(模版类、模版函数)。 > > 如果**模版的声明与定义分离**,例如**模版声明放在 `.h` 头文件中而不引入定义**,则**对其他引入该 `.h` 头文件的源文件而言,其编译期的 "翻译单元上下文" 中无法找到模版定义,故尝试实例化模版时将导致编译错误**。 > > Ps:对 C++编译器而言,编译器在**处理各个==翻译单元 TPU== 时生成==符号引用==**,交由 **==链接器==在==链接阶段==解析这些符号引用**。 > > - 当**调用函数时**,编译器**只需要看到函数的声明**; > - 当创建类类型的实例对象时,编译器只需看到**类的定义**,而不需要知道类的实现代码。 > > ^cpj1sg 因此,在处理 "**模版声明**" 与 "**模版定义**" 的处理上有两种方式: - (1)**模版声明与实现集中**:**两者放置在==同一个头文件==中**。 - 头文件后缀通常命名为 `.hpp` 或 `.hxx`; - (2)**模版声明与实现分离**: **在头文件内末尾通过 `#include "xxx.tcc"` ==引入模版实现==**; - **头文件 `.hpp` 中只包含模版声明**:**类模版定义**(对成员函数只有声明),**函数模版声明**; - **源文件 `.tcc` / `.impl` / `.inl` 中**包含 "**==模版函数的具体定义实现==**","**==内联函数的定义==**"。 示例: ```cpp title=MyTemplate.hpp // MyTemplate.hpp(声明) #pragma once template<typename T> class MyTemplate { public: void print(); }; // include实现的文件 #include "MyTemplate.impl" ``` > [!example] 示例一:STL 头文件中,类模版成员函数大多都直接定义在类定义中。 > > `libstdc++` 中,头文件 `<vector>` 内部先后引入了 `bits/stl_vector.h` 与 `bits/vector.tcc` 两个文件。 > > - 前者**包含 `std::vector<>` 类模版的定义**,**一些较高层的成员函数(调用其他底层函数)则在类定义中的直接实现**,例如 `push_back()`。 > - 后者则**对 `emplace_back()`、`reserve()` 等 "最底层" 的模版函数**进行了实现。 > > ![[_attachment/02-开发笔记/01-cpp/预处理相关/cpp-头文件说明.assets/IMG-cpp-头文件说明-5C80BA3A829AC411B194650B94A5893D.png|794]] > > > [!example] 示例二: > > workflow 项目代码中,**类模版的函数定义放入 ==`xxx.inl` 文件==** 中,而在**在头文件中末尾通过 `#include "xxx.inl"` 引入该文件**。 > > ![[_attachment/02-开发笔记/01-cpp/预处理相关/cpp-头文件说明.assets/IMG-cpp-头文件说明-D1C3E9F2C7E011E69343995830989443.png|595]] > > <br><br><br> # 头文件使用建议 - 用户自定义头文件**应当使用 `#include "xxx"` 引入**。 - 头文件中**不应该使用任何 using 指令**,避免导致命名空间污染。 ### 头文件中不应当使用任何 using 指令 在**头文件**中,**不应当使用任何 using 指令**,包括 using 声明(例如`using std::vector;`)和 using 指示(`using namespact std;`),而是**明确指定命名空间**,如`std::vector<int>` [^1] 。 > [!NOTE] 在源文件中使用 `using` 指令是安全的,因为其影响范围仅仅局限于该源文件,不会影响到其它文件。 ###### 原因说明 由于 "**头文件中的内容在预处理阶段会==被拷贝插入到引用该头文件的源文件==中**", 因此 **"头文件中的 `using` 指令" 会被加入到所有 "==引入该头文件的源文件==" 中**,从而可能引起每个源文件的**命名空间污染**,导致**命名冲突**而引发一系列问题。 - (1)可能 **导致名称解析的模糊性**,编译器报错 - 示例一:假设一个源文件**自定义了一个 `count()` 函数**,而**其引入的头文件中使用 `using std::count` 或 `using namespace std` 引入了 `std` 命名空间中的 `count()` 函数**,则将导致**编译器对该源文件报错**——名称解析混淆,编译器无法知道调用的是哪一个函数。 - 示例二:C++11 与 boost 库中都有 `shared_ptr`,不指定 namespace 或者 using 了两个 namespace 可能导致混淆问题,使得编译器无法确定代码中调用的是哪一个函数。 - 示例三:Windows 头文件里自带一种类型 `byte`,而 C++17 中新增了 `std::byte`,如果一个头文件里包含有`using namespace std` ,则当编译器使用 C++17 标准时**将报错名称解析混淆**——编译器就不知道它是 Windows 的 `byte` 还是 `std: byte` 省略了 `std` 。 - (2)**代码可读性下降**:对于源代码中的**一个与 "`std` 命名空间中名称" 同名的名称**,无法该名称是来自 `std` 还是其他地方,特别是当与其他库交互时。 > [!quote] > ![image-20231003203817562|554](_attachment/02-开发笔记/01-cpp/预处理相关/cpp-头文件说明.assets/IMG-cpp-头文件说明-08A6B4BFAC18EADBC64361325BC352D2.png) <br><br> ### 引入头文件时的先后顺序 为了避免潜在的编译问题,推荐的 `#include` 顺序: 1. 先包含**对应的头文件**(对于 `.cpp` 源文件)。 2. 再包含 C++ 标准库头文件。 3. 接着包含第三方库头文件(如 Boost, OpenCV)。 4. 最后包含项目内部的其他头文件。 示例: ```cpp // MyClass.cpp #include "MyClass.h" // 先包含自己的头文件 #include <iostream> // C++ 标准库 #include <vector> #include "OtherClass.h" // 其他项目头文件 ``` ![image-20230912093343546|890](_attachment/02-开发笔记/01-cpp/预处理相关/cpp-头文件说明.assets/IMG-cpp-头文件说明-94C36A73096D2A0B8815A11E1FEADE9B.png) <br><br> ### 头文件中应当编写的内容 头文件中通常放置 "**只能被定义一次的实体**",例如: - 函数相关: - 函数原型,即函数声明; - **内联函数定义** - 内联函数的特殊规则要求在每个使用它的文件中都进行定义。 - 确保内联定义对多文件中各个文件可用,最简便的方法是:**将内联定义放在定义类的头文件中**。 - **static 函数定义**(内部链接性) - 变量/常量相关: - **`#define` 定义的符号常量** - **`const` 、`constexpr` 定义的全局常量**(内部链接性) - **`static` 全局常量**(内部链接性) - `extern` 变量声明( `extern` 仅作声明,**定义和初始化赋值放到头文件对应的 `.cpp` 文件中进行**) - 用户自定义类型的 "**声明**":类类型、结构体、联合体、枚举类型的声明 - **==模版声明==** - 模版定义通过**在头文件中使用 `#include "..."` 引入** > [!caution] 头文件中禁止定义具有 "**==外部链接性==**" 的变量! > > 否则当头文件被其它多个源文件包含时,将出现 "**重定义错误**"。 > 正确用法: 在头文件中用 `extern` 声明外部变量,而在头文件对应的 `.cpp` 源文件中进行定义和赋值。 > [!NOTE] `static` 修饰的全局变量、函数具有 "**内部链接性**"。 > > 因此多个源文件引入后不会导致重定义错误,而是**每个翻译单元一份独立实例**。 > > ![[_attachment/02-开发笔记/01-cpp/预处理相关/cpp-头文件说明.assets/IMG-cpp-头文件说明-14B3F960FAA3E9A13E5B2577B2AC5BA6.png|377]] > > > > [!NOTE] 文件组织说明 > > - 头文件:包含结构声明、**使用/操纵这些结构的函数原型**、类声明、模版声明等 > - 源代码文件:与头文件中的声明对应的具体实现,包含与结构有关的函数的代码,例如函数**定义**等。 > - 源代码文件:包含**调用与结构相关的函数**的代码; <br><br> # 参考资料 # Footnotes [^1]: 《C++ Primer》P75、P704