被隐藏的过程
起初计算机并没有高级语言
计算机能够直接看懂并能够执行命令的是机器语言(二进制语言)。我们只要将这些文件链接起来就能够生成可执行文件。
但是对于程序员来说机器语言并不是那么地友好,毕竟它使用的是二进制来表示某一个命令,例如:加法的机器指令是
00000011
。因此人们将这些功能包装成了一个个程序员能够看懂的标签,从而形成了汇编语言,加法的汇编指令是ADD
,而高级语言的加法指令则是“ + ”
。但是随着语言更加地高级,计算机并不能直接读懂,因此它内部会进行翻译,看懂之后再进行执行。就好比我们使用中文去命令一个不懂中文的外国人,必须要翻译成外国人能够听懂的语言,然后他才能够按照你的命令去做。
因此在汇编语言及高级语言所写的程序中,存在着翻译环境和执行环境。
我们平时在编写程序的时候都是一气呵成,但是实际上它是怎样从源文件产生出目标文件的呢?
在翻译和执行的环境下,它是这样产生的:
实际上一个编译链接的步骤有四个:预编译、编译、汇编、链接。
预编译又被称为预处理。
在GCC编译器下,源文件经过预处理产生.i
文件,.i
文件经过编译生成.s
文件,.s
文件经过汇编生成.o
文件,最后.o
文件经过链接生成可执行文件
那么这些步骤中每一步都具体干了些什么呢?
我们先来看看预处理。
预处理
预处理阶段主要处理的内容:
- 展开宏定义,并删除# define
- 处理# include 预编译指令
- 处理条件预编译指令
- 删除所有的注释
- 添加行号和文件标识
- 保留# pragram编译器指令
那么预编译指令具体又有哪些?每个指令都是在命令其做什么?
预编译指令 | 含义 |
---|---|
# define | 宏定义 |
# undef | 撤销已定义的宏 |
# include | 将文件插入该指令处 |
# if 、# else、# elif、#endif | 这几个指令提供了多种编译选择,当#if后面为真时,编译#if~#endif之间代码,如果为假则跳过,实际上和if……else的用法比较相似 |
#ifdef、#ifndef | 条件编译指令,表示“如果有定义”和“如果没有定义” |
#line | 改变文件当前的函数和文件名,形式:#line number[“Filename”] |
#error | 编译程序时,只要遇到#error会生成一个编译错误提示信息并停止编译 |
#pragram | 设定编译器的一些状态,因此预处理阶段会保留它 |
此外,编译器中还预定义了一些宏
_FILE_ //在编译中的文件名
_LINE_//在编译中的文件的行号
_DATE_//文件在编译时的日期
_TIME_//文件在编译时的时间
_STDC_//查看所用编译器是否遵循ANSI C,如果是则值为1,否则未定义
注意这些预定义的宏标识符两边是两个下划线。
例如:
# include <stdio.h>
int main(void)
{
printf("flie:%s\n", __FILE__);
printf("line:%d\n", __LINE__);
printf("date:%s\n", __DATE__);
printf("time:%s\n", __TIME__);
//printf("%d\n", __STDC__); //由于该编译器不遵循ANSI C,所以显示未定义
return 0;
}
在我这的VS2019下运行效果:
定义标识符常量
#define能够定义一些常量,比如数值常量、字符常量……等。这意味着你可以将任何的文本给替换到程序中,毕竟所有的文本都可以看作是字符。
形式如下:
#define name stuff
#define只是进行了简单地替换,即将name全部替换成stuff!!!
例如:
#define PI 3.14159265
//将该宏生命周期内的PI全部替换成3.14159265
如果有时候后面定义的字符太长呢?例如将一段语句插入程序中(可以把它单纯地看作是字符常量),那么写起来非常地别扭,这个时候就可以使用续接符,实际上它是一个反斜杠,但是它所在的那一行后面什么字符都没有。
例如:
//续接符
#define PROCESS \
for(int i = 0; i < 12; i++){\
sum += i; \
if (i % 2 == 0){\
count++;\
}\
}
虽然从理论上来说,#define可以替换所有的文本及符号,但是你不能定义注释符。因为实际上注释符的处理先于预编译指令的处理。
宏定义表达式
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
//宏的声明方式
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
例如:
#define SQUARE( x ) x * x
#define SUM(X, Y) X + Y
#define SUB(X, Y, Z) X - Y - Z
需要注意大部分定义的宏名采用的是大写。
拿第一个定义的宏来说:
它接收了一个参数,如果在
#define SQUARE( x ) x * x
声明之下,将SQUARE( 3 )
置于程序之中,那么在预处理阶段,就会将该宏生命周期内的程序中的SQUARE( 3 )
替换成3 * 3
。
虽然看上去这个宏定义一点问题都没有,但是问题可大着呢!
#define SQUARE( x ) x * x
# include <stdio.h>
int main(void)
{
int result = 0;
result = 2 + SQUARE(2 + 3) + 5;
printf("%d\n", result);
return 0;
}
你的预期结果是多少呢?
是不是2+25+5=32,那打印的结果就是32呢?
这是错误的!
#define只是简单地进行了替换,原封不动地替换到程序中
实际上:
result = 2 + 2 + 3 * 2 + 3 + 5;
故而结果为18
如何解决这类问题呢?
只要我们别太小气,在该有括号的地方加上括号就可以了。
此外当#define与类型挂钩的时候
//#define和类型挂钩时
#define INT int //简单地替换
typedef int INT_T; //INT_T本质上就是int,别忘记分号
#define PTR_T int*
typedef int* PTR_R; //别忘记分号
PTR_T a, b;
PTR_R c, d;
//宏定义只是简单地将PTR_T替换成int*,但实际上*默认跟的是后面的标识符
//int *a, b; 所以b只是整型
//而重定义,只是把它换了一个名字,本质不变,PTR_R本质上是int*,是一个完完整整的类型
//int* a, b; //所以a, b都是int*类型
#undef
#undef
用于撤销已经定义过的宏名。
例如:
MAX的生命周期在#define 与#undef之间。
文件包含
#include 是将它包含的文件插入到该指令处以取代这条指令。
#include 指令包含文件的两种格式:
//格式一
# include <file>
//格式二
# include "file"
这两种格式的区别在于:
- 格式一在预处理时到系统规定的路径下寻找(可以理解为库文件)
- 格式二先在当前目录下查找,找不到再从系统规定的路径下查找(可以理解为先找本地文件,再找库文件)
这样是不是可以说,对于库文件也可以使用 “” 的形式包含? 答案是肯定可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
如果一个程序中同一个文件包含了N多次,这种情况会发生什么呢?
那么它就会将这个文件插入N多次,导致很多重复且无用的内容占用了空间。
如何防止多次重复包含同一文件呢?
不知道你有没有注意过这个情况。当我们创建一个头文件时,文件的开头都会有这样一个预处理指令:
#pragram once
//可以防止头文件的重复引用
另外一种方法就是:条件编译
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif __TEST_H__
上述条件编译可以理解为:如果没有定义文件__TEST_H__ ,则定义文件__TEST_H__ ,下一次在碰到__TEST_H__ 文件的包含命令,则就不会进行编译了。
条件编译
条件编译的功能使得我们可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。
形式一
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
如果定义了标识符,则编译程序段1,否则就编译程序段2
可以没有#else
形式二:
#ifdef 标识符
程序段
#endif
如果定义了标识符,则编译程序段
形式三:
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
如果没有定义标识符,则编译程序段1,否则编译程序段2
形式四:
#if 常量表达式
程序段 1
#else
程序段 2
#endif
如果表达式常量为真则编译程序段1,否则编译程序段2
而#elif 命令意义其实与 else if 差不多
#line
#line 预处理指令的作用是改变当前文件的行数和文件名称,它们是在编译程序中预先定义的标识符。
定义形式如下
命令的基本形式如下
#line linenumber["filename"]
例如:
#line 10 test.c
其中的文件名也就是test.c可以省略
这条指令可以改变当前的行号和文件名,例如上面的这条预处理指令就可以改变当前的行号为 10,文件名是 test.c。另外,编译器对 C 源码编译过程中会产生一些中间文件,通过这条指令,可以保证文件名不会被这些中间文件代替,有利于进行分析。
#pragram
详见《C语言深度解剖》