宏定义#define的一些总结

宏定义#define的一些总结


  • 宏定义#define

#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;
}

 宏定义#define的一些总结_第1张图片

我们可以看到, 宏是在进行着文本替换(预处理阶段)

这里要注意一下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;
}

宏定义#define的一些总结_第2张图片

 我们发现, 类函数宏也是在预编译阶段进行着简单的替换 , 所以会出现M1(i + i, j + j)结果为29, 是因为编译器在预处理阶段将M1(i + i, j + j)替换成了i + i * j + j 即4 + 4 * 5 + 5 ;真是好粗暴的的文本(代码)替换, 同样A2A3也存在这样的问题, 为了避免这样的错误产生, 我们可以像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;
}

 宏定义#define的一些总结_第3张图片

 代码中用到了#与##, 分别是啥意思呢

  • #的作用

          把一个宏参数变成对应的字符串

  • ##的作用

          ##可以把位于##两边的字符(串)合成一个符号, 这样的操作必须产生一个合法的标识符,否则是无意义的, 结果是未定义的,      就如上面的代码中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;
}

宏定义#define的一些总结_第4张图片

宏真的是太神奇了, 在刚刚学习到这些内容时, 感觉无所不能啊

刚看完一个宏让函数无法望其项背的优点, 那再来看一个函数让宏可望不可即的地方
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,
导致不可预测的后果。例如:

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;
}

宏定义#define的一些总结_第5张图片

我们发现函数运算结果与我们预期一样, 但宏出现的错误, 
原因是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

这条指令用于移除一个宏定义。

#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;
}

宏定义#define的一些总结_第6张图片


扩展

防止头文件被重复包含或引用

#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 

只要在头文件的最开始加入这条指令就能够保证头文件被编译一次

#pragma once 

 

  •  条件编译

我们可以使用条件编译。头文件都像下面这样编写: 

#ifndef _HEADERNAME_H
#define _HEADERNAME_H

...//(头文件内容)

#endif

那么多重包含的危险就被消除了。当头文件第一次被包含时,它被正常处理,符号_HEADERNAME_H被定义为1。如果头文件被再次包含,通过条件编译,它的内容被忽略。符号_HEADERNAME_H按照被包含头文件的文件名进行取名,以避免由于其他头文件使用相同的符号而引起的冲突。

但是,必须记住预处理器仍将整个头文件读入,即使这个头文件所有内容将被忽略。由于这种处理将托慢编译速度,所以如果可能,应该避免出现多重包含。

你可能感兴趣的:(C语言)