宏定义#define的一些总结

宏定义#define的一些总结


  • 宏定义#define

#define 指令将 标识符 定义为宏,即指示编译器会将之后出现的所有 标识符 都替换为 替换列表,而它也可以被进一步处理

#表示这是一条预处理指令

在C/C++源代码中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。宏定义是由源程序中的宏定义命令完成的。宏代换是由预处理程序自动完成的。

#define的基本语法非常简单, 但完全不影响它的强大
不过有时候正因为宏(#define)的简单、方便和强大,就会导致我们平常在使用的时候,其中会有很多的注意点和细节需要我们去注意,如果不小心将其忽略, 那么可能会带来我们意料之外的不想要的结果。

推荐用宏但不能滥用, 比如在定义常量时
C中const定义的常量比较局限(不能作为定义数组的大小等) ,但在C++中, 常量完全可以用const, 而且宏不做检查,只是代码替换,而const会进行编译检查,const要比宏更安全。所以应尽可能的使用const来代替类对象宏。
链接: C++中举例说明可以使用const代替#define以消除#define的不安全性


目录

类对象宏(无参宏)
类函数宏(带参数的宏)
#的作用
##的作用
类函数宏(带参数的宏)和函数的对比
#undef
防止头文件被重复包含或引用


类对象宏(无参宏)

类对象宏(无参宏), 即宏名之后不带参数,只是简单的文本替换

其定义的一般形式为:

#define 标识符 字符串       

下面来看段代码

#include<stdio.h>
#include<stdlib.h>
#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<stdio.h>
#include<stdlib.h>
#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 ;真是好粗暴的的文本(代码)替换, 同样A2A3也存在这样的问题, 为了避免这样的错误产生, 我们可以像M3, A3那样处理, 就是给每个参数加括号, 再整体加括号, 就不会出现替换后运算级的问题了, 这种处理方法也最保险 . 

类函数宏还可以定义多个语句

注意: 如果宏定义太长, 可以分成几行书写, 除了最后一行外, 其余每行都在末尾加上 \ (反斜杠)(续行符)

看这段代码
代码2:

#include<stdio.h>
#include<stdlib.h>
#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<stdio.h>
#include<stdlib.h>
#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<stdio.h>
#include<stdlib.h>
#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

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

#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
直接上代码:

#include<stdio.h>
#include<stdlib.h>
#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 

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

#pragma once 

 

  •  条件编译

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

#ifndef _HEADERNAME_H
#define _HEADERNAME_H

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

#endif

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

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

  • 40
    点赞
  • 169
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值