目录
一.常见预处理符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预处理符号均为C语言内置,可直接使用,如下图,打印显示了相关信息,且可看出在vs2019编译器环境,__STDC__未定义
二.#define 定义标识
语法: #define name stuff
例如:
#define MAX 1000
#define INT_P int*
int a=MAX;
INT_P b;
行1将MAX标识符定义为1000,MAX则为标识符常量,行2将register替换为reg
其原理为标识符的替换,在预处理完成后,程序会将目标代码中相关标识符直接进行替换,如MAX将被直接替换为100,替换完成后效果如下:
int a=100;
int* b;
三.#define 定义宏
语法:#define name( parament-list ) stuff
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro),其中的 parament-list 是一个由逗号隔开的符号表。
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
#define MUL(X,Y) X*Y
以上代码便定义了一个宏,用于实现X与Y的相加。此时若在代码中使用此宏,如:
int ret=MUL(3,5)
则预处理后实际效果为:
int ret=3*5;
可能存在的问题
1.宏定义中的参数
宏定义的机制十分类似与函数,而其中符号表部分也与函数参数极其相似,这也容易使人产生错误,要注意,#define的机制始终是替换的效果,而非参数传值!如下,不妨想想结果是什么:
int a = 4;
printf("%d\n" ,MUL( a+1,2) );
以上代码极容易被误解成5*2=10,但实际上,按照宏定义,实际结果应为4+1*2=6。因此在定义时,我们最好给"参数"部分加上括号以改变优先级。
#define MUL(X,Y) (X)*(Y)
2.宏定义的值
除了参数,宏定义的值和函数"返回值"的相似性也会造成误解
#define ADD(X,Y) (X) + (Y)
printf ("%d\n",10 * ADD(2,3))
对于以上代码,结果又是多少呢? 由于函数返回值的错觉,结果容易被误解为10*5=50,然而实际上,运算应该为 10*(2)+(3)=23,因此在对于整个宏定义表达式,最好也加上括号
#define ADD(X,Y) ((X) + (Y))
3.宏参数的副作用
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就也可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如:
//x+1;//不带副作用
//x++;//带有副作用
#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);//输出的结果是什么?
由于"替换"的特性,其预处理实际执行后为: ( (x++) > (y++) ? (x++) : (y++) ),因此执行完毕后,实际x=6,y=10,z=9。由此我们可以看出,若参数带有副作用,可能产生意料之外的效果
四.#define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意: 1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。 2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
五.宏和函数对比
宏通常被应用于执行简单的运算。 比如在两个数中找出较大的一个。 那为什么不用函数来完成这个任务?
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。 宏是类型无关的。
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。 宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int *)malloc(10 * sizeof(int));
由此代码可以看出,宏的函数可以出现类型等,因此可以实现函数难以完成的工作,当然函数也能实现宏定义无法实现的递归,两者各有优势。
六.宏用法的拓展-#和##
首先来看以下代码:
char* p = "hello ""world\n";
printf("%s", p);
打印结果仍为hello world,我们发现字符串是有自动连接的特点的。了解这一点之后,我们可以学习宏参数中的#和##
1.#的作用-把一个宏参数变成对应的字符串
如何把参数名插入到字符串中?
int i = 10;
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE " is "FORMAT "\n", VALUE);
PRINT("%d", i);/
由宏定义的#修饰和字符串的自动拼接,其实际效果为printf("the value of " "i" " is "FORMAT "\n", VALUE),打印为the value of i is 10,便实现了参数名插入字符串。
2.##的作用-##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value) \
n##um += value;
ADD_TO_SUM(5, 10)
其实际效果为把参数num+=10 ,即n##um合成为了标识符num。当然,这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
七.命名约定
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 我们平时的一个习惯是: 把宏名全部大写,函数名不要全部大写