众所周知,我们不管用哪种编程语言去写出一个程序,对于计算机而言,它都是无法直接识别的,所以从写好一个程序到计算机将它运行出来这个过程中,势必会发生一系列的变化,将我们所写的高级语言(例如:C语言)的代码转化成计算机所能识别的机器语言(即0,1序列)。
而这个过程之中大致包括这几个部分:预处理----->编译-------->汇编------->链接。
下面逐步分析这四部分:
1.预处理
首先熟悉几个预处理符号:
__FILE__ | 进行编译的当前源文件 |
__LINE__ | 文件的当前行号 |
__DATE__ | 文件被编译时的日期 |
__TIME__ | 文件被编译时的时间 |
__STDC__ | 若当前编译器遵循ANSI C,则它的值为1;否则就未定义 |
测试代码(VS2010):
#include <stdio.h>
int main()
{
printf("file:%s line:%d date:%s time:%s \n",__FILE__,__LINE__,__DATE__,__TIME__);
return 0;
}
运行结果:
因为vs2010并不是完全遵循ANSI C的,所以对于__STDC__我们放在Linuxs中测试
除了上面的5个预处理符号,#,##也是常见的预处理符号。
在代码中#argument这种结构被翻译成"argument";而M##N被翻译成MN。
由于#VALUE被翻译成字面值并以字符串形式表示,所以最后结果#VALUE被解释成"M",而且由于邻近字符串可以拼接的原因,而且根据宏替换的方式在PRINT("%d\n",M);这句话处在预处理后就变为printf("the M is %d\n",10);
在Linuxs环境下我们可以获得预处理后的结果如下:
而对于##而言,例如:
若是M##N的结果不是MN的话,根据宏替换的规则,最终的输出语句是不会为“cc”的。
在预处理过程中,预处理符号只是占了极少的一部分,最重要的大致分为4个方面:宏替换,去注释,头文件展开,条件编译。
去注释:顾名思义就是将我们在代码中所注释掉的一部分东西预处理时将它们给去掉,以便于后面的编译。
头文件展开:顾名思义就是将我们调用的头文件在预处理时给打开,以方便后面的编译可以找到相应的变量,函数等的定义或声明。
而下面我们着重来看看宏替换与条件编译:
一.宏替换(以下的测试均在Linuxs下实现)
a.#define M 10
显而易见,代码中的M均被替换成10.
b.#define ADD(X) ((X)+(X))
显而易见,上述替换成表达式的同时还将代码所给参数给调用了进去。
在这里,要注意的一点,尽量给每一个参数以及整个表达式分别都加上(),以避免在替换之后表达式因为优先级的问题,而无法得到我们想要的结果。
例如:
#define FUN(X) X*X
而代码中有一段这样的代码:FUN(5+1);根据宏替换的原理,这里将会变成5+1*5+1,结果为12,而我们预想的应该是6*6从而得到36。
除此之外,#define FUN(X) (X)+(X)
而代码中有这样一段:3*FUN(5+1);根据宏替换的原理,这里将会变成3*(5+1)+(5+1),结果为24,而我们想要的应该是3*12从而得到36。
c.除了上述两种替换之外,我们还可以运用宏替换来替换一段代码。
#include <stdio.h>
#define FUN() do{fun1();fun2();}while(0)
void fun1()
{
printf("hehe\n");
}
void fun2()
{
printf("haha\n");
}
int main()
{
int flag=1;
if(flag)
FUN();
return 0;
}
在Linuxs下测试预处理结果如下:
在这里我们要注意一点,就是如果宏是一段代码的话最好用do{}while(0)将它们包含在内,以避免出现替换之后缺少分号或多出分号,或者是想if()下面本来想跟多条语句却因为缺少{}而导致错误的问题。
以上三种就是宏替换的大致三种情况,在这里我们发现宏替换的作用在某种意义上与函数的调用非常相似,尤其是最后一种情况,因此我们也可以将最后一种称为定义宏函数。
既然如此,那么宏函数与函数又有那些区别呢,或者说它们各自又有什么优缺点呢?
(1)首先,宏函数只是单纯的替换,并且在预处理阶段就已经一次性的替换完成,相较于函数,每一次都要进行一次调用,并返回相应的值,在这里就可以发现宏函数的速率更快。
(2)其次,宏函数并没有对于参数的类型有过限制,只要是类型可以用宏函数中的操作的,那就可以使用;而对于函数而言,对于类型有着非常严格的规定。
(3)但是,因为宏函数只是单纯的替换,那么在预处理之后,如果宏函数比较长的话,那么就会造成整个代码十分庞大,不利于编译;但是对于函数,在这个程序中这份代码只出现一次,其他的地方都是通过调用,返回来实现相应的功能的,所以不会有类似的问题。
(4)再者,通过讨论第二种宏替换中要注意的情况,我们发现宏函数很容易会因为操作符的优先级的关系,而导致结果难以预料;而对于函数而言,这种问题就会大大减少。
(5)还有就是如果在同一个宏当中同一个参数出现了多次,而这个参数所进行的运算会改变它原本的值,那么在这里就很有可能会出现难以预料的错误。
例如:
#include <stdio.h>
#define FUN(X,Y) ((X)>(Y))?(X):(Y)
int main()
{
int a=10;
int b=15;
int c=FUN(a++,b++);
printf("a=%d b=%d c=%d \n",a,b,c);
return 0;
}
原本这个程序想要的结果是a,b,c分别为11,16,15,然而实际结果是:
而这里就是因为宏函数只是单纯的替换,会将宏函数中所有的参数都替换掉,因此((X)>(Y))?(X):(Y)就变成了((a++)>(b++))?(a++):(b++),在这个表达式中b++进行了两次,因此导致结果和预想有出入。
而相对应的函数就很好的规避了这种问题。
#include <stdio.h>
int FUN(int a,int b)
{
int z=(((a)>(b))?(a):(b));
return z;
}
int main()
{
int a=10;
int b=15;
int c=FUN(a++,b++);
printf("a=%d b=%d c=%d \n",a,b,c);
return 0;
}
运行结果:
这里是将a,b先调入函数进行比较得到最大值,然后在分别进行+1。
所以,综上所述,在以下条件下使用宏函数更好:
(1)宏函数中代码不会过长;
(2)需要处理多种类型的进行同一种功能;
(3)宏中的参数不会被重复调用多次,或者在进行相关运算时不会改变原本的参数,又或者改变原本参数值对功能没有影响;
二.条件编译
关于条件编译,主要是了解几个标识符:#if,#elif,#else,#endif,#ifdef(#if defined),#ifndef(#if !defined)。
1.#if,#elif,#else,#endif
预处理结果:
很明显,#if,#else相当于if...else...的功能,只不过#if,#else后面需要加上#endif来作为结束标志,而且它们所判断 的是要不要对它们后面的语句进行编译,不进行编译的将它去掉。而对于#elif它的作用相当于else if。
2.#ifdef(#if defined),#ifndef(#if !defined),#endif
预处理结果如下:
与#if,#elif,#else类似,只不过#ifdef(#if defined),#ifndef(#if !defined)是判断有没有进行相关定义的,通常用于头文件中以防止头文件被重复使用。
2.编译
在编译过程中,主要是将预处理过的文件经过一系列处理将高级语言(如C语言)转换成汇编语言。
举一个最简单的例子:
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
在Linuxs下测试生成汇编语言如下:
3.汇编
在汇编过程中,主要是将汇编的文件经过一系列处理将汇编语言转换成机器语言。
例如上面的“hello world”程序
在Linuxs下测试生成机器语言如下:
4.链接
在链接过程中,将生成的若干个.o文件捆绑在一起,形成一个单一而完整的可执行程序,也就是说一个程序不管是仅有一个源文件或者是有多个源文件,都让每个源文件分别进行预处理,编译,汇编,然后生成多个一一对应的.o文件(其中均为机器语言(0,1序列)),最后将他们进行一起进行一系列操作(链接过程),共同生成一个可执行的文件。