C语言的经典,helloworld程序是每个程序员能随手写出来的
#include<stdio.h>
int main()
{
printf("helloworld\n");
return 0;
}
用codeblock写完后,按bulid
就能直接在屏幕上 显示
helloworld
事实上过程可以分为四个步骤
预编译
编译
汇编
链接
1.预编译
c语言的源文件经过预编译后的文件扩展名是 .i文件
预编译过程主要处理那些源码文件中以 #开始的预编译指令
比如 #include #define
主要处理规则是
1.将所有#define 删除,并且展开所有宏定义
比如
#define a 100
就将源文件中所有的a 转换成 100
2.处理所有条件预编译指令
如 #if #ifdef
3.处理#include预编译指令,讲被包含文件插入该预编译指令位置
4.删除所有注释 // /**/
5.添加行号和文件名标识
比如 #2
6.保留所有#pragma编译器指令,因为编译器需要使用它们
例子
链接:https://www.zhihu.com/question/20346424/answer/14840923
来源:知乎
著作权归作者所有,转载请联系作者获得授权。
说白了,他就是一个预编译指令,在正式编译之前,预编译器会对源文件中的#include指令进行处理,而这个指令的含义,就像她的名字一样,就是include,就是包含,再说白话一点,就是将他后面的文本文件中的文本内容展开到这个命令的位置。所以,他可以包含任何文本文件,而至于正确与否,就不是他的事情了。那就是后面的编译器的事情了。
打个比方,我们有个a.c,他include了b.h,这两个文件的内容分别是
// a.c
#include "b.h"
int main()
{
foo();
return 0;
}
而另外一个b.h中的内容是:
void foo()
{
}
在编译之前,预处理器会对源文件进行处理,执行其中的预处理指令,比如这里的#include,当然也还有其他,比如#define等等。按照上面的解释,include指令是将文件展开插入到相应的位置,所以,经过预处理器处理后的a.c变为:
<-----这里,程序中的注释被去掉
void foo() <------这里,include指令被执行,其后的文件中的内容被添加到他所在的位置
{
}
<------其他保持不变
int main()
{
foo();
return 0;
}
最后参与编译的,实际上是经过处理后的a.c,这就是include的整个过程
所以,结论是,#include可以包含任何文本文件,可以是头文件,也可以是源文件,甚至某个文本文件,都可以。
所以当我们无法判断宏定义是否正确,或头文件是否包含正确,我们可以查看经过预编译的文件
2.编译
将预编译处理过的文件进行一系列的 词法分析,语法分析,语义分析及优化后产生相应的汇编代码文件
3.汇编
将汇编代码转换成机器指令
每一个汇编语句对应一条机器指令
一一翻译就行
输出目标文件 .o
4.链接
照理说汇编后的文件就已经是机器指令文件了,为什么还要经过链接呢?
留个问题
请看下文
汇编语言的产生
从前没有汇编语言,程序员用的是机器语言,把代码写在纸条带上,穿孔和不穿孔分别表示0和1
现有一个程序
比如
0001 0100
......
......
......
1000 0100
第一条指令就是一条跳转指令
他的目的是第五条指令(第五条指令的绝对地址是4)
高四位0001表示跳转
低四位0100表示跳转的地址
程序经常被修改
比如我们在第一条指令和第五条指令中插入了一条指令
那么第五条指令及后面的指令位置会后移
第一条指令的第四位数字需要调整
程序员需要人工调整费时费力
这种重新计算各个目标的地址过程叫 重定位
于是 先驱发明了 汇编语言
用符号代表 机器指令
比如跳转指令为 jmp
汇编语言还可以用符号表示地址
第四位的地址可以表示为foo
这样指令可表示为 jmp foo
这样,不过插入多少语句,或指令发生了变化
都可以根据符号 foo找到相应地址
软件规模越来越大,人们考虑将不同功能代码以一定方式组织,行成了模块
但最终的程序是多个模块的组合,
模块组合可以归结为模块如何通信的问题,
最常见的两种方式 模块间调用函数 模块间调用变量
都可以归结为模块间符号的引用
模块间拼接就是链接,把一些指令对其他符号地址的引用进行修正
过程包括
地址和空间分配,符号决议,重定位
由于模块都是单独编译的
所以当引用别的模块的符号时不知道确切地址
就暂时搁置这些地址
当链接时再修正这些地址
这叫重定位