C语言的预处理介绍

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

前言

编译一个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

当常量表达式(constant-expression)的值是非零,那么statements部分就被正常编译,否则预处理器就静默的删除它们。

同时,条件编译的另一个用途是在编译时选择不同的代码部分。所以#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区别对待。可以先对本地头文件使用一种特殊的处理方式,如果失败,编译器再按照函数库头文件的处理方式对待它们进行处理。

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

总结

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

到此这篇关于C语言的预处理介绍的文章就介绍到这了,更多相关C语言预处理内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 你必须知道的C语言预处理的问题详解

    C语言预处理器执行宏替换.条件编译和文件包含.通常采用以"#"为行首的提示.下面是C语言预处理的应用场合: 1.三字母词(Trigraph Sequences) C源程序的字符集被包含在7位的ASCII字符集中,但是它是ISO 646-1983 Invariant Code Set的超集.为了让程序可以在缩减集(reduced set)中呈现出来,下面的三字母词会被替换成相应的单字符. 三字母词 单字符 ??= # ??/ \ ??' ^ ??( [ ??) ] ??! | ??<

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

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

  • 详解C语言编程中预处理器的用法

    预处理最大的标志便是大写,虽然这不是标准,但请你在使用的时候大写,为了自己,也为了后人. 预处理器在一般看来,用得最多的还是宏,这里总结一下预处理器的用法. #include <stdio.h> #define MACRO_OF_MINE #ifdef MACRO_OF_MINE #else #endif 上述五个预处理是最常看见的,第一个代表着包含一个头文件,可以理解为没有它很多功能都无法使用,例如C语言并没有把输入输入纳入标准当中,而是使用库函数来提供,所以只有包含了stdio.h这个头文

  • C语言的预处理介绍

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

  • C语言之预处理命令的深入讲解

    c提供的预处理功能有: 宏定义 文件包含 条件编译 为了与其她c语句区分,命令经常以符号"#"开头. 宏定义 #define 标识符 字符串 可以避免反复输入字符串,后面不加:宏定义在默认时的有效范围是全部.也可以用#undef终止宏定义区域. 不含参数 宏展开带入程序 含参数 #include<stdio.h> #define PI 3.1415 #define S(r) PI*r*r int main() { int a; float area; scanf("

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

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

  • R语言的历史介绍

    R语言来自S语言,是S语言的一个变种.S语言由Rick Becker, John Chambers等人在贝尔实验室开发, 著名的C语言.Unix系统也是贝尔实验室开发的. S语言第一个版本开发于1976-1980,基于Fortran: 于1980年移植到Unix, 并对外发布源代码. 1984年出版的"棕皮书" (Becker and Chambers 1984) 总结了1984年为止的版本, 并开始发布授权的源代码. 这个版本叫做旧S.与我们现在用的S语言有较大差别. 1989–19

  • Go语言基础知识点介绍

    Go 语言教程 Go 是一个开源的编程语言,它能让构造简单.可靠且高效的软件变得容易. Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本.现在Go的开发已经是完全开放的,并且拥有一个活跃的社区. Go 语言特色 简洁.快速.安全 并行.有趣.开源 内存管理.数组安全.编译迅速 Go 语言用途

  • 用C语言实现链式栈介绍

    目录 堆栈的基本概念 常见的栈有顺序栈和链式栈 - 链式栈的C代码实现 代码运行效果 堆栈的基本概念 堆栈是只能在一端增删元素的表结构,该位置称为栈顶堆栈的基本运算是压入和弹出,前者相当于插入,而后者则是删除最后插入的元素,形成后进先出的运算规则最后插入的元素在被弹出之前可以作为栈顶被外界访问从空栈中弹出,或向满栈中压入,都被认为是一种错误 常见的栈有顺序栈和链式栈 顺序栈 链式栈 - 链式栈的C代码实现 #include <stdio.h> #include <stdlib.h>

  • C# 语言入门基础介绍

    目录 一..NET 体系结构 二.Hello world 三.类型和变量 四.程序结构 前言: C#(读作"See Sharp")是一种新式编程语言,不仅面向对象,还类型安全. 开发人员利用 C# 能够生成在 .NET 中运行的多种安全可靠的应用程序. C# 源于 C 语言系列,C.C++.Java 和 JavaScript 程序员很快就可以上手使用. 本教程概述了 C# 8 及更高版本中该语言的主要组件. C# 是面向对象的.面向组件的编程语言. C# 提供了语言构造来直接支持这些概

  • C语言数组详细介绍

    目录 什么是数组 一维数组 二维数组 数组越界 数组名 结尾 什么是数组 数组(Array)是一种用来存储同一种类型的集合,是一种有序的线性结构表.并且数组元素的地址是连续的. 数组最大的优点就是支持随机访问,当想访问数组的某个数时,只需要找到数组的对应下标就可以直接找到该数组对应元素.但是数组也有相应的缺点,那就是数组的元素个数和数组空间大小在创建时就已经被固定死了,如果数组的空间没有使用完也会造成空间浪费,并且因为数组的地址是连续的,这本应该是一个优点的,但是这导致数组在进行删除或增加元素时

  • golang语言map全方位介绍

    目录 一.map 1.基本介绍 2.声明基本语法 二.map 的使用 2.map[string]map[string]string使用案例 三.map 的增删改查操作 1.map 增加和更新 2.map 删除 3.map 查找 四.map的其他操作 1.map 遍历: 2.map 的长度 3.map 切片 4.map 排序 五.map 使用细节 总结 一.map 1.基本介绍 map 是 key-value 数据结构,又称为字段或者关联数组.类似其它编程语言的集合, 在编程中是经常使用到 2.声

  • C语言循环控制入门介绍

    目录 循环控制: for 循环 while 循环 do while 循环 switch case break: continue: break continue 区别: 总结 循环控制: for 循环 for (语句1; 表达式; 语句2) { 语句块 } 1)循环开始时,先执行语句1. 2)接着判断表达式的条件,如果条件成立,就执行一次循环体中的语句块. 3)语句块执行完后,接下来会执行语句2. 4)重复第2)步和第3),直到表达式的条件不成立才结束for循环. 常见形式: for (int

随机推荐