今天我们就来说说在程序界里无人不知无人不晓大名鼎鼎的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文件后是什么样的,
让我们定义一个叫做sum.h的include文件,编辑
---------------------------sum.h
include " math.h"
int sum(int a, int b);
同时这个sum.h又include了math.h
---------------------------math.h
void math();
在main.c中引用了sum.h
--------------------------main.c
include "sum.h"
int main()
{
return 0;
}
那么编译器是怎么做的呢,首先找到sum.h(请思考编译器是怎么找到sum.h的呢), 然后编译器发现了sum.h还include了math.h,所以编译器又去找到了math.h,把math.h的内容在sum.h中展开,最后再把sum.h中的内容放到main.c中就是这样的效果啦
--------------------------main.c
void math();
int sum(int a, int b);
int main()
{
return 0;
}
怎么样,你看明白了吗
使用gcc -E hello.c 这个命令你就可以看到一个hello.c真正展开后的样子啦。
所以这里想说的是我们经常使用的工具已经帮助我们完成了很多繁琐的事情,但我们最好能了解这些规则,因为只有了解规则,才能更好的利用规则。
Aha! 原来如此
在把源代码编译成机器指令的过程中首先要完成的就是预处理,这个过程是由一个叫做预编译器(Preprocessor)的程序完成,其工作不仅仅包括对include文件的处理还包括各种宏替换等。
在预编译阶段,当预编译器在遇到#include "abc.h"时仅仅找到abc.h文件然后把abc.h的内容展开到当前位置,你可以简单的理解为copy到当前位置,当然也会进行一些处理,但是这样理解没有问题。
在编译阶段,添加相应的头文件仅仅是让编译器帮你检查一下你是不是正确的使用了某个函数,这里的用对就是指函数三要素是不是相符。
关于编译的具体信息,我会在后面的文章中给大家详细介绍。
这里还有一个问题,就是编译器是怎么找到include文件的呢,请听下回分解。
评论区
登录后即可参与讨论
立即登录