c语言中的宏定义_第1页
c语言中的宏定义_第2页
c语言中的宏定义_第3页
c语言中的宏定义_第4页
c语言中的宏定义_第5页
已阅读5页,还剩7页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

/24994073_d.html C 语言中的宏定义 14.3 宏定义 我们从第 2 章以来使用的宏被称为简单的宏,它们没有参数。预编译器也 支持带参数的宏。本节会先讨论简单的宏,然后再讨论带参数的宏。在分别讨 论它们之后,我们会研究一下二者共同的特性。 14.3.1 简单的宏 简单的宏定义有如下格式: #define 指令(简单的宏) #define 标识符 替换列表 替换列表是一系列的 C 语言记号,包括标识符、关键字、数、字符常量、 字符串字面量、运算符和标点符号。当预处理器遇到一个宏定义时,会做一个 “标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在任 何位置出现,预处理器都会用替换列表代替它。 不要在宏定义中放置任何额外的符号,否则它们会被作为替换列 表的一部分。一种常见的错误是在宏定义中使用 = : #define N = 100 /* WRONG */ int aN; /* 会成为 int a= 100; */ 在上面的例子中,我们(错误地)把 N 定义成一对记号(= 和 100)。 在宏定义的末尾使用分号结尾是另一个常见错误: #define N 100; /* WRONG */ int aN; /* become int a100; */ 这里 N 被定义为 100 和;两个记号。 在一个宏定义中,编译器可以检测到绝大多数由多余符号所导致 的错误。但不幸的是,编译器会将每一处使用这个宏的地方标为错误, 而不会直接找到错误的根源宏定义本身,因为宏定义已经被预处 理器删除了。 简单的宏主要用来定义那些被 Kernighan 和 Ritchie 称为“明示常量” (manifest constant)的东西。使用宏,我们可以给数值、字符和字符串命名。 #define STE_LEN 80 #define TRUE 1 #define FALSE 0 #define PI 3.14159 #define CR r #define EOS 0 使用#define 来为常量命名有许多显著的优点: l 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则, 程序将包含大量的“魔法数”,使读者难以理解。 l 程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出 现的所有该常量的值。“硬编码的”常量会更难于修改,特别是有时候当他们 以稍微不同的形式出现时。(例如,如果一个程序包含一个长度为 100 的数组, 它可能会包含一个从 0 到 99 的循环。如果我们只是试图找到所有程序中出现的 100,那么就会漏掉 99。) l 可以帮助避免前后不一致或键盘输入错误。假如数值常量 3.14159 在程序中 大量出现,它可能会被意外地写成 3.1416 或 3.14195。 虽然简单的宏常用于定义常量名,但是它们还有其他应用。 l 可以对 C 语法做小的修改。实际上,我们可以通过定义宏的方式给 C 语言符 号添加别名,从而改变 C 语言的语法。例如,对于习惯使用 Pascal 的 begin 和 end(而不是 C 语言的和)的程序员,可以定义下面的宏: #define BEGIN #define END 我们甚至可以发明自己的语言。例如,我们可以创建一个 LOOP“语句”, 来实现一个无限循环: #define LOOP for (;) 当然,改变 C 语言的语法通常不是个好主意,因为它会使程序很难被其 他程序员所理解。 l 对类型重命名。在 5.2 节中,我们通过重命名 int 创建了一个 Boolean 类型: #define BOOL int 虽然有些程序员会使用宏定义的方式来实现此目的,但类型定义(7.6 节) 仍然是定义新类型的最佳方法。 l 控制条件编译。如将在 14.4 节中看到的那样,宏在控制条件编译中起重要的 作用。例如,在程序中出现的宏定义可能表明需要将程序在“调试模式”下进 行编译,来使用额外的语句输出调试信息: #define DEBUG 这里顺便提一下,如上面的例子所示,宏定义中的替换列表为空是合法的。 当宏作为常量使用时,C 程序员习惯在名字中只使用大写字母。但是并没有 如何将用于其他目的的宏大写的统一做法。由于宏(特别是带参数的宏)可能 是程序中错误的来源,所以一些程序员更喜欢使用大写字母来引起注意。其他 人则倾向于小写,即按照 Kernighan 和 Ritchie 编写的 The C Programming Language 一书中的样式。 14.3.2 带参数的宏 带参数的宏定义有如下格式: #define 指令带参数的宏 #define 标识符( x1, x2, xn)替换列 表 其中 x1, x2, xn是标识符(宏的参数)。这些参数可以在替换列表中根据需 要出现任意次。 在宏的名字和左括号之间必须没有空格。如果有空格,预处理器 会认为是在定义一个简单的宏,其中( x1, x2, xn)是替换列表的 一部分。 当预处理器遇到一个带参数的宏,会将定义存储起来以便后面使用。在后 面的程序中,如果任何地方出现了标识符( y1, y2, yn)格式的宏调用(其 中 y1, y2, yn是一系列标记),预处理器会使用替换列表替代,并使用 y1 替换 x1, y2替换 x2,依此类推。 例如,假定我们定义了如下的宏: #define MAX(x,y) (x)(y) ? (x) :(y) #define IS_EVEN(n) (n)%2=0) 现在如果后面的程序中有如下语句: i = MAX(j+k, m-n); if (IS_EVEN(i) i+; 预处理器会将这些行替换为 i = (j+k)(m-n)?(j+k):(m-n); if (i)%2=0) i+; 如这个例子所显示的,带参数的宏经常用来作为一些简单的函数使用。MAX 类 似一个从两个值中选取较大的值的函数。IS_EVEN 则类似于另一种函数,该函 数当参数为偶数时返回 1,否则返回 0。 下面的例子是一个更复杂的宏: #define TOUPPER(c) (a( 23.4 节)中提供了大量的类似的宏。其中之一就是 toupper,与 我们上面的 TOUPPER 例子作用一致(但会更高效,可移植性也更好)。 带参数的宏可以包含空的参数列表,如下例所示: #define getchar() getc(stdin) 空的参数列表不是一定确实需要,但可以使 getchar 更像一个函数。(没错, 这就是中的 getchar,getchar 的确就是个宏,不是函数虽然它 的功能像个函数。) 使用带参数的宏替代实际的函数有两个优点: l 程序可能会稍微快些。一个函数调用在执行时通常会有些额外开销存储 上下文信息、复制参数的值等。而一个宏的调用则没有这些运行开销。 l 宏会更“通用”。与函数的参数不同,宏的参数没有类型。因此,只要预处 理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用 MAX 宏从两个数中选出较大的一个,数的类型可以是 int,long int,float,double 等等。 但是带参数的宏也有一些缺点。 l 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表,由此 导致程序的源代码增加(因此编译后的代码变大)。宏使用得越频繁,这种效 果就越明显。当宏调用嵌套时,这个问题会相互叠加从而使程序更加复杂。思 考一下,如果我们用 MAX 宏来找出 3 个数中最大的数会怎样? n = MAX(i, MAX(j, k); 下面是预处理后的这条语句: n=(i)(j)(k)?(j):(k)?(i):(j)(k)?(j):(k); l 宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确 认它们是否是正确的类型。如果不是,或者将参数转换成正确的类型,或者由 编译器产生一个出错信息。预处理器不会检查宏参数的类型,也不会进行类型 转换。 l 无法用一个指针来指向一个宏。如在 17.7 节中将看到的,C 语言允许指针指 向函数。这一概念在特定的编程条件下非常有用。宏会在预处理过程中被删除, 所以不存在类似的“指向宏的指针”。因此,宏不能用于处理这些情况。 l 宏可能会不止一次地计算它的参数。函数对它的参数只会计算一次,而宏可 能会计算两次甚至更多次。如果参数有副作用,多次计算参数的值可能会产生 意外的结果。考虑下面的例子,其中 MAX 的一个参数有副作用: n = MAX(i+, j); 下面是这条语句在预处理之后的结果: n = (i+)(j)?(i+):(j); 如果 i 大于 j,那么 i 可能会被(错误地)增加了两次,同时 n 可能被赋 予了错误的值。 由于多次计算宏的参数而导致的错误可能非常难于发现,因为宏 调用和函数调用看起来是一样的。更糟糕的是,这类宏可能在大多数 情况下正常工作,仅在特定参数有副作用时失效。为了自保护,最好 避免使用带有副作用的参数。 带参数的宏不仅适用于模拟函数调用。他们特别经常被作为模板,来处理我们 经常要重复书写的代码段。如果我们已经写烦了语句 printf(“%d“n, x); 因为每次要显示一个整数 x 都要使用它。我们可以定义下面的宏,使显示整数 变得简单些: #define PRINT_INT(x) printf(“%dn“, x) 一旦定义了 PRINT_INT,预处理器会将这行 PRINT_INT(i/j); 转换为 printf(“%dn“, i/j); 14.3.3 #运算符 宏定义可以包含两个运算符:#和#。编译器不会识别这两种运算符相反, 它们会在预处理时被执行。 #运算符将一个宏的参数转换为字符串字面量。它仅允许出现在带参数的宏 的替换列表中。 (一些 C 程序员将#操作理解为 “stringization(字符串 化)”;其他人则认为这实在是对英语的滥用。) #运算符有大量的用途,这里只来讨论其中的一种。假设我们决 定在调试过程中使用 PRINT_INT 宏作为一个便捷的方法,来输出一个整型变量 或表达式的值。#运算符可以使 PRINT_INT 为每个输出的值添加标签。下面是改 进后的 PRINT_INT: #define PRINT_INT(x) printf(#x “ = %dn“, x) x 之前的#运算符通知预处理器根据 PRINT_INT 的参数创建一个字符串字面量。 因此,调用 PRINT_INT(i/j); 会变为 printf(“i/j“ “ = %dn“, i/j); 在 C 语言中相邻的字符串字面量会被合并,因此上边的语句等价于: printf(“i/j = %dn“, i/j); 当程序执行时,printf 函数会同时显示表达式 i/j 和它的值。例如,如果 i 是 11,j 是 2 的话,输出为 i/j = 5 14.3.4 #运算符 #运算符可以将两个记号(例如标识符)“粘”在一起,成为一个记号。 (无需惊讶,#运算符被称为“记号粘合”。)如果其中一个操作数是宏参数, “粘合”会在当形式参数被相应的实际参数替换后发生。考虑下面的宏: #define MK_ID(n) i#n 当 MK_ID 被调用时(比如 MK_ID(1)),预处理器首先使用自变量(这个例子中 是 1)替换参数 n。接着,预处理器将 i 和 1 连接成为一个记号(i1)。下面的 声明使用 MK_ID 创建了 3 个标识符: int MK_ID(1), MK_ID(2), MK_ID(3); 预处理后声明变为: int i1, i2, i3; #运算符不属于预处理器经常使用的特性。实际上,想找到一些使用它的情 况是比较困难的。为了找到一个有实际意义的#的应用,我们来重新思考前面 提到过的 MAX 宏。如我们所见,当 MAX 的参数有副作用时会无法正常工作。一 种解决方法是用 MAX 宏来写一个 max 函数。遗憾的是,往往一个 max 函数是不 够的。我们可能需要一个实际参数是 int 值的 max 函数,还需要参数为 float 值的 max 函数,等等。除了实际参数的类型和返回值的类型之外,这些函数都 一样。因此,这样定义每一个函数似乎是个很蠢的做法。 解决的办法是定义一个宏,并使它展开后成为 max 函数的定义。 宏会有唯一的参数 type,它表示形式参数和返回值的类型。这里还有个问题, 如果我们是用宏来创建多个 max 函数,程序将无法编译。(C 语言不允许在同 一文件中出现两个同名的函数。)为了解决这个问题,我们是用#运算符为每 个版本的 max 函数构造不同的名字。下面是宏的显示形式: #define GENERIC_MAX (type) type type#_max(type x, type y) return x y ? x : y; 请注意宏的定义中是如何将 type 和_max 相连来形成新函数名的。 现在,假如我们需要一个针对 float 值的 max 函数。下面是如何使用 GENERIC_MAX 宏来定义函数: GENERIC_MAX(float) 预处理器会将这行展开为下面的代码: float float_max(float x, float y) return x y ? x : y; 14.3.5 宏的通用属性 现在我们已经讨论过简单的宏和带参数的宏了,我们来看一下它们都需要 遵守的规则。 l 宏的替换列表可以包含对另一个宏的调用。例如,我们可以用宏 PI 来定义宏 TWO_PI: #define PI 3.14159 #define TWO_PI (2*PI) 当预处理器在后面的程序中遇到 TWO_PI 时,会将它替换成(2*PI)。接 着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这 个例子中,调用了宏 PI)。 预处理器会不断重新检查替换列表,直 到将所有的宏名字都替换掉为止。 l 预处理器只会替换完整的记号,而不会替换记号的片断。因此,预处理器会 忽略嵌在标识符名、字符常量、字符串字面量之中的宏名。例如,假设程序含 有如下代码行: #define SIZE 256 int BUFFER_SIZE; if (BUFFER_SIZE SIZE) puts (“Error : SIZE exceeded“); 预处理后,这些代码行会变为: int BUFFER_SIZE; if (BUFFER_SIZE 256) puts (“Error :SIZE exceeded“); 标识符 BUFFER_ZISE 和字符串“Error: SIZE exceeded“没有被预处理影 响,虽然它们都包含 SIZE。 l 一个宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器 处理的,他们不遵从通常的范围规则。一个定义在函数中的宏并不是仅在函数 内起作用,而是作用到文件末尾。 l 宏不可以被定义两遍,除非新的定义与旧的定义是一样的。小的间隔上的差 异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。 l 宏可以使用#undef 指令“取消定义”。#undef 指令有如下形式: #undef 指令 #undef 标识符 其中标识符是一个宏名。例如,指令 #undef N 会删除宏 N 当前的定义。(如果 N 没有被定义成一个宏,#undef 指令没 有任何作用。)#undef 指令的一个用途是取消一个宏的现有定义,以便 于重新给出新的定义。 14.3.6 宏定义中的圆括号 在我们前面定义的宏的替换列表中有大量的圆括号。确实需要它们吗?答 案是绝对需要。如果我们少用几个圆括号,宏可能有时会得到意料之外的 而且是不希望有的结果。 对于在一个宏定义中哪里要加圆括号有两条规则要遵守。首先,如果宏的 替换列表中有运算符,那么始终要将替换列表放在括号中: #define TWO_PI (2*3.14159) 其次,如果宏有参数,每次参数在替换列表中出现时都要放在圆括号中: #define SCALE(x) (x)*10) 没有括号的话,我们将无法确保编译器会将替换列表和参数作为完整的表达式。 编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。 为了展示为替换列表添加圆括号的重要性,考虑下面的宏定义,其中的替 换列表没有添加圆括号: #define TWO_PI 2*3.14159 /* 需要给替换列表加圆括号 */ 在预处理时,语句 conversion_factor = 360/TWO_PI; 变为 conversion_factor = 360/2*3.14159; 除法会在乘法之前执行,产生的结果并不是期望的结果。 当宏有参数时,仅给替换列表添加圆括号是不够的。参数的每一次出现都 要添加圆括号。例如,假设 SCALE 定义如下: #define SCALE(x) (x*10) /* 需要给 x 添加括号 */ 在预处理过程中,语句 j = SCALE(i+1); 变为 j = (i+1*10); 由于乘法的优先级比加法高,这条语句等价于 j = i+10; 当然,我们希望的是 j = (i+1)*10; 在宏定义中缺少圆括号会导致 C 语言中最让人讨厌的错误。程序 通常仍然可以编译通过,而且宏似乎也可以工作,仅在少数情况下会 出错。 14.3.7 创建较长的宏 在创建较长的宏时,逗号运算符会十分有用。特别是可以使用逗号运算符 来使替换列表包含一系列表达式。例如,下面的宏会读入一个字符串,再把字 符串显示出来: #define ECHO(s) (get(s), puts(s) gets 函数和 puts 函数的调用都是表达式,因此使用逗号运算符连接它们是合 法的。我们甚至可以把 ECHO 宏当作一个函数来使用: ECHO(str); /* 替换为 (gets(str), puts(str); */ 除了使用逗号运算符,我们也许还可以将 gets 函数和 puts 函数的调用放 在大括号中形成复合语句: #define ECHO(s) gets(s); puts(s); 遗憾的是,这种方式并不奏效。假如我们将 ECHO 宏用于下面的 if 语句: if (echo_flag) ECHO(str); else gets(str); 将 ECHO 宏替换会得到下面的结果: if (echo_flag) gets(str); puts(str); ; else gets(str); 编译器会将头两行作为完整的 if 语句: if (echo_flag) gets(str); puts(str); 编译器会将跟在后面的分号作为空语句,并且对 else 子句产生出错信息,因为 它不属于任何 if 语句。我们可以通过记住永远不要在 ECHO 宏后面加分号来解 决这个问题。但是这样做会使程序看起来有些怪异。 逗号运算符可以解决 ECHO 宏的问题,但并不能解决所有宏的问题。假如一 个宏需要包含一系列的语句,而不仅仅是一系列的表达式,这时逗号运算符就 起不到帮助的作用了。因为它只能连接表达式,不能连接语句。解决的方法是 将语句放在 do 循环中,并将条件设置为假: do . while (0) do 循环必须始终随跟着一个分号,因此我们不会遇到在 if 语句中使用宏那样 的问题了。为了看到这个技巧(嗯,应该说是技术)的实际作用,让我们将它 用于 ECHO 宏中: #define ECHO(s) do gets (s) ; puts (s) ; while (0) 当使用 ECHO 宏时,一定要加分号: ECHO(str); /* becomes do gets(str); puts(str); while (0); */ 14.3.8 预定义宏 在 C 语言中预定义了一些有用的宏,见表

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论