C语言预处理

预处理指令:#define、#include、#ifdef、#else、#endif、#ifndef、#if、#elif、#line、#error、#pragma

C预处理器在程序执行之前查看程序(故称之为预处理器)。根据程序中的预处理指令,预处理器把符号缩写替换成其表示的内容。预处理器可以包含程序所需的其他文件,可以选择让编译器查看哪些代码。预处理器并不知道C。基本上它的工作是吧一些文本转换为另外一些文本。这样描述预处理器无法体现它的真正效用和价值。

16.1翻译程序的第一步:

在预处理之前,编译器必须对该程序进行一些翻译处理。首先,编译器吧源代码中出现的字符映射到源字符集。该过程处理多字节字符和三字节字符----字符扩展让C更加国际化。

第二,编译器定位每个反斜杠后面跟着换行符的实例,并删除他们。也就是说,把下面两个物理行(physical line):

printf("That's wond\

erful!\n");

转换为一个逻辑行(logical line);

第三,编译器吧文本划分为预处理记号序列、空白序列和注释序列(记号是由空格、制表符或换行符分隔的项,详见16.2.1)。这里要注意的是,编译器将用一个空格字符替换每一条注释。因此,下面的代码:

int /*这看起来并不像一个空格*/fox;

将变成:

int fox;

而且,实现可以用一个空格替换所有的空白字符序列(不包括换行符)。最后,程序已经准备好进入预处理阶段了,预处理查找一行中以#开始的预处理指令。

16.2  明示常量:#define

#define 预处理指令和其他预处理指令一样,以#号作为一行的开始。其定义从指令出现的地方到该文件末尾有效。该指令还有一些其他的用途。

预处理指令从#开始运行,到后面的第一个换行符为止。也就是说,指令的长度仅限于一行,然而,前面提到过,在预处理开始之前,编译器会把多行物理行处理为一行逻辑行。

#include<stdio.h>

#define TWO 2

#define OW "Consistency is the refuge of the unimagina\
tive.-Oscar Wilde"  /*反斜杠吧该定义延续到下一行*/

#define FOUR TWO*TWO
#define PX printf("X is %d.\n",x)
#define FMT "X is %d.\n"


int main()
{
  int x=TWO;
  PX;
  x=FOUR;
  printf("FMT,x");
  printf("%s\n",OW);

  printf("TWO:OW\n");

  return 0;
}

#define PX printf("x is %d.\n",x)

其中#define 是预处理指令,PX为宏,printf("x is %d.\n",x)是替换体。有些宏代表值(如本例),这些宏被称为类对象宏(object-like macro)。C语言还有类函数宏(function-like macro),稍加讨论。宏的名称重不允许有空格,而且必须遵循C变量的命名规则:只能使用字符、数字和下划线(_)字符,而且首字符不能是数字。

运行该程序后,输出如下:

X is 2.

X is 4.

Consistency is the last refuge of the unimaginative.-Oscar Wilde

TWO:OW

 

 

一般而言,预处理器发现程序中的宏后,会用宏等价的替换文本进行替换。如果替换的字符串中还包含其他宏,则继续替换这些宏。唯一例外的是双引号中的宏。因此,下面的语句:

printf("TWO:OW");

打印的是TWO:OW,而不是打印:

2:Consistency is the last refuge of the unimaginative.- Oscar Wilde

要打印这行,应该这样写:

printf("%d:%s\n",TWO,OW);

这行代码中,宏不在双引号内。

C语言现在也支持const关键字,提供了更为灵活的方法。用const可以创建在程序中运行过程中不能改变的变量,可具有文件作用域或块作用域。另一方面,宏常量可用于指定标准数组的大小和const 变量的初始值。

#define LIMIT 20

const int LIM=50;

static int data1[LIMIT];  //有效

static int data2[LIM];// 无效

const int LIM2=2*LIMIT;//有效

const int LIM3=2*LIM;//无效

 

这里解释下上面代码中的“无效”注释,在C 中,非自动数组的大小应该是整型常量表达式,这意味着表示数组大小的不许是整型常量的组合、枚举常量和sizeof表达式,不包括const 生命的值(这也是C++和C 的区别之一,在C++这个可以吧const值作为常量表达式的一部分),但是,有的实现可能接受其他形式的常量表达式,例如,GCC4.7.3不允许data2的声明,但是Clang4.6允许。

16.2.1   记号

16.2.2  重定义常量

16.3 在#define 中使用参数

在#define 中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号,类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中。

下面是一个类函数宏的示例:

#define SQUARE(x)  X*X

在程序中可以这样用:

z=AQUARE(2);

这看上去很像函数调用,但是它的行为和函数调用完全不同。下面的程序演示了类函数宏和另一个宏的用法、

#include<stdio.h>
#define SQUARE(x) X*X
#define PR(x)  printf("The result is %d.\n",X)
int main()
{
  int x=5;
  int z;
  printf("x=%d\n",x);
  z=SQUARE(x);
  printf("Evaluating SQUARE(x):");
  PR(z);
  z=SQUARE(2);
  printf("Evaluating SQUARE(2):");
  PR(z);
  printf("Evaluating SQUARE(x+2):");
  PR(SQUARE(x+2));
  printf("Evaluating 100/SQUARE(2):");
  PR(100/SQUARE(2));
  printf("x is %d.\n",x);
  printf("Evaluating SQUARE(++x):");
  PR(SQUARE(++x));
  printf("After incrementing,x is %x.\n",x);
  return 0;
}

程序的输出:

x=5

Evaluating SQUARE(x): The result is 25.

Evaluating SQUARE(2): The result is 4.

 Evaluating SQUARE(x+2): The result is 17.

Evaluating 100/SQUARE(2): The result is 100.

x is 5.

Evaluating SQUARE(++x): The result is 42.

After incrementing,x is 7.

前两行与预期相符,但是接下来的结果有点奇怪。17是怎么来的呢?原来预处理器不做计算、不求值,只替换字符序列。预处理器把出现x的地方都替换为x+2.因此,x*x变成了x+2*x+2.

该例演示了函数调用和宏调用的重要区别。函数调用在程序运行时吧参数的值传递给函数。宏调用在编译之前把参数记号传递给程序。这两个不同的过程发生在不同时期。是否可以修改宏定义让SQUARE(x+2)得36?当然可以,要多加几个圆括号:

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

但是,这并未解决所有的问题。下面的输出行:

100/SQUARE(2)

将变成:

100/2*2

把SQUARE(x)定义为下面的形式可以解决这种混乱:

#define SQUARE(x) (x*x)

要处理前面的两种情况,要这样定义:

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

因此,必要时要使用足够多的圆括号来确保运算和结合的正确顺序

尽管如此,这样做还是无法避免程序中最后一种情况的问题。SQUARE(++x)变成了++x*++x,递增了两次x,一次在乘法运算之前,一次在乘法运算之后。

 解决这个问题最简单的方法是,避免用++x作为宏参数。一般而言,不要在宏中国实用递增或递减运算符。但是,++x可作为函数参数,因为编译器会对++x求值得5后,再把5传递给函数。

16.3.1  用宏参数创建字符串:#运算符

下面是一个类函数宏:

#define PSQR(X)  printf("The square of X is %d.\n",((x)*(x)))

假设这样使用宏:

PSQR(8);

输出为:

The square of X is 64.

注意双引号字符串中的X被视为普通文本,而不是;一个可被替换的记号。

C允许在字符串中包含宏参数,在类函数宏的替换体中,#号作为一个预处理运算符,可以吧记号转化为字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串“x”的形参名。这个过程称为字符串化。

#include<stdio.h>
#define PSQR(X)  printf("The square of X is %d.\n",((x)*(x)))
int main()
{
  int y=5;
  PSQR(y);
  PSQR(2+4);
  return 0;
}

该程序的输出如下:

The square of y is 25.

The square of 2+4 is 36.

调用第1个宏时,用“y”替换#x。调用第2个宏时,用“2+4”替换#x。

16.3.2  预处理器黏合剂:##运算符

与#运算符类似,##运算符可用于类函数宏的替换部分,而且,##还可用于对象宏的替换部分,##运算符吧两个记号组合成一个记号,例如,可以这样做:

#define XNAME(n)  x ## n

然后,宏XNAME(4)将展开为x4

16.3.3  变参宏:. . .和_ _VA_ARGS_ _

一些函数(如printf())接受数量可变的参数,stdvar.h头文件提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。

通过吧宏参数列表中的最后的参数写成省略号(即,3个点...)来实现这一功能,这样,预定义宏_ _VA_ARGS_ _可用在替换部分中,表明省略号代表什么,例如,下面的定义:

#define PR(...) printf(_ _VA_ARGS_ _)

假设稍后调用该宏:

PR("Howdy");

PR("weight=%d,shipping =$%.2f\n",wt,sp);

对于第一次调用,_ _VA_ARGS_ _展开为1个参数,“Howdy”,

对于第二次调用,_ _VA_ARGS_ _展开为3个参数,“weight=%d,shipping =$%.2f\n”,wt,sp

因此,展开后的代码是:

printf(“Howdy”);

printf("weight=%d,shipping =$%.2f\n",wt,sp);

16.4  宏和函数的选择

有些编程任务既可以用带参数的宏完成,也可以用函数完成,应该使用宏还是函数?这没有硬性规定,但是可以参考下面的情况。

使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的福做一个。一些编译器规定宏只能定义成一行,不过,即使编译器没有这个限制,也应该这样做。

宏和函数的选择实际上是时间个空间的权衡。宏生成内联代码,即在程序中生成语句。如果调用20次好哦哦那个,即在程序中插入20行代码。如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间,然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间。

宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值),因此,只要能用int 或float 类型都可以用SQUARE(x)宏。

C99提供了第3中可替换的方法——内联函数。

对于简单的函数,程序猿通常使用宏,如下所示:

#define  MAX(X,Y)  ((X)>(Y)?(X):(Y))

 

要注意一下几点:

  • 记住宏名中不允许有空格,但是在替换字符串中可以有空格。ANSI C 允许在参数列表中使用空格。
  • 用圆括号吧宏的参数和整个替换体括起来。这样能确保被括起来的部分在下面的表达式中正确的展开;
  • 用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛。但是,大写字母可以提醒程序员注意,宏可能产生的副作用。
  • 如果打算使用宏来加快程序的运行速度,那么首先要确定使用宏和使用函数是否会导致较大差异,在程序中只使用一次的宏无法明显减少程序的运行时间。在嵌套循环中国使用宏有助于提高效率。

 

 

 

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值