欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言预处理

程序员文章站 2024-02-22 15:59:40
...

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

 

 

 

 

 

 

 

 

 

 

 

相关标签: C语言预处理