首页 > 编程笔记 > C语言笔记 阅读:109

C语言头文件的作用(新手必看)

但凡写过 C 程序的人都知道头文件的存在。以“Hello, world!”程序为例,通常我们如下书写这个程序:
#include <stdio.h>
 
int main(void)
{
    printf("Hello, world!\n");
    return 0;
}
其中包含了 <stdio.h> 这个头文件,这是因为上述程序用到的 printf() 函数之原型声明出现在 <stdio.h> 头文件中。

在 Linux 系统中,标准 C 库的头文件通常位于 /usr/include 目录下。使用任意编辑器打开 /usr/include/stdio.h 文件,就可以轻松发现 printf() 函数的如下原型声明:
/* Write formatted output to stdout. */
extern int printf (const char *__restrict __format, ...);

类似地,假如要增强上面的代码,打印圆周率 π 的值,则需要增加一个新的头文件 math.h:
#include <stdio.h>
#include <math.h>
 
int main(void)
{
    printf("PI in the current system: %f.\n", M_PI);
    return 0;
}
上面代码中的 M_PI 是一个宏,它定义了一个常量,这个常量定义了 π 的一个近似值。使用任意编辑器打开 /usr/include/math.h 文件,很容易就能找到 M_PI 以及其他常用的数学常量宏的宏定义:
/* Some useful constants.  */
#if defined __USE_MISC || defined __USE_XOPEN
# define M_E        2.7182818284590452354   /* e */
# define M_LOG2E    1.4426950408889634074   /* log_2 e */
# define M_LOG10E   0.43429448190325182765  /* log_10 e */
# define M_LN2      0.69314718055994530942  /* log_e 2 */
# define M_LN10     2.30258509299404568402  /* log_e 10 */
# define M_PI       3.14159265358979323846  /* pi */
# define M_PI_2     1.57079632679489661923  /* pi/2 */
# define M_PI_4     0.78539816339744830962  /* pi/4 */
# define M_1_PI     0.31830988618379067154  /* 1/pi */
# define M_2_PI     0.63661977236758134308  /* 2/pi */
# define M_2_SQRTPI 1.12837916709551257390  /* 2/sqrt(pi) */
# define M_SQRT2    1.41421356237309504880  /* sqrt(2) */
# define M_SQRT1_2  0.70710678118654752440  /* 1/sqrt(2) */
#endif

现在,读者可以尝试回答下面这个问题,为什么要将这些内容放入单独的头文件而不是直接放到普通的源文件中呢?毕竟也可以按如下方式编写打印圆周率 π 的程序,而不需要包含任何头文件,此时编译器不会有任何错误或警告输出:
int printf(const char *__format, ...);
 
#define M_PI 3.14159265358979323846
 
int main()
{
    printf("PI in the current system: %f.\n", M_PI);
    return 0;
}
上述不使用头文件的版本增加了对 printf() 和 M_PI 的声明或定义,前者被声明为一个函数并给出了函数原型,后者被定义为一个数学常量宏。

上面这个问题不难回答。因为像 printf() 和 M_PI 这样的声明或定义会被很多 C 程序用到,所以将这些声明或定义用某种方式组织成公共的内容,并通过某种方式引用这些公共的内容,将为代码的书写带来极大的便利。

这种方式就是本节讨论的头文件,引用头文件的方法是使用像 #include <stdio.h> 这样的特殊代码行,其中以 # 打头的行定义了一条 C 程序的预处理指令。

可以想象,有了头文件的概念,对程序中常用的、不易记忆的常量、字符串等,使用宏定义的方式定义一个常量,并给它一个助记符放到头文件中(如同我们看到的 M_PI 一样),便是自然而言的事情。按此逻辑继续延伸,预处理指令中便出现了头文件和数学常量宏以外的内容,比如带有参数的宏、条件编译,以及用于控制编译器行为的指令等。

众所周知,由于头文件,以及宏、条件编译等的引入,C 程序的编译分为 3 个步骤。

第一步:预处理。在这一步,按照指定的头文件搜索路径找到所有 #include 指令中指定的头文件,并就地展开;然后处理其中的宏定义指令 #define,以及由 #if、#else 和 #endif 等组成的条件编译指令;最后生成只有枚举量声明、结构体声明、类型定义、原型声明以及立即数或常量的 C 代码。通常,构建系统会把经过预处理的 C 程序保存在临时文件中。

第二步:编译。这一步执行真正的 C 程序编译,编译器将编译经过预处理的 C 源文件并生成对应的目标文件(object file)。

第三步:链接。将所有的目标文件整合成一个大的目标文件,然后链接默认的 C 标准函数库以及其他指定的函数库,最终生成可执行程序或者函数库。

头文件、宏以及条件编译为开发者提供了一种非常有效的机制,使得开发者可以将代码的公共部分有机地组织起来。这是一种很好的机制,读者在其他编程语言中也能看到类似的机制。

相关文章