本文学习编译器在预处理过程中对头文件的处理。Hightec编译器版本是tricore v4.9.1.0。博主在<基于模型的设计>板块就经常提到头文件,本文会从编译器的角度对头文件进行学习。
1 Include语法
头文件一般包含C语言的声明和宏定义。在C语言源文件中,通常在最上面用#include指令调用,俗称为包含某个头文件。
头文件一般分为系统头文件和用户头文件。
- 系统头文件通常是用来调用系统库,在#include后面要用尖括号。
#include <file>
- 用户头文件中通常是函数、全局变量的外部声明, 宏定义,结构体定义,类型定义等。用户头文件起到了一个接口的作用,将不同的独立C文件通过头文件联系起来。用户头文件在#include后面要用引号。
#include "file"
2 编译器对Include的处理
调用编译器对某个C源文件进行预处理时,如果编译器看到了#include指令,就会先扫描一下#include后的头文件,然后把头文件展开到#include指令的位置。
为了直观地体会到这个过程,可以自己创建一些文件,然后调用编译器进行预处理。
1)首先,创建一个 header.h 头文件如下;
在这个头文件中对test函数进行了外部声明。
2)然后再建立一个program.c文件,如下;
在C文件中显示定义了一个变量x,然后包含了header.h这个头文件,然后定义了test2函数。
3)通过如下命令行调用编译器,对当前目录下地 program.c 代码进行预处理。
tricore-gcc -E program.c -o program.i
4)在生成的.i文件中,可以看到 header.h 头文件的内容被插入到了 program.c 源文件中;
另外,头文件中也可以嵌套包含头文件,这样的话就会一层一层地展开。
3 头文件的搜索路径
上一章是把头文件和源文件放在了一个路径下,通过编译器进行预处理。如果不特别地指出,编译器会在几个安装路径下和当前路径搜索头文件。如果要指定搜索头文件的路径,可以用 -I 参数指明.
例如,把头文件放在inc文件夹中,如下:
这样的话,命令行就要写成下面的样子。
tricore-gcc -E program.c -o program.i -I ./inc
如果不加 -I 参数,编译器会报出找不到头文件的错误,如下图。
所以工作中如果看到了这种错误,就要考虑是头文件没加到路径中,还是路径没加到命令行中。
4 只加载一次头文件
4.1 问题
实际项目中会有大量地头文件嵌套,一不小心就会重复包含了头文件。重复包含头文件会有两个问题:一方面如果头文件中有结构体定义,在编译的阶段会报错(预处理过程是不会报错的);另一方面,就算没有重复定义的报错,重复加载也会浪费时间。
对于结构体重复定义的报错,可以自己复现出来。
1)首先把第2章中的头文件稍作修改,定义一个结构体。
2)然后在C文件中包含两次该头文件。
3)再通过命令行调用编译器,对源文件进行预处理。注意这里没有 -E 参数,因为结构体重复定义是在编译过程中报错。
tricore-gcc program.c -o program.i
如上图中,指明了struct1这个结构体类型重复定义了。
4.2 通过条件编译只加载一次头文件
有一种标准的方法解决上述问题,就是将源文件的整个内容包含在条件编译中,范例如下:
/* File foo. */
#ifndef FILE_FOO_SEEN
#define FILE_FOO_SEEN
the entire file
#endif /* !FILE_FOO_SEEN */
其中 #ifndef 表示如果后面的宏没有被定义,则执行下面到#endif之前的内容。当头文件第一次被包含的时候,还没有定义 FILE_FOO_SEEN 这个宏,就会先定义这个宏,扫描头文件的内容(也就是上述范例的 the entire file)然后展开到源文件中;当头文件第二次被包含的时候,由于第一次定义了这个宏,所以就直接跳到 #endif 了,也就不会再加载一次了。
对4.1中的源文件做预处理,就可以看到对比结果。
1)当没有用条件编译的时候,预处理后会把结构体类型的定义加载两次,也就导致了后面编译过程的报错。
2)在头文件中加上条件编译如下。
然后再进行一次预处理,输出的i文件如下图。这样就只有一个结构体定义了。
5 总结
本文研究了预处理过程中对头文件的处理,有助于理解头文件的作用。