C 语言头文件作用的简单理解

C 语言是一种先声明后使用的语言。

举个例子:

如果你要在 main() 函数里调用一个你的函数 foo(),那么你有两种写法:

  1. foo() 的定义写在 main() 之前。此时 foo() 的声明和定义是同时发生的:

    int foo() {
        ...
    }
    
    int main() {
        foo();
    }
    
  2. foo() 的定义写在 main() 之后。此时 foo() 的声明必须出现在被 main() 调用之前:

    int foo();
    
    int main() {
        foo();
    }
    
    int foo() {
        ...
    }
    

实际上,我们只要保证在 foo()main() 调用之前声明 foo() 就好了。无论是写法 1 还是写法 2,foo() 的声明都是在被 main() 调用之前发生的。


对于单文件 C 项目来讲这样还好说。然而当我们项目中的代码越来越多之后,把所有代码放到单个文件里会使我们的项目变得难以维护。这时我们就需要把代码拆分,把功能相近的代码放到一个文件里,功能不同的代码分别放到不同的文件里。这样有利于我们后期对项目的维护。

比如说,我可以在 main.c 文件中只写 main() 函数,而把我的其他函数写到一个单独的文件 foo.c 中,就像这样:

// main.c
int main() {
    ...
}
// foo.c
int foo1 {
    ...
}

int foo2 {
    ...
}

然而这里有一个问题:我们如何在 main() 函数中调用 foo.c 文件中的函数呢?

首先有一个笨方法,就是你在调用 main() 函数之前手动加上 foo.c 文件中函数的声明:

int foo1();
int foo2();

int main() {
    foo1();
    foo2();
}

然后我们编译的时候,两个文件都要编译并链接:

cc -c main.c
cc -c foo.c

这将生成目标文件 main.ofoo.o,接下来我们再对这两个文件进行链接:

cc main.o foo.o -o program

这样就生成了可执行程序 program


如果我们只用到两个函数,这样也不算麻烦。然而现实中我们可能要调用成百上千个函数,这样一来这种方法就有些过于麻烦了。

那么有没有一种方法能一次声明所有函数?

首先我们介绍一下 #include 预处理指令。它的功能是将一个文件的内容插入到这个 #include 指令所在的位置。

那我们只要把 foo.c 文件的内容插入到 main.c 文件中 main() 函数之前的位置不就好了?就像这样:

#include "foo.c"

int main() {
    foo1();
    foo2();
}

我们让编译器对 main.c 文件进行预处理:

cc -E main.c

编译器输出的内容是这样的:

int foo1() {
    ...
}

int foo2() {
    ...
}

int main() {
    foo1();
    foo2()
}

可以看到,#include 指令将 foo.c 文件的内容原封不动地插入到了 main.c 中。

如果要构建项目,我们只需要编译 main.c 就够了。因为预处理阶段已经把 foo.c 的内容全部加入到 main.c 中了。

cc main.c -o program

这种方式就类似我们前面提到的方法 1 —— 将函数定义放在 main() 函数之前。

对于小项目来说,这种方式够用了。然而对于比较大的项目,这种方式有一个显著的缺点 —— 你会发现这种方式其实还是相当于把所有代码写入到了一个文件中。对于代码量大的项目,编译一个这样的文件可能相当耗时。并且你一旦对项目文件的任何部分做了改动,都要重新编译整个项目。显然这种方式不适合大型项目。

参考我们之前的做法,我们能不能只把函数声明的部分提取出来,然后把它们 includemain.c 文件中?这样 main.c 文件就只包含其他文件的声明部分,而不是全部代码。这样我们在编译的时候,就可以各个文件分别编译。如果其中某个文件发生了变动,我们只需要重新编译这个变动的文件,再重新链接即可。而链接的过程是比较快的。相比重新编译整个项目,显然这是更优的选择。

对于我们的这个例子,我们只需再创建一个 foo.h 文件,并将 foo.c 文件中所有函数的声明提取出来放入其中,这样我们只需在 main.c 文件中加入 #include "foo.h" 命令,就可以只将这些函数声明加入 main.c 文件,而不是全部代码。

我们把这种从源文件 foo.c 中提取函数声明组成的文件 foo.h 叫做头文件(header)。

// main.c
#include "foo.h"

int main() {
    foo1();
    foo2();
}
// foo.h
int foo1();
int foo2();
// foo.c
#include "foo.h"

int foo1() {
    ...
}

int foo2() {
    ...
}

在这里你看到源文件 foo.c 也包含了其自身的头文件 foo.h,是因为在实际应用中头文件往往不止包括函数声明,也包括结构体声明、常量定义等源文件也必须用到的信息。因此在实际应用中源文件也常常包括其自身的头文件。

此时我们让编译器对 main.c 文件进行预处理:

cc -E main.c

就会看到 main.c 文件只包含了 foo.c 文件中函数的声明:

int foo1();
int foo2();

int main() {
    foo1();
    foo2();
}

让编译器对 foo.c 文件进行预处理:

cc -E main.c

可以看到 foo.c 文件也包含了自己的函数声明:

int foo1();
int foo2();

int foo1() {
    ...
}

int foo2() {
    ...
}

如果我们想要构建项目,需要分别编译 main.cfoo.c,最后再进行链接:

# 编译
cc -c main.c
cc -c foo.c

# 链接
cc main.o foo.o -o program

实际上,使用头文件的好处远不止上面提到的这点。因此使用头文件是编程中的一个好习惯。

posted @ 2024-03-15 03:58  Undefined443  阅读(11)  评论(0编辑  收藏  举报