1.3.2宏定义
我们经常不会用名字去称呼自己比较亲近的人,我就经常管一些在企业里工作的同学叫唐总,张总之类的,虽然他们离这个称号还有很长的路要走。在C语言中其实也同样存在这样一个用法,那就是#define,作为出场率很高的预处理语句,关于它的考察题目也是常见的。对于初学者来说,最容易写出来的一厢情愿的代码莫过于#define N(x) x+100和#define ToString(a) "a" 了。
问题描述:
通常我们在编写C语言程序时,允许用一个标识符来表示一个字符串,称为“宏”。而这种标识符我们称为“宏名”。比如下面语句就是一个合法的宏:
#define PI 3.1415926
在C编译器对程序进行预处理的时候,就会将将程序中出现的PI全部替换成为3.1415926。使用宏最直接的好处就是可以程序更简洁,而且如果使用的PI需要改变的时候,不需要一个个去修改,直接修改宏就可以了。以下面这个程序为例:
/*example1_3_5.c*/ #include<stdio.h> #define PI 3.1415926 int main(void) { float Radius,Area; scanf("%f",&Radius); /*输入半径的值*/ Area=PI*Radius*Radius; printf("%f\n",Area); /*输出圆的面积*/ return 0; } |
如果上面程序中的PI精度不要求这么高的话,只需要把这条语句改为#define PI 3.14就可以了。尤其是在编写大型程序的时候,这样使用便于程序的维护和修改。
实例分析:
#define的另一种用法,就是用宏定义来代替一个表达式,这个表达式可以有参数,也可以没有参数。通常一个实现很简单功能的函数,往往写成一个宏定义会更节省系统的开销。
带参宏定义的一般形式为:#define 宏名(形参表) 字符串
使用宏的好处就是可以提高程序运行的效率,但这是对函数调用时而言,比如:
/*example1_3_6.c*/ #include "stdafx.h" #include<stdio.h> #define PI 3.1415926 float ComputeArea(float x){ return PI*x*x; } int _tmain(int argc, _TCHAR* argv[]) { float Radius,Area; scanf("%f",&Radius); Area= ComputeArea(Radius); printf("%f\n",Area); return 0; } |
/*example1_3_7.c*/ #include "stdafx.h" #include<stdio.h> #define PI 3.1415926 #define ComputeArea( x) (PI*x*x) int _tmain(int argc, _TCHAR* argv[]) { float Radius,Area; scanf("%f",&Radius); Area= ComputeArea(Radius); printf("%f\n",Area); return 0; }
|
example1_3_6.c和example1_3_7.c相比,一个采用了函数的写法,另一个采用了宏定义的方式。这两种方法看起来都很容易阅读,但是区别是否很大呢
/*example1_3_6.c*/ float ComputeArea(float x){ 004113A0 push ebp 004113A1 mov ebp,esp 004113A3 sub esp,0C4h 004113A9 push ebx 004113AA push esi 004113AB push edi 004113AC lea edi,[ebp-0C4h] 004113B2 mov ecx,31h 004113B7 mov eax,0CCCCCCCCh 004113BC rep stos dword ptr es:[edi] return PI*x*x; 004113BE fld dword ptr [x] 004113C1 fmul qword ptr [ __real@400921fb4d12d84a (415740h)] 004113C7 fmul dword ptr [x] 004113CA fstp dword ptr [ebp-0C4h] 004113D0 fld dword ptr [ebp-0C4h] } 004113D6 pop edi 004113D7 pop esi 004113D8 pop ebx 004113D9 mov esp,ebp 004113DB pop ebp 004113DC ret Area= ComputeArea(Radius); 00411429 push ecx 0041142A fld dword ptr [Radius] 0041142D fstp dword ptr [esp] 00411430 call ComputeArea (4111B8h) 00411435 add esp,4 00411438 fstp dword ptr [Area] |
/*example1_3_7.c*/ /*由于采用了宏定义的写法,因此避免了函数调用时使用的大量资源,两个程序相对比可以发现,采用函数的方式比宏定义的方式多产生了20多条语句*/
Area= ComputeArea(Radius); 004113C9 fld dword ptr [Radius] 004113CC fmul qword ptr [ __real@400921fb4d12d84a (415740h)] 004113D2 fmul dword ptr [Radius] 004113D5 fstp dword ptr [Area]
|
example1_3_6.c将圆面积的计算公式采用函数的方法,这样使得程序看起来容易阅读,但是这样的程序加大了系统的开销,在函数调用时要消耗一部分系统资源,如果函数完成的功能很复杂,那么这段开销可以忽略不计。但如果像上文这样简单的功能,如果使用函数的话,就有些得不偿失了。这样的话,如果既想程序模块分明,又想减少程序开销的话,就可以选择example1_3_7.c的这种写法:
深入剖析:
现在来看几个常见的#define问题
第一个常见问题,如果做出这个定义,系统是否会接受
#define printf “hello world”吗?
这里先不要去管宏名是否大写了,大写只是建议并不是规定!小写的宏名同样可以通过编译器的检查。
这个定义是有问题的,但是确实是可以通过编译的。在系统的预处理阶段结束以后,C编译器可以将所有出现的printf都替换为hello world,也就是说宏名是可以用系统的保留字的。但是通常我们不要这样去做。
第二个常见问题,当我们按照下面定义以后
#define PI 3.1415926以后
程序中出现了:
Int CPI;
这里面的变量名CPI也包含有PI,那么该语句会不会被替换为
Int C3.1415926呢?
答案显然是不会,这里面预编译器的查找与替换指的的都是完全匹配,就是必须完全一样,不能多也不能少。
第三个常见问题,括号里面的内容是否会被替换同样是
#define PI 3.1415926以后
程序中出现了:
char[] p=”PI”的话
那么预编译器会不会把这个语句替换成下面这个样子
char[] p=” 3.1415926”
预编译器是不会做这个替换的,也就是说,预编译器是不会对程序中引号里面的内容进行替换的
最后一个常见的问题,这也是很多人喜欢出的程序员面试的一道题目
如果我这样定义了这样一段程序,程序的功能是将参数转换为对应字符串,比如参数为5,那么希望的结果为“5”。
/*example1_3_8.c*/ #include "stdafx.h" #define ToString(a) "a" int _tmain(int argc, _TCHAR* argv[]) { char p[]=ToString(5); printf("%S\n",p); return 0; } |
那么这段语句执行完成以后,输出的会是什么呢?
出乎很多人意料的是,这里输出的结果是“a”,也就是ToString(5)以后程序的结果是“a”。
事实上,无论ToString()里面的是什么,这段语句执行完以后结果都只能是“a”。
/*example1_3_8.c*/ char p[]=ToString(5); 0041139E mov ax,word ptr [string "a" (415740h)] // 这里编译器将字符“a”赋值给寄存器ax 004113A4 mov word ptr [p],ax //将寄存器中的值赋给了数组p中的第一个元素 |
根据char p[]=ToString(5);汇编得到的代码可以看出来,结果和ToString()中的参数好像没有任何关系,实际上无论我们将参数设置为多少,结果都会是一样的。
在宏定义中,如果宏名后面是一个字符串的话,那么宏定义是不会去改变字符串里面的内容。实现字符串转换的方法需要采用这种写法:
#define ToString(x) #x
这里面的#x指的就是将变量x转化为字符串,比如程序example1_3_8.c的正确写法因该是
/*example1_3_9.c*/ #include "stdafx.h" #define ToString(a) #a int _tmain(int argc, _TCHAR* argv[]) { char p[]=ToString(5); printf("%S\n",p); return 0; } |
在#define中,标准只定义了#和##两种操作。#用来把参数转换成字符串,##则用来连接两个前后两个参数,把它们变成一个字符串。
例如#define Join(x,y) x##y
这个宏定义的作用就是将两个变量连接成一个字符串,比如
Join(123,100)的结果就是123100。
总结:
宏定义中最容易犯的错误还是运算优先性的问题,比如
#define N(x) x+100
这种定义方式,乍看之下视乎没有任何问题,但是如果在程序中出现了下面的语句:
Int x=2*N(20)时
本来我们的目的是希望预处理结束后,该语句变为:
Int x=2*(x+100);
希望计算的结果是240。
但是实际上替换完成以后确是:
Int x=2*x+100;
实际计算的结果是140。
出现以上的问题,其实只要见过类似的情形,以后就都不会犯这样的错误了。这里还是需要重点提到的就是,最好在宏定义外面加上一个括号,免得在运行中被其他语句改变了初衷。