为什么需要头文件?

前言

C++程序中使用头文件并不是强制的,但其是一种非常重要且实用的编程组织方式,目的是提高可维护性、可重用性和编译效率。

头文件和源文件的定义

  • 头文件(.h.hpp):主要用于声明类、函数、常量、宏等。头文件不会生成目标代码(.oobj文件)。
  • 源文件(.cpp):用于实现函数和类中的具体功能,其会被编译器编译成目标文件。

为什么需要头文件

.cpp文件在编译时不需要利用其他文件,其独立编译成目标文件。之后再通过一次链接,实现对其他文件中内容的调用。例如对文件a.cpp和文件b.cppb.cpp中使用到了a.cppvoid a()函数,在编译时b.cpp不需要事先知道a.cppvoid a()函数的定义,两个文件分别进行编译,编译成目标文件之后再链接,得到可执行文件。这是因为C++在编译阶段会为.cpp文件生成符号表,利用其在别的文件中查找所需的内容。因此,在b.cpp中声明void a()函数,编译时将其存放到b.cpp 的符号表(Symbol Table)中,就可以实现a.cppvoid a()函数的调用。

通过上述编译模式,我们只需要在一个文件中定义所需的函数和类,在其他文件中声明对应的函数和类即可。但是这样需要记住所有函数和类的名字,且代码冗余。

头文件就是用来解决上述问题的,其不会被编译。我们将所有的函数声明全部放到头文件中,当一个 .cpp文件需要它们时,就可以通过宏命令#include将它们的内容合并到.cpp文件中,当.cpp文件被编译时,这些被包含进去的头文件就可以提供函数的声明。

#include是一个C语言的宏命令,其在预编译阶段将其后所写的文件名完整替换为这个文件中的内容。

头文件中应该写什么?

头文件可能被多个.cpp文件使用,而定义只能有一次,所以头文件中只能包含变量、函数或类的声明,而不能存放它们的定义。该规则有以下三个例外:

  1. 头文件中可以写const对象的定义
    const默认具有内部链接性,因此不会暴露给链接器。每个.cpp文件都会创建一份const对象的副本,互不影响,因此不会产生冲突。
    同理,static对象的定义也可以放进头文件。

    链接性:指的是一个名字(变量、函数等)在多个源文件中的可见性。

    无链接性:名字只在当前作用域中有效(如局部变量)

    内部链接性:名字只在当前翻译单元(.cpp文件)中可见

    外部链接性:名字可以被其他.cpp文件引用和共享

  2. 头文件中可以写内联(inline)函数的定义
    C++规定内联函数可以在程序中多次定义,编译器会在每个使用点内联替换函数体,或者产生一个局部副本,因此链接器不会因为重复定义报错。

  3. 头文件中可以写类的定义
    类定义需要被多个源文件看到,因此可以将其写在头文件中,但函数体的实现通过写在.cpp文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// MyClass.h(头文件) —— 声明
class MyClass {
public:
MyClass();
void sayHello();
private:
int number;
};

// MyClass.cpp(源文件) —— 实现
#include "MyClass.h"
#include <iostream>

MyClass::MyClass() : number(0) {}

void MyClass::sayHello() {
std::cout << "Hello from MyClass!" << std::endl;
}

此外,类内定义的成员函数也可以写在头文件中,因此类内定的成员函数默认是内联的。而模版类必须放到头文件中定义函数体,因为模板是在编译阶段实例化的,编译器在看到模板使用时,必须能看到完整的定义(包括函数体)才能生成代码。

头文件中的保护措施

上述三个例外情况导致头文件可能存在定义,虽然这些定义可以在多个源文件中出现,但在一个源文件中只能出现一次。如果a.h中定义了类A,b.h中定义了类B,且类B的定义依赖了类A,则b.h中包含了a.h。一个源文件,如果同时用到了类A和类B,将a.hb.h包含进来,则类A会被重复定义,导致程序报错,无法通过编译。

解决办法就是通过#pragma once#ifndef...#define...#endif条件编译使得编译器可以根据头文件名是否被定义,再决定要不要继续编译该头文件后续的内容。

#pragma once#ifndef...#define...#endif的区别

#pragma once

#pragma once是非标准但广泛支持的预处理指令,用于指示编译器文件头在单个文件的编译过程中只能出现一次。

优点:不需要生成唯一的宏名,简单。

缺点:不是C++标准的一部分,在某些旧的或特定的编译器中可能不被支持。

宏定义#ifndef...#define...#endif

#ifndef...#define...#endif通过检查特定的宏是否已被定义来避免重复定义问题。

优点:是C++标准的一部分,在所有C+编译器中都是可用的。

缺点:需要为每个头文件生成一个唯一的宏名。

参考

理解C++中的头文件和源文件的作用


为什么需要头文件?
https://delta0406.github.io/2025/05/23/技术/语言/CPP/为什么需要头文件?/
作者
执妄
发布于
2025年5月23日
许可协议