C语言基础专题 - 头文件引用
1.🧐什么是头文件?
头文件是扩展名为 .h 的文件,这是一个文本文件,内容包含了:
- 函数声明
- 宏定义
这些内容按照一定的方式写在某个头文件中才能通过使用#include指令
被多个源文件中引用共享。
2.🧐如何引用头文件?
这个事情我们所有人写的第一个程序Hello World.c
中都做过:
#include <stdio.h> // 引用头文件 int main(){ printf("Hello World") }
这里被引用的头文件stdio.h
它是编译器自带的,属于系统头文件。
我们不但可以引用系统头文件,也可以引用自己写的头文件,即用户头文件,但这再语法上由略微差别:
#include <file> // 用于引用名为file的系统头文件,默认在系统目录的标准列表中搜索该文件 #include "file" // 用于引用名为file的用户头文件 ,默认在包含当前文件的目录中搜索该文件
C语言的编译器有很多,对于以上两种使用头文件的方法一般都提供了相关选项以加入搜索头文件的路径,也常有在名为INCLUDE
的环境变量中加入搜索路径,以告诉编译器头文件的位置。
比如,在GCC中(以下引用内容摘录自GCC官方文档3.16目录搜索的选项章节,有外语基础的读者可以自行阅读。)
这些选项指定目录以搜索头文件,库和编译器的一部分:
-I dir -iquote dir -isystem dir -idirafter dir
它们在预处理(可以参考C语言预处理部分内容)的时候将目录dir添加到要在预处理过程中搜索头文件的目录列表中。如果dir以
'='
或>$SYSROOT
,然后是'='
或被$SYSROOTsysroot
前缀替换;看到--sysroot
和->isysroot
。指定的目录
-I
引用仅适用于指令的引号形式。指定的目录#include "file" -I
,-isystem
,或者-idirafter
适用于#include "file"
和#include <file>
指令的查找 。您可以在命令行上指定任何数目或这些选项的组合,以在多个目录中搜索头文件。查找顺序如下:
- (1)对于include指令的引号形式,将首先搜索当前文件的目录。
- (2)对于include指令的引号形式,目录由 -我引用 选项在命令行中按从左到右的顺序搜索。
- (3)指定目录
-I
选项以从左到右的顺序扫描。- (4)指定目录
-isystem
选项以从左到右的顺序扫描。- (5)扫描标准系统目录。
- (6)指定目录
-idirafter
选项以从左到右的顺序扫描。您可以使用
-I
覆盖系统头文件,替代您自己的版本,因为这些目录是在标准系统头文件目录之前搜索的。但是,您不应使用此选项来添加包含供应商提供的系统头文件的目录。采用-isystem
为了那个原因。
-isystem
和-idirafter
选项还将该目录标记为系统目录,以便对它进行与标准系统目录相同的特殊处理。
如果标准系统包含目录,或使用以下命令指定的目录
-isystem
,也指定了-I
,-I
选项将被忽略。该目录仍在搜索,但作为系统目录位于系统包含链中的正常位置。这是为了确保#include_next
不会错误地更改GCC修正错误的系统头的过程以及该指令的顺序。如果您确实需要更改系统目录的搜索顺序,请使用-nostdinc
和/或-isystem
选项。
3.🧐头文件中有一般写了什么?
如果想自己写一个头文件,那还是有必要了解一下头文件的内容的。我们不妨先打开我们从写HelloWorld.c
就开始使用的stdio.h一探究竟。
/* Checking macros for stdio functions. Copyright (C) 2004, 2005, 2009 Free Software Foundation, Inc. This file is part of GCC. GCC is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. In addition to the permissions in the GNU General Public License, the Free Software Foundation gives you unlimited permission to link the compiled version of this file into combinations with other programs, and to distribute those combinations without any restriction coming from the use of this file. (The General Public License restrictions do apply in other respects; for example, they cover modification of the file, and distribution when not linked into a combine executable.) GCC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. Under Section 7 of GPL version 3, you are granted additional permissions described in the GCC Runtime Library Exception, version 3.1, as published by the Free Software Foundation. You should have received a copy of the GNU General Public License and a copy of the GCC Runtime Library Exception along with this program; see the files COPYING3 and COPYING.RUNTIME respectively. If not, see <http://www.gnu.org/licenses/>. */ #ifndef _SSP_STDIO_H #define _SSP_STDIO_H 1 #include <ssp.h> #include_next <stdio.h> #if __SSP_FORTIFY_LEVEL > 0 #include <stdarg.h> #undef sprintf #undef vsprintf #undef snprintf #undef vsnprintf #undef gets #undef fgets extern int __sprintf_chk (char *__restrict__ __s, int __flag, size_t __slen, __const char *__restrict__ __format, ...); extern int __vsprintf_chk (char *__restrict__ __s, int __flag, size_t __slen, __const char *__restrict__ __format, va_list __ap); #define sprintf(str, ...) \ __builtin___sprintf_chk (str, 0, __ssp_bos (str), \ __VA_ARGS__) #define vsprintf(str, fmt, ap) \ __builtin___vsprintf_chk (str, 0, __ssp_bos (str), fmt, ap) extern int __snprintf_chk (char *__restrict__ __s, size_t __n, int __flag, size_t __slen, __const char *__restrict__ __format, ...); extern int __vsnprintf_chk (char *__restrict__ __s, size_t __n, int __flag, size_t __slen, __const char *__restrict__ __format, va_list __ap); #define snprintf(str, len, ...) \ __builtin___snprintf_chk (str, len, 0, __ssp_bos (str), __VA_ARGS__) #define vsnprintf(str, len, fmt, ap) \ __builtin___vsnprintf_chk (str, len, 0, __ssp_bos (str), fmt, ap) extern char *__gets_chk (char *__str, size_t); extern char *__SSP_REDIRECT (__gets_alias, (char *__str), gets); extern inline __attribute__((__always_inline__)) char * gets (char *__str) { if (__ssp_bos (__str) != (size_t) -1) return __gets_chk (__str, __ssp_bos (__str)); return __gets_alias (__str); } extern char *__SSP_REDIRECT (__fgets_alias, (char *__restrict__ __s, int __n, FILE *__restrict__ __stream), fgets); extern inline __attribute__((__always_inline__)) char * fgets (char *__restrict__ __s, int __n, FILE *__restrict__ __stream) { if (__ssp_bos (__s) != (size_t) -1 && (size_t) __n > __ssp_bos (__s)) __chk_fail (); return __fgets_alias (__s, __n, __stream); } #endif /* __SSP_FORTIFY_LEVEL > 0 */ #endif /* _SSP_STDIO_H */
可以看到:
- 写在最前边的一般是文件内容概述(该文件为
检查标准函数的宏(Checking macros for stdio functions.)
)、版权说明等。 - 对于程序预处理与之后编译实质内容时后面的c代码,这些代码一般是从以前中写在源文件最前面(头部)的位置,我们将这些代码摘出来,因此叫做头文件。
- 该文件中还引入了大量其它的头文件。
- 除此之外我们还看到了大量的
extern
有关的语句,extern
用于调用另一个c文件里的变量或者函数。
4.👨🏫条件引用
更多相关指令可以参考:C语言-预处理
这里我们先了解下面这几条预处理指令:
指令 | 说明 |
#if |
如果给定条件为真,则执行预处理的内容 |
#else |
表示#if 后的条件为假时执行预处理的内容 |
#elif |
如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif |
结束一个 #if ……#else 条件编译块 |
比如: |
#if CONDITION_1 // 若第1个条件成立 # include "headfile_1.h" // 引入头文件"headfile_1.h" #elif CONDITION_2 // 若第2个条件成立 # include "headfile_2.h" // 引入头文件"headfile_2.h" ... #endif // 条件指令的结束
5.👨🏭用宏定义确保单次引用
在C语言中提供了以下两个指令:
指令 | 说明 |
#ifdef |
如果宏已经定义,则返回真 |
#ifndef |
如果宏没有定义,则返回真 |
为了防止由于多次头文件多次引用导致某些情况下产生错误,我们需要使用上面两个指令。请看代码: |
#ifndef A // 当没有宏定义A时 #define A 包含所有头文件的文件B // 宏定义A为包含所有头文件的文件B #endif // 用于结束一个 #if……#else 条件编译块
为了防止意外,而把头文件的内容都放在#ifndef
和#endif
指令块中。这样作无论头文件会不会被多个文件引用,也就是当再次引用到头文件HeadFileB时,条件为假。