您当前的位置: 首页 >  c语言

韦东山

暂无认证

  • 0浏览

    0关注

    506博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

C语言预处理命令详解

韦东山 发布时间:2018-11-29 11:59:39 ,浏览量:0

本文参考诸多资料,详细介绍常用的几种预处理功能。因成文较早,资料来源大多已不可考证,敬请谅解。全文字数2万,阅读时间50分钟,建议先收藏。

一 前言

预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。

预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。

C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

二 宏定义

C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为宏的标识符称为“宏名”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为宏替换或宏展开。

宏定义是由源程序中的宏定义命令完成的。宏替换是由预处理程序自动完成的。

在C语言中,宏定义分为有参数和无参数两种。下面分别讨论这两种宏的定义和调用。

2.1 无参宏定义

无参宏的宏名后不带参数。其定义的一般形式为:

#define  标识符  字符串

其中,“#”表示这是一条预处理命令(以#开头的均为预处理命令)。“define”为宏定义命令。“标识符”为符号常量,即宏名。“字符串”可以是常数、表达式、格式串等。

宏定义用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名。这只是一种简单的文本替换,预处理程序对它不作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

注意理解宏替换中“换”的概念,即在对相关命令或语句的含义和功能作具体分析之前就要进行文本替换。

【例1】定义常量:

#define MAX_TIME 1000

若在程序里面写if(time < MAX_TIME){…},则编译器在处理该代码前会将MAX_TIME替换为1000。

注意,这种情况下使用const定义常量可能更好,如const int MAX_TIME = 1000;。因为const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行简单的字符文本替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。

【例2】反例:

#define pint (int*)
pint pa, pb;

本意是定义pa和pb均为int型指针,但实际上变成int* pa,pb;。pa是int型指针,而pb是int型变量。本例中可用typedef来代替define,这样pa和pb就都是int型指针了。

因为宏定义只是简单的字符串代换,在预处理阶段完成,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名,被命名的标识符具有类型定义说明的功能。

typedef的具体说明见附录6.4。

无参宏注意事项:

宏名一般用大写字母表示,以便于与变量区别。宏定义末尾不必加分号,否则连分号一并替换。宏定义可以嵌套。

可用#undef命令终止宏定义的作用域。

使用宏可提高程序通用性和易读性,减少不一致性,减少输入错误和便于修改。如数组大小常用宏定义。预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。字符串" "中永远不包含宏,否则该宏名当字符串处理。

宏定义不分配内存,变量定义分配内存。

2.2 带参宏定义

C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。

对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。

带参宏定义的一般形式为:

#define  宏名(形参表)  字符串

在字符串中含有各个形参。

带参宏调用的一般形式为:

宏名(实参表);

在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。

在带参宏定义中,形参不分配内存单元,因此不必作类型定义。而宏调用中的实参有具体的值,要用它们去代换形参,因此必须作类型说明,这点与函数不同。函数中形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中只是符号代换,不存在值传递问题。

【例3】

 #define INC(x) x+1  //宏定义

 y = INC(5);         //宏调用

在宏调用时,用实参5去代替形参x,经预处理宏展开后的语句为y=5+1。

【例4】反例:

#define SQ(r)    r*r

上述这种实参为表达式的宏定义,在一般使用时没有问题;但遇到如area=SQ(a+b);时就会出现问题,宏展开后变为area=a+b*a+b;,显然违背本意。

相比之下,函数调用时会先把实参表达式的值(a+b)求出来再赋予形参r;而宏替换对实参表达式不作计算直接地照原样代换。因此在宏定义中,字符串内的形参通常要用括号括起来以避免出错。

进一步地,考虑到运算符优先级和结合性,遇到area=10/SQ(a+b);时即使形参加括号仍会出错。因此,还应在宏定义中的整个字符串外加括号,

综上,正确的宏定义是#define SQ® (®*®),即宏定义时建议所有的层次都要加括号。

【例5】带参函数和带参宏的区别:

 #define SQUARE(x) ((x)*(x))

 int Square(int x){

   return (x * x); //未考虑溢出保护

}

int main(void){

    int i = 1;

   while(i  _y ? _x : _y; })


#define TMAX_S(type, x, y) ({ \

    type _x = (x);  \

    type _y = (y);  \

   _x > _y ? _x: _y; })

Gcc编译器将包含在圆括号和大括号双层括号内的复合语句看作是一个表达式,它可出现在任何允许表达式的地方;复合语句中可声明局部变量,判断循环条件等复杂处理。而表达式的最后一条语句必须是一个表达式,它的计算结果作为返回值。MAX_S和TMAX_S宏内就定义局部变量以消除参数副作用。

MAX_S宏内(void)(&_x == &_y)语句用于检查参数类型一致性。当参数x和y类型不同时,会产生” comparison of distinct pointer types lacks a cast”的编译警告。

注意,MAX_S和TMAX_S宏虽可避免参数副作用,但会增加内存开销并降低执行效率。若使用者能保证宏参数不存在副作用,则可选用普通定义(即MAX宏)。

  1. 得到一个成员在结构体中的偏移量(lint 545告警表示"&用法值得怀疑",此处抑制该警告):
#define FPOS(type, field) \

/*lint -e545 */ ((int)&((type *)0)-> field) /*lint +e545 */
  1. 得到一个结构体中某成员所占用的字节数:
#define FSIZ(type, field)    sizeof(((type *)0)->field)
  1. 按照LSB格式把两个字节转化为一个字(word):
#define FLIPW(arr)          ((((short)(arr)[0]) * 256) + (arr)[1])
  1. 按照LSB格式把一个字(word)转化为两个字节:
#define FLOPW(arr, val) \

   (arr)[0] = ((val) / 256); \

   (arr)[1] = ((val) & 0xFF)
  1. 得到一个变量的地址:
#define B_PTR(var)       ((char *)(void *)&(var))

#define W_PTR(var)       ((short *)(void *)&(var))
  1. 得到一个字(word)的高位和低位字节:
#define WORD_LO(x)       ((char)((short)(x)&0xFF))

#define WORD_HI(x)       ((char)((short)(x)>>0x8))
  1. 返回一个比X大的最接近的8的倍数:
#define RND8(x)           ((((x) + 7) / 8) * 8)
  1. 将一个字母转换为大写或小写:
#define UPCASE(c)         (((c) >= 'a' && (c) = 'A' && (c) = '0' && (c) = '0' && (c) = 'A' && (c) = 'a' && (c)  (val)) ? (val)+1 : (val))
  1. 返回数组元素的个数:
#define ARR_SIZE(arr)     (sizeof((arr)) / sizeof((arr[0])))
  1. 对于IO空间映射在存储空间的结构,输入输出处理:
#define INP(port)           (*((volatile char *)(port)))

#define INPW(port)          (*((volatile short *)(port)))

#define INPDW(port)         (*((volatile int *)(port)))

#define OUTP(port, val)     (*((volatile char *)(port)) = ((char)(val)))

#define OUTPW(port, val)    (*((volatile short *)(port)) = ((short)(val)))

#define OUTPDW(port, val)   (*((volatile int *)(port)) = ((int)(val)))
  1. 使用一些宏跟踪调试: ANSI标准说明了五个预定义的宏名(注意双下划线),即:LINE、__FILE __、DATE、TIME、__STDC __。

若编译器未遵循ANSI标准,则可能仅支持以上宏名中的几个,或根本不支持。此外,编译程序可能还提供其它预定义的宏名(如__FUCTION__)。

__DATE__宏指令含有形式为月/日/年的串,表示源文件被翻译到代码时的日期;源代码翻译到目标代码的时间作为串包含在__TIME__中。串形式为时:分:秒。

如果实现是标准的,则宏__STDC__含有十进制常量1。如果它含有任何其它数,则实现是非标准的。

可以借助上面的宏来定义调试宏,输出数据信息和所在文件所在行。如下所示:

 #define MSG(msg, date)      printf(msg);printf(“[%d][%d][%s]”,date,__LINE__,__FILE__)
  1. 用do{…}while(0)语句包含多语句防止错误:
#define DO(a, b) do{\
   a+b;\
   a++;\
}while(0)
  1. 实现类似“重载”功能

C语言中没有swap函数,而且不支持重载,也没有模板概念,所以对于每种数据类型都要写出相应的swap函数,如:

IntSwap(int *,  int *);  

LongSwap(long *,  long *);  

StringSwap(char *,  char *); 

可采用宏定义TSWAP (t,x,y)或SWAP(x, y)交换两个整型或浮点参数:

#define TSWAP(type, x, y) do{ \

     type _y = y; \

     y = x;       \

     x = _y;      \

 }while(0)

 #define SWAP(x, y) do{ \

     x = x + y;   \

     y = x - y;   \

     x = x - y;   \

}while(0)


int main(void){

    int a = 10, b = 5;

    TSWAP(int, a, b);

    printf(“a=%d, b=%d\n”, a, b);

    return 0;

}
  1. 1年中有多少秒(忽略闰年问题) :
#define SECONDS_PER_YEAR    (60UL * 60 * 24 * 365)

该表达式将使一个16位机的整型数溢出,因此用长整型符号L告诉编译器该常数为长整型数。

注意,不可定义为#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL,否则将产生(31536000)UL而非31536000UL,这会导致编译报错。

以下几种写法也正确:


#define SECONDS_PER_YEAR    60 * 60 * 24 * 365UL

#define SECONDS_PER_YEAR    (60UL * 60UL * 24UL * 365UL)

#define SECONDS_PER_YEAR    ((unsigned long)(60 * 60 * 24 * 365))
}
  1. 取消宏定义:
#define [MacroName] [MacroValue]       //定义宏

#undef [MacroName]                     //取消宏

宏定义必须写在函数外,其作用域为宏定义起到源程序结束。如要终止其作用域可使用#undef命令:

 #define PI   3.14159

 int main(void){
     //……
 }

 #undef PI
 int func(void){
     //……
 }

表示PI只在main函数中有效,在func1中无效。

2.3.2 特殊用法

主要涉及C语言宏里#和##的用法,以及可变参数宏。

2.3.2.1 字符串化操作符#

在C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。#只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。例如:

 #define EXAMPLE(instr)      printf("The input string is:\t%s\n", #instr)

 #define EXAMPLE1(instr)     #instr

当使用该宏定义时,example(abc)在编译时将会展开成printf(“the input string is:\t%s\n”,“abc”);string str=example1(abc)将会展成string str=“abc”。

又如下面代码中的宏:

  define WARN_IF(exp) do{ \

     if(exp) \

         fprintf(stderr, "Warning: " #exp"\n"); \

 } while(0)

则代码WARN_IF (divider == 0)会被替换为:


do{
     if(divider == 0)
     
       fprintf(stderr, "Warning" "divider == 0" "\n");
 } while(0)

这样,每次divider(除数)为0时便会在标准错误流上输出一个提示信息。

注意#宏对空格的处理:

忽略传入参数名前面和后面的空格。如str= example1( abc )会被扩展成 str=“abc”。

当传入参数名间存在空格时,编译器会自动连接各个子字符串,每个子字符串间只以一个空格连接。如str= example1( abc def)会被扩展成 str=“abc def”。

2.3.2.2 符号连接操作符##

##称为连接符(concatenator或token-pasting),用来将两个Token连接为一个Token。注意这里连接的对象是Token就行,而不一定是宏的变量。例如:

 #define PASTER(n)     printf( "token" #n " = %d", token##n)

 int token9 = 9;

则运行PASTER(9)后输出结果为token9 = 9。

又如要做一个菜单项命令名和函数指针组成的结构体数组,并希望在函数名和菜单项命令名之间有直观的、名字上的关系。那么下面的代码就非常实用:

struct command{

     char * name;

     void (*function)(void);

 };

#define COMMAND(NAME)   {NAME, NAME##_command}

然后,就可用一些预先定义好的命令来方便地初始化一个command结构的数组:

 struct command commands[] = {

     COMMAND(quit),

     COMMAND(help),

     //...

 }

COMMAND宏在此充当一个代码生成器的作用,这样可在一定程度上减少代码密度,间接地也可减少不留心所造成的错误。

还可以用n个##符号连接n+1个Token,这个特性是#符号所不具备的。如:

 #define  LINK_MULTIPLE(a, b, c, d)      a##_##b##_##c##_##d
 typedef struct record_type LINK_MULTIPLE(name, company, position, salary);

这里这个语句将展开为typedef struct record_type name_company_position_salary。

注意:

当用##连接形参时,##前后的空格可有可无。

连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。

凡是宏定义里有用’#‘或’##'的地方,宏参数是不会再展开。如:


 #define STR(s)       #s

 #define CONS(a,b)    int(a##e##b)

则printf(“int max: %s\n”, STR(INT_MAX))会被展开为printf(“int max: %s\n”, “INT_MAX”)。其中,变量INT_MAX为int型的最大值,其值定义在中。printf("%s\n", CONS(A, A))会被展开为printf("%s\n", int(AeA)),从而编译报错。

INT_MAX和A都不会再被展开,多加一层中间转换宏即可解决这个问题。加这层宏是为了把所有宏的参数在这层里全部展开,那么在转换宏里的那一个宏(如_STR)就能得到正确的宏参数。

#define _STR(s)         #s 

#define STR(s)          _STR(s)       // 转换宏

#define _CONS(a,b)      int(a##e##b)

#define CONS(a,b)       _CONS(a,b)    // 转换宏

则printf(“int max: %s\n”, STR(INT_MAX))输出为int max: 0x7fffffff;而printf("%d\n", CONS(A, A))输出为200。

这种分层展开的技术称为宏的Argument Prescan,参见附录6.1。

2.3.2.3 字符化操作符@#

@#称为字符化操作符(charizing),只能用于有传入参数的宏定义中,且必须置于宏定义体的参数名前。作用是将传入的单字符参数名转换成字符,以一对单引号括起来。

 #define makechar(x)    #@x

 a = makechar(b);

展开后变成a= ‘b’。

2.3.2.4 可变参数宏

在C语言宏中称为Variadic Macro,即变参宏。C99编译器标准允许定义可变参数宏(Macros with a Variable Number of Arguments),这样就可以使用拥有可变参数表的宏。

可变参数宏的一般形式为:

#define  DBGMSG(format, ...)  fprintf (stderr, format, __VA_ARGS__)

省略号代表一个可以变化的参数表,变参必须作为参数表的最右一项出现。使用保留名__VA_ARGS__ 把参数传递给宏。在调用宏时,省略号被表示成零个或多个符号(包括里面的逗号),一直到到右括号结束为止。当被调用时,在宏体(macro body)中,那些符号序列集合将代替里面的__VA_ARGS__标识符。当宏的调用展开时,实际的参数就传递给fprintf ()。

注意:可变参数宏不被ANSI/ISO C++所正式支持。因此,应当检查编译器是否支持这项技术。

在标准C里,不能省略可变参数,但却可以给它传递一个空的参数,这会导致编译出错。因为宏展开后,里面的字符串后面会有个多余的逗号。为解决这个问题,GNU CPP中做了如下扩展定义:

#define  DBGMSG(format, ...)  fprintf (stderr, format, ##__VA_ARGS__)

若可变参数被忽略或为空,##操作将使编译器删除它前面多余的逗号(否则会编译出错)。若宏调用时提供了可变参数,编译器会把这些可变参数放到逗号的后面。

同时,GCC还支持显式地命名变参为args,如同其它参数一样。如下格式的宏扩展:

#define  DBGMSG(format, args...)  fprintf (stderr, format, ##args)

这样写可读性更强,并且更容易进行描述。

用GCC和C99的可变参数宏, 可以更方便地打印调试信息,如:

 #ifdef DEBUG

     #define DBGPRINT(format, args...) \

         fprintf(stderr, format, ##args)

 #else

     #define DBGPRINT(format, args...)

 #endif

这样定义之后,代码中就可以用dbgprint了,例如dbgprint (“aaa [%s]”, FILE)。

结合第4节的“条件编译”功能,可以构造出如下调试打印宏:

 #ifdef LOG_TEST_DEBUG

      /* OMCI调试日志宏 */

    //以10进制格式日志整型变量

      #define PRINT_DEC(x)          printf(#x" = %d\n", x)

      #define PRINT_DEC2(x,y)       printf(#x" = %d\n", y)

     //以16进制格式日志整型变量

      #define PRINT_HEX(x)          printf(#x" = 0x%-X\n", x)

      #define PRINT_HEX2(x,y)       printf(#x" = 0x%-X\n", y)

      //以字符串格式日志字符串变量

     #define PRINT_STR(x)          printf(#x" = %s\n", x)

     #define PRINT_STR2(x,y)       printf(#x" = %s\n", y)


     //日志提示信息

     #define PROMPT(info)          printf("%s\n", info)

     //调试定位信息打印宏

     #define  TP                   printf("%-4u - [%s]\n", __LINE__, __FILE__, __FUNCTION__);

     //调试跟踪宏,在待日志信息前附加日志文件名、行数、函数名等信息

     #define TRACE(fmt, args...)\

     do{\

        printf("[%s(%d)]", __FILE__, __LINE__, __FUNCTION__);\

        printf((fmt), ##args);\

     }while(0)

 #else

     #define PRINT_DEC(x)

     #define PRINT_DEC2(x,y)

     #define PRINT_HEX(x)

     #define PRINT_HEX2(x,y)

     #define PRINT_STR(x)

     #define PRINT_STR2(x,y)

     #define PROMPT(info)

     #define  TP

     #define TRACE(fmt, args...)

 #endif

三 文件包含

文件包含命令行的一般形式为:

#include "文件名"

通常,该文件是后缀名为"h"或"hpp"的头文件。文件包含命令把指定头文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。

在程序设计中,文件包含是很有用的。一个大程序可以分为多个模块,由多个程序员分别编程。有些公用的符号常量或宏定义等可单独组成一个文件,在其它文件的开头用包含命令包含该文件即可使用。这样,可避免在每个文件开头都去书写那些公用量,从而节省时间,并减少出错。

对文件包含命令要说明以下几点:

包含命令中的文件名可用双引号括起来,也可用尖括号括起来,如#include "common.h"和#include。但这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时设置的include目录),而不在当前源文件目录去查找;

使用双引号则表示首先在当前源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。

一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。

四 条件编译

一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当满足某条件时对一组语句进行编译,而当条件不满足时则编译另一组语句。

条件编译功能可按不同的条件去编译不同的程序部分,从而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。

条件编译有三种形式,下面分别介绍。

4.1 #ifdef形式

#ifdef  标识符  (或#if defined标识符)

    程序段1

#else

    程序段2

#endif

如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),#else可以没有,即可以写为:

#ifdef  标识符  (或#if defined标识符)


    程序段

#endif

这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。

【例6】

#define NUM OK

 int main(void){

     struct stu{

          int num;

          char *name;

          char sex;

          float score;

     }*ps;

     ps=(struct stu*)malloc(sizeof(struct stu));

     ps->num = 102;

     ps->name = "Zhang ping";

     ps->sex = 'M';

     ps->score = 62.5;

 #ifdef NUM

     printf("Number=%d\nScore=%f\n", ps->num, ps->score); /*--Execute--*/

 #else

     printf("Name=%s\nSex=%c\n", ps->name, ps->sex);

 #endif

     free(ps);

     return 0;

 }

由于在程序中插入了条件编译预处理命令,因此要根据NUM是否被定义过来决定编译哪个printf语句。而程序首行已对NUM作过宏定义,因此应对第一个printf语句作编译,故运行结果是输出了学号和成绩。

程序首行定义NUM为字符串“OK”,其实可为任何字符串,甚至不给出任何字符串,即#define NUM也具有同样的意义。只有取消程序首行宏定义才会去编译第二个printf语句。

4.2 #ifndef 形式

#ifndef  标识符

    程序段1

#else

    程序段2

#endif

如果标识符未被#define命令定义过,则对程序段1进行编译,否则对程序段2进行编译。这与#ifdef形式的功能正相反。

“#ifndef 标识符”也可写为“#if !(defined 标识符)”。

4.3 #if形式

#if 常量表达式

    程序段1

#else

    程序段2

#endif

如果常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译。因此可使程序在不同条件下,完成不同的功能。

【例7】输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写或小写字母输出。

#define CAPITAL_LETTER   1

  int main(void){

      char szOrig[] = "C Language", cChar;

      int dwIdx = 0;

      while((cChar = szOrig[dwIdx++]) != '\0')

      {

  #if CAPITAL_LETTER

         if((cChar >= 'a') && (cChar = 'A') && (cChar             
关注
打赏
1658827356
查看更多评论
0.3140s