一、背景
鄙人菜鸡程序员毕业两个月,自学C++半年,找了份工作,目前已经工作2个月出头,前一周熟悉公司环境,大致了解公司是做啥的,处理一些简单的任务。熟悉:SVN,git、gitlab、禅道、代码规范等等。然后完成一些简单的任务,调用一些已经完备的方法,完成需求任务,总体来说还是挺轻松的;后续就是用googletest框架做单元测试和系统测试,总的来说,问题不大。话不多说,切入正题,在工作两个月的过程中,我发现 “宏” 是每一段代码中必不可少,真的是极大地减少了代码量,所以今天结合 《C和指针》 一书来细细回顾下 “宏”。
二、知识回顾——C程序的一生
这块知识在往期博客内存管理(一)——背景扩充 中已经进行了了解,大概内容就是: 一段C语言程序,从你点击运行程序,到程序输出结果做了些啥事儿。
而本博客的主人翁 “宏” 就是在 “预处理” 这步骤中,发挥了极大的作用。《C和指针》中这样定义预处理的作用:C预处理器(preprocessor)在源代码编译之前对其进行一些文本性质的操作。 注意这里的关键字是:文本性质
。下面通过树状图来展示具体有哪些文本性质
的操作。
三、预定义符号
定义: C预处理器(preprocessor)中有很多默认定义的符号,如下表。
作用: 在调试过程中确认输出来源,加入版本信息,结合条件编译等。
|符号|实例 |含义|
|--|--|--|
| _ _FILE_ _
| "love.c"
|进行编译的源文件名|
| _ _LINE_ _
| "25"
|文件当前行的行号|
| _ _DATE_ _
| "Nov 25 1999"
|文件被编译的日期|
| _ _TIME_ _
| "12:05:22"
|文件被编译的时间|
| _ _STDC_ _
| 1
|如果编译器遵循ANSIC,其值就为1,否则未被定义|
四、#define
首先define
在英文钟的意思是:定义,用起来也简单明了,比如下面这条语句
#define MAX 100
#define name stuff
上者把MAX
定义成了100,下者把name
定义成了stuff
,加入这条指令后,只要本文档代码中出现了MAX
那么它都会把他替换成100,出现了name
就会把它替换成stuff
,当我们要改变某值的时候,只需要操作这块宏定义即可。说白了:就是起到一个文本替换的作用。
使用#define
指令,可以把任何文本替换到程序中:这些妙用法都极大减少了代码量:
#define reg register
#define do_forever for(;;)
#define CASE break;case//自动放一个break在case之前
注意点: 如果你被定义的结果是一串特别特别长的代码,你可以将它分成几行,但是要注意的是:除了最后一行,每行的末尾都要加一个反斜杠\
。例子如下:
#define DEBUG_PRINT printf("File %s line %d:"\
"x=%d,y=%d,z=%d"\
__FILE__,__LINE__,\
x,y,z)
五、宏(macro)
5.1#define 替换
#define
的进阶版本,允许把参数替换到文本中,实现:
#define name(parameter-list) stuff
其中paramerter-list
是参数列表的意思,注意: 参数列表的左括号必须与列表相邻,如果有空格,就会被当成stuff
的一部分。举个小学都明白的例子如下:
#define SQUARE(x) x*x
SQUARE(5)//等价于5*5
当然我们尽量少用宏玩一些花活:猜猜SQUARE(5+1)
是多少?36?NO!NO!NO! 不妨把x
换成5+1
变成了5+1*5+1==11
所以我们要回顾到预处理的本质:文本替换而不是参数的传递。那怎么才能让它达到我们想要的效果呢?加格括号就行了。如下:带入其中变成了(5+1)*(5+1)
#define SQUARE(x) (x)*(x)
再看这个例子:
#define DOUBLE(x) (x)+(x)
又问10*DOUBLE(a)
是多少?100?不,不,不,带入文本是105+5=55,我们可以得出下
*最终的结论:所有用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时,参数中的操作符或邻近的操作符之间发生不可预料的相互作用。
如:
#define SQUARE(x) ((x)*(x))
#define DOUBLE(x) ((x)+(x))
5.2 #和
在C++中有函数重载和模板类,但是在C语言中,我们如果我们想要实现带入不同类型的参数可以用宏实现,比如:
#define PRINT(FORMAT,VALUE)\
printf("the value is "FORMAT"\n",VALUE)
此时我们就可以根据需求,带入想要输出的值:输出小数PRINTF("%f",0.2)
输出整形PRINTF("%d",3)
等等,注意整个技巧:只有当字符串常量作为宏参数给出时候才能使用。 还有一种方式:
#define PRINTF(FORMAT,VALUE)\
printf("The value of "#VALUE" is "FORMAT""\n"",VALUE)
上面的#VALUE
被预处理器(preprocessor)
处理为了VALUE
,这里可能有点头脑不清醒了,举个例子:PRINTF("%d",x+3)
的输出结果是The value of x+3 is 25
为了帮助初学者理解,我们细节化一下处理过程,如下:
printf("The value of "#VALUE" is "FORMAT""\n"",VALUE)//↓将#VALUE处理为VALUE
printf("The value of "VALUE" is "FORMAT""\n"",VALUE)//↓将VALUE 替换为 x+3 将FORMAT 替换为 %d
printf("The value of "x+3" is "%d""\n"",VALUE)//
最后介绍宏的##
用法,整个符号把自己两边的符合连接成一个符号:
#define ADD_TO_SUM(sum_number,value)\
sum##sum_number+=value
ADD_TO_SUM(5,25)
我们按照文本替换规则最终替换成:sum5+=sum5+25
也就是在sum5
的基础上增加25。
5.3 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
x+1;//不带副作用
x++;//带有副作用
MAX
宏可以证明具有副作用的参数所引起的问题。
define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
分析:
z = ( (x++) > (y++) ? (x++) : (y++));
x=6 y=10 z=9
虽然较小的值只增加了一次,但是较大的值却增加了2次,第1次是再比较时,第2次是在执行时。
5.4 undef
用于移除一个宏定义:
#undef name
如果一个现存的名字需要被重定义,那么首先必须用#undef移除它的旧定义
六、宏和函数
先看一个比较大小的宏:
#define MAX(a, b) ((a)>(b)?(a):(b))
与函数相比它不需要知道类型,相对于函数更灵活,宏适用于:整形、长整型、单精度浮点型、双精度浮点型等等,总之一句话:宏是与类型无关的。我们再通过宏能够实现,但是函数实现不了的例子来展现宏的优点:这块我们可以将参数类型,用宏传递过去,但是如果用函数那就实现不了。
#define MALLOC(n,type)
((type*)malloc(n)*sizeof(type))
通过简单的文本替换,我们完成下面的例子:
pi=MALLOC(25,int)
pi=((int*)malloc((25)*sizeof(int)));
我们通过表格对比宏和函数的区别:
|特性|宏|函数 |
|--|--|--|
| 代码长度 |每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 |函数代码只出现于个地方;每次使用这个函数时,都调用那个地方的同一份代码|
|执行速度|单纯文本替换,快极了|存在函数的调用和返回的额外开销,所以相对慢一些|
|操作符优先级|宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。|函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。|
|带有副作用的参数|参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。|函数参数只在传参的时候求值一次,结果更容易控制。|
|参数类型|宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。|函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。|
|调试|宏是不方便调试的|逐语句调试|
|递归性|不能|能|
七、条件编译(conditional compilation)
7.1 作用一:选择编译或者忽略
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃,可以使用条件编译。即:可以选择代码的一部分是被正常编译还是完全忽略。举个例子如下:
#if constant-expression
statements
#endif
其中constant-expression
(常量表达式)由预处理器进行求指,如果它的值是非零值(真),那么statements
部分就被正常编译,否则预处理器就将删除它们。
常量表达式的定义: 所谓常量表达式,就是说是字面值常量,或者是一个由#define定义的符号。如果变量在执行期之前无法获得它们的值,那么它们在常量表达式中就是非法的,因为它们要在编译过程中才能获得值。
#if DEBUG
printf("i love zzy")
#endif
如果想编译,就使用#define DEBUG 1
,否则使用#define DEBUG 0
,根本不需要手动注解,也不需要手动删除。
7.2 作用二:编译时选择不同代码部分
#if constant-expression
statements
#elif constant-expression
other statements
#else
other statements
#endif
每一个constant-expression(常量表达式)
只有当前面所有常量的值都为假的时候才会被编译。
7.3 作用三:判断是否被定义
测试一个符号是否已经被定义,以下没对定义的两条语句是等价的,但是#if
形式功能更强,因为常量表达式可能包括额外的条件。
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
如下例子:
#if X>0||defined(ABV)&&defined(BCD)
7.4嵌套指令
操作系统的选择将决定不同的选项可以使用那些方案。
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
下面有一个好的规范:为了帮助大家记住复杂的嵌套指令,可以为每个#endif加上一个注释标签
#ifdef OPTION1
OPTION1;
#else
alternative;
#endif /*OPTION1*/注释标签
八、文件包含
8.1 本地文件包含和函数库文件包含
编译器支持两种不同类型的#include文件包含:函数库文件和本地文件。
函数库头文件包含使用下面的语法:#inlcude<filename>
本地文件包含:#inlcude"filename"
注意: 编译器自行决定是否把本地形式的#include
和函数库形式的 #include
区别对待,可以先对本地头文件使用一种特殊处理的方式,如果失败,编译器再按照函数库头文件的处理方式对它们进行处理。处理本地头文件的一种场景策略就是再源文件所在目录进行查找,如果没找到,编译器就像查找函数库头文件一样再标准位置查找本地头文件。
扩充: 运行于UNIX系统上的C编译器在/user/include
目录查找函数库头文件。
8.2 嵌套文件包含
comm.h
和comm.c
是公共模块。test1.h
和test1.c
使用了公共模块。test2.h
和test2.c
使用了公共模块。test.h
和test.c
使用了test1
模块和test2
模块。
这样最终程序中就会出现两份comm.h
的内容。这样就造成了文件内容的重复
如何解决?
#ifndef __HEAD__H
#define __HEAD_H
#endif //_TEST_H_
#pragma once