12、头文件与源文件

厨子大约 6 分钟C语言基础程序程序厨

C/C++项目中,头文件(.h 或 .hpp)和源文件(.c 或 .cpp 或.ccopen in new window)扮演着至关重要的角色。

基本概念

头文件(.h 或 .hpp)

  • 作用:头文件通常用于声明函数、变量、类型定义(如结构体、类)、宏定义和常量等。有了这些声明,其他文件能够引用这些符号,而无需了解它们的具体实现细节。
  • 特点:头文件不应包含任何实际的代码实现(即函数体),除非这些代码是内联函数或模板定义或static函数(好多除非...)。

源文件(.c 或 .cpp 或 .cc)

  • 作用:源文件包含函数的实际实现、全局变量的定义以及程序的入口点(比如 main 函数)。
  • 特点:源文件是编译器生成目标文件的基本单位。

#include指令

  • 作用#include 用于将指定的头文件内容插入到包含该指令的源文件中。有了#include,源文件可访问头文件中声明的符号。
  • 预处理器处理:在编译之前,预处理器会查找并替换所有的 #include 指令,将头文件的内容插入到相应的位置,像复制粘贴一样。

示例代码

头文件(math_utils.h)

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// 函数声明
int add(int a, int b);
int subtract(int a, int b);

#endif // MATH_UTILS_H

源文件(math_utils.cpp)

#include "math_utils.h"

// 函数实现
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

主程序文件(main.cpp)

#include <iostream>
#include "math_utils.h"

int main() {
    int x = 5, y = 3;
    std::cout << "Sum: " << add(x, y) << std::endl;
    std::cout << "Difference: " << subtract(x, y) << std::endl;
    return 0;
}

模块化编程

通过头文件和源文件的分离,可以实现模块化编程,提高代码的可读性。

  • 头文件定义了模块的接口,提供了模块对外暴露的功能和数据结构。其他模块通过包含头文件来访问这些接口,而无需关心模块的具体实现细节。
  • 源文件实现了头文件中声明的接口,提供了模块的具体功能实现。源文件是模块的内部实现部分,通常不对外公开。

头文件重复包含问题:

  • 问题:如果头文件被多次包含,可能会导致重复声明错误。
  • 解决方案:使用头文件保护符(header guards),即 #ifndef#define#endif 指令,确保头文件的内容只被包含一次。或者使用#pragma once

编译链接过程

编译

  • 编译器将每个源文件转换为目标文件(.o 或 .obj),目标文件包含程序的机器代码,但还尚未链接到其他目标文件或库。
  • 头文件在编译过程中会被预处理器包含到源文件中,但不会被直接编译成目标文件。(源文件才是编译的基本单位)

链接

  • 链接器将多个目标文件和库链接成一个可执行文件或库。
  • 链接器解析所有外部符号引用,确保每个引用都有对应的定义。

头文件在编译链接过程中的作用

  • 提供声明,使链接器能够找到正确的符号定义。
  • 允许多个源文件共享相同的声明,而无需重复代码。

命名规范

下面是一些常见的命名约定:

  • 头文件:使用 .h.hpp 后缀,通常命名与源文件相对应,但去掉 .cpp 部分。例如,math_utils.cpp 的头文件是 math_utils.h
  • 源文件:使用 .c.cpp.cc后缀,命名应反映其包含的内容或功能,我个人更多的会用.cc后缀。
  • 命名风格:驼峰命名法或下划线分隔法,哪种风格都可,但整个项目用一致,我个人更多的用下划线分割法。

示例命名规范

  • 头文件:math_utils.h, data_structures.hpp
  • 源文件:math_utils.cpp, data_structures.cc

C常用标准库头文件总结

见下表:

头文件作用
stdio.h提供输入输出函数,如 printfscanffopenfclose 等,用于文件操作和格式化输入输出。
stdlib.h提供常用的函数,如内存分配(mallocfree)、随机数生成(rand)、字符串转换(atoiitoa)、程序控制(exitabort)等。
string.h提供字符串处理函数,如 strlenstrcpystrcmpstrcatstrstr 等,用于字符串的复制、比较、查找等操作。
math.h提供数学函数,如 sincostansqrtexplog 等,用于各种数学运算。
ctype.h提供字符处理函数,如 isalnumisalphaisdigittolowertoupper 等,用于字符类型的判断和大小写转换。
time.h提供日期和时间处理函数,如 timelocaltimegmtimedifftimestrftime 等,用于获取和格式化时间。
stdarg.h提供处理可变参数列表的函数,如 va_startva_argva_end,用于实现可变参数函数。
signal.h提供信号处理函数,如 signalraise,用于设置和处理信号。
assert.h提供断言宏 assert,用于在调试时检查条件表达式,帮助发现程序中的错误。
setjmp.h提供非局部跳转函数 setjmplongjmp,用于在程序的不同部分之间跳转。
errno.h定义错误代码变量 errno,用于表示程序运行过程中发生的错误。
stddef.h提供标准库的一些常用定义,如 size_tptrdiff_twchar_tNULLoffsetof 等。
locale.h提供本地化函数,如 setlocalelocaleconv,用于处理与地域相关的设置。
float.h提供与浮点类型相关的一些常量和宏定义,如 FLT_MAXDBL_MAXLDBL_MAX,用于描述浮点类型的特性和限制。
limits.h提供与整数类型相关的常量和宏,如 INT_MAXINT_MINCHAR_BIT,用于获取整数类型的限制信息。
stdbool.h提供布尔类型和布尔常量 truefalse,用于在C语言中更方便地使用布尔类型的变量和常量。
stdint.h提供固定宽度的整数类型,如 int8_tint16_tint32_tuint64_t 等,用于跨平台编程中确保整数类型的大小和行为的一致性。
tgmath.h提供一种泛型的数学函数宏定义,根据参数的类型自动选择合适的函数版本进行调用。
wchar.h提供宽字符处理函数,如 wcslenwcscpywcstombs 等,用于宽字符字符串的处理。
wctype.h提供用于分类宽字符的函数,如 iswalphaiswdigittowlowertowupper 等,用于宽字符的类型判断和大小写转换。

练习

  1. 创建一个名为math_utils.h的头文件,声明一个计算两个整数和的函数int add(int a, int b);,一个计算两个整数差的函数int subtract(int a, int b);,以及一个宏定义PI(值为3.14159)。
  2. 创建两个源文件math_utils.cmain.c。在math_utils.c中实现addsubtract函数,在main.c中编写主函数,调用这两个函数并打印结果,同时打印PI的值。

进阶

  1. 使用#pragma once避免头文件重复包含更好,还是使用#ifndef更好?
  2. #include 源文件会发生什么?
  3. 在头文件中定义函数会发生什么?