7.4、模块化编程中的头文件、标准库 & 标准头文件

1、模块化编程中的头文件

  • 前面演示多文件编程时创建 main.cmodule.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 中编译运行:
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.hstdlib.hstring.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.hstdlib.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.hE:\images\123.jpgE:/videos/me.mp4D://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 类型的指针,以使得所有的模块都能使用它:
    • FILEstdio.h中自定义的一个类型(本质上是一个结构体),使用必须包含stdio.h
#include <stdio.h>
extern FILE *fp;
  • 现在假设程序的主模块 main.c 中需要使用 fp 变量和 printf() 函数,则需要同时引入 xyz.hstdio.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 中定义了类型 TYPE1xyz2.h 中定义了类型 TYPE2,并且它们都包含了 stdio.h,如果主模块需要同时使用 TYPE1TYPE2,就必须将 xyz1.hxyz2.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
  • 这种宏保护方案使得程序员可以 “任性” 地引入当前模块需要的所有头文件,不用操心这些头文件中是否包含了其他的头文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

融码一生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值