12、头文件与源文件
大约 6 分钟C语言基础程序程序厨
在C/C++项目中,头文件(.h 或 .hpp)和源文件(.c 或 .cpp 或.cc)扮演着至关重要的角色。
基本概念
头文件(.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 | 提供输入输出函数,如 printf、scanf、fopen、fclose 等,用于文件操作和格式化输入输出。 |
| stdlib.h | 提供常用的函数,如内存分配(malloc、free)、随机数生成(rand)、字符串转换(atoi、itoa)、程序控制(exit、abort)等。 |
| string.h | 提供字符串处理函数,如 strlen、strcpy、strcmp、strcat、strstr 等,用于字符串的复制、比较、查找等操作。 |
| math.h | 提供数学函数,如 sin、cos、tan、sqrt、exp、log 等,用于各种数学运算。 |
| ctype.h | 提供字符处理函数,如 isalnum、isalpha、isdigit、tolower、toupper 等,用于字符类型的判断和大小写转换。 |
| time.h | 提供日期和时间处理函数,如 time、localtime、gmtime、difftime、strftime 等,用于获取和格式化时间。 |
| stdarg.h | 提供处理可变参数列表的函数,如 va_start、va_arg、va_end,用于实现可变参数函数。 |
| signal.h | 提供信号处理函数,如 signal、raise,用于设置和处理信号。 |
| assert.h | 提供断言宏 assert,用于在调试时检查条件表达式,帮助发现程序中的错误。 |
| setjmp.h | 提供非局部跳转函数 setjmp 和 longjmp,用于在程序的不同部分之间跳转。 |
| errno.h | 定义错误代码变量 errno,用于表示程序运行过程中发生的错误。 |
| stddef.h | 提供标准库的一些常用定义,如 size_t、ptrdiff_t、wchar_t、NULL、offsetof 等。 |
| locale.h | 提供本地化函数,如 setlocale、localeconv,用于处理与地域相关的设置。 |
| float.h | 提供与浮点类型相关的一些常量和宏定义,如 FLT_MAX、DBL_MAX、LDBL_MAX,用于描述浮点类型的特性和限制。 |
| limits.h | 提供与整数类型相关的常量和宏,如 INT_MAX、INT_MIN、CHAR_BIT,用于获取整数类型的限制信息。 |
| stdbool.h | 提供布尔类型和布尔常量 true、false,用于在C语言中更方便地使用布尔类型的变量和常量。 |
| stdint.h | 提供固定宽度的整数类型,如 int8_t、int16_t、int32_t、uint64_t 等,用于跨平台编程中确保整数类型的大小和行为的一致性。 |
| tgmath.h | 提供一种泛型的数学函数宏定义,根据参数的类型自动选择合适的函数版本进行调用。 |
| wchar.h | 提供宽字符处理函数,如 wcslen、wcscpy、wcstombs 等,用于宽字符字符串的处理。 |
| wctype.h | 提供用于分类宽字符的函数,如 iswalpha、iswdigit、towlower、towupper 等,用于宽字符的类型判断和大小写转换。 |
练习
- 创建一个名为
math_utils.h的头文件,声明一个计算两个整数和的函数int add(int a, int b);,一个计算两个整数差的函数int subtract(int a, int b);,以及一个宏定义PI(值为3.14159)。 - 创建两个源文件
math_utils.c和main.c。在math_utils.c中实现add和subtract函数,在main.c中编写主函数,调用这两个函数并打印结果,同时打印PI的值。
进阶
- 使用
#pragma once避免头文件重复包含更好,还是使用#ifndef更好? #include 源文件会发生什么?- 在头文件中定义函数会发生什么?





