预处理与编译阶段
⼀、C语⾔由源代码⽣成的各阶段如下:
C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执⾏⽂件
其中编译预处理阶段,读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进⾏处理。或者说是扫描源代码,对其进⾏初步的转换,产⽣新的源代码提供给编译器。预处理过程先于编译器对源代码进⾏处理。
在C 语⾔中,并没有任何内在的机制来完成如下⼀些功能:在编译时包含其他源⽂件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些⼯作,就需要使⽤预处理程序。尽管在⽬前绝⼤多数编译器都包含了预处理程序,但通常认为它们是独⽴于编译器的。预处理过程读⼊源代码,检查包含预处理指令的语句和宏定义,并对源代码进⾏响应的转换。预处理过程还会删除程序中的注释和多余的空⽩字符。
⼆、伪指令(或预处理指令)定义
预处理指令是以#号开头的代码⾏。#号必须是该⾏除了任何空⽩字符外的第⼀个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空⽩字符。整⾏语句构成了⼀条预处理指令,该指令将在编译器进⾏编译之前对源代码做某些转换。下⾯是部分预处理指令:
指令⽤途
# 空指令,⽆任何效果
#include 包含⼀个源代码⽂件
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下⾯代码
#ifdef 如果宏已经定义,则编译下⾯代码
#ifndef 如果宏没有定义,则编译下⾯代码
#elif 如果前⾯的#if给定条件不为真,当前条件为真,则编译下⾯代码,其实就是else if的简写
#endif 结束⼀个#if……#else条件编译块
#error 停⽌编译并显⽰错误信息
三、预处理指令主要包括以下四个⽅⾯:
1、宏定义指令
宏定义了⼀个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏最常见的⽤法是定义代表某个值的全局符号。宏的第⼆种⽤法是定义带参数的宏(宏函数),这样的宏可以象函数⼀样被调⽤,但它是在调⽤语句处展开宏,并⽤调⽤时的实际参数来代替定义中的形式参数。
1.1 #define指令
1.1.1 #define预处理指令⽤来定义宏。该指令最简单的格式是:声明⼀个标识符,给出这个标识符代表的代码(⽐如像圆周率这样的数)。在后⾯的源代码中,我们就可以使⽤定义的宏取代要使⽤的代码,举例如下:
//例1
#define MAX_NUM 10
int array[MAX_NUM];
for(i=0;i<MAX_NUM;i++)
在这个例⼦中,对于阅读该程序的⼈来说,符号MAX_NUM就有特定的含义,它代表的值给出了数组所能容纳的最⼤元素数⽬。程序中可以多次使⽤这个值。作为⼀种约定,习惯上总是全部⽤⼤写字母来定义宏,这样易于把程序的宏标识符和⼀般变量标识符区别开来。如果想要改变数组的⼤⼩,只需要更改宏定义并重新编译程序即可。
1.1.2 使⽤宏的好处有两点:
⼀是使⽤⽅便。如下:
//例2
#define PAI 3.1415926
PAI显然⽐3.1415926写着⽅便。
⼆是定义的宏有了意义,可读性强。如例1,MAX_NUM,望⽂⽣意便知是最⼤数量的意思,⽐单纯使⽤10这个数字可读性要强的多。
三是容易修改。如例1,如果在程序中有⼏⼗次会使⽤到MAX_NUM,修改只需要在宏定义⾥⾯修改⼀次就可以,否则你会修改到崩溃。
1.1.3 宏表⽰的值可以是⼀个常量表达式,允许宏嵌套(必须在前⾯已定义)。例如:
//例3
#define ONE 1
#define TWO 2
#define SUM(ONE+TWO)
这⾥需要注意两点:
⼀是注意上⾯的宏定义使⽤了括号。尽管它们并不是必须的。但出于谨慎考虑,还是应该加上括号的。例如:
six=THREE*TWO;
预处理过程把上⾯的⼀⾏代码转换成:
six=(ONE+TWO)*TWO;
如果没有那个括号,就转换成six=ONE+TWO*TWO;了。
也就是说预处理仅是简单的字符替换,要时刻注意这⼀点,很多错误都会因此出现。
⼆是虽然我们举例⽤了#define ONE 1 这个例⼦,但是⼀般要求宏定义要有其实际意义,#define ONE 1这种没意义的宏定义是不推荐的。(⼤概是这么个意思,忘记具体怎么说了)
1.1.4 宏还可以代表⼀个字符串常量,例如:
#define VERSION "Version 1.0 Copyright(c) 2003"
1.2 带参数的#define指令(宏函数)
带参数的宏和函数调⽤看起来有些相似。看⼀个例⼦:
//例4
#define Cube(x) (x)*(x)*(x)
可以时任何数字表达式甚⾄函数调⽤来代替参数x。这⾥再次提醒⼤家注意括号的使⽤。宏展开后完全包含在⼀对括号中,⽽且参数也包含在括号中,这样就保证了宏和参数的完整性。看⼀个⽤法:
//例4⽤法
int num=8+2;
volume=Cube(num);
展开后为(8+2)*(8+2)*(8+2);
如果没有那些括号就变为8+2*8+2*8+2了。
下⾯的⽤法是不安全的:
volume=Cube(num++);
如果Cube是⼀个函数,上⾯的写法是可以理解的。但是,因为Cube是⼀个宏,所以会产⽣副作⽤。这⾥的书写不是简单的表达式,它们将产⽣意想不到的结果。它们展开后是这样的:
volume=(num++)*(num++)*(num++);
很显然,结果是10*11*12,⽽不是10*10*10;
那么怎样安全的使⽤Cube宏呢?必须把可能产⽣副作⽤的操作移到宏调⽤的外⾯进⾏:
int num=8+2;
volume=Cube(num);
num++;
宏函数使⽤不当会出现⼀些难以发现的错误,请慎重使⽤。
1.3 #运算符
出现在宏定义中的#运算符把跟在其后的参数转换成⼀个字符串。有时把这种⽤法的#称为字符串化运算符。例如:
//例5
#define PASTE(n) "adhfkj"#n
int main()
{
printf("%s\n",PASTE(15));
return 0;
}
//输出adhfj15
宏定义中的#运算符告诉预处理程序,把源代码中任何传递给该宏的参数转换成⼀个字符串。所以输出应该是adhfkj15。
1.4 ##运算符(很少⽤)
##运算符⽤于把参数连接到⼀起。预处理程序把出现在##两侧的参数合并成⼀个符号。看下⾯的例⼦:
//例6
#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c
int main()
{
printf("%d\n",NUM(1,2,3));
printf("%s\n",STR("aa","bb","cc"));
return 0;
}
//最后程序的输出为:
123
aabbcc
2、条件编译指令。
程序员可以通过定义不同的宏来决定编译程序对哪些代码进⾏处理。条件编译指令将决定那些代码被编译,⽽哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。
2.1 #if/#endif/#else/#elif指令
#if指令检测跟在制造另关键字后的常量表达式。如果表达式为真,则编译后⾯的代码,知道出现#else、#elif或#endif为⽌;否则就不编译。
#endif⽤于终⽌#if预处理指令。
#else指令⽤于某个#if指令之后,当前⾯的#if指令的条件不为真时,就编译#else后⾯的代码。
//例7
#define DEBUG //此时#ifdef DEBUG为真
//#define DEBUG 0 //此时为假
int main()
{
#ifdef DEBUG
printf("Debugging\n");
#else
printf("Not debugging\n");
#endif
printf("Running\n");
return 0;
}
这样我们就可以实现debug功能,每次要输出调试信息前,只需要#ifdef DEBUG判断⼀次。不需要了就在⽂件开始定义#define DEBUG 0
#elif预处理指令综合了#else和#if指令的作⽤。
//例8
#define TWO
int main()
{字符串常量的用法
#ifdef ONE
printf("1\n");
#elif defined TWO
printf("2\n");
#else
printf("3\n");
#endif
}
//输出结果是2。
2.2 #ifdef和#ifndef
这⼆者主要⽤于防⽌重复包含。我们⼀般在.h头⽂件前⾯加上这么⼀段:
//头⽂件防⽌重复包含
//funcA.h
#ifndef FUNCA_H
#define FUNCA_H
//头⽂件内容
#end if
这样,如果a.h包含了funcA.h,b.h包含了a.h、funcA.h,重复包含,会出现⼀些type redefination之类的错误。
#if defined等价于#ifdef; #if !defined等价于#ifndef
3、头⽂件包含指令。
采⽤头⽂件的⽬的主要是为了使某些定义可以供多个不同的C源程序使⽤。因为在需要⽤到这些定义的C源程序中,只需加上⼀条#include 语句即可,⽽不必再在此⽂件中将这些定义重复⼀遍。预编译程序将把头⽂件中的定义统统都加⼊到它所产⽣的输出⽂件中,以供编译程序对之进⾏处理。
#include预处理指令的作⽤是在指令处展开被包含的⽂件。包含可以是多重的,也就是说⼀个被包含的⽂件中还可以包含其他⽂件。标准C 编译器⾄少⽀持⼋重嵌套包含。预处理过程不检查在转换单元中是否已经包含了某个⽂件并阻⽌对它的多次包含,这个的处理办法上⾯已经给出。
在程序中包含头⽂件有两种格式:
#include <my.h>
#include "my.h"
第⼀种⽅法是⽤尖括号把头⽂件括起来。这种格式告诉预处理程序在编译器⾃带的或外部库的头⽂件中搜索被包含的头⽂件。第⼆种⽅法是⽤双引号把头⽂件括起来。这种格式告诉预处理程序在当前被编译的应⽤程序的源代码⽂件中搜索被包含的头⽂件,如果不到,再搜索编译器⾃带的头⽂件。
采⽤两种不同包含格式的理由在于,编译器是安装在公共⼦⽬录下的,⽽被编译的应⽤程序是在它们⾃⼰的私有⼦⽬录下的。⼀个应⽤程序既包含编译器提供的公共头⽂件,也包含⾃定义的私有头⽂件。采
⽤两种不同的包含格式使得编译器能够在很多头⽂件中区别出⼀组公共的头⽂件。
4、特殊符号。
预编译程序可以识别⼀些特殊的符号。预编译程序对于在源程序中出现的这些串将⽤合适的值进⾏替换。
4.1 __LINE__
注意,是双下划线,⽽不是单下划线。
__FILE__ 包含当前程序⽂件名的字符串
__LINE__ 表⽰当前⾏号的整数
__DATE__ 包含当前⽇期的字符串
__STDC__ 如果编译器遵循ANSI C标准,它就是个⾮零值
__TIME__ 包含当前时间的字符串
//例9
#include<stdio.h>
int main()
{
printf("Hello World!\n");
printf("%s\n",__FILE__);
printf("%d\n",__LINE__);
return 0;
}
4.2 #line等
#error指令将使编译器显⽰⼀条错误信息,然后停⽌编译。
#line指令改变_LINE_与_FILE_的内容,它们是在编译程序中预先定义的标识符。
#pragma指令没有正式的定义。编译器可以⾃定义其⽤途。典型的⽤法是禁⽌或允许某些烦⼈的警告信息。
//例10,#line举例
#line 100 //初始化⾏计数器
#include<stdio.h> //⾏号100
int main()
{
printf("Hello World!\n");
printf("%d",__LINE__);
return 0;
}
//输出104
四、预编译程序所完成的基本上是对源程序的“替代”⼯作。经过此种替代,⽣成⼀个没有宏定义、没有条件编译指令、没有特殊符号的输出⽂件。这个⽂件的含义同没有经过预处理的源⽂件是相同的,但内容有所不同。下⼀步,此输出⽂件将作为编译程序的输出⽽被翻译成为机器指令。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论