C++头文件

C++头文件(Header File)是包含在C++程序中的文本文件,通常具有扩展名 “.h” 或 “.hpp”(更现代的C++代码使用 “.hpp”)。头文件主要用于包含声明、函数原型、类和模板的定义,以及全局变量的声明。

C++头文件(Header File)是包含在C++程序中的文本文件,通常具有扩展名 “.h” 或 “.hpp”(更现代的C++代码使用 “.hpp”)。头文件主要用于包含声明、函数原型、类和模板的定义,以及全局变量的声明。头文件的主要目的是提供接口,允许不同的源文件共享声明和定义,以实现代码的模块化和可重用性。

使用标准库头文件

#include <iostream>

int main()
{
    std::cout << "Hello, world!";
    return 0;
}

该程序打印“Hello, world!” 使用 std::cout 到控制台。 但是,该程序从未提供 std::cout 的定义或声明,那么编译器如何知道 std::cout 是什么?

答案是 std::cout 已在“iostream”头文件中前向声明。 当我们 #include 时,我们请求预处理器将名为“iostream”的文件中的所有内容(包括 std::cout 的前向声明)复制到执行 #include 的文件中。

考虑一下如果 iostream 标头不存在会发生什么。 无论您在何处使用 std::cout,都必须手动键入或复制与 std::cout 相关的所有声明到使用 std::cout 的每个文件的顶部! 这需要大量关于如何声明 std::cout 的知识,并且需要大量工作。 更糟糕的是,如果添加或更改了函数原型,我们就必须手动更新所有前向声明。

只需 #include 就容易多了!

使用头文件传播前向声明

现在让我们回到上一课中讨论的例子。 我们有两个文件,add.cpp 和 main.cpp,如下所示:

add.cpp:

int add(int x, int y)
{
    return x + y;
}

main.cpp:

#include <iostream>

int add(int x, int y); // 使用函数原型进行前向声明

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';
    return 0;
}

(如果您从头开始重新创建此示例,请不要忘记将 add.cpp 添加到您的项目中,以便对其进行编译)。

在这个例子中,我们使用了前向声明,以便编译器在编译main.cpp时知道add是什么标识符。 如前所述,为您想要使用的位于另一个文件中的每个函数手动添加前向声明可能会很快变得乏味。

让我们写一个头文件来减轻我们的负担。 编写头文件非常简单,因为头文件仅由两部分组成:

  • 头文件保护,我们将在下一课中更详细地讨论头文件保护。
  • 头文件的实际内容,应该是我们希望其他文件能够看到的所有标识符的前向声明。

向项目添加头文件的工作方式与添加源文件类似(具有多个代码文件的程序中介绍)。

如果使用 IDE,请执行相同的步骤,并在询问时选择“Header”而不是“Source”。 头文件应该作为项目的一部分出现。

如果使用命令行,只需在编辑器中与源 (.cpp) 文件位于同一目录中创建一个新文件。 与源文件不同,头文件不应添加到编译命令中(它们由 #include 语句隐式包含并作为源文件的一部分进行编译)。

头文件通常与代码文件配对,头文件为相应的代码文件提供前向声明。 由于我们的头文件将包含 add.cpp 中定义的函数的前向声明,因此我们将调用新的头文件 add.h。

这是我们完成的头文件:

add.h:

// 1) 我们确实应该在这里有一个头文件保护,但为了简单起见,我们将省略它(我们将在下一课中介绍标头文件保护)

// 2) 这是 .h 文件的内容,也是声明所在的位置
int add(int x, int y); // add.h 的函数原型——不要忘记分号!

为了在 main.cpp 中使用这个头文件,我们必须 #include 它(使用引号,而不是尖括号)。

main.cpp:

#include "add.h" // 此时插入add.h的内容。 请注意此处使用双引号。
#include <iostream>

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';
    return 0;
}

add.cpp:

#include "add.h" // 此时插入add.h的内容。 请注意此处使用双引号。

int add(int x, int y)
{
    return x + y;
}

当预处理器处理 #include “add.h” 行时,它将 add.h 的内容复制到当前文件中。 因为我们的 add.h 包含函数 add() 的前向声明,所以该前向声明将被复制到 main.cpp 中。 最终结果是一个与我们在 main.cpp 顶部手动添加前向声明的程序在功能上相同的程序。

因此,我们的程序将正确编译和链接。

C++头文件

注意:在上图中,“标准运行时库”应标记为“C++ 标准库”。

在头文件中包含定义如何导致违反单一定义规则

目前,您应该避免将函数或变量定义放在头文件中。 如果头文件包含在多个源文件中,这样做通常会导致违反单一定义规则 (ODR)。

让我们来说明一下这是如何发生的:

add.h:

// We really should have a header guard here, but will omit it for simplicity (we'll cover header guards in the next lesson)

//头文件中 add() 的定义——不要这样做!
int add(int x, int y)
{
    return x + y;
}

main.cpp:

#include "add.h" // Contents of add.h copied here
#include <iostream>

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';

    return 0;
}

add.cpp:

#include "add.h" // Contents of add.h copied here

编译main.cpp时,会将#include “add.h”替换为add.h的内容,然后进行编译。 因此,编译器将编译如下所示的内容:

main.cpp(预处理后):

int add(int x, int y)
{
    return x + y;
}
include <iostream>

int main()
{
    std::cout << "The sum of 3 and 4 is " << add(3, 4) << '\n';

    return 0;
}

这样编译没有问题。

编译器编译add.cpp时,会将#include “add.h”替换为add.h的内容,然后进行编译。 因此,编译器将编译如下内容:

add.cpp(预处理后):

int add(int x, int y)
{
    return x + y;
}

这也能编译得很好。

最后,链接器将运行。 链接器将看到函数 add() 现在有两个定义:一个在 main.cpp 中,一个在 add.cpp 中。 这违反了 ODR 第 2 部分,其中规定:“在给定程序中,变量或普通函数只能有一个定义。”

源文件应包含其配对的标头

在 C++ 中,代码文件的最佳做法是 #include 其配对的头文件(如果存在)。 在上面的示例中,add.cpp 包含 add.h。

这允许编译器在编译时而不是链接时捕获某些类型的错误。 例如:

something.h:

int something(int); // 前向声明的返回类型是int

something.cpp:

#include "something.h"

void something(int) // error: wrong return type
{
}

因为something.cpp #includesomething.h,编译器会注意到函数something()的返回类型不匹配,并给我们一个编译错误。 如果something.cpp没有#include Something.h,我们就必须等到链接器发现差异,这会浪费时间。

我们还将在未来的课程中看到许多示例,其中源文件所需的内容在配对标头中定义。 在这种情况下,包含标头是必要的。

不要#include .cpp 文件

尽管预处理器很乐意这样做,但您通常不应该#include .cpp 文件。 这些应该添加到您的项目中并进行编译。

造成这种情况的原因有很多:

  • 这样做可能会导致源文件之间的命名冲突。
  • 在大型项目中,很难避免单一定义规则 (ODR) 问题。
  • 对此类 .cpp 文件的任何更改都将导致该 .cpp 文件和包含该文件的任何其他 .cpp 文件重新编译,这可能需要很长时间。标头的更改频率往往低于源文件。
  • 这样做是非常规的。

故障排除

如果您收到编译器错误,指示未找到 add.h,请确保该文件确实名为 add.h。 根据您创建和命名的方式,该文件可能被命名为 add(无扩展名)、add.h.txt 或 add.hpp 之类的名称。 还要确保它与其余代码文件位于同一目录中。

如果您收到有关函数 add 未定义的链接器错误,请确保您已在项目中包含 add.cpp,以便可以将函数 add 的定义链接到程序中。

尖括号与双引号

您可能好奇为什么我们对 iostream 使用尖括号,对 add.h 使用双引号。 具有相同文件名的头文件可能存在于多个目录中。 我们使用尖括号与双引号有助于为预处理器提供应在何处查找头文件的线索。

当我们使用尖括号时,我们告诉预处理器这是一个不是我们自己编写的头文件。 预处理器将仅在包含目录指定的目录中搜索标头。 包含目录被配置为项目/IDE 设置/编译器设置的一部分,通常默认为包含编译器和/或操作系统附带的头文件的目录。 预处理器不会在项目的源代码目录中搜索头文件。

当我们使用双引号时,我们告诉预处理器这是我们编写的头文件。 预处理器首先会在当前目录中查找头文件。 如果在那里找不到匹配的标头,它将搜索包含目录。

为什么 iostream 没有 .h 扩展名?

另一个常见问题是“为什么 iostream(或任何其他标准库头文件)没有 .h 扩展名?”。 答案是iostream.h是与iostream不同的头文件!

当 C++ 最初创建时,标准库中的所有文件都以 .h 后缀结尾。 cout 和 cin 的原始版本在 iostream.h 中声明。 当 ANSI 委员会对该语言进行标准化时,他们决定将标准库中使用的所有名称移至 std 命名空间中,以帮助避免与用户声明的标识符发生命名冲突。 然而,这提出了一个问题:如果他们将所有名称移到 std 命名空间中,则所有旧程序(包括 iostream.h)都将不再工作!

为了解决此问题,引入了一组缺少 .h 扩展名的新头文件。 这些新的头文件声明了 std 命名空间内的所有名称。 这样,包含#include 的旧程序不需要重写,而较新的程序可以#include 。

此外,许多从 C 继承但在 C++ 中仍然有用的库都被赋予了 c 前缀(例如 stdlib.h 变成了 cstdlib)。

包含其他目录中的头文件

另一个常见问题涉及如何包含其他目录中的头文件。

一种(不好的)方法是在 #include 行中包含要包含的头文件的相对路径。 例如:

#include "headers/myHeader.h"
#include "../moreHeaders/myOtherHeader.h"

虽然这可以编译(假设文件存在于这些相对目录中),但这种方法的缺点是它要求您在代码中反映目录结构。 如果您更新了目录结构,您的代码将不再工作。

更好的方法是告诉你的编译器或 IDE,你在其他位置有一堆头文件,这样当它在当前目录中找不到它们时,它就会在那里查找。 这通常可以通过在 IDE 项目设置中设置包含路径或搜索目录来完成。

对于 Visual Studio 用户

在解决方案资源管理器中右键单击您的项目,然后选择“属性”,然后选择“VC++ 目录”选项卡。 从这里,您将看到名为“包含目录”的行。 添加您希望编译器在其中搜索其他标头的目录。

对于 Code::Blocks 用户

在 Code::Blocks 中,转到“项目”菜单并选择“构建选项”,然后选择“搜索目录”选项卡。 添加您希望编译器在其中搜索其他标头的目录。

对于 GCC/G++ 用户

使用 g++,您可以使用 -I 选项指定备用包含目录:g++ -o main -I/source/includes main.cpp

-I 后面没有空格。

对于 VS Code用户

在你的tasks.json配置文件中,在“Args”部分添加一个新行:
“-I/source/includes”,

这种方法的好处是,如果您更改目录结构,则只需更改单个编译器或 IDE 设置,而不是更改每个代码文件。

标头可能包含其他标头

头文件通常需要位于不同头文件中的声明或定义。 因此,头文件通常会 #include 其他头文件。

当您的代码文件 #includes 第一个头文件时,您还将获得第一个头文件包含的任何其他头文件(以及其中包含的任何头文件,依此类推)。 这些附加头文件有时称为传递包含,因为它们是隐式包含的,而不是显式包含的。

这些传递包含的内容可在您的代码文件中使用。 但是,您通常不应依赖可传递包含的标头内容(除非参考文档表明需要这些可传递包含)。 头文件的实现可能会随着时间的推移而改变,或者在不同的系统中有所不同。 如果发生这种情况,您的代码可能只能在某些系统上编译,或者现在可以编译但将来不能编译。 通过显式包含代码文件内容所需的所有头文件可以轻松避免这种情况。

不幸的是,没有简单的方法可以检测您的代码文件何时意外依赖于另一个头文件包含的头文件的内容。

问:我没有包含<someheader> ,但我的程序仍然有效! 为什么?

这是该网站上最常见的问题之一。 答案是:它可能有效,因为您包含了一些其他标头(例如<iostream> ),它本身包含<someheader>。

头文件的#include顺序

如果你的头文件编写正确并且 #include 他们需要的一切,那么包含的顺序并不重要。

现在考虑以下场景:假设标头 A 需要标头 B 的声明,但忘记包含它。 在我们的代码文件中,如果我们在标头 A 之前包含标头 B,我们的代码仍然可以编译! 这是因为编译器将先编译 B 中的所有声明,然后再编译 A 中依赖于这些声明的代码。

但是,如果我们首先包含标头 A,那么编译器会报错,因为 A 中的代码将在编译器看到 B 中的声明之前被编译。实际上,这是更可取的,因为错误已经被发现,然后我们可以修复它。

为了最大程度地提高编译器发现缺失包含的机会,请按照以下顺序排列你的 #include 指令:

  1. 配对的头文件(The paired header file)
  2. 项目中的其他头文件(Other headers from your project)
  3. 第三方库的头文件(3rd party library headers)
  4. 标准库的头文件(Standard library headers)

每个分组的标题应按字母顺序排序(除非第三方库的文档指示您这样做)。

这样,如果您的用户定义标头之一缺少第三方库或标准库标头的#include,则更有可能导致编译错误,因此您可以修复它。

头文件最佳实践

以下是创建和使用头文件的一些最佳实践建议:

  1. 始终使用头文件保护:在头文件中使用头文件保护(header guards),以防止同一个头文件被多次包含,避免重复定义和编译错误。
  2. 不要在头文件中定义变量和函数:通常情况下,头文件应该只包含声明和接口,而不是定义。变量和函数的定义应该放在源文件中,而头文件应该只包含相应的声明。
  3. 给头文件和源文件相同的名称:通常情况下,一个头文件应该与其关联的源文件具有相同的名称。例如,”grades.h” 应该与 “grades.cpp” 配对使用。
  4. 头文件应该具有特定的任务:每个头文件应该有明确定义的任务,并尽可能独立。例如,将与功能 A 相关的所有声明放在 A.h 中,将与功能 B 相关的所有声明放在 B.h 中。这样,如果以后只关心功能 A,你可以只包含 A.h 而不获取与 B 相关的内容。
  5. 谨慎选择需要显式包含的头文件:在代码文件中,仔细考虑哪些头文件是你需要显式包含的,以满足你的功能需求。不要包含不需要的头文件,避免不必要的依赖。
  6. 每个头文件应该可以单独编译:确保每个头文件都可以独立编译,它应该包含所有其依赖的头文件,以便能够单独进行编译。
  7. 只包含你需要的内容:不要因为可以而包含所有内容。只包含你在代码中实际需要的头文件,以减小编译时间和依赖关系。
  8. 不要 #include .cpp 文件:不要将源文件(.cpp)包含到其他源文件或头文件中。这是一个不良实践,会导致编译错误和混乱。
  9. 在头文件中提供文档注释:在头文件中添加关于函数、类、变量等的文档注释,以便其他开发人员能够更容易地了解如何使用它们。描述如何工作的详细信息应该留在源文件中。

原创文章,作者:jkhxw,如若转载,请注明出处:https://www.jkhxw.com/cpp-header-files/

(0)
上一篇 2023年10月9日
下一篇 2023年10月10日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注