Share via


头文件 (C++)

必须在使用变量、函数、类等程序元素的名称之前对其进行声明。 例如,不能在没有声明“x”之前编写 x = 42

int x; // declaration
x = 42; // use x

声明告知编译器,元素是 intdouble、函数、class 还是其他内容。 此外,必须在使用每个名称时所在的每个 .cpp 文件中(直接或间接)声明每个名称。 编译程序时,每个 .cpp 文件都会独立编译为一个编译单元。 编译器不知道在其他编译单元中声明了哪些名称。 这意味着,如果你定义类、函数或全局变量,则必须在使用它的每个附加 .cpp 文件中提供对它的声明。 在所有文件中,对它的每个声明必须完全相同。 当链接器尝试将所有编译单元合并成单个程序时,出现轻微的不一致会导致错误或意外行为。

为了最大程度地减少出错的可能性,C++ 采用了使用头文件来包含声明的约定。 在一个头文件中进行声明,然后在每个 .cpp 文件或其他需要该声明的头文件中使用 #include 指令。 #include 指令在编译之前将头文件的副本直接插入 .cpp 文件中。

注意

在 Visual Studio 2019 中,C++20 模块功能作为头文件的改进和最终替代引入。 有关详细信息,请参阅 C++ 中的模块概述

示例

以下示例演示了一种声明类的常见方法,然后在另一源文件中使用它。 我们将从头文件 my_class.h 开始。 它包含类定义,但请注意,定义不完整;未定义成员函数 do_something

// my_class.h
namespace N
{
    class my_class
    {
    public:
        void do_something();
    };

}

接下来,创建一个实现文件(通常使用 .cpp 或类似的扩展名)。 我们将调用文件 my_class.cpp,并为成员声明提供定义。 我们为“my_class.h”文件添加一个 #include 指令,以便立刻将 my_class 声明插入到 .cpp 文件中。我们包括 <iostream>,用于拉入 std::cout 的声明。 请注意,引号用于源文件所在目录中的头文件,尖括号用于标准库标头。 此外,许多标准库标头没有 .h 或任何其他文件扩展名。

在实现文件中,可以选择使用 using 语句来避免使用“N::”或“std::”限定每个提及的“my_class”或“cout”。 不要在头文件中放置 using 语句!

// my_class.cpp
#include "my_class.h" // header in local directory
#include <iostream> // header in standard library

using namespace N;
using namespace std;

void my_class::do_something()
{
    cout << "Doing something!" << endl;
}

现在,我们可以在另一个 .cpp 文件中使用 my_class。 我们 #include 头文件,以便编译器拉入声明。 所有编译器都需要知道的是,my_class 是一个类,它有一个名为 do_something() 的公共成员函数。

// my_program.cpp
#include "my_class.h"

using namespace N;

int main()
{
    my_class mc;
    mc.do_something();
    return 0;
}

编译器完成将每个 .cpp 文件编译为 .obj 文件的操作后,会将 .obj 文件传递给链接器。 链接器合并对象文件时,会发现 my_class 的一个定义;它位于为 my_class.cpp 生成的 .obj 文件中,生成成功。

Include 防范

通常,头文件有一个 include 防范#pragma once 指令,用于确保它们不会多次插入到单个 .cpp 文件中。

// my_class.h
#ifndef MY_CLASS_H // include guard
#define MY_CLASS_H

namespace N
{
    class my_class
    {
    public:
        void do_something();
    };
}

#endif /* MY_CLASS_H */

要放入头文件的内容

由于一个头文件可能会被多个文件执行 include 操作,因此它不能包含可能生成多个同名定义的定义。 不允许以下操作,否则会被视为非常糟糕的做法:

  • 命名空间或全局范围内的内置类型定义
  • 非内联函数定义
  • 非常量变量定义
  • 聚合定义
  • 未命名的命名空间
  • using 指令

使用 using 指令不一定会导致错误,但可能会导致问题,因为它将命名空间引入每个直接或间接包含该标头的 .cpp 文件中的范围。

示例头文件

以下示例显示了头文件中允许的各种声明和定义:

// sample.h
#pragma once
#include <vector> // #include directive
#include <string>

namespace N  // namespace declaration
{
    inline namespace P
    {
        //...
    }

    enum class colors : short { red, blue, purple, azure };

    const double PI = 3.14;  // const and constexpr definitions
    constexpr int MeaningOfLife{ 42 };
    constexpr int get_meaning()
    {
        static_assert(MeaningOfLife == 42, "unexpected!"); // static_assert
        return MeaningOfLife;
    }
    using vstr = std::vector<int>;  // type alias
    extern double d; // extern variable

#define LOG   // macro definition

#ifdef LOG   // conditional compilation directive
    void print_to_log();
#endif

    class my_class   // regular class definition,
    {                // but no non-inline function definitions

        friend class other_class;
    public:
        void do_something();   // definition in my_class.cpp
        inline void put_value(int i) { vals.push_back(i); } // inline OK

    private:
        vstr vals;
        int i;
    };

    struct RGB
    {
        short r{ 0 };  // member initialization
        short g{ 0 };
        short b{ 0 };
    };

    template <typename T>  // template definition
    class value_store
    {
    public:
        value_store<T>() = default;
        void write_value(T val)
        {
            //... function definition OK in template
        }
    private:
        std::vector<T> vals;
    };

    template <typename T>  // template declaration
    class value_widget;
}