不简单的Hello World之头文件

今天我们就来说说在程序界里无人不知无人不晓大名鼎鼎的Hello World,好比数学界里大名鼎鼎1+1=2,无数码农入坑正是从写这样一段代码开始的,从此踏入了万劫不复的深渊一发不可收拾。。。这段代码是这样写的:

#include <stdio.h>

int main()

{

    printf("Hello World!\n");

    return 0;

}

很简单啊,有没有,我们就喜欢写hello world啊,各种语言的都会写啊,写完一门语言的hello world就在简历上写下了让他在面试时痛不欲生的一句话:"精通**语言"。。扯远了,我们首先来看看第一句话, "#include <stdio.h>",这句话到底是什么意思呢?我们为什么要写这样一句话呢?

话说在编程界的上古时代,古老到还没有Android,iOS,甚至Windows,Linux还没有出现的蛮荒时代,那个时候写程序还没有include这种写法,想引用一个函数可以直接在文件开头列出函数原型,假如我们在add.c中写了一个sum函数就像这样:

----------------------sum.c

int add(int a, int b)

{

    int c = a + b;

    return c;

}

我们想在main.c中使用这个函数,可以这样来写:

--------------------main.c

int add(int a,int b);

int main()

{

    int sum;

    sum = add(1, 2);

    return 0;

}

这种古老的写法至今是正确的,实际上我们使用的编译器在预编译完成后的main.c文件就是这样的,既然可以这样写那我们为什么要用include的写法呢?

让我们再次回到编程界的上古时代,假设这个add函数是你写出来的巨牛巨拽巨拉轰以至于人人都爱用,所以在你参与的某个划时代的大项目中多次被引用到,所有引种这个函数的地方都要加上刚才那函数原型的声明,作为实现这个函数的你隐隐约约觉得自己项目成功后马上就可以升职加薪,当上总经理出任CEO迎娶白富美从此走上人生巅峰。。

然而有一天作为add函数的撰写人你突然灵光一现想到了一种更牛更拽更加拉轰闪瞎众人的写法,唯一一点不足之处就是要修改一下返回值类型。。那么接下来的情况就可以想而知了,那就是你需要把所有用到这个函数的地方的函数原型修改掉,假如你的划时代项目工程比较浩大,有500个.c文件其中250个用到了这个函数,想象一下你的老板要你找到出这250个文件然后依次修改。。你的内心应该是崩溃的。。

能直接修改到想撞墙有没有!!!


所以为了避免你出现上面的状况,实现程序员能早点下班好好享受生活的美好愿望,伟大的计算机先驱们发明了include文件,这都是先驱们趟过的坑,用智慧汗水还有勤劳的双手发明出来的利器,它的作用是这样的,所有的函数原型放到include文件当中,编译的时候预编译器负责在include的位置展开include文件,所谓展开include文件就是把这个文件的内容copy到当前位置。这样每次编译的时候最新版的函数原型就在里面啦,先驱们是不是很有智慧很伟大!

那么include文件里到底有什么呢?

主要是数据类型定义以及函数原型

那什么又是函数原型呢,所谓函数原型就是返回值+函数名+参数,这三要素唯一的定义了一个函数原型,有了函数原型你就可以调用一个函数啦,为什么要使用include这样的设计呢,我直接写一个函数原型然后引用不可以吗?当然是可以的,但是为什么不直接这样写的,在自己写的玩具程序中当然是可以这样写的,但是在大型项目中如果这样做请参考上面的撞墙系列。。

include文件是如何处理的呢?

把这项工作委托给了编译器的一个组成部分也就是预编译器,所谓预编译器也就是真正开始编译前的准备阶段,就好比大厨做饭之前要准备好各种食材后才可以开始烹饪,而预编译器的工作就是在为编译器准备食材。这样你就能理解了吧,头文件展开还有宏展开都是在这个阶段由预编译器完成的。


所以include文件就好比一本书的目录,而每个函数原型就好比其中的一个目录项,根据目录项(函数原型)就开始找到目录对应的真正内容了(.c文件),通过include文件我们可以就可以引用别人的代码还有操作系统提供给我们的服务啦,而不用自己一项一项的写函数原型了,是不是很方便,就像下面这样:


     所以include文件也就是头文件到底是做什么的呢,一句话:

                            "头文件就是用来告诉你怎么使用其它模块的代码"

     仅此而已,至于什么是其它模块呢,这里的模块可以是你自己写的.c文件以及第三方库。

下面我们就用一个例子来看看编译器展开include文件后是什么样的,

我们编辑了三个文件,分别是math.h,定义了四个函数加减乘除,以及这四个函数的实现math.c,接下里我们要在main.c中引用这四个函数,如图所示:


接下里就是编译过程中第一部,预编译。预编译器把include文件展开,如图所示:


可以看到,预编译器把math.h中的内容放到了#include "math.h"的位置上了,生成了新的main.c,至此,预编译器的工作完成,看到了吧,预编译器仅仅就是玩了一些文字游戏,预编译器是不关心你的程序是做什么的。

经过预编译器处理后剩下两个.c文件,main.c以及math.c,如图所示,这两个文件才是编译器需要处理的。编译器是不关心头文件的,头文件处理是预编译器完成的。


怎么样,你看明白了吗

使用gcc -E hello.c 这个命令你就可以看到一个main.c真正展开后的样子啦。

所以这里想说的是我们经常使用的工具已经帮助我们完成了很多繁琐的事情,但我们最好能了解这些规则,因为只有了解规则,才能更好的利用规则。

--------------------------------

Aha! 原来如此

在把源代码编译成机器指令的过程中首先要完成的就是预处理,这个过程是由一个叫做预编译器(Preprocessor)的程序完成,其工作不仅仅包括对include文件的处理还包括各种宏替换等。

在预编译阶段,当预编译器在遇到#include "abc.h"时仅仅找到abc.h文件然后把abc.h的内容展开到当前位置,你可以简单的理解为copy到当前位置。

在编译阶段,添加相应的头文件仅仅是让编译器帮你检查一下你是不是正确的使用了某个函数,这里的用对就是指函数三要素是不是相符。

关于编译器的具体信息,我会在后面的文章中给大家详细介绍,当然是不同于计算机课程上的方式,因为这种计算机课程上的授课方式我!也!听!不!懂!,所以我会用我自己的方式给大家讲明白。


这里还有一个问题,就是编译器是怎么找到include文件的呢,我会在下一篇文章当中给大家详细讲解,也欢迎大家关注我的公共账号,码农的荒岛求生,那里会有最近的系列文章,我会用最直观最形象的方式给大家讲解各种计算机知识,如果哪里没有看明白希望大家能在评论区中提出改进意见,我会持续进行修改,直到大家都能很容易的  谢看明白为止,谢谢大家。



猜你喜欢

转载自blog.csdn.net/github_37382319/article/details/80875416