详解C语言的预处理效果

目录
  • 前言
  • 一、预定义符号
  • 二、#define
    • 1.宏
    • 2.宏与函数
    • 3.带副作用的宏参数
    • 4. 宏和函数的不同
    • 5.#undef
  • 三、条件编译
  • 四、文件包含
    • 1.函数库文件包含
    • 2.本地文件包含
  • 总结

前言

编译一个C语言程序涉及很多步骤。其中第一个步骤被称为预处理。C语言的预处理器在源代码编译之前对其进行一些文本性质的操作。它的主要任务包括删除注释、插入被#include指令包含的文件内容、定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译。

一、预定义符号

下表为C语言预处理器定义的符号。他们的值有的是字符串常量,有的是十进制数字常量。

符号 示例值 含义
__ FILE__ “test.c” 当前编译的源文件名
__ LINE__ 25 本文件当前行号
__ DATE__ “Dec 27 2021” 文件被编译日期
__ TIME__ “21:30:23” 文件被编译时间
__ STDC__ 1 如果编译器遵循ANSI C,其值就为1,否则未定义

二、#define

我们先来看一下它的用法

#define name stuff

有了这条指令以后,每当有符号name出现在这条指令之后时,预处理器就会把它替换为stuff。

如果定义中的stuff非常长,那就可以将它分成几行,除了最后一行,每行的末尾都要加一个反斜杠,如下面例子所示:

#define DEBUG_PRINT printf("File %s line %d:" \
					"x = %d, y = %d, z = %d, \
					__FILE__, __LINE__, x, y, z)

这里利用了一个特性:相邻的字符串常量被自动连接为一个字符串。在调试一个存在许多涉及一组变量的不同计算过程的程序时,这种类型的声明非常有用。我们可以很容易的插入一条测试语句,打印出它们的当前值。

x *= 2;
y += x;
z = x * y;
DEBUG_PRINT;

1.宏

#define机制包括一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或者定义宏(define macro)。下面是宏的声明方式:

#define name(parameter-list) stuff

其中,parameter-list(参数列表)是一个由逗号分隔的符号列表,它们可能出现在stuff 中。参数列表的左括号必须与name紧邻。否则,参数列表就会被解释为stuf的一部分。

当宏被调用时,名字后面是一个由逗号分隔的值的列表,每个值都与宏定义中的一个参数相对应。当参数出现在程序中时,与每个参数对应的实际值都将会被替换到stuff中。

例如:

#define SQUARE(x) x*x

SQUARE(5)

当这两条语句位于程序中时,预处理器就会用上面的表达式替换下面的表达式,就会变成:5 * 5。

但是上面这个宏存在一个问题,请大家观察下面的代码:

a = 5;
printf("%d\n", SQUARE(a + 1));

可能我们直观的觉得这段代码将打印36这个值。但是事实上,它会打印11。Why?来,我们按照宏的规则做一个替换,这条语句将变成:

printf("%d\n", a + 1 * a + 1);

发现问题了吗,这里由替换而产生的表达式并没有按照预想的次序进行求值。所以,我们要对宏定义的参数加上括号,包括stuff整体。这样就能产生我们预期的结果了。

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果存在,它们会首先被替换掉。

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替代。

3.最后,才气对结果文本进行扫描,看看它是否包含了任何由#define定义的符号。如果是的话,就重复上述处理过程。

2.宏与函数

宏非常频繁的用于执行简单的计算,比如在两个表达式中寻找其中较大的一个(或较小):

#define MAX(a, b) ((a) > (b) ? (a) : (b))

这个功能好像我们也能用函数来实现,那为什么不使用函数呢?有两个原因。首先,用于调用和从函数返回的代码很可能比实际执行这个小型工作的代码更大,所以使用宏要比使用函数在程序中的规模和速度都更胜一筹。

其次,更为重要的是,函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式中使用。但是宏是与类型无关的。

有优点就会有缺点,和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的副本都将会插入到程序中。除非宏非常短,否则使用宏可能会大幅度增加程序的长度。

还有一些任务无法用函数来实现,比如下面这个代码:

#define MALLOC(n, type) ((type*) malloc ((n) * sizeof(type)))

type是一个数据类型,而函数是无法将类型作为参数传递的。

3.带副作用的宏参数

当宏参数在宏定义中出现的次数吵过一次时,如果这个参数具有副作用,那么在使用这个宏时就可能出现危险,导致不可预料的后果。副作用就是在表达式求值时出现永久性的后果。如下:

x + 1;

这个表达式无论执行几百次都是一样的,所以它没有副作用。

x++;

但是这个表达式就不同了,每次执行都会改变x的值,每一次执行都是一个不同的结果。所以,这个表达式是具有副作用的。我们看下面的例子,你觉得它会打印出什么:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

x = 5;
y = 8;
z = MAX(x++, y++);
printf("x =%d, y = %d, z = %d\n", x, y, z);

其结果是: x = 6, y = 10, z = 9。产生这个结果的原因是那个较小的值只增加了一次,而那个较大的值却增加了两次——第一次是在比较的时候,第二次是在执行?后面的表达式时。这就是一个具有副作用的宏参数,我们在使用的时候一定要注意。

4. 宏和函数的不同

通过一个表格来分析:

属性 #define宏 函数
代码长度 每次使用时,宏代码都被插入到程序中。除了非常小的宏,程序的长度将大幅增长 函数代码只出现在一个地方,每次使用函数时,都调用那个地方的同一份代码
执行速度 更快 存在函数调用和返回的额外开销
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则临近操作符的优先级可能会发生改变 函数参数指在函数调用时求值一次,其结果传递给函数,求值结果更容易预测
参数求值 参数每次用于宏定义时,它们都将重新求值。由于多次求值,具有副作用的参数可能会产生不可预料的后果 参数在函数被调用前只求值一次,在函数中多次使用参数并不会导致多个求职过程。参数的副作用并不会造成任何特殊的问题
参数类型 宏与类型无关。只要对参数的操作是合法的,它可以使用任何类型的参数 函数的参数是与类型有关的,如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的

5.#undef

#undef这个预处理指令用于移除一个宏定义:

#undef name

如果现存的名字需要被重新定义,那么首先必须用#undef移除它的旧定义。

三、条件编译

在编译一个程序时,如果可以翻译或忽略选定的某条语句或某组语句,会给我们带来极大的便利。而条件编译(conditional compilation)可以实现这个目的。使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略。用于支持条件编译的基本结构是#if指令和#endif指令。一下是其使用方式:

#if constant-expression
	statements
#endif
#if constant-expression
	statements
#elif constant-expression
	other statements
#else
	other statements
#endif

同时,条件编译的另一个用途是在编译时选择不同的代码部分。所以#if指令还具有可选的#elif和#else子句。如下:

#if constant-expression
	statements
#elif constant-expression
	other statements
#else
	other statements
#endif

#elif子句出现的次数可以不限。但是每个常量表达式(constant-expression)只有当前面所有常量表达式的值都为假时才会被编译。#else子句中的语句只有当前面所有常量表达式的值都为假时才会被编译。

四、文件包含

#include指令使另一个文件的内容被编译,就好像它实际出现在#include指令出现的位置一样。这种替换执行的方式很简单:预处理器删除这条指令,并用包含文件的内容取而代之。如果一个头文件被包含到十个源文件中,那它实际上被编译了十次。

1.函数库文件包含

C语言编译器支持两种不同类型的#include文件包含:函数库文件和本地文件。事实上,他们之间的区别很小。

函数库文件的包含使用以下语法:

#include<filename>

对于filename(文件名),并没有任何的限制,不过根据规定,标准库文件以一个.h后缀结尾。

编译器通过观察由编译器定义的“一系列标准位置”查找函数库头文件。你所使用的编译器文档会说明这些标准的位置是什么,以及怎样修改它们或者在列表中添加其他位置。

2.本地文件包含

以下是#include指令的另外一种形式:

#include"filename"

标准允许编译器自行决定是否把本地形式的#include和函数库形式的#include区别对待。可以先对本地头文件使用一种特殊的处理方式,如果失败,编译器再按照函数库头文件的处理方式对待它们进行处理。

处理本地头文件的一种常见策略就是在源文件所在的当前目录进行查找,如果该头文件并未找到,编译器就像查找函数库头文件一样在标准位置查找本地头文件。

总结

不要在一个宏定义的末尾加上分号,使其成为一条完整语句。在宏定义中使用参数,不要忘了在其周围加上括号。同时不要忘了在宏定义两边加上括号.

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C语言预处理预编译命令及宏定义详解

    目录 程序翻译环境和执行环境 翻译环境:详解编译+链接 1. 编译 - 预处理/预编译 test.c ---- test.i 2. 编译 - 编译 test.i ---- test.s 3. 编译 - 汇编 test.s ---- test.obj 4. 链接 test.obj ---- test.exe 运行环境 预处理/预编译详解 #define 定义标识符 #和## #的作用 ##的作用 命名约定 命令行定义 条件编译 常见的条件编译指令 文件包含 offsetof(宏类型,成员名字)偏移

  • 详解C语言之预处理(下)

    目录 #define定义宏带副作用的宏参数 #define定义宏的优点 #define定义宏劣势 预处理 预定义符号 预处理指令 条件编译 1.调试性代码 2.防止重复的头文件多次编译 总结 #define定义宏带副作用的宏参数 我们来看如下一段代码 结果分别为12,11,13 当参数替换后,首先判断表达式 (a++)>(b++)?,判断后a的值加1 b的值加1,然后执行表达式(b++)此时执行的值为12,执行完成后b的值加1,则a的值为11,b的值为13.可以看出对于这种情况下的宏是带有副作用

  • C语言中#define预处理语法总结

    目录 一.使用#define定义标识符常量 语法1: 语法2: 二.#define来定义带有参数的宏 三.C语言给我们定义好的一些标识符常量 一.使用#define定义标识符常量 语法1: #define MAX 100 注意使用#define末尾不需要带; MAX是一个常量,是使用#define定义的一个标识符常量 !!! 我为什么要在这里强调这是一个常量呢?因为它和普通常量一样不可被修改 大家可能会疑惑使用const修饰的变量不也是不可被修改的吗,这两者有什么不同呢? 这里要强调一下,使用c

  • C语言预处理详解

    目录 一,预定义符号 二,#define 1,#define 定义标识符 2,#define 定义宏 3,#define 替换规则 三,##的作用 1,概念 2,带副作用的宏参数 3,宏和函数对比 四,命名约定 1,#undef 2,文件包含 总结 一,预定义符号 __FILE__ //进行编译的源文件 __LINE__ //文件当前的行号 __DATE__ //文件被编译的日期 __TIME__ //文件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义 in

  • 详解C语言之预处理

    目录 程序的翻译环境 编译 预编译: 编译: 汇编: 链接 合并段表: #define的用法 1.#define定义标识符,例如 2.#define定义宏 3.#define实现将参数插入到字符串中 总结 程序的翻译环境 源文件被转换成可执行的机器指令时所处的环境称为翻译环境. 由源文件(.c)转换成可执行文件(.exe)需要两步 编译通过编译器实现,链接通过链接器实现 每个源文件都会经过编译器处理后生成对应的目标文件,然后链接器将目标文件和链接库链接在一起生成可执行程序 编译和链接的具体操作

  • C语言的预处理介绍

    目录 前言 一.预定义符号 二.#define 1.宏 2.宏与函数 3.带副作用的宏参数 4. 宏和函数的不同 5.#undef 三.条件编译 四.文件包含 总结 前言 编译一个C语言程序涉及很多步骤.其中第一个步骤被称为预处理.C语言的预处理器在源代码编译之前对其进行一些文本性质的操作.它的主要任务包括删除注释.插入被#include指令包含的文件内容.定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译. 一.预定义符号 下表为C语言预处理器

  • C语言程序的编译与预处理详解

    目录 一.程序的编译 1. 编译阶段 2.链接 二.预处理详解 1.预定义符号 2.#define定义的标识符 3.#define定义的宏 4.#unef 总结 一.程序的编译 我们写的源文件(*.c)是经过怎样的处理生产可执行文件(*.exe)的呢?这种处理有两个步骤-编译和链接.源文件在编译阶段通过编译器将每个源文件转换为目标文件(这些文件是可执行的机器指令),再通过链接器将其捆绑到一起,生成一个完整的可执行程序. 1. 编译阶段 编译阶段可细分为3个阶段:预处理(即预编译).编译.汇编 预

  • 详解C语言的预处理效果

    目录 前言 一.预定义符号 二.#define 1.宏 2.宏与函数 3.带副作用的宏参数 4. 宏和函数的不同 5.#undef 三.条件编译 四.文件包含 1.函数库文件包含 2.本地文件包含 总结 前言 编译一个C语言程序涉及很多步骤.其中第一个步骤被称为预处理.C语言的预处理器在源代码编译之前对其进行一些文本性质的操作.它的主要任务包括删除注释.插入被#include指令包含的文件内容.定义和替换由#define指令定义的符号,同时确定代码的部分内容是否应该根据一些条件编译指令进行编译.

  • 详解C语言#define预处理宏定义

    目录 #define介绍: #define宏定义无参的一般形式为:#define  标识符 常量 #define宏定义有参的一般形式为:#define  标识符(参数表) 表达式 #运算符: ##运算符: 可变宏...和__VA_ARGS__: 开发项目中常用的宏定义: #define介绍: C语言里可以用#define定义一个标识符来表示一个常量.特点是:定义的标识符不占内存,只是一个临时的符号,预编译后这个符号就不存在了,也不做类型定义.预编译又叫预处理.预编译就是编译前的处理.这个操作是在

  • 详解C语言之预处理(上)

    目录 程序的翻译环境 编译 预编译: 编译: 汇编: 链接 合并段表: #define的用法 1.#define定义标识符,例如 2.#define定义宏 3.#define实现将参数插入到字符串中 总结 程序的翻译环境 源文件被转换成可执行的机器指令时所处的环境称为翻译环境. 由源文件(.c)转换成可执行文件(.exe)需要两步 编译通过编译器实现,链接通过链接器实现 每个源文件都会经过编译器处理后生成对应的目标文件,然后链接器将目标文件和链接库链接在一起生成可执行程序 编译和链接的具体操作

  • 详解C 语言项目中.h文件和.c文件的关系

    详解C 语言项目中.h文件和.c文件的关系 在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代,那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件.但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件. 于是人们将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句.这样即

  • 详解C语言常用的一些转换工具函数

    1.字符串转十六进制 代码实现: 2.十六进制转字符串 代码实现: 或者 效果:十六进制:0x13 0xAA 0x02转为字符串:"13AAA2" 3.字符串转十进制 代码实现: 第一种,如果带负号 这个就是atoi函数的实现: 效果:字符串:"-123" 转为 -123 第二种,如果不带负号: 效果:字符串:"123" 转为 123 第三种:包含转为浮点数: 效果:字符串:"123.456" 先转为 123456,然后除以1

  • 详解C语言数组越界及其避免方法

    所谓的数组越界,简单地讲就是指数组下标变量的取值超过了初始定义时的大小,导致对数组元素的访问出现在数组的范围之外,这类错误也是 C 语言程序中最常见的错误之一. 在 C 语言中,数组必须是静态的.换而言之,数组的大小必须在程序运行前就确定下来.由于 C 语言并不具有类似 Java 等语言中现有的静态分析工具的功能,可以对程序中数组下标取值范围进行严格检查,一旦发现数组上溢或下溢,都会因抛出异常而终止程序.也就是说,C 语言并不检验数组边界,数组的两端都有可能越界,从而使其他变量的数据甚至程序代码

  • 详解R语言中的表达式、数学公式、特殊符号

      在R语言的绘图函数中,如果文本参数是合法的R语言表达式,那么这个表达式就被用Tex类似的规则进行文本格式化. y <- function(x) (exp(-(x^2)/2))/sqrt(2*pi) plot(y, -5, 5, main = expression(f(x) == frac(1,sqrt(2*pi))*e^(-frac(x^2,2))), lwd = 3, col = "blue") library(ggplot2) x <- seq(0, 2*pi, b

  • 详解Go语言的错误处理和资源管理

    目录 一.defer 二.错误处理 三.统一错误处理的逻辑 四.panic 五.recover 六.error vs panic 七.错误处理综合示例 一.defer 1. defer保证在函数结束时发生. 2. defer列表为先进后出 3. 参数在defer语句时计算. 下面来看一个例子: 写入文件 package main import ( "aaa/functional/fbi" "bufio" "fmt" "os"

随机推荐