第四章预处理指令熟悉预处理指令对阅读专业级的源代码(比如linux)和实际编程中用到的库(比如MFC,WindowsAPI,DirectX)是很重要的。预处理指令不是C/C++本身的组成部分,预处理指令在C/C++代码编译之前被处理,故称“预处理”。认识这一点对理解预处理也很重要。C++继承了C的预处理指令,其常用的预处理指令分为三种(大多与C兼容)有:文件包含:#include宏定义:#define,#undef条件编译:#if,#else,#elif(否则如果),#endif#ifdef(或#ifdefined,如果定义了一个符号,就执行操作)#ifndef(或#if!defined,如果没有定义一个符号,就执行操作)其它:#line(重新定义当前行号和文件名)#error(输出编译错误,停止编译)#pragma提供机器专用的特性,同时保证C与C++的兼容)不过在C++里,预处理指令不如在C语言中那么重要,因为C++的一些语言成份可以替代预处理的作用。一、文件包含include”filename”和includefilename,两者的区别在课件第一章已讨论过了。二、宏定义1#define#definePI3.14main(){floati=PI;//或floatconsti=PI;}上述程序怎么被编译器处理?首先编译器对预处理指令#definePI3.14进行宏替换(预处理),变成main(){floati=3.14;//宏替换}然后编译它。也就是说宏定义只起到C/C++源程序的“占位符”作用。Q1:#definePI3.14与floatconst=3.14有什么区别?答:前者是预处理指令,只起到C/C++源程序的“占位符”作用,并未指定类型。后者是C++语言成份,有严格的类型限制。前者不做类型正确性检查,不如后者严格,比如:#definePI=3.1A//无法产生正常的宏替换main(){floati=PI;}2#undef可以取消宏定义,比如#definePI3.14main(){floati=PI;}。。。。。。#undefPI//PI不再有效3带参数的宏定义#definePI3.14#defineS(r)PI*r*r//r是该宏的“形式参数”main(){floatarea;floata=3.6;area=S(a);//宏替换为3.14*a*a,}带参数的宏定义很像函数,它在解决一些编程问题中非常有用。Q2:有个C语言程序中,有几行代码被重复使用,按常识应把它写成函数,以便于“一次编码,重复使用”。但它对效率要求特别高,而函数调用又影响效率(要进行现场保护/恢复和很多栈操作)。怎么解决这个矛盾?答:用宏定义定义该函数,可以做到“一次编码”,而宏定义在代码中只进行宏替换,产生的是嵌入的代码,并不产生函数调用,可以做到“重复使用,而又不影响效率”。举例:i++;j++;k++;//这三行代码在程序中被反复使用,而且对效率要求极高写成函数:fun(int&x,int&y,int&z){x++;y++;z++;}main(){inti=1,j=1,k=1;for(intkk=1,100000,kk++)fun(i,j,k);}但调用函数影响效率。改写成:#defineS(a,b,c){a++;b++;c++;}main(){inti=1,j=1,k=1;for(intkk=1,100000,kk++)S(i,j,k);//宏替换不产生函数调用}解决了题目中的矛盾。上面的例子只在一个点发生了宏替换,只是演示性的,实际意义不大。但多点使用,就很有实际意义了,可以做到“一次编码,重复使用,而又不影响效率”。在实际的C/C++库中和源代码中,用宏定义(代参数的或不带参数的)指代一段代码相当常见,常见到有些初学者怀疑那不是C/C++程序。4inline函数前面演示了的宏定义“函数”及其实际使用价值,但是宏定义代换一些复杂表达式不太方便(谭浩强的课本中讲了括号表达式问题),C++增加了一个新的关键字inline。inline是C++的关键字,而不是宏定义,inline代表“内联函数”,其语法为:在函数声明前加inline,其它与普通函数的声明一样。inline函数体的实现也与普通函数一样。inline函数在编译时,产生嵌入式二进制代码(嵌入到调用inline函数的地方,N次出现调用inline函数会产生N次嵌入),而不产生函数调用。Q3:对于一小段频繁使用、效率要求很高的代码,C怎么做?C++?答:C使用宏定义,C++也可以使用宏定义,但最好是用inline函数。虽然宏形式上类似于函数,但编译器仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,从而也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用宏就存在着一系列的隐患和局限性。因此C++推荐使用inline函数代替宏。Q4:对于一大段频繁使用、效率要求很高的代码,使用宏定义或inline合适吗?为什么?答:不合适,宏定义或inline会占用大量内存,N次调用会占用N块内存。不像函数在内存中只有一个副本。所以大段代码不合适用宏定义或inline。你可以优化设计,把大段代码中与效率至关重要的几行代码挑出来做成单独的宏定义或inline,来解决内存占用问题。Q5:是否C++所有普通函数都可改成inline。答:不太清楚,应该不可以。inline内部最好不要有循环,最好不要有return,最好不要调用别的函数。短短几行清晰明了的代码即可。我们要清楚的是:编译器对inline的处理,不是函数形式,而是嵌入代码形式。Q6:多行的宏定义怎么写?答:如果宏有多行,每行的后面必须有\,但是\后不能有空格。比如:#defineS(a,b,c){a++;b++;c++;}可写成:#defineS(a,b,c)\{a++;\b++;\c++;}多行语句宏需要用{}进行保护,最保险的做法是用do{…….}while(0),do{}while(0)会被编译程序优化.因此不会影响程序的运行效率三、条件编译:#if,#else,#elif(否则如果),#endif#ifdef(或#ifdefined,如果定义了一个符号,就执行操作)#ifndef(或#if!defined,如果没有定义一个符号,就执行操作)。在C/C++实际编程中,我们希望一套程序很方便的有多个版本(调试版/发布版,各种语言版,各个用户定制版),条件编译就很有用。1#ifdef(#ifdefined),#else跟据某个宏是否定义,走不同的分枝。比如一个程序的调试版为:#defineDEBUG……//C代码#ifdefDEBUGprintf(…..)//输出调试信息#endif在正式发布版,只需把#defineDEBUG去掉,程序运行就不输出调试信息了,这对程序员很方便,你不需要手工删除调试语句,而且以后加上#defineDEBUG还可以再恢复调试语句.另外,正式版编译时,多余的调试语句不参与编译,节约了正式版运行所需的资源。也可以用逻辑算符判断多个宏定义的关系:#defineCPU#definePENTIUM4……..#ifdefinedCPU&&definedPENTIUM...#endif#ifCPU==PENTIUM...#endif#ifPENTIUM==4...#endif三、标准的预处理器宏标准C/C++预定义了一些宏,这些预定义在代码调试中很有用__LINE__当前源文件中的行号,十进制整数例:cout__LINE__;//输出当前行源程序是第几行__FILE__当前源文件的完整路径,字符串字面量例:cout__FILE__;//输出当前源文件的完整路径__DATE__源文件的编译日期,__TIME__源文件的编译时间,也是字符串字面量格式是hh:mm:ss__STDC__这取决于实现方式,如果编译器选项设置为编译标准的C代码,通常就定义它,否则就不定义它_DEBUG代表程序的调试版__cplusplus在编译C++程序时,它就定义为199711L四、其它宏指令1#line(重新定义当前行号)#includestdio.h;intmain(void)//这是源程序第一行{#line1000//下面一行原来应是第3行,改为第1000行printf(__LINE__);//上机试试return0;}2#error(错误提示宏)当编译器执行到#error所在行时,会将这一行程序员自定义的错误提示打印在屏幕上,以提醒开发人员注意某事,同时中止编译。通常#error与条件编译混和使用,以提醒开发者,可能到了一个不应该出现的地方。格式:#error提示字符串(无需引号)例:#ifndef__cplusplus//当你所用的编译器是C++时,__cplusplus是预定义的宏。//如果你用的是C编译器,则__cplusplus未定义#errorError-ShouldbeC++//输出自定义的错误信息,同时中止编译#endif3#pragma(提供机器专用的特性,同时保证C与C++的兼容)#Pragma指令作用是设定编译器的状态或者是指示编译器完成一些特定的动作。其格式为:#PragmaPara其中Para为参数,下面是几个简单常用的参数:(1)message参数。能够在编译信息输出窗口中输出相应的信息:#Pragmamessage(“消息文本”)例:#ifdef_X86#Pragmamessage(“_X86macroactivated!”)#endif当定义了_X86这个宏以后,我们可以让编译输出窗口里显示“X86macroactivated!”,帮助我们记住该宏已定义过了。(2)code_seg参数。格式如:#pragmacode_seg([section-name[,section-class]])它能够设置程序中函数代码存放的代码段,一些底层程序开发时会使用到它。(3)#pragmaonce只要在头文件的最开始加入这条指令就能够保证头文件只被编译一次。五、断言(assertion)断言是常用的概念。Q7:什么是断言?有些時候,程序员预期程序中何时处于何种状态,例如某些情況下某个值必然是多少,这称之为一种断言(Assertion),断言有两种情況:成立或不成立。当預期結果与实际执行相同时,断言成立,否則断言失败。很多编程语言支持断言(比如JAVA)。下面讲解C语言中的断言。断言在ANSIC/C++标准库头文件中声明:C语言:#includeassert.hC++:#includecassert断言实现为两种形式:(1)voidassert(intexpression);它是以标准库函数(而不是以宏)的面目出现的。调试版和发布版都能使用。(2)_ASSERT(expression)或_ASSERTE(expression)它是以预定义宏的面目出现的,只能在调试版中使用,在#includeassert.h前面应有一行#ifdef_DEBUG判断预定义的_DEBUG是否存在,如不存在则断言不工作。例:#ifdef_DEBUG//只能用于调试版#includeassert.hmain(){intx=1,y=0,z;_ASSERT(y!=0)//一旦y==0,程序就会报错,并自动退出。z=x/y;}上面的例子,因为用了_ASSERT,起到了抛出异常的作用。因为用了_ASSERT,它只适合于调试版,这可能正是我们希望的。在正式版中,可以在_ASSERT(y!=0)前一行加上#defineNDEBUG正式版就不执行_ASSERT宏了。如果使用函数形式的assert(y!=0)呢?它忽略#ifdef_DEBUG和#defineNDEBUG高级语言编译系统,一般都可生成目标代码的调试版和发布版,调试版目标代码的形式比较原始,没经过编译器优化,内部含有很多调试符号,目标文件大,运行慢。而发布版则经过优化。调试版更容