目录

前言

1.#define定义标识符

2. #define定义宏

3.#define 替换规则

4.宏和函数对比


前言

在很多C程序中,我们会在一个源文件的开头看到#define的字眼,和define的英文解释相同——可以理解为给xxx下定义,这里可以是标识符甚至是一个函数,接下来的内容将会对#define进行详解

1.#define定义标识符

使用#define可以用来定义标识符,所谓标识符可以简单理解为一个能够表示一定内容的符号,其格式为

#定义   标识符 内容#define name  stuff

简单介绍几个例子

#define MAX 100            // 将 MAX 这个标识符和 100 这个数字关联起来#define REG register       // 为 register 这个关键字,创建一个简短的名字REG#define STR "test_string"  // 用 STR 这样一个名字来代替 test_string 这样一个字符串int main(){REG int a = MAX;       // 这里的 reg 被解释成 register关键字,MAX 被解释成 100printf("%d\n", a);printf("%s\n", STR);     return 0; }

这样一段代码的输出是

由于预先对标识符进行了定义,在程序的预处理阶段,编译器会将所有定义的标识符替换成相应的内容并生成 .i 文件,使用vs打开 .i 文件,可以看到我们的源代码在经过预处理后变成了下面的代码

int main(){register int a = 100;       printf("%d\n", a);printf("%s\n", "test_string");return 0;}

可以直观地看出,#define定义标识符的本质就是替换


2. #define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏 (define macro)

#define 定义宏可分为两种

  1. 一种是不带参数的宏定义,这也就是第1小节提到的使用#define定义标识符
  2. 第二种是带参数的宏定义,其定义格式如下
#定义   宏名(参数表)           内容#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中

#define ADD(a,b) a + b

这个例子定义了一个名为ADD的宏,宏的参数有两个 a 和 b,这个宏所要实现的功能是将 a 和 b 相加

如何使用这个宏呢?对比下面两段代码

#define ADD(a,b) (a + b)int main(){int a = 10;int b = 20;int sum = ADD(a, b);printf("%d\n", sum);return 0;}
int ADD(int x, int y){return x + y;}int main(){int a = 10;int b = 20;int sum = ADD(a, b);printf("%d\n", sum);return 0;}

其实,宏的使用和我们已知的C语言中的函数类似,都需要进行传参,对比函数版本的求两数之和,可以看到在main函数中,两者的实现方式是几乎一样的,但在具体的实现方式却很不一样

宏定义的程序中,预处理器会将ADD(a,b)替换成 (a + b),.i文件中的代码为

int main(){int a = 10;int b = 20;int sum = (a + b);printf("%d\n", sum);return 0;}

宏定义是将 a + b这样一个求和操作重新命名并置于main函数中,而ADD函数是通过函数调用来实现求和,使用了新的函数栈帧

注意

  • 参数列表的左括号必须与name紧邻
  • 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#define ADD (a,b)在这个宏定义中,ADD与()并不直接相连,中间存在空格,因此(a,b)不是宏ADD的参数,而是变成了stuff

考虑下面的代码

#define SQUAREA(x) (x * x)#define SQUAREB(x) ((x) * (x)) int main(){int ansa = SQUAREA(3 + 1);int ansb = SQUAREB(3 + 1);printf("%d\n%d\n", ansa, ansb);return 0;}

ansa 和 ansb 的输出分别是 7 和 16

这一点似乎出人意料,实际上,宏就是替换,并且是相当直接的替换,上述代码中最重要的一行代码被分别解释为

int ansa = (3 + 1 * 3 + 1)     // 7int ansb = ((3 + 1) * (3 + 1)) // 16

因此,所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

3.#define 替换规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程

注意

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

4.宏和函数对比

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:

  • 把宏名全部大写
  • 函数名不要全部大写

宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个

#define MAX(a, b) ((a)>(b)?(a):(b))

那为什么不用函数来完成这个任务?原因有二:

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。
    所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

宏的缺点:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是没法调试的
  3. 宏由于类型无关,也就不够严谨
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到

#define MALLOC(num, type)\(type *)malloc(num * sizeof(type))...//使用MALLOC(10, int);//类型作为参数//预处理器替换之后:(int*)malloc(10 * sizeof(int));

宏和函数的对比

属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的