1.头文件的作用
通常,一个常规的C语言程序会包含多个源码文件(.c),当某些公共资源需要在各个源码文件中使用时,为了避免多次编写相同的代码,一般的做法是将这些大家都需要用到的公共资源放入头文件(.h)当中,然后在各个源码文件中直接包含即可。
头文件的内容
-
头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括:
- 全局变量的声明。
- 普通函数的声明。
- 静态函数的定义。
- 宏定义。
- 结构体、联合体的定义。
- 枚举常量列表的定义。
- 其他头文件。
-
示例代码:
// head.h
extern int global; // 1,全局变量的声明
extern void f1(); // 2,普通函数的声明
static void f2() // 3,静态函数的定义
{
...
}
#define MAX(a, b) ((a)>(b)?(a):(b)) // 4,宏定义
struct node // 5,结构体的定义
{
...
};
union attr // 6,联合体的定义
{
...
};
#include <unistd.h> // 7,其他头文件
#include <string.h>
#include <stdint.h>
-
特别说明:
-
全局变量、普通函数的定义一般出现在某个源文件(*.c *.cpp)中,其他的源文件想要使用都需要进行声明,因此一般放在头文件中更方便。
-
静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要使用的话,放到头文件中定义是最方便,也是最安全的选择。
-
头文件的使用
头文件编写好了之后,就可以被各个所需要的源码文件包含了,包含头文件的语句就是如下预处理指令:
// main.c
#include "head.h" // 包含自定义的头文件
#include "stdio.h" // 包含系统预定义的文件
int main()
{
...
}
可以看到,在源码文件中包含指定的头文件有两种不同的形式:
- 使用双引号:在指定位置 + 系统标准路径搜索 head.h
- 使用尖括号:在系统标准路径搜索 stdio.h
一个简易示例
由于自定义的头文件一般放在源码文件的周围,因此需要在编译的时候通过特定的选项来指定位置,而系统头文件都统一放在标准路径下,一般无需指定位置。
假设在源码文件 main.c 中,包含了两个头文件:head.h 和 stdio.h ,由于他们一个是自定义头文件,一个是系统标准头文件,前者放在项目 pro/inc 路径下,后者存放于系统头文件标准路径下(一般位于 /usr/include),因此对于这个程序的编译指令应写作:
gec@ubuntu:~/pro$ gcc main.c -o main -I /home/gec/pro/inc
其中,/home/gec/pro/inc 是自定义头文件 head.h 所在的路径
- 语法要点:
- 预处理指令 #include 的本质是复制粘贴:将指定头文件的内容复制到源码文件中。
- 系统标准头文件路径可以通过编译选项 -v 来获知,比如:
gec@ubuntu:~/pro$ gcc main.c -I /home/gec/pro/inc -v
... ...
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/7/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/7/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include
... ...
头文件的格式
由于头文件包含指令 #include 的本质是复制粘贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现一种情况是:头文件被重复包含。
- 使用条件编译,解决头文件重复包含的问题,格式如下:
#ifndef _HEADNAME_H
#define _HEADNAME_H
...
... (头文件正文)
...
#endif
件编译,解决头文件重复包含的问题,格式如下:
#ifndef _HEADNAME_H
#define _HEADNAME_H
...
... (头文件正文)
...
#endif
其中,HEADNAME一般取头文件名称的大写
2.预处理
在C语言程序源码中,凡是以井号(#)开头的语句被称为预处理语句,这些语句严格意义上并不属于C语言语法的范畴,它们在编译的阶段统一由所谓预处理器(cc1)来处理。所谓预处理,顾名思义,指的是真正的C程序编译之前预先进行的一些处理步骤,这些预处理指令包括:
- 头文件:#include
- 定义宏:#define
- 取消宏:#undef
- 条件编译:#if、#ifdef、#ifndef、#else、#elif、#endif
- 显示错误:#error
- 修改当前文件名和行号:#line
- 向编译器传送特定指令:#progma
- 基本语法
- 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠连接成一个逻辑行
- 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
- 可以通过如下编译选项来指定来限定编译器只进行预处理操作:
gcc example.c -o example.i -E
编译过程
编译过程分为以下四个阶段:
1)预处理:处理预处理语句(#开头的语句),删除注释、头文件展开、宏替换...
gcc hello.c -o hello.i -E
2)编译:将C语言程序转化为汇编语言
gcc hello.c -o hello.s -S
3)汇编:将程序代码转化为二进制代码
gcc hello.c -o hello.o -c
4)链接:将所有二进制代码合并起来,根据应用规则生成一个专门针对某个平台执行的应用程序镜像
gcc hello.c -o hello
3.宏的概念
宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。例如:
#define PI 3.14
此处,PI 就是宏(宏一般习惯用大写字母表达,以区分于变量和函数,但这并不是语法规定,只是一种习惯),是一段特定的字串,这个字串在源码中出现时,将被替换为3.14。例如:
int main()
{
printf("圆周率: %f\n", PI);
// 此语句将被替换为:printf("圆周率: %f\n", 3.14);
}
-
宏的作用:
- 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
- 使得程序修改更容易:修改宏定义,即修改了所有该宏替换的表达式。
- 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。
#include