1、模块化编程中的头文件
- 前面演示多文件编程时创建
main.c
和 module.c
两个源文件,并在 module.c
中定义一个函数和全局变量,然后在 main.c
中进行了声明。 - 实际开发一般是将函数和变量的声明放到头文件,并在当前源文件中
#include
进来。如果变量的值是固定的,最好使用宏来代替。
#include <stdio.h>
#include "module.h"
int main(){
printf("a = %d\n", a);
func();
printf("OS: %s\n", OS);
return 0;
}
#include <stdio.h>
int a = 100;
void func(){
printf("www.yuque.com/it-coach\n");
}
#define OS "Linux"
extern int a;
extern void func();
gcc main.c module.c
./a.out
:<<!
a = 100
www.yuque.com/it-coach
OS: Linux
!
.c
和.h
文件都是源文件,除了后缀不一样便于区分和管理外,其他均相同,在.c
中编写的代码同样也可以写在.h
中,包括函数定义、变量定义、预处理等。- 但是
.h
和 .c
在项目中承担的角色不同:
-
.c
文件主要负责实现(定义函数和变量).h
文件主要负责声明(变量声明和函数声明)、宏定义、类型定义等- 这些不是 C 语法规定的内容,而是约定成俗的规范(长期形成的事实标准)。
- 根据这份规范,头文件可包含如下内容:
-
-
- 可以声明函数/变量,但不可以定义函数/变量
- 可以定义宏(带参的宏、不带参的宏)
- 结构体的定义、自定义数据类型
- 在项目开发中可以将一组相关的变量和函数定义在一个
.c
文件中,并用一个同名的 .h
文件(头文件)进行声明,其他模块如果需要使用某个变量或函数,就引入这个头文件。
-
- 这样做的另外一个好处:可以保护版权,在发布相关模块之前可以将它们都编译成目标文件,或打包成静态库,只向用户提供头文件,用户就可以将这些模块链接到自己的程序中。
2、标准库、标准头文件
- 源文件通过编译可以生成目标文件(如 GCC 下的
.o
和 Visual Studio 下的 .obj
),并提供一个头文件向外暴露接口,除了保护版权,还可以将散乱的文件打包,便于发布和使用。 - 实际上一般不直接向用户提供目标文件,而是将多个相关的目标文件打包成一个静态链接库(Static Link Library)(如:Linux 下的
.a
和 Windows 下的 .lib
)。 - 打包静态库就是将多个目标文件捆绑在一起形成一个新的文件,然后再加上一些索引,方便链接器找到,这和压缩文件的过程非常类似。
- C 语言在发布时已经将标准库打包到了静态库,并提供了相应的头文件(如
stdio.h
、stdlib.h
、string.h
等)。 - Linux 一般将静态库和头文件放在
/lib
和/user/lib
目录下,C 语言标准库的名字是libc.a
,可以通过locate
命令来查找它的路径:
locate libc.a
:<<!
/usr/lib/x86_64-redhat-linux6E/lib64/libc.a
!
locate stdio.h
:<<!
/root/anaconda3/include/H5FDstdio.h
/root/anaconda3/include/unistdio.h
/root/anaconda3/include/glib-2.0/glib/gstdio.h
/root/anaconda3/include/unicode/ustdio.h
/root/anaconda3/lib/python3.9/site-packages/pyarrow/include/arrow/io/stdio.h
/root/anaconda3/pkgs/glib-2.69.1-h4ff587b_1/include/glib-2.0/glib/gstdio.h
/root/anaconda3/pkgs/hdf5-1.10.6-hb1b8bf9_0/include/H5FDstdio.h
/root/anaconda3/pkgs/icu-58.2-he6710b0_3/include/unicode/ustdio.h
/root/anaconda3/pkgs/libunistring-0.9.10-h27cfd23_0/include/unistdio.h
/usr/include/stdio.h
/usr/include/bits/stdio.h
/usr/include/c++/4.8.2/tr1/stdio.h
/usr/include/glib-2.0/glib/gstdio.h
!
- 在 Windows 下,标准库由 IDE 携带,如果使用 Visual Studio,则在安装目录下的
\VC\include
文件夹中会看到很多头文件,包括 stdio.h
、stdlib.h
等;在\VC\lib
文件夹中有很多 .lib
文件,这就是链接器要用到的静态库。 - ANSI C 标准共定义了 15 个头文件,称为 C 标准库,所有的编译器都必须支持,如何正确并熟练的使用这些标准库,可以反映出一个程序员的水平:
-
- 合格程序员:<stdio.h>、<ctype.h>、<stdlib.h>、<string.h>
- 熟练程序员:<assert.h>、<limits.h>、<stddef.h>、<time.h>
- 优秀程序员:<float.h>、<math.h>、<error.h>、<locale.h>、<setjmp.h>、<signal.h>、<stdarg.h>
- 除了 C 标准库,编译器一般也会附带自己的库,以增加功能。这些库中的每一个函数都在对应的头文件中声明,可以通过
#include
预处理命令导入,编译时会被合并到当前文件。
3、头文件的路径
- 引入编译器自带的头文件(包括标准头文件)用尖括号,引入程序自定义的头文件用双引号:
#include <stdio.h> // 引入标准头文件
#include "myFile.h" // 引入自定义的头文件
- 使用尖括号
< >
,编译器会到系统路径下查找头文件;而使用双引号" "
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
-
- 使用双引号比使用尖括号多了一个查找路径,它的功能更为强大(完全可以使用双引号来包含标准头文件)
#include "stdio.h"
#include "stdlib.h"
(1)绝对路径、相对路径
- 理论上讲可以将头文件放在磁盘上的任意位置,只要带路径包含进来就可以。
- 以 Windows 为例,在 D 盘下创建一个自定义的文件夹
abc
,它里面有一个头文件xyz.h
,则在程序开头使用#include "D:\\abc\xyz.h"
就能够引入该头文件。 - 假设
xyz.h
中有一个宏定义和一个变量:
-
- 不鼓励在头文件中定义变量,否则多次引入后会出现重复定义错误,这里仅是一个演示案例,并不规范
#define NAME "C 语言"
int age = 5;
#include<stdio.h>
#include "D:\\abc\xyz.h"
int main(){
printf("%s已经 %d 岁了!\n", NAME, age);
return 0;
}
/*
C 语言已经 5 岁了!
*/
(a)绝对路径
- 像
D:\\abc\xyz.h
这种从盘符开始、完整地描述文件位置的路径就是绝对路径(Absolute Path)。 - 绝对路径从文件系统的 “根部” 开始查找文件:
-
- 在 Windows 下,根部就是 C、D、E 这样的盘符,如
D:\\a.h
、E:\images\123.jpg
、E:/videos/me.mp4
、D://abc/xyz.h
等,分隔符可以是正斜杠/
也可以是反斜杠\
,盘符后面的斜杠可以有一个或两个。 - Linux 没有盘符,根部就是
/
,例如/home/xxx/abc.h
、/user/include/module.h
等,分隔符只能是正斜杠/
,比 Windows 简洁很多。 - 为了增强代码的可移植性,引入头文件时请尽量使用正斜杠
/
。
(b)相对路径
- 相对路径(relative path)是从当前目录(文件夹)开始查找文件;当前目录是指需要引入头文件的源文件所在的目录,这也是本文开头提到的 “当前路径”。
- 以 Windows 为例,假设在
E:/cDemo/
中有源文件 main.c
和头文件 xyz.h
,那么在 main.c
中使用#include "./xyz.h"
语句就可以引入 xyz.h
,其中./
表示当前目录,也即E:/cDemo/
。 - 如果将
xyz.h
移动到E:/cDemo/include/
(main.c
所在目录的下级目录),那么包含语句就应该修改为#include "./include/xyz.h"
;对于 main.c
来说,此时的 “当前目录” 依然是E:/cDemo/
。 - 如果将
xyz.h
移动到E:/
(main.c
所在目录的上级目录),那么包含语句就应该修改为#include "./../xyz.h"
,其中../
表示上级目录。./../xyz.h
的意思是:在当前目录的上级目录中查找 xyz.h
文件。 - 如果将
xyz.h
移动到E:/include
目录,那么包含语句就应该修改为#include "./../include/xyz.h"
。 - 注意:可以将
./
省略,此时默认从当前目录开始查找。如#include "xyz.h"
、#include "include/xyz.h"
、#include "../xyz.h"
、#include "../include/xyz.h"
。 - 上面介绍的相对路径的写法同样适用于 Linux。
- 在实际开发中都是将头文件放在当前工程目录下,并使用相对路径,这样即使后来改变了工程所在目录,也无需修改包含语句,因为源文件的相对位置没有改变。
(2)系统路径
- Windows 下的 C 语言标准库由 IDE 自己携带,Linux 下的 C 语言标准库一般在固定的路径下,标准库不在工程目录下,要使用绝对路径才能引入头文件,这样每次切换平台或者 IDE 都要修改包含路径,非常不方便。
- 为了让头文件更加具有实践意义,Windows 下的 IDE 都可以为静态库和头文件设置默认目录。以 Visual Studio 为例,在当前工程名处单击鼠标右键,选择 “属性”,在弹出的对话框中就可以看到已经设置好的路径,如下图所示:
- 这些已经设置好的路径就是本文开头提到的 “系统路径”。
- 当使用相对路径的方式引入头文件时:
-
- 如果使用
< >
,那么 “相对” 的就是系统路径,编译器会直接在这些系统路径下查找头文件 - 如果使用
" "
,那么首先 “相对” 的是当前路径,编译器首先在当前路径下查找头文件,找不到才会继续在系统路径下查找。 - 使用绝对路径的方式引入头文件时,
< >
和" "
没有任何区别,因为头文件路径已经写死了(从根部开始查找)。
4、防止 C 语言头文件被重复包含
- 头文件包含命令
#include
的效果与直接复制粘贴头文件内容的效果一样,预处理器实际上也是这样做的,它会读取头文件的内容,然后输出到 #include
命令所在的位置。 - 头文件包含是一个递归(循环)的过程,如果被包含的头文件中还包含了其他的头文件,预处理器会继续将它们也包含进来;这个过程会一直持续下去,直到不再包含任何头文件。
- 递归包含会导致一个问题,就是重复引入同一个源文件。例如在某个自定义头文件
xyz.h
中声明了一个 FILE
类型的指针,以使得所有的模块都能使用它:
-
FILE
是stdio.h
中自定义的一个类型(本质上是一个结构体),使用必须包含stdio.h
#include <stdio.h>
extern FILE *fp;
- 现在假设程序的主模块
main.c
中需要使用 fp
变量和 printf()
函数,则需要同时引入 xyz.h
和 stdio.h
:
#include <stdio.h>
#include "xyz.h"
int main(){
if( (fp = fopen("demo.txt", "r")) == NULL ){
printf("File open failed!\n");
}
//TODO:
return 0;
}
- 对于
main.c
这个模块,stdio.h
就被包含了两次。stdio.h
中除了有函数声明,还有宏定义、类型定义、结构体定义等,它们都会出现两次,如果不做任何处理,不仅会出现重复定义错误,而且不符合编程规范。 - 假设
xyz1.h
中定义了类型 TYPE1
,xyz2.h
中定义了类型 TYPE2
,并且它们都包含了 stdio.h
,如果主模块需要同时使用 TYPE1
和 TYPE2
,就必须将 xyz1.h
和xyz2.h
都包含进来,这样也会导致 stdio.h
被重复包含。
-
- 头文件的交叉包含是非常普遍的现象,不仅我们自己创建的头文件是这样,标准头文件也是如此。
- 标准头文件
limits.h
中定义了一些与数据类型相关的宏(最大值、最小值、一个字节所包含的比特位等),stdlib.h
就包含了它。
- 实际开发中往往使用宏保护来解决这个问题。例如,在
xyz.h
中可以添加如下的宏定义:
#ifndef _XYZ_H
#define _XYZ_H
/* 头文件内容 */
#endif
- 第一次包含头文件,会定义宏
_XYZ_H
,并执行 “头文件内容” 部分的代码。 - 第二次包含时因为已经定义了宏
_XYZ_H
,不会重复执行 “头文件内容” 部分的代码(头文件只在第一次包含时起作用,再次包含无效)。 - 标准头文件也是这样做的,例如在 Visual Studio 2010 中
stdio.h
就有如下的宏定义:
#ifndef _INC_STDIO
#define _INC_STDIO
/* 头文件内容 */
#endif
- 这种宏保护方案使得程序员可以 “任性” 地引入当前模块需要的所有头文件,不用操心这些头文件中是否包含了其他的头文件。