曾经在初读 CSAPP
之时,我初步了解了 C 语言的全部编译过程,即大概可分为以下三大步:
- 预处理
- 编译
- 链接
今天,又看了节公开课,对这部分有了相对较为深入的理解,并且知道了一些有意思的事情。
开始之前,我们应该有这样一个概念:在使用 gcc
编译器进行编译之时,以上三步在某种意义上来说是相互分离的。
预处理阶段
对于此阶段,我之前所理解的全部,便是曾经所做的笔记:
通过预处理器(
cpp
) 处理之后,将其变成了.i
文件,.i
文件实际上对你写的.c
文件里以井号#
开头的语句进行了处理,其发现你引入了某个头文件,于是预处理器便将头文件的内容插入到了你的代码之中。
而如今,我想可以进一步扩展(或许不准确),预处理阶段所处理的内容,实质上就是对 C语言中的宏进行处理,而其中需要处理的部分,即通过宏语法限定的,在程序中有效的宏语言1。其中主要有两大类:
#define
:对以#define
打头的宏定义部分进行替换。#include
:对以#include
格式引入的库文件进行添加。
#define
最为常见的宏定义,新手常常使用其表示变量,老手便会使用宏来表示一些简单的函数等等:
1 |
一般来说,既然已经有了函数,但是还是有许多人使用宏来替代函数,那么必有原因,一般来说,宏的优点主要有以下几点:
- 同函数一般,简单可复用!同时能够使代码含义更加清晰。
- 与函数相比,其直接在预处理阶段进行替换,所以其省去了函数调用的时间,所以,我觉得宏适合在下列情境下使用:
- 函数功能较为简单,只有一行代码,函数调用所花时间对函数功能影响较大
- 函数需要多次重复使用(不适合有递归的情况)
#include
只要学过C语言,想必就不会对此陌生,人生中第一个 hello world
就使用了这个语句,也就是所谓的头文件,头文件的格式有以下两种:
#include<>
:以<>
引用的一般是系统自带的库文件#include""
:以""
引用的一般是自己写的头文件
此部分无需多叙。
编译阶段
所谓编译,就是将C语言文件编译为汇编文件,以方便计算机执行,而此阶段所做工作的目的是: 将C语言进行初步编译,并对程序进行语法上的一次检查,对不符合语法规则的,进行报错,也就是 Error
,并会停止程序的编译 ;对有争议,或是不明确的内容提出警告 Warning
,但是并不会停止编译,最终还是会生成可执行文件 。
既然如此,那么有这样一个有趣的问题:如果,我们在写代码的时候,不加上头文件,那么该代码是否能通过编译呢?
示例
有这样一段毫无意义的代码,其目的仅是为了说明上述问题:
1 |
|
我们首先将 #include<stdio.h>
注释掉,那么此时,编译到 printf
语句的时候,编译器由于找不到 printf
的声明,于是会提出警告,并将 printf
以默认的方式进行编译——将之认为是一个函数(参数为字符串,返回值为 int
)。
所以在编译阶段,此代码即使去掉了 #include<stdio.h>
头文件,也依旧能够编译成功
链接阶段
链接阶段看似是根据编译的结果进行的,但是却不尽然,因为链接阶段是相对独立的,在链接阶段,编译器会根据编译之后的结果,在库中去依次寻找对应的函数。
所以,上述代码,在编译成功之后,进行链接之时,依旧能够从库中寻找到 printf
函数,并执行成功。也就是说,上述代码,去掉了 #include<stdio.h>
头文件,使用 gcc
编译,能够成功地编译并产生可执行文件,唯一的影响仅是:在编译过程中会产生一条警告信息。我自己实验的结果也同理论相同,结果如下图:
可以看到,仅产生了一个 warning
,并且能够执行成功。
普适与特例
同样地,在去掉 #include<stdlib.h>
之后,也如是。但是,正如上面所说,链接阶段会重新在库函数中寻找函数原型,那么对于所有的库函数,都能够如此吗? 答案当然是否定的!
#include<assert.h>
头文件之中,assert()
的实现,实质上并不是一个函数,而是一个宏定义,所以如果去掉此头文件,在编译阶段便会将 assert()
解释为一个函数。但是显然,其并非一个函数,那么链接之时便不可能找得到了,所以如此便会出错!验证结果如下:
意义
乍一看,这种写法好像没什么意义,但是,如果需要追求极度的精简,减小文件体积,这样的写法还是有意义的。事实上,我觉得这种写法,其实是远古时期,内存极度短缺时候的小技巧【实际测试中,其最后产生的可执行文件大小是一样的】。
至于为何会影响文件大小?因为加上头文件之后,在预处理阶段就会将头文件中的所有内容加到你的程序当中,而尽管你只使用了该头文件中的几个函数。而以现在大内存当道的现状,估计很少会有人这么写了。
所以,此技巧更多地还是体现了C语言的 ”自由“,并且还是蛮有趣的。
深入
既然不加头文件,编译器会将其理解为一个函数,那么我们就会想到:我直接在自己的代码中以函数的方式进行声明,那么是不是就不会产生警告了?答案是可行的!2验证如下:
代码改为:
1 |
|
编译结果如下:
拓展
有个很有趣的事情,如果不加库,并且利用一个库函数中已有的函数,但是将其参数进行改变(传参数目变多或变少),那么此时程序又会如何被编译,或是产生何种后果?
比如下面一段代码:
1 | int mian() { |
话不多说,首先来看编译结果:
哈哈哈哈哈,报错了,本来还想着正确之后讲述一番教授的思路呢!看来时代变了啊,编译器是变得安全了不少!那这部分也只能提前结束了。