第14章 预处理器

作者: 橡树人 | 来源:发表于2020-03-09 07:42 被阅读0次

英文原版:P315

诸如#define#include预处理指令都是由预处理指令是由预处理器来处理的。

预处理器是一个在编译前对C程序进行编辑的小型软件。

C语言对预处理器的依赖使其在主流编程语言中显得与众不同。

预处理器虽然是一个功能强大的工具,但也可能产生许多难发现的错误。而且,预处理器很容易被误用来编写几乎读不懂的程序。因此,建议:不要过分依赖预处理器,请适度地使用

本章的主要内容

  • 14.1节描述预处理器的工作原理。
  • 14.2节给出一些影响所有预处理指令的通用规则。
  • 14.3节介绍预处理的宏定义功能。
  • 14.4节介绍预处理器条件编译功能。
  • 14.5节介绍较少被使用的预处理指令:#error#line#progma

14.1 预处理器是如何工作的?

什么是预处理指令?

#开头的指令,末尾不包含分号``;

预处理指令的工作原理

  • 输入:C源程序
  • 输出:源程序的修改版本,该版本不包含预处理指令
  • 功能:执行预处理指令,并移除它们。

注意事项:

  • 预处理指令的末尾不包含分号;

例1 celsius.c

/* Converting a Fahrenheit temperature to Celsius */

#include <stdio.h>

#define FREEZING_PT 32.0f
#define SCALE_FACTOR (5.0f/9.0f)


int main(void)
{
    float fahrenheit, celsius;

    printf("Enter Fahrenheit temperature: ");
    scanf("%f", &fahrenheit);

    celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR;

    printf("Celsius equivalent: %.1f\n", celsius);
    
    return 0;
}

生成.i 文件

gcc -E celsius.c -o celsius.i

文件celsius.i

空行
空行
从stdio.h中引入的行
空行
空行
空行
空行
int main(void)
{
 float fahrenheit, celsius;

 printf("Enter Fahrenheit temperature: ");
 scanf("%f", &fahrenheit);

 celsius = (fahrenheit - 32.0f) * (5.0f/9.0f);

 printf("Celsius equivalent: %.1f\n", celsius);

 return 0;
}

解释:
在这个例子中,预处理器都做了哪些事?

  1. 预处理器执行#include指令:
    • 引入stdio.h的内容
  2. 预处理器执行#define指令:
    • 移除#define指令;
    • 将源文件中出现FREEZING_PTSCALE_FACTOR的地方替换为宏定义的值;
  3. 用空格符来替换注释

14.2 预处理指令都遵循哪些规则?

常见的预处理指令有哪几类?

  1. 宏定义
    可使用#define来定义一个宏;
    可使用#undefine来移除一个宏的定义;
  2. 文件包含
    可使用#include来让一个程序包含某个具体文件的内容;
  3. 条件编译
    基于预处理器测试的条件,可使用#if#ifdef#ifndef#elif#else#endif来将一个文本块包含到程序中,或者从程序中移除一个文本块。

所有的预处理指令都遵循的一些规则:

  • 指令都以#号开头;
  • 在一个指令的tokens间可放入任意数量的空格符和水平制表符;
  • 指令总是在第一个换行符处结束,除非显式地指出
  • 指令可出现在程序中的任何地方;
  • 注释可以和指令在同一行里;

例1 需要多行的单条指令

#define DISK_CAPACITY (SIDES *                     \
                       TRACKS_PER_SIDE *          \
             SECTORS_PER_TRACK *          \
                       BYTES_PER _SECTORS)

例2 在宏的定义后面加上注释

#define FREZZING_PT 32.0f //水的凝固点

14.3 宏的定义

如何定义宏?

宏定义注意事项:
不要在宏定义里添加额外的字符

错误示例1 在宏定义里放等号=

#define N = 100
...
int a[N];//等价于int a[=100]

错误示例2 在宏定义里放分号;

#define N 100;
...
int a[N];//等价于int a[100;]

不带参数的宏

格式:

#define indentifier replace-list

常用情形:
给数值常量、字符常量、字符串字面量起个别名

解释:

  • 替换列表可以包含标识符、关键字、数值常量、字符常量、字符串字面量、操作符及标点符号等。
  • 当预处理器碰到一个宏定义时,它会做下记录:标识符代表替换列表。之后,不管在文件哪里出现了标识符,则预处理器就会用替换列表代替。

例1 无参数宏定义

#define STR_LEN 80
#define TRUE 1
#define FALSE 0
#define PI 3.14159
#define CR '\r'
#define EOS '\0'
#define MEM_ERR "Error: not enough memeory"

使用宏来给常量起别名有若干个好处:

  • 使程序更易读;
  • 使程序更易修改;
  • 避免出现不一致错误和输入错误;
  • 对C语法做最小的修改;
  • 重命名一些类型;
  • 控制条件编译;

带参数的宏

格式:

  • #define identifier(x_1,x_2, ..., x_n) replace_list
  • x_1,x_2,...,x_n是宏的参数,宏的参数可在替换列表中出现多次;
  • 在宏名字和左括号之间没有空格;如果有空格,则预处理器假设该宏是不带参数的宏,且将(x_1,x_2,...,x_n)看做是替换列表的一部分;

宏定义宏调用

预处理器是如何处理带参数宏的?

  • 当预处理器遇见一个带参数宏的定义时,它会存储该定义以备后续使用。
  • 诸如identifier(y_1,y_2,...,y_n)形式的宏调用,后续无论出现在程序哪里,预处理器都会用替换列表来替换它,比如y_1替换x_1y_2替换x_2等。

带参数的宏的用法:

  • 充当简单的函数

例1 宏定义

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

解释:

  • MAX没有运行时开销。
  • MAX支持泛型,即可使用宏MAX来找出任意类型的两个值的较大者,比如intlongfloatdouble等。

例1 宏定义

#include <stdio.h>

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

int main(void)
{
    int i;

    i = 98;
    if(IS_EVEN(i)){
        i++;
    }

    printf("%d\n", i);

    MAX(100,34);
    return 0;
}

例2 复杂点的带参数宏定义

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

例3 空参数列表的带参数宏定义

#define getchar() getc(stdin)

函数调用都有哪些运行时开销?

  • 保存上线文信息;
  • 拷贝参数;

如何避免函数调用的运行时开销?

  • 使用带参数的宏;
  • 声明一个函数是inline

带参数的宏 VS 真正的函数
优点:

  • 没有运行时开销;
  • 宏是泛型的;

函数调用在程序执行过程中是有开销的,比如保存上下文信息、拷贝参数等。
宏调用没有运行时开销。

宏支持泛型
宏的形参跟函数形参不一样,没有特定的类型。
宏可接受任意类型的实参

带参数宏的缺点有哪些?

导致编译后的代码过长。
每个宏调用都会导致在源代码中插入宏的替换列表,因此会增加源代码的长度。
宏使用的次数越多,这种效果越明显。特别是当嵌套进行宏调用时,效果会加倍。

编译器不对宏调用实参进行类型检查,也不会发生类型转换。
当函数调用发生时,编译器会挨个检查实参来看其是否有合适的类型。如果存在某个实参不是合适的类型,则该实参就会被转换成合适的类型,或者发出一条错误消息。

不存在指向宏的指针
存在指向函数的指针
因为宏在预处理后就被移除了。

宏调用可能对实参求值多次
函数调用只会对实参求值一次
如果实参有副作用的话,则对一个参数求值多次会出现不在期望内的行为。

带参数的宏

  • 模拟函数
  • 重复代码段

带参数的宏注意事项:

对宏实参多次求值导致的错误是很难被发现的,因为宏调用跟函数调用看起来是一样的。
更糟的是,一个宏在大部分时候是正常工作的,仅对某些有副作用的参数会有错误。
最好不要使用有副作用的实参。

14.4 条件编译指令

  • #if指令和#endif指令
  • ifdef指令和ifndef指令
  • #elif指令和#else指令

知识补充

什么是条件编译?
根据其执行的测试的结果,预处理器可包含或者移除一段程序文本。

#if指令和#endif指令

格式:

#if 常量表达式
....
#endif 

预处理器是如何处理#if指令和#endif指令的?
当遇见#if指令时,预处理器会对常量表达式求值。

  • 如果该表达式的值是0,就移除在#if#endif之间的代码。
  • 否则,就保留在#if#endif之间的代码,待编译器处理。

注意事项:
如果在#if中出现未定义的宏,则#if指令会默认该未定义的宏的值是0。

例1 保留测试代码,且让编译器忽略这些代码

#include <stdio.h>

#define DEBUG 1

int main(void)
{
    int i, j;

    i = 100;
    j = 98;

    #if DEBUG
    printf("Value of i : %d\n", i);
    printf("Value of j : %d\n", j);
    #endif  /** DEBUG*/

    return 0;
}

defined运算符

defined运算符是预处理器特有的运算符。

功能:

  • 如果该标识符是个当前已被定义的宏,则该defined表达式的值就是1,否则是0。

例2 defined运算符

#include <stdio.h>

//注意这里没有给DEBUG定义一个值,但是预处理后的文件中仍包含了printf语句
#define DEBUG

int main(void)
{
    int i, j;

    i = 100;
    j = 98;
        
    // defined只是测试DEBUG是否已被定义过
    #if defined(DEBUG)
    printf("Value of i : %d\n", i);
    printf("Value of j : %d\n", j);
    #endif  /** DEBUG*/

    return 0;
}

ifdef指令和ifndef指令

#ifdef

  • 用于测试一个标识符是否已被定义为一个宏

#ifndef

  • 用于测试一个标识符是否未被定义为一个宏

例1 等价关系

#ifdef 标识符

等价于

#if defined(标识符)

例2 等价关系2

#ifndef 标识符

等价于

#if !defined(标识符)

#elif指令和#else指令

为了提供多一点便利,预处理器还支持:#elif指令和#else指令

格式:

#elif 常量表达式

#else

相关文章

  • 关于预处理器LESS的使用

    要了解预处理器LESS的使用,首先得知道什么是预处理器,说到预处理就会讲到后处理器 一、预编译器和后编译 1.预处...

  • 【Java学习干货】SpringMVC中拦截器的使用

    什么是拦截器?SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处...

  • 初识css预编译之Less

    什么是less less是CSS的预处理器,学过C语言的同学应该对预处理器挺熟悉的把,C语言的编译过程就分为:预处...

  • jmeter随机生成11位手机号

    方法一 随机数生成方法,使用BeanShell 预处理程序 1、【线程组-添加-后置处理器-BeanShell 预...

  • /Zc:__plusplus的意义

    预 __cplusplus 处理器宏通常用于报告对特定版本的 C++ 标准的支持,默认情况下,Visual Stu...

  • 9、SpringMVC-拦截器

    一、概要 Spring Web MVC的拦截器,类似于Servlet开发中的过滤器Filter,用于对处理器进行预...

  • 二十二. SpringMVC-拦截器

    一、概要 Spring Web MVC的拦截器,类似于Servlet开发中的过滤器Filter,用于对处理器进行预...

  • C 程序结构

    C 程序主要包括以下部分: 预处理器指令 函数 变量 语句 & 表达式 注释 程序第一行#include 是预处...

  • 2018-05-07

    第 三 轮 抢 票预 告

  • 电脑技巧:酷睿i3、i5、i7

    Intel 第 7 代处理器型号与规格 Intel 处理器大类 在大分类上有着 酷睿(Core)、奔腾(Penti...

网友评论

    本文标题:第14章 预处理器

    本文链接:https://www.haomeiwen.com/subject/svvydhtx.html