Skip to content

Latest commit

 

History

History
175 lines (116 loc) · 9.32 KB

File metadata and controls

175 lines (116 loc) · 9.32 KB

#Define

#define 最常见的用法是为数值命名一个符号,本文将介绍 #define 更多的用途。首先,观察一下它的正式描述:

#define name stuff

有了这条指令以后,每当有符号 name 出现在这条指令后面时,预处理器就会把它替换成 stuff 。

替换文本并不仅限于数值字面值常量。使用 #define 指令,可以把任何文本替换到程序中。这里有几个例子:

#define reg         register
#define do_forever  for(;;)
#define CASE        break;case

第一个定义知识为关键字 register 创建了一个简短的别名。这个较短的别名使各个声明更容易通过制表符进行排列。第二个声明用一个更具描述性的符号来代替一种用于实现无限循环的 for 语句类型。最后一个声明定义了一种简短记法,以便在 switch 语句中使用。它自动地把一个 break 放在每个 case 之前,使得 switch 语句看上去更像其他语言的 switch 语句。

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏( macro )。下面使宏的声明方式:

#define name(parameter-list) stuff

其中, parameter-list (参数列表)是一个由逗号分隔的符号列表,它们可能出现在 stuff 中。参数列表的左括号必须要与 name 紧邻。如果两者之间又任何空白存在,参数列表就会被解释为 stuff 的一部分。

当宏被调用时,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应,整个列表用一对括号包围。当参数出现在程序中时,与每个参数对应的实际值都将被替换到 stuff 中。

这里有一个宏,它接受一个参数:

#define SQUARE(x) x * x

如果在上述声明之后,把 SQUARE(5) 置于程序中,预处理器就会用下面这个表达式替换上面的表达式: 5 * 5

但是这个宏存在一个问题。观察下面的代码段:

a = 5;
printf("%d\n", SQUARE(a + 1));

乍一看,可能觉得这段代码将打印 36 这个值。事实上,它将打印 11。想知道为什么?请观察被替换的宏文本。参数 x 被文本 a + 1 替换,所以这条语句实际上变成了 printf("%d\n", a + 1 * a + 1); 。现在问题清楚了:由替换产生的表达式并没有按照预想的次序进行求值。

在宏定义中加上两个括号,这个问题便很轻松地解决了: #define SQUARE(x) (x) * (x) 。在前面那个例子中,预处理器现在将用下面这条语句执行替换,从而产生预期的结果: printf("%d\n", (a + 1) * (a + 1));

这里有另一个宏定义。

#define DOUBLE(x) (x) + (x)

定义中使用了括号,用于避免前面出现的问题。但是这个宏,可能会出现另外一个不同的错误。下面这段代码将打印出什么值?

a = 5;
printf("%d\n", 10 * DOUBLE(a));

看上去,它好像将打印 100 ,但事实上它打印的是 55 。再一次,通过观察替换产生的文本,能够发现问题所在: printf("%d\n", 10 * (a) + (a)) 。乘法运算在宏所定义的加法运算之前执行。这个错误很容易修正:在定义宏时,只要在整个表达式两边加上一对括号就可以了。 #define DOUBLE(x) ((x) + (x))

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

下面是一对有趣的宏:

#define repeat      do
#define until(x)    while(!(x))

这两个宏创建了一种“新”的循环,其工作过程类似于其他语言中的 repeat/until 循环。它按照下面这种方式使用:

repeat {
    statements
} until(i >= 10);

预处理器将用下面的代码进行替换:

do {
    statements
} while(!(i >= 10));

表达式 1 >= 10 两边的括号用于确保在 ! 操作符执行之前先完成这个表达式的求值。

创建一套 #define 宏,用一种看上去很像其他语言的方式编写 c/cpp 程序是完全可能的。在绝大多数情况下,应该避免这种诱惑,因为这样编写出来的程序使其他 c/cpp 程序员很难理解。他们必须时常查阅这些宏的定义以便弄清实际代码是什么意思。即使每个和这个项目生命周期各个阶段相关的人都熟悉那种被模仿的语言,这个技巧仍然可能产生混淆,因为准确地模仿其他语言的各个方面是极其困难的。

#define 替换

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

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

这样,宏参数和 #define 定义可以包含其他 #define 定义的符号。但是,宏不可以出现递归。

当预处理器搜索 #define 定义的符号时,字符串常量的内容并不进行检查。如果想把宏参数插入到字符串常量中,可以使用两种技巧。

第一个技巧是临近字符串自动连接的特性使得很容易将一个字符串分成几段,每段实际上都是一个宏参数。这里有一个这种技巧:

#define PRINT(FORMAT,VALUE) printf("The value is " FORMAT "\n", VALUE)
...
PRINT("%d", x + 3); // The value is 25

这种技巧只有当字符串常量作为宏参数给出时才能使用。

第二个技巧使用预处理器把一个宏参数转换成字符串。

#argument 这种结构被预编译器翻译为 argument 。这种翻译可以让程序员像这样编写代码:

#define PRINT(FORMAT,VALUE) printf("The value of " #VALUE " is " FORMAT "\n", VALUE)
...
PRINT("%d", x + 3); // The value of x + 3 is 25

## 结构则执行一种不同的任务。它把位于它两边的符号连接成一个符号。作为用途之一,它允许宏定义从分离的文本片段创建标识符。下面例子使用这种连接把一个值添加到几个变量之一:

#define ADD_TO_SUM(sum_number,value) sum ## sum_number += value
...
ADD_TO_DUM(5, 25);

最后一条语句把值 25 加到变量 sum5 。注意这种连接必须产生一个合法的标识符。否则,其结果就是未定义的。

宏与函数

宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大(或较小)的一个:

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

为什么不用函数来完成这个任务呢?有两个原因。首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数再程序的规模和速度方面都更胜一筹。但是,更为重要的是,函数的参数必须声明为一种特定的类型,所以它只能再类型合适的表达式上使用。反之,上面这个宏可以用于整型、长整型、单浮点数、双浮点数一起其他任何可以用 > 操作符比较大小的类型。换句话说,宏是与类型无关的。

和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都将插入到程序中,除非宏非常短,否则使用宏可能大幅度增加程序的长度。

还有一些任务根本无法使用函数实现。例如下面这个例子:

#define MALLOC(n,type) ((type*)malloc((n)*sizeof(type)))

这个宏的第二个参数是一种类型,它无法作为函数参数进行传递。

调用时,下面例子中的第一条语句会被预处理器转换为第二条语句:

pi = MALLOC(25, int);
pi = ((int*)malloc((25)*sizeof(int)));

注意,宏定义并没有用一个分号结尾。分号出现在调用这个宏的语句中。

下面的表格总结了宏与函数的不同之处:

属性 #define 宏 函数
代码长度 每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长。 函数代码只出现在一个地方,每次使用这个函数时,都调用那个地方的同一份代码。
执行速度 更快 存在函数调用/返回的额外开销
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果。 函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
参数求值 参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的结果。 参数在函数被调用前只求值一次。在函数中多次使用参数并不会导致多种求值过程。参数的副作用并不会造成任何特殊的问题。
参数类型 宏与类型无关。只要对参数的操作是合法的,它可以适用于任何参数类型。 函数的参数是与类型相关的。如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。