C中的宏

1. 简单宏定义

 

简单的宏定义有如下格式:

[#define指令(简单的宏)]  #define  标识符替换列表

替换列表是一系列的C语言记号,包括标识符、关键字、数、字符常量、字符串字面量、运算符和标点符号。当预处理器遇到一个宏定义时,会做一个 “标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在任何位置出现,预处理器都会用替换列表代替它。

 

不要在宏定义中放置任何额外的符号,否则它们会被作为替换列表的一部分。一种常见的错误是在宏定义中使用 = :

 

  1. #define N = 100       /*** WRONG ***/  
  2. int a[N];            /* 会成为 int a[= 100]; */  

 

 

在上面的例子中,我们(错误地)把N定义成一对记号(= 和100)。

在宏定义的末尾使用分号结尾是另一个常见错误:

  1. #define N 100;       /*** WRONG ***/  
  2. int a[N];            /*    become int a[100;]; */  

这里N被定义为100和;两个记号。

在一个宏定义中,编译器可以检测到绝大多数由多余符号所导致的错误。但不幸的是,编译器会将每一处使用这个宏的地方标为错误,而不会直接找到错误的根源——宏定义本身,因为宏定义已经被预处理器删除了。

简单的宏主要用来定义那些被Kernighan和Ritchie称为“明示常量”(manifest constant)的东西。使用宏,我们可以给数值、字符和字符串命名。

  1. #define STE_LEN 80  
  2.   
  3. #defineTRUE     1  
  4.   
  5. #defineFALSE    0  
  6.   
  7. #definePI        3.14159  
  8.   
  9. #defineCR        '\r'  
  10.   
  11. #defineEOS       '\0'  

使用#define来为常量命名有许多显著的优点:

1) 、 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的“魔法数”,使读者难以理解。

2) 、 程 序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。“硬编码的”常量会更难于修改,特别是有时候当他们以稍微不同的 形式出现时。(例如,如果一个程序包含一个长度为100的数组,它可能会包含一个从0到99的循环。如果我们只是试图找到所有程序中出现的100,那么就 会漏掉99。)

3) 、可以帮助避免前后不一致或键盘输入错误。假如数值常量3.14159在程序中大量出现,它可能会被意外地写成3.1416或3.14195。

虽然简单的宏常用于定义常量名,但是它们还有其他应用。

4) 、可以对C语法做小的修改。实际上,我们可以通过定义宏的方式给C语言符号添加别名,从而改变C语言的语法。例如,对于习惯使用Pascal的begin和end(而不是C语言的{和})的程序员,可以定义下面的宏:

  1. #define BEGIN  {  
  2.   
  3. #define END    }  

我们甚至可以发明自己的语言。例如,我们可以创建一个LOOP“语句”,来实现一个无限循环:

#define LOOP   for (;;)

当然,改变C语言的语法通常不是个好主意,因为它会使程序很难被其他程序员所理解。

5) 、对类型重命名。在5.2节中,我们通过重命名int创建了一个Boolean类型:

  1. #define BOOL int  

虽然有些程序员会使用宏定义的方式来实现此目的,但类型定义(7.6节)仍然是定义新类型的最佳方法。

6) 、控制条件编译。如将在14.4节中看到的那样,宏在控制条件编译中起重要的作用。例如,在程序中出现的宏定义可能表明需要将程序在“调试模式”下进行编译,来使用额外的语句输出调试信息:

  1. #define DEBUG  

这里顺便提一下,如上面的例子所示,宏定义中的替换列表为空是合法的。

当宏作为常量使用时,C程序员习惯在名字中只使用大写字母。但是并没有如何将用于 其他目的的宏大写的统一做法。由于宏(特别是带参数的宏)可能是程序中错误的来源,所以一些程序员更喜欢使用大写字母来引起注意。其他人则倾向于小写,即 按照Kernighan和Ritchie编写的The C Programming Language一书中的样式。

 

 

2. 带参数的宏

带参数的宏定义有如下格式:

[#define指令—带参数的宏]  #define 标识符(x1, x2,…,xn)替换列表

其中x1, x2,…,xn是标识符(宏的参数)。这些参数可以在替换列表中根据需要出现任意次。

 

在宏的名字和左括号之间必须没有空格。如果有空格,预处理器会认为是在定义一个简单的宏,

 

#define kSquare( a ) ((a) * (a))

and this macro

#define kSquare ( a ) ((a) * (a))第二个是错的,宏名字后有空格。

 

其中(x1,x2,…,xn)是替换列表的一部分。

当预处理器遇到一个带参数的宏,会将定义存储起来以便后面使用。在后面的程序中,如果任何地方出现了标识符(y1,y2,…,yn)格式的宏调用(其中y1,y2,…,yn是一系列标记),预处理器会使用替换列表替代,并使用y1替换x1y2替换x2,依此类推。

例如,假定我们定义了如下的宏:

  1. #define MAX(x,y)    ((x)>(y) ? (x) :(y))  
  2.   
  3. #define IS_EVEN(n)   ((n)%2==0)  

现在如果后面的程序中有如下语句:

  1. i = MAX(j+k, m-n);  
  2.   
  3. if (IS_EVEN(i)) i++;  

预处理器会将这些行替换为

  1. i = ((j+k)>(m-n)?(j+k):(m-n));  
  2. if (((i)%2==0)) i++;  

如这个例子所显示的,带参数的宏经常用来作为一些简单的函数使用。MAX类似一个从两个值中选取较大的值的函数。IS_EVEN则类似于另一种函数,该函数当参数为偶数时返回1,否则返回0。

下面的例子是一个更复杂的宏:

  1. #define TOUPPER(c)('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))  

这个宏检测一个字符c是否在'a'与'z'之间。如果在的话,这个宏会用'c'减 去'a'再加上'A',来计算出c所对应的大写字母。如果c不在这个范围,就保留原来的c。像这样的字符处理的宏非常有用,所以C语言库 在<ctype.h>(23.4节)中提供了大量的类似的宏。其中之一就是toupper,与我们上面的TOUPPER例子作用一致(但会更 高效,可移植性也更好)。

带参数的宏可以包含空的参数列表,如下例所示:

  1. #define getchar() getc(stdin)  

空的参数列表不是一定确实需要,但可以使getchar更像一个函数。(没错,这就是<stdio.h>中的getchar,getchar的确就是个宏,不是函数——虽然它的功能像个函数。)

          使用带参数的宏替代实际的函数的优点

1) 、  程序可能会稍微快些。一个函数调用在执行时通常会有些额外开销——存储上下文信息、复制参数的值等。而一个宏的调用则没有这些运行开销。

2) 、 宏会更“通用”。与函数的参数不同,宏的参数没有类型。因此,只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用MAX宏从两个数中选出较大的一个,数的类型可以是int,long int,float,double等等。

        但是带参数的宏也有一些缺点。

1) 、 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表,由此导致程序的源代码增加(因此编译后的代码变大)。宏使用得越频繁,这种效果就越明 显。当宏调用嵌套时,这个问题会相互叠加从而使程序更加复杂。思考一下,如果我们用MAX宏来找出3个数中最大的数会怎样?

  1. n = MAX(i, MAX(j,k));  

下面是预处理后的这条语句:

  1. n=((i)>(((j)>(k)?(j):(k)))?(i):(((j)>(k)?(j):(k))));  

2) 、宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确认它们是否是正确的类型。如果不是,或者将参数转换成正确的类型,或者由编译器产生一个出错信息。预处理器不会检查宏参数的类型,也不会进行类型转换。

3) 、无法用一个指针来指向一个宏。如在17.7节中将看到的,C语言允许指针指向函数。这一概念在特定的编程条件下非常有用。宏会在预处理过程中被删除,所以不存在类似的“指向宏的指针”。因此,宏不能用于处理这些情况。

4) 、宏可能会不止一次地计算它的参数。函数对它的参数只会计算一次,而宏可能会计算两次甚至更多次。如果参数有副作用,多次计算参数的值可能会产生意外的结果。考虑下面的例子,其中MAX的一个参数有副作用:

  1. n = MAX(i++, j);  

下面是这条语句在预处理之后的结果:

  1. n =((i++)>(j)?(i++):(j));  

如果i大于j,那么i可能会被(错误地)增加了两次,同时n可能被赋予了错误的值。

 

由于多次计算宏的参数而导致的错误可能非常难于发现,因为宏调用和函数调用看起来是一样的。更糟糕的是,这类宏可能在大多数情况下正常工作,仅在特定参数有副作用时失效。为了自保护,最好避免使用带有副作用的参数。

带参数的宏不仅适用于模拟函数调用。他们特别经常被作为模板,来处理我们经常要重复书写的代码段。如果我们已经写烦了语句

 

  1. printf("%d"\n, x);  

 

因为每次要显示一个整数x都要使用它。我们可以定义下面的宏,使显示整数变得简单些:

 

  1. #define PRINT_INT(x)    printf("%d\n", x)  

 

一旦定义了PRINT_INT,预处理器会将这行

 

  1. PRINT_INT(i/j);  
  2. //转换为  
  3. printf("%d\n", i/j);  

 

3. #运算符

       宏定义可以包含两个运算符:#和##。编译器不会识别这两种运算符相反,它们会在预处理时被执行

#运算符将一个宏的参数转换为字符串字面量(字符串字面量(string literal)是指双引号引住的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多个字符),, 简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号. 它仅允许出现在带参数的宏的替换列表中。(一些C程序员将#操作理解为“stringization(字符串化)”;其他人则认为这实在是对英语的滥用。)用比较官方的话说就是将语言符号(Token)转化为字符串。

        #运算符有大量的用途,这里只来讨论其中的一种。假设我们决定在调试过程中使用PRINT_INT宏作为一个便捷的方法,来输出一个整型变量或表达式的值。#运算符可以使PRINT_INT为每个输出的值添加标签。下面是改进后的PRINT_INT:

 

  1. #define PRINT_INT(x) printf(#x " = %d\n", x)  

 

x之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字符串字面量。因此,调用

 

  1. PRINT_INT(i/j);  
  2. //会变为  
  3. printf("i/j" " = %d\n", i/j);  

 

在C语言中相邻的字符串字面量会被合并,因此上边的语句等价于:

 

  1. printf("i/j = %d\n", i/j);  

 

当程序执行时,printf函数会同时显示表达式i/j和它的值。例如,如果i是11,j是2的话,输出为

i/j = 5

TIPI例子:

 

  1. #define STR(x) #x  
  2.    
  3. int main(int argc char** argv)  
  4. {  
  5.     printf("%s\n", STR(It's a long string)); // 输出 It's a long str  
  6.     return 0;  
  7. }  

 

4. ##运算符

 

         在C语言的宏中,"##"被称为 连接符(concatenator),它是一种预处理运算符, 用来把两个语言符号(Token)组合成单个语言符号。 这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在.

##运算符可以将两个记号(例如标识符)“粘”在一起,成为一个记号。(无需惊讶,##运算符被称为“记号粘合”。)如果其中一个操作数是宏参数,“粘合”会在当形式参数被相应的实际参数替换后发生。考虑下面的宏:

如下例子:当MK_ID被调用时(比如MK_ID(1)),预处理器首先使用自变量(这个例子中是1)替换参数n。接着,预处理器将i和1连接成为一个记号(i1)。下面的声明使用MK_ID创建了3个标识符:

 

  1. #define MK_ID(n) i##n  
  2. int MK_ID(1), MK_ID(2), MK_ID(3);  
  3. //预处理后声明变为:  
  4. int i1, i2, i3;  

 

        ##运算符不属于预处理器经常使用的特性。实际上,想找到一些使用它的情况是比较困难的。为了找到一个有实际意义的##的应用,我们来重新思考前面提到过 的MAX宏。如我们所见,当MAX的参数有副作用时会无法正常工作。一种解决方法是用MAX宏来写一个max函数。遗憾的是,往往一个max函数是不够 的。我们可能需要一个实际参数是int值的max函数,还需要参数为float值的max函数,等等。除了实际参数的类型和返回值的类型之外,这些函数都 一样。因此,这样定义每一个函数似乎是个很蠢的做法。

         解决的办法是定义一个宏,并使它展开后成为max函数的定义。宏会有唯一的参数type,它表示形式参数和返回值的类型。这里还有个问题,如果我们是用 宏来创建多个max函数,程序将无法编译。(C语言不允许在同一文件中出现两个同名的函数。)为了解决这个问题,我们是用##运算符为每个版本的max函 数构造不同的名字。下面的例子:请注意宏的定义中是如何将type和_max相连来形成新函数名的。假如我们需要一个针对float值的max函数。

 

  1. #define GENERIC_MAX (type)           \  
  2. type type##_max(type x,  type y)    \  
  3. {                                      \  
  4.   return x > y ? x :y;              \  
  5. }  
  6. GENERIC_MAX(float)  

 

//预处理器会将这行展开为下面的代码:
float float_max(float x, float y) { return x > y ? x :y; }

再如:

 

  1. #define PHP_FUNCTION            ZEND_FUNCTION  
  2. #define ZEND_FUNCTION(name)             ZEND_NAMED_FUNCTION(ZEND_FN(name))  
  3. #define ZEND_FN(name) zif_##name  
  4. #define ZEND_NAMED_FUNCTION(name)       void name(INTERNAL_FUNCTION_PARAMETERS)  
  5. #define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value, zval **return_value_ptr, \  
  6. zval *this_ptr, int return_value_used TSRMLS_DC  
  7.    
  8. PHP_FUNCTION(count);  

 

//  预处理器处理以后, PHP_FUCNTION(count);就展开为如下代码

  1. void zif_count(int ht, zval *return_value, zval **return_value_ptr,  
  2.         zval *this_ptr, int return_value_used TSRMLS_DC)  

 

宏ZEND_FN(name)中有一个"##", 它的作用一如之前所说,是一个连接符,将zif和宏的变量name的值连接起来。 以这种连接的方式以基础,多次使用这种宏形式,可以将它当作一个代码生成器,这样可以在一定程度上减少代码密度, 我们也可以将它理解为一种代码重用的手段,间接地减少不小心所造成的错误。

 

5. 宏的通用属性

 

现在我们已经讨论过简单的宏和带参数的宏了,我们来看一下它们都需要遵守的规则。

1) 、宏的替换列表可以包含对另一个宏的调用。例如,我们可以用宏PI来定义宏TWO_PI:

#definePI      3.14159

#defineTWO_PI  (2*PI)

当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换掉为止。

 


2) 、预处理器只会替换完整的记号,而不会替换记号的片断。因此,预处理器会忽略嵌在标识符名、字符常量、字符串字面量之中的宏名。例如,假设程序含有如下代码行:

  1. #define SIZE 256  
  2. int BUFFER_SIZE;  
  3. if (BUFFER_SIZE> SIZE)  
  4.    puts("Error : SIZEexceeded");  
  5. //预处理后,这些代码行会变为:  
  6. int BUFFER_SIZE;  
  7. if (BUFFER_SIZE> 256)  
  8.   puts("Error :SIZEexceeded");  

标识符BUFFER_ZISE和字符串"Error:SIZE exceeded"没有被预处理影响,虽然它们都包含SIZE。

 

3) 、一个宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,他们不遵从通常的范围规则。一个定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。

4) 、宏不可以被定义两遍,除非新的定义与旧的定义是一样的。小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。

5) 、宏可以使用#undef指令“取消定义”。#undef指令有如下形式:
[#undef指令]  #undef  标识符 
其中标识符是一个宏名。例如,指令
#undef N
会删除宏N当前的定义。(如果N没有被定义成一个宏,#undef指令没有任何作用。)#undef指令的一个用途是取消一个宏的现有定义,以便于重新给出新的定义。

 

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语言中最让人讨厌的错误。程序通常仍然可以编译通过,而且宏似乎也可以工作,仅在少数情况下会出错。

 

7. 创建较长的宏

 

1. 较长的宏中的逗号运算符

      在创建较长的宏时,逗号运算符会十分有用。特别是可以使用逗号运算符来使替换列表包含一系列表达式。例如,下面的宏会读入一个字符串,再把字符串显示出来:

#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语句:

  1. if (echo_flag)  
  2.   ECHO(str);  
  3. else  
  4.   gets(str);  
  5. //将ECHO宏替换会得到下面的结果:  
  6. if (echo_flag)  
  7.   { gets(str); puts(str);  };  
  8. else  
  9.   gets(str);  

 

编译器会将头两行作为完整的if语句:

  1. if (echo_flag)  
  2.   { gets(str);  puts(str);  }  

        编译器会将跟在后面的分号作为空语句,并且对else子句产生出错信息,因为它不属于任何if语句。我们可以通过记住永远不要在ECHO宏后面加分号来解 决这个问题。但是这样做会使程序看起来有些怪异。逗号运算符可以解决ECHO宏的问题,但并不能解决所有宏的问题。假如一个宏需要包含一系列的语句,而不 仅仅是一系列的表达式,这时逗号运算符就起不到帮助的作用了。因为它只能连接表达式,不能连接语句。解决的方法是将语句放在do循环中,并将条件设置为 假:

2. 宏定义中的do-while循环do 

do循环必须始终随跟着一个分号,因此我们不会遇到在if语句中使用宏那样的问题了。为了看到这个技巧(嗯,应该说是技术)的实际作用,让我们将它用于ECHO宏中:

  1. #define ECHO(s)       \  
  2.       do{           \  
  3.            gets (s) ;      \  
  4.            puts (s) ;      \  
  5.       } while  (0)  

当使用ECHO宏时,一定要加分号:

ECHO(str);

  /* becomes do {  gets(str); puts(str); } while (0);  */

为什么在宏定义时需要使用do-while语句呢? 我们知道do-while循环语句是先执行循环体再判断条件是否成立, 所以说至少会执行一次。当使用do{ }while(0)时由于条件肯定为false,代码也肯定只

 

执行一次, 肯定只执行一次的代码为什么要放在do-while语句里呢? 这种方式适用于宏定义中存在多语句的情况。 如下所示代码:

  1. #define TEST(a, b)  a++;b++;  
  2.    
  3. if (expr)  
  4.     TEST(a, b);  
  5. else  
  6.     do_else();  
  7. 代码进行预处理后,会变成:  
  8. if (expr)  
  9.     a++;b++;  
  10. else  
  11.     do_else();  

 

      这样if-else的结构就被破坏了if后面有两个语句,这样是无法编译通过的,那为什么非要do-while而不是简单的用{}括起来呢。 这样也能保证if后面只有一个语句。例如上面的例子,在调用宏TEST的时候后面加了一个分号, 虽然这个分号可有可无, 但是出于习惯我们一般都会写上。 那如果是把宏里的代码用{}括起来,加上最后的那个分号。 还是不能通过编译。 所以一般的多表达式宏定义中都采用do-while(0)的方式。

3. "空操作"的定义

      了解了do-while循环在宏中的作用,再来看"空操作"的定义。在PHP源码中,由于PHP需要考虑到平台的移植性和不同的系统配置, 所以需要在某些时候把一些宏的操作定义为空操作。例如在sapi\thttpd\thttpd.c

文件中的VEC_FREE():

  1. #ifdef SERIALIZE_HEADERS  
  2.     # define VEC_FREE() smart_str_free(&vec_str)  
  3. #else  
  4.     # define VEC_FREE() do {} while (0)  
  5. #endif  

 

这里涉及到条件编译,在定义了SERIALIZE_HEADERS宏的时候将VEC_FREE()定义为如上的内容,而没有定义时, 不需要做任何操作,所以后面的宏将VEC_FREE()定义为一个空操作,不做任何操作,通

 

常这样来保证一致性, 或者充分利用系统提供的功能。

有时也会使用如下的方式来定义“空操作”,这里的空操作和上面的还是不一样,例如很常见的Debug日志打印宏:

  1. #ifdef DEBUG  
  2. #   define LOG_MSG printf  
  3. #else  
  4. #   define LOG_MSG(...)  
  5. #endif  

 

在编译时如果定义了DEBUG则将LOG_MSG当做printf使用,而不需要调试,正式发布时则将LOG_MSG()宏定义为空, 由于宏是在预编译阶段进行处理的,所以上面的宏相当于从代码中删除了。

 

上面提到了两种将宏定义为空的定义方式,看上去一样,实际上只要明白了宏都只是简单的代码替换就知道该如何选择了。

8. 预定义宏

在C语言中预定义了一些有用的宏,见表预定义宏。这些宏主要是提供当前编译的信息。宏__LINE__和__STDC__是整型常量,其他3个宏是字符串字面量。

表预定义宏:

__LINE__      被编译的文件的行数

__FILE__      被编译的文件的名字

__DATE__    编译的日期(格式"Mmm dd yyyy")

__TIME__     编译的时间(格式"hh:mm:ss")

__STDC__   如果编译器接受标准C,那么值为1

 

1)、 __DATE__宏和__TIME__宏指明程序编译的时间。例如,假设程序以下面的语句开始:

printf("Wacky Windows (c) 1996 Wacky Software, Inc.\n");

printf("Compiled on %s at %s\n", __DATE__,__TIME__);

 

每次程序开始执行,程序都会显示下面两行:

Wacky Windows (c) 1996 Wacky Software, Inc.

Compiled on Dec 23 1996 at 22:18:48

这样的信息可以帮助区分同一个程序的不同版本。

2)、我们可以使用__LINE__宏和__FILE__宏来找到错误。考虑下面这个检测被零除的除法的发生位置的问题。当一个C程序因为被零除而导致中止时,通常没有信息指明哪条除法运算导致错误。下面的宏可以帮助我们查明错误的根源:

#define CHECK_ZERO(divisor)  \

  if (divisor == 0)  \

    printf("*** Attempt to divide byzero on line %d  "  \

            "of file %s  ***\n",__LINE__, __FILE__)

CHECK_ZERO宏应该在除法运算前被调用:

CHECK_ZERO(j);

k = i / j;

如果j是0,会显示出如下形式的信息:

*** Attempt to divide by zero on line 9 of file FOO.c ***

类似这样的错误检测的宏非常有用。实际上,C语言库提供了一个通用的、用于错误检测的宏——assert宏

 再如:

#line 838 "Zend/zend_language_scanner.c"

 

#line预处理用于改变当前的行号(__LINE__)和文件名(__FILE__)。 如上所示代码,将当前的行号改变为838,文件名Zend/zend_language_scanner.c 它的作用体现在编译器的编写中,我们知道

 

编译器对C 源码编译过程中会产生一些中间文件,通过这条指令, 可以保证文件名是固定的,不会被这些中间文件代替,有利于进行调试分析。

 

 

9. C语言中常用的宏

 

01: 防止一个头文件被重复包含

 

#ifndef COMDEF_H

#define COMDEF_H

//头文件内容

#endif

02: 重新定义一些类型

防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植。

typedef  unsigned char      boolean;     /* Boolean value type. */

typedef  unsigned long int  uint32;      /* Unsigned 32 bit value */

typedef  unsigned short     uint16;      /* Unsigned 16 bit value */

typedef  unsigned char      uint8;       /* Unsigned 8  bit value */

typedef  signed long int    int32;       /* Signed 32 bit value */

typedef  signed short       int16;       /* Signed 16 bit value */

typedef  signed char        int8;        /* Signed 8  bit value */

 

//下面的不建议使用

typedef  unsigned char     byte;         /* Unsigned 8  bit value type. */

typedef  unsigned short    word;         /* Unsinged 16 bit value type. */

typedef  unsigned long     dword;        /* Unsigned 32 bit value type. */

typedef  unsigned char     uint1;        /* Unsigned 8  bit value type. */

typedef  unsigned short    uint2;        /* Unsigned 16 bit value type. */

typedef  unsigned long     uint4;        /* Unsigned 32 bit value type. */

typedef  signed char       int1;         /* Signed 8  bit value type. */

typedef  signed short      int2;         /* Signed 16 bit value type. */

typedef  long int          int4;         /* Signed 32 bit value type. */

typedef  signed long       sint31;       /* Signed 32 bit value */

typedef  signed short      sint15;       /* Signed 16 bit value */

typedef  signed char       sint7;        /* Signed 8  bit value */

 

03: 得到指定地址上的一个字节或字

#define  MEM_B(x) (*((byte *)(x)))

#define  MEM_W(x) (*((word *)(x)))

04: 求最大值和最小值

#define  MAX(x,y) (((x)>(y)) ? (x) : (y))

#define  MIN(x,y) (((x) < (y)) ? (x) : (y))

05: 得到一个field在结构体(struct)中的偏移量

#define FPOS(type,field) ((dword)&((type *)0)->field)

06: 得到一个结构体中field所占用的字节数

#define FSIZ(type,field) sizeof(((type *)0)->field)

07: 按照LSB格式把两个字节转化为一个Word

#define FLIPW(ray) ((((word)(ray)[0]) * 256) + (ray)[1])

08: 按照LSB格式把一个Word转化为两个字节

#define FLOPW(ray,val) (ray)[0] = ((val)/256); (ray)[1] = ((val) & 0xFF)

09: 得到一个变量的地址(word宽度)

#define B_PTR(var)  ((byte *) (void *) &(var))

#define W_PTR(var)  ((word *) (void *) &(var))

 

10: 得到一个字的高位和低位字节

#define WORD_LO(xxx)  ((byte) ((word)(xxx) & 255))

#define WORD_HI(xxx)  ((byte) ((word)(xxx) >> 8))

 

11: 返回一个比X大的最接近的8的倍数

#define RND8(x) ((((x) + 7)/8) * 8

12: 将一个字母转换为大写

#define UPCASE(c) (((c)>='a' && (c) <= 'z') ? ((c) – 0×20) : (c))

13: 判断字符是不是10进值的数字

#define  DECCHK(c) ((c)>='0' && (c)<='9')

14: 判断字符是不是16进值的数字

#define HEXCHK(c) (((c) >= '0' && (c)<='9') ((c)>='A' && (c)<= 'F') \

((c)>='a' && (c)<='f'))

 

15: 防止溢出的一个方法

#define INC_SAT(val) (val=((val)+1>(val)) ? (val)+1 : (val))

 

16: 返回数组元素的个数

#define ARR_SIZE(a)  (sizeof((a))/sizeof((a[0])))

 

17: 返回一个无符号数n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)

#define MOD_BY_POWER_OF_TWO( val, mod_by ) ((dword)(val) & (dword)((mod_by)-1))

 

18: 对于IO空间映射在存储空间的结构,输入输出处理

#define inp(port) (*((volatile byte *)(port)))

#define inpw(port) (*((volatile word *)(port)))

#define inpdw(port) (*((volatile dword *)(port)))

#define outp(port,val) (*((volatile byte *)(port))=((byte)(val)))

#define outpw(port, val) (*((volatile word *)(port))=((word)(val)))

#define outpdw(port, val) (*((volatile dword *)(port))=((dword)(val)))

 

19: 使用一些宏跟踪调试

ANSI标准说明了五个预定义的宏名。它们是:

__LINE__

__FILE__

__DATE__

__TIME__

__STDC__

 

C++中还定义了 __cplusplus

如果编译器不是标准的,则可能仅支持以上宏名中的几个,或根本不支持。记住编译程序也许还提供其它预定义的宏名。

__LINE__ 及 __FILE__ 宏指示,#line指令可以改变它的值,简单的讲,编译时,它们包含程序的当前行数和文件名。

__DATE__ 宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期。

__TIME__ 宏指令包含程序编译的时间。时间用字符串表示,其形式为: 分:秒

__STDC__ 宏指令的意义是编译时定义的。一般来讲,如果__STDC__已经定义,编译器将仅接受不包含任何非标准扩展的标准C/C++代码。如果实现是标准的,则宏__STDC__含有十进制常量1。如果它含有任何其它数,则实现是非标准的。

__cplusplus 与标准c++一致的编译器把它定义为一个包含至少6为的数值。与标准c++不一致的编译器将使用具有5位或更少的数值。

 

可以定义宏,例如:当定义了_DEBUG,输出数据信息和所在文件所在行

#ifdef _DEBUG

#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)

#else

#define DEBUGMSG(msg,date)

#endif

20: 宏定义防止错误使用小括号包含。

例如:

有问题的定义:#define DUMP_WRITE(addr,nr) {memcpy(bufp,addr,nr); bufp += nr;}

应该使用的定义: #difne DO(a,b) do{a+b;a++;}while(0)

例如:

  1. if(addr)  
  2.     DUMP_WRITE(addr,nr);  
  3. else  
  4.     do_somethong_else();  
  5. //宏展开以后变成这样:  
  6. if(addr)  
  7.     {memcpy(bufp,addr,nr); bufp += nr;};  
  8. else  
  9.     do_something_else();  

 

gcc 在碰到else前面的“;”时就认为if语句已经结束,因而后面的else不在if语句中。而采用do{} while(0)的定义,在任何情况下都没有问题。而改为 #difne DO(a,b) do{a+b;a++;}while(0) 的定义则在任何情况下都不会出错。

 

 

posted @ 2014-03-15 10:06  管小木  阅读(413)  评论(0编辑  收藏  举报