C语言预处理

本文详细介绍了C语言预处理中的#include指令,如何使用#define定义常量和宏,以及条件编译和错误处理。重点讲解了带参数的宏、符号常量的区别,以及如何避免宏替换带来的问题。#undef命令和条件编译的#if...#endif结构也进行了深入剖析。
摘要由CSDN通过智能技术生成

C的预处理实在程序被编译之前执行的。执行的处理操作有:

  • 将其他文件包含到正在被编译的文件中来
  • 定义符号常量和宏
  • 程序代码的条件编译
  • 有条件地执行预处理命令

所有的预处理命令都是以#开头的。在同一行中,只有空格和注释可以出现在预处理命令之前。

#include预处理命令

大部分的程序都会用到 #include预处理命令。
该命令的功能是:将指定文件的一个副本包含到该命令所在的位置上。#include命令有如下两种形式:
#include<filename>
#include"filename"
它们的差别在于查找欲包含文件的起始位置不同。
若文件名用尖括号<>括起来,用于标准函数库的头文件,则预处理程序就会按照一种依赖于系统实现的方式,通常实在预先指定的编译器和系统目录中进行查找。
若文件名用双引号""括起来,则预处理程序就会从待编译文件所在的目录里开始查找欲包含的文件。这方法通常用来包含程序员自定义的头文件。若编译器在当前目录中没有找到目标文件,则转至预先指定的编译器和系统目录中继续查找。

被不同程序源文件所共用的声明通常被编辑成一个头文件,然后被包含到源程序中。这种声明的例子有:

  • 结构体、共用体的声明
  • typedef声明
  • 枚举
  • 函数原型

#define预处理命令:符号常量

#define预处理命令用来创建符号常量(以符号表示的常量)和宏(以符号形式定义的操作)。
#define命令的格式:
#define identifier replacement-text
当这一行出现在一个文件中时,其后出现的所有identifier (标识符)都会在程序编译前被自动地替换成replacement-text(替换文本),除非它出现在字符串文本或注释中。习惯上,只用大写字母和下划线来为符号常量命名。
例如:
#define PI 3.14159
将使得其后出现的符号常量PI全部被自动替换成数值常量3.14159。符号常量允许你为一个常量起名字并在整个程序中使用这个名字。

#define命令在预处理时符号常量都会被右边的所有内容进行简单的替换。例如:
#define PI = 3.14159
预处理后所有的PI都会被= 3.14159替换,从而导致很多难以捉摸的逻辑错误和语法错误。为此,相比于#define,最好还是用const来声明常量。例如:
const double PI = 3.14159;

#define预处理命令:宏

宏时#define预处理命令定义的一种标识符。与符号常量一样,程序中的所有宏标识符也要在程序被编译之前用其对应的替换文本来替换。
宏的定义可以带参数,也可以不带。不带参数的宏在处理上与符号常量没有差别。但对于带参数的宏,参数将会被代入到替换文本中,这样宏就被展开了——即程序中的宏标识符和参数列表都被替换文本替换了。
符号常量也是一种类型的宏。

带参数的宏

#define CIRCLE_AREA(x)((PI) * (x) * (x))
#define RECTANGLE_AREA(y,z) ((y) * (z))
上面两个宏分别是为计算一个圆和矩形的面积而定义的带参数的宏。
此后,凡是出现CIRCLE_AREA(y)RECTANGLE_AREA(a,b)的地方,y、a、b的值都会被代入到替换文本x、y、z的位置。符号常量PI也会被它自己的值替换,然后宏就被展开了。
例如:
circleArea = CIRCLE_AREA(4);
rectArea = RECTANGLE_AREA(a+2,b+3) ;
将分别会被展开为:
circleArea = ((3.14159) * (4) * (4));
rectArea = ((a+2) * (b+3));

圆括号的重要性
当一个宏的参数是表达式时,替换文本中将x括起来的圆括号能够保证表达式按照正确的顺序计算。
如:rectArea = RECTANGLE_AREA(a+2,b+3) ;
将被展开为:rectArea = ((a+2) * (b+3));
如果没有圆括号,将被展开为:rectArea = a+2 * b+3;
根据运算符优先级的规定,表达式的值将会被错误计算。

上面这两个带参数的宏最好定义成一个函数,这样会更安全。因为它们都能完成相同的计算,且函数的参数只有在函数被调用时才会被定值一次且仅一次,而且编译器会对函数进行类型检查——预处理不支持类型检查。

在定义宏时,如果这一行的剩余空间不够写下一条完整语句,则必须在行末加上一个反斜杠(\),表示下一行是续写的语句,在写其他语句出现这样的情况也可以这样解决。

#undef预处理命令

符号常量和宏可以用#undef预处理命令来撤销。
符号常量和宏的作用域是从它们的定义开始到它们被#undef命令撤销为止,或者到文件末尾。
一旦被撤销,宏名或符号常量可以用#define重新定义。

条件编译

条件编译使用户能够控制预处理命令的执行以及对程序代码的编译。每一个条件预处理命令都要计算一个整型常量表达式的值。但是类型强转表达式、sizeof表达式、以及枚举常量的值不能在预处理命令中计算。

#if… #endif预处理命令

条件预处理命令的结果非常类似于if选择语句。例如:

#if !defined(MY_CONSTANT)
	#define MY_CONSTANT 0
#endif

这些命令首先判断MY_CONSTANT是否被定义了,若已经定义了,则表达式
!defined(MY_CONSTANT)被定值为0,否则为1。若为0,则#define命令被跳过,否则定义宏MY_CONSTANT。

每个#if结构都以#endif结束。
#ifdef#ifndef#if defined(name)#if !defined(name)的缩写形式。
对于多分支的条件预处理结构,需使用命令#elif#else来测试。

这些命令常被用来防止头文件被多次包含到同一源文件中,还可以用来启用或停用部分代码以使得软件可移植于不同的平台上。

在程序开发过程中,常常需要将某部分代码注释起来使得它们不参与编译。但是如果代码本身已经含有多行注释,那么就不能使用注释符号/**/来完成这个任务,因为这样的注释是不能嵌套的。这时可以改用如下的预处理命令:

#if 0
	不需要参与编译的代码
#endif

若需要让这部分代码参与编译,只需把0改为1即可。

#error和#pragma预处理命令

预处理命令#error
#error tokens
将打印出包含预处理命令中指定tokens(标记)的信息,信息的具体内容与系统的实现有关。标记是用空格分隔的一个字符序列。例如,下面这条#error命令:
#error 1 - Out of range error
包含了6个标记。当某些系统中执行#error命令时,命令中的标记将被作为出错信息显示出来,然后终止预处理,并停止程序编译。

预处理命令#pragma
#pragma token
将执行一个系统实现中已经定义好的操作。不能被系统实现识别出来的 pragma 将被忽略掉。

#和# #运算符

#运算符将替换文本中的标记转换成一个用引号引起来的字符串。来看下面这个宏定义:
#define HELLO(x) puts("HELLO, " #x);
当程序中出现HELLO(John)时,它将被展开成
puts("HELLO, " "John");
字符串John代替了替换文本中的#x。用空格分开的字符串将在预处理时被拼接起来。所以上面这条语句等价于:
puts("HELLO, John");
注意:#运算必须用在一个带参数的宏中,因为#的操作数就是宏的参数。

##运算符用于将两个标记拼接在一起。来看下面这个宏定义:
#define TOKENCONCAT(x, y) x ## y
当程序中出现 TOKENCONCAT 时,它的两个参数将被拼接在一起并用来替换宏。
例如程序中的 TOKENCONCAT (O, K)将会被OK替换。## 运算符必须要有两个操作数。

断言和符号常量NDEBUG

宏 assert 在头文件<assert.h>中定义——将在允许时测试一个表达式的值。若值为假,则assert打印出错信息并调用函数abort来结束程序的执行。
这是一个用于测试某个变量的值是否正确的调试工具。例如,假设程序中变量x的值绝对不超过10,那么我们就可以引入一个断言来测试x的值,并在x的值大于10 时打印一条出错信息。这个断言可以写成assert(x <= 10);。当它的上一条语句执行完后,若x大于10,则包含有行号及断言所在文件的文件名的出错信息就被打印出来,然后程序终止。于是就可以关注这一部分的代码,找出其中的错误。

若不在需要断言时,只需在程序中插入这一行即可。
#define NDEBUG
其后的所有断言将被忽略掉,不必逐条地删除每一个断言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_200_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值