#define 指令将 标识符 定义为宏,即指示编译器会将之后出现的所有 标识符 都替换为 替换列表,而它也可以被进一步处理
#表示这是一条预处理指令
在C/C++源代码中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。
#define的基本语法非常简单, 但完全不影响它的强大
不过有时候正因为宏(#define)的简单、方便和强大,就会导致我们平常在使用的时候,其中会有很多的注意点和细节需要我们去注意,如果不小心将其忽略, 那么可能会带来我们意料之外的不想要的结果。推荐用宏但不能滥用, 比如在定义常量时
C中const定义的常量比较局限(不能作为定义数组的大小等) ,但在C++中, 常量完全可以用const, 而且宏不做检查,只是代码替换,而const会进行编译检查,const要比宏更安全。所以应尽可能的使用const来代替类对象宏。
链接: C++中举例说明可以使用const代替#define以消除#define的不安全性
目录
类对象宏(无参宏)
类函数宏(带参数的宏)
#的作用
##的作用
类函数宏(带参数的宏)和函数的对比
#undef
防止头文件被重复包含或引用
类对象宏(无参宏), 即宏名之后不带参数,只是简单的文本替换
其定义的一般形式为:
#define 标识符 字符串
下面来看段代码
#include
#include
#define 整型 int
#define N 10
#define str "哈哈哈"
#define D1 double*
typedef double* D2;
typedef char 字符型;
int main() {
整型 num = 5;
printf("num = %d\n%d\n", num, sizeof(整型));
printf("N = %d\n", N);
printf("str :%s\n", str);
//double* a1, b1;
D1 a1, b1;
D2 a2, b2;
字符型 ch = 'a';
printf("ch = %c\n", ch);
printf("%d %d\n", sizeof(a1), sizeof(b1));
printf("%d %d\n", sizeof(a2), sizeof(b2));
system("pause");
return 0;
}
我们可以看到, 宏是在进行着文本替换(预处理阶段)
这里要注意一下typedef和#define的区别, 在程序运行过程中我们发现, #define只是进行了文本替换
D1 a1, b1;语句与double* a1, b1无异, a1为double* 类型, b1为double类型, 要都为指针类型, 应double *a1,*b1;定义或分开定义
double* a1;double* b1;
而typedef double* D2; 语句将D2作为double* 类型的别名, 所以a2, b2这两个变量都是double* 类型, 在给变量类型起别名时尽量用typedef
C/C++允许宏带有参数。
带参数的宏定义,宏名中不能有空格,宏名与形参表之间也不能有空格,而形参表中形参之间可以有空格, 在行末不必加分号,如加上分号则在预处理时连分号也一起添加到代码中。
约定 : 一般来说函数和类函数宏使用语法很相近, 所以语言本身无法帮我们区分二者, 那我们平时的一个习惯是:
把宏名全部大写 函数名不要全部大写
直接来看一段代码
代码1:
#include
#include
#define M1(a,b) a*b
#define M2(a,b) (a)*(b)
#define M3(a,b) ((a)*(b))
#define A1(a,b) a+b
#define A2(a,b) (a)+(b)
#define A3(a,b) ((a)+(b))
int main() {
int i = 4;
int j = 5;
printf("%d\t%d\t%d\n", M1(i, j), M2(i, j), M3(i, j));//结果20,20,20
printf("%d\n", M1(i + i, j + j));//预期结果80,实际处理为i+i*j+j=4+20+5=29
printf("%d\n", M2(i + i, j + j));//预期结果80,实际处理为(i+i)*(j+j)*(i+i)*(j+j)=80
printf("%d\n", M3(i + i, j + j));//预期结果80,实际处理为((i+i)*(j+j)*(i+i)*(j+j))=80
printf("%d\t%d\t%d\n", A1(i, j), A2(i, j), A3(i, j));//结果9,9,9
printf("%d\n", A1(i, j)*A1(i, j));//预期结果9*9=81,实际处理为i+j*i+j=29
printf("%d\n", A2(i, j)*A2(i, j));//预期结果9*9=81,实际处理为(i)+(j)*(i)+(j)=29
printf("%d\n", A3(i, j)*A3(i, j));//预期结果9*9=81,实际处理为((i)+(j))*((i)+(j))=81
system("pause");
return 0;
}
我们发现, 类函数宏也是在预编译阶段进行着简单的替换 , 所以会出现M1(i + i, j + j)结果为29, 是因为编译器在预处理阶段将M1(i + i, j + j)替换成了i + i * j + j 即4 + 4 * 5 + 5 ;真是好粗暴的的文本(代码)替换, 同样A2和A3也存在这样的问题, 为了避免这样的错误产生, 我们可以像M3, A3那样处理, 就是给每个参数加括号, 再整体加括号, 就不会出现替换后运算级的问题了, 这种处理方法也最保险 .
类函数宏还可以定义多个语句
注意: 如果宏定义太长, 可以分成几行书写, 除了最后一行外, 其余每行都在末尾加上 \ (反斜杠)(续行符)
看这段代码
代码2:
#include
#include
#define M(a,b,c,d) a=c+d;b=c-d
#define PRINT_1(a,b) \
printf(#a"+"#b"=%d\n",(a)+(b))
#define SUM_PRINT(x,y,z) x##y += z;\
printf(#x#y"=%d\n",x##y)
int main() {
int a, b, c, d, ab;
c = 6;
d = 2;
ab = 0;
M(a, b, c, d);
printf("%d %d %d %d\n", a, b, c, d);
PRINT_1(a, b);
SUM_PRINT(a, b, 5);
system("pause");
return 0;
}
代码中用到了#与##, 分别是啥意思呢
把一个宏参数变成对应的字符串
##可以把位于##两边的字符(串)合成一个符号, 这样的操作必须产生一个合法的标识符,否则是无意义的, 结果是未定义的, 就如上面的代码中a和b(a##b)形成了新的标识符ab, ab是我们事先定义的, 所以有意义且正确
类函数宏通常用于一些简单的运算, 比如说在两个数中找出最大或最小的一个
#define MAX(a,b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
原因有二:
1. 调用函数(需要开辟内存,释放内存等)可能比执行这个带参数的宏(插入一小段代码)进行小型计算所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等可以用于大小比较的类型。宏是类型无关的。当然和宏相比函数也有劣势的地方:
1. 每次使用宏的时候,一个宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是不方便调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。宏有时候还可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
什么意思呢, 举个例子:#include
#include #define MALLOC(num, type)\ (type *)malloc(num * sizeof(type)) int main() { int* p; p = MALLOC(10, int);//类型作为参数 for (int i = 0; i < 10; ++i) { *(p + i) = i + 1; } for (int i = 0; i < 10; ++i) { printf("%d ", *(p + i) = i + 1); } printf("\n"); system("pause"); return 0; } 宏真的是太神奇了, 在刚刚学习到这些内容时, 感觉无所不能啊
刚看完一个宏让函数无法望其项背的优点, 那再来看一个函数让宏可望不可即的地方
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,
导致不可预测的后果。例如:x+1;//不带副作用 x++;//带有副作用
直接来看一段代码
#include
#include #define MAX(a,b) ((a)>(b)?(a):(b)) int mymax(int a, int b) { return a > b ? a : b; } int main() { int x1, y1, z1; int x2, y2, z2; x1 = 4; y1 = 5; z1 = MAX(x1++, y1++); x2 = 4; y2 = 5; z2 = mymax(x2++, y2++); printf("x1=%d, y1=%d, z1=%d\n", x1, y1, z1); printf("x2=%d, y2=%d, z2=%d\n", x2, y2, z2); system("pause"); return 0; } 我们发现函数运算结果与我们预期一样, 但宏出现的错误,
原因是MAX进行了(x1++)>(y1++)?(x1++):(y1++) 即 4 > 5 ? 5 : 6 , 之后又将6(y1++)的值赋给z1, 所以z1等于6, 但y1++又在赋值过程中执行了一遍, 所以结果为7
类函数宏(带参数的宏)和函数的对比总结
属性 | #define宏定义 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长, 重复使用时代码会重复增多 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码,当重复使用时代码不会重复增多 |
执行速度 | 直接执行,更快 | 存在函数的的调用和返回的开销,所以相对慢一点 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用传参时求一次值, 会将这个值传给实参, 函数的执行结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在函数调用传参时求一次值, ,运行结果更容易控制 |
参数类型 | 1.宏的参数与类型无关,只要对参数的操作是合法的,就可以使用任何类型可以操作的参数 2.可以是只是类型(如 int) |
1.函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数(C中为不同名的函数, C++中可以函数重载,可以重名),即使他们执行的任务是不同的。 2.不能只是类型 |
调试 | 不方便调试 | 函数是可以逐语句调试的 |
递归 | 不能 | 能 |
这条指令用于移除一个宏定义。
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
直接上代码:#include
#include #define M 10 int main() { printf("%d\n", M); #undef M #define M "#undef用于移除一个宏定义" printf("%s\n", M); system("pause"); return 0; }
#include文件的一个不利之处在于一个头文件可能会被多次包含,为了说明这种错误,考虑下面的代码:
#include "x.h"
#include "x.h"
显然,这里文件x.h被包含了两次,没有人会故意编写这样的代码。但是下面的代码:
#include "a.h"
#include "b.h"
看上去没什么问题。如果a.h和b.h中都包含了一个头文件x.h。那么x.h在此也同样被包含了两次,只不过它的形式不是那么明显而已。
多重包含在绝大多数情况下出现在大型程序中,它往往需要使用很多头文件,因此要发现重复包含并不容易。
解决方法有如下 :
只要在头文件的最开始加入这条指令就能够保证头文件被编译一次
#pragma once
我们可以使用条件编译。头文件都像下面这样编写:
#ifndef _HEADERNAME_H
#define _HEADERNAME_H
...//(头文件内容)
#endif
那么多重包含的危险就被消除了。当头文件第一次被包含时,它被正常处理,符号_HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号_HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。
但是,必须记住预处理器仍将整个头文件读入,即使这个头文件所有内容将被忽略。由于这种处理将托慢编译速度,所以如果可能,应该避免出现多重包含。