C++ 头文件

当项目增长时,通常将代码分成不同的源文件。当遇到大型项目时,接口和实现通常是分开的。接口放在头文件中,此类文件通常与源文件有相同的名字,且以.h作为扩展名。该头文件包含项目中其他编译单元需要访问的源文件实体的前置声明。编译单元由源文件(.cpp)和任何包含在原文件中的头文件(.h或.hpp)组成。

为什么要使用头文件

C ++要求在使用之前声明所有内容。仅在同一项目中编译源文件是不够的。例如,如果将函数放置在MyFunc.cpp中,并且同一项目中的另一个名为MyApp.cpp的文件试图调用该函数,则编译器将报告找不到该函数。

// MyFunc.cpp
void myFunc() {}
// MyApp.cpp
int main()
{
  myFunc(); // error: myFunc identifier not found
}

为此,该函数的原型必须包含在MyApp.cpp中。

// MyApp.cpp
void myFunc(); // prototype
int main()
{
  myFunc(); // ok
}

使用头文件

如果将原型放置在名为MyFunc.h的头文件中,并且通过使用#include指令将此头文件包含在MyApp.cpp中,则可以更方便地进行此操作。 这样,如果对MyFunc进行了任何更改,则无需更新MyApp.cpp中的原型。 此外,任何想要在MyFunc中使用共享代码的源文件都可以只包含一个标头。

// MyFunc.h
void myFunc(); // prototype
// MyApp.cpp
#include "MyFunc.h"

头文件中要包含的内容

就编译器而言,头文件和源文件之间没有区别。区别只是概念上的。关键思想是头文件应包含实现文件的接口,即其他源文件将要用到的代码。这可能包括共享常量、宏和类型别名。

// MyApp.h - Interface
#define DEBUG 0
const double E = 2.72;
typedef unsigned long ulong;

如前所述,头文件可以包含源文件中定义的共享函数的原型。

void myFunc(); // prototype

此外,共享类通常在头文件中指定,而共享类的方法在源文件中实现。

// MyApp.h
class MyClass
{
 public:
  void myMethod();
};
// MyApp.cpp
void MyClass::myMethod() {}

与函数一样,必须先声明全局变量,然后才能在包含其定义的变量之外的编译单元中引用它们。 这是通过将共享变量放在头文件中并用关键字extern进行标记来完成的。 此关键字指示变量在另一个编译单元中初始化。 默认情况下,函数是extern,因此函数原型不需要包含此说明符。 请记住,全局变量和函数可以在程序中多次在外部声明,但只能定义一次。

// MyApp.h
extern int myGlobal;
// MyApp.cpp
int myGlobal = 0;

应当指出,不建议使用共享的全局变量。这是因为程序越大,跟踪哪个函数访问和修改这些变量就越困难。首选方法是改为仅根据需要将变量传递给函数,以最小化这些变量的范围。

头文件不应包含任何可执行语句,但有两个例外。首先,如果将共享类方法或全局函数声明为内联,则必须在头文件中定义该函数。否则,从另一个源文件调用内联函数将产生未解决的外部错误。请注意,内联修饰符取消了通常应用于代码实体的单一定义规则。

// MyApp.h
inline void inlineFunc() {}
class MyClass
{
 public:
  void inlineMethod() {}
};

第二个例外是共享模板。当遇到模板实例化时,编译器需要访问该模板的实现,以便创建其实例并填充类型实参。因此,模板的声明和实现通常都放在头文件中。

// MyApp.h
template<class T>
class MyTemp { /* ... */ };
// MyApp.cpp
MyTemp<int> o;

在许多编译单元中实例化具有相同类型的模板会导致编译器和链接器完成大量的冗余工作。 为防止这种情况,C ++ 11引入了extern模板声明。 标记为extern的模板实例化会向编译器发出信号,请不要在此编译单元中实例化该模板。

// MyApp.cpp
MyTemp<int> b; // instantiation is done here
// MyFunc.cpp
extern MyTemp<int> a; // suppress redundant instantiation

如果头文件需要其他引用其它头文件,则通常也将它们包含其中,使得该头文件可以独立存在。这样可以确保所需的所有内容都以正确的顺序包含在内,从而解决了每个需要头文件的源文件的潜在依赖性问题。

// MyApp.h
#include <cstddef.h> // include size_t
void mySize(std::size_t);

请注意,由于头文件主要包含声明,因此所包含的任何其他标头都不会影响程序的大小,尽管它们可能会减慢编译速度。

内联变量

从C ++ 17开始,除了函数和方法外,还可以将变量指定为内联变量。 这允许在头文件中定义常量和静态变量,因为inline修饰符会删除通常会阻止这种情况的单个定义规则。 定义内联变量后,所有引用该头文件的编译单元都将使用相同的定义。

struct MyStruct
{
  static const int a;
  inline static const int b = 10; // alternative
};
inline int const MyStruct::a = 10;

constexpr关键字表示内联,因此声明为constexpr的变量也可以在头文件中初始化。 但是,必须将此类变量初始化为编译时常量。

struct MyStruct {
  static constexpr int a = 10;
};

内联变量不仅限于常量表达式,如以下示例所示,其中内联变量被初始化为1-6之间的随机值。 即使使用runtime才设置此值,对于使用此头文件的所有编译单元,也应确保该值相同。

#include <cstdlib> // rand, srand
#include <ctime> // time
struct MyStruct {
  static const int die;
};
inline const int MyStruct::die =
  (srand((unsigned)time(0)), rand()%6+1); // 1-6

请注意此处使用逗号运算符,该运算符首先计算左表达式,然后计算并返回右表达式。 左边的表达式使用当前时间作为种子的srand函数当作随机数生成器。 正确的表达式使用rand函数检索随机整数,并将该整数格式化为1-6范围。

包含保护

使用头文件时要记住的重要一点是,共享代码实体只能定义一次。 因此,多次包含同一头文件可能会导致编译错误。 防止这种情况的标准方法是使用所谓的“包含看守”。 通过将头文件的开头括在#ifndef节中来创建包含保护,该节检查是否有特定于该头文件的宏。 仅当未定义宏时,才包含文件。 然后定义该宏,这可以有效地防止再次包含该文件。

// MyApp.h
#ifndef MYAPP_H
#define MYAPP_H
// ...
#endif // MYAPP_H

在包含头文件之前检查头文件是否存在也是一个好主意。为此,C ++ 17添加了__has_include预处理程序表达式,如果找到头文件,该表达式的值为true。

#if __has_include("myapp.h")
#include("myapp.h")

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据