C语言_编译和预处理
内容
本章主要讲解了预处理的三种方式:宏定义、文件包含、条件编译
学习目标
- 掌握无参数宏定义和带参数宏定义的使用方法
- 学会使用文件包含
- 熟悉条件编译指令的使用方法
预处理命令的作用不是实现程序的功能,而是给C语言编译系统提供信息,通知C编译器在对源程序进行编译之前应该做哪些预处理工作。预处理是指在进行编译之前所作的处理,由预处理程序负责完成。接下来还要经过编译、链接,才能变成可执行程序。本章节将结合案例对编译和预处理的相关知识进行详细讲解。
最简单的预处理-不带参宏定义
案例描述
为了引入“预处理”这个概念,本案例要求将矩形的长和宽设置为宏,然后再求出矩形的面积。这事最简单的预处理。
案例分析
宏定义是预处理最常用的功能之一,它用于将一个标识符定义为一个字符串。这样,在源程序被编译器处理之前,预处理器会将标识符替换成定义的字符串。根据是否带参数,可以将宏定义分为无参数宏定义和带参数宏定义。本案例要学习的是不带参数的宏定义。
必备知识
不带参数的宏定义
在程序中,经常会定义一些常量,例如,3.14、“ABC”。如果这些常量在程序中被频繁使用,难免会出现书写错误的情况。为了避免程序书写错误,可以使用不带参数的宏定义来定义这些常量,其语法格式如下:
1 |
在上述语法格式中,"#define"用于标识一个宏定义,"标识符"指的是所定义的宏名,"字符串"指的是宏体,它可以是常量、表达式等。一般情况下,宏定义需要放在源程序的开头,函数定义之外。它的有效范围是从宏定义语句开始到源文件结束。一般宏名都是大写字母,以便于与其他的操作符区别。
1 |
#undef
指令取消宏定义
与#define相对,还有#undef指令用于取消宏定义,当使用#define定义了一个宏之后,如果预处理器在接下来的源代码中看到了#undef指令,那么#undef后面的代码中这个宏将会失效,如下代码所示。
1 |
|
运行这段程序,会报错
1 | IntelliSense: 未定义标识符 "PI" //7行17列 |
第二简单的预处理-带参宏定义
案例描述
在之前的章节中,我们已经学过简单的数据交换。本案例要求使用宏定义,依次交换两个一维数组中的元素。
案例分析
本案例要实现两个一维数组中元素的依次交换,整个交换过程包含多次数组元素的交换。结合之前学习的知识,可以使用函数实现简单的数据交换功能,在使用循环遍历数组的同时,调用交换函数,实现数组元素的交换。本案例要求使用宏定义实现此功能。
因为数组遍历的过程中,数据在不断改变,而不带参宏定义中只能定义固定的内容。这里我们需要使用第二简单的预处理方法——带参宏定义来完成本案例。
必备知识
带参数的宏定义
语法格式如下所示:
1 |
上述语法格式和不带参数的宏定义有些类似,不同的是多了一个括号,括号中的“形参表”由一个或多个形参组成,当多于一个形参时,形参之间要用逗号进行分隔。
对于带参数的宏定义来说,同样需要使用字符串替换宏名,使用实参替换形参。
与带参函数的区别
带参宏定义和带参函数有时可以实现同样的功能,但两者有本质的不同,具体如表所示。
基本操作 | 带参数的宏定义 | 带参数的函数 |
---|---|---|
处理时间节点 | 预处理时 | 程序运行时 |
参数类型 | 无 | 需定义参数类型 |
参数传递 | 不分配内存,无值传递的问题 | 分配内存,将实参值代入形参 |
运行速度 | 快 | 相对较慢,因为函数的调用会涉及到参数的传递、压栈、出栈等操作 |
务必要注意的问题
来看一个例子
1 |
这是一个求绝对值的带参宏定义,调用这个宏定义,代码如下所示:
1 | double x = 12; |
输出的结果是14,显然与我们的初意中的12不相符。这是因为在预处理时,表达式"ABS(++x)“会被替换为”( (++x)>=0 ? (++x): -(++x) ) ",因此结果是14。
那么,这就是带参宏定义时要注意的问题,宏定义中的参数替换是“整体”替换,不像是函数中只是参数之间的值传递。
案例实现
1 |
|
关于宏定义中参数的替换-要注意的问题
表达式字符串中出现运算符
若宏定义中的字符串出现运算符,需要在合适的位置上加上括号,如果不添加括号可能会出现错误。例如
1 |
|
宏定义的末尾不要加分号
如果加了分号,将被视为被替换字符串的一部分。
宏定义不会进行严格的语法检查,因此宏替换的错误要等到系统编译时才能被发现,例如:
1 |
|
宏定义允许嵌套
在宏定义的字符串中可以使用已经定义的宏名。
1 |
|
但宏定义不支持递归,因此下面的宏定义是错误的:
1 |
预处理的第二种方式-文件包含
案例描述
要求设计一个头文件,将经常使用的输出模式都写进头文件中,方便编写代码。
案例分析
除宏定义外,文件包含也是一种预处理语句,它的作用就是将一个源程序文件包含到另外一个源程序文件中。
必备知识
文件包含命令的形式
同引入头文件一样,文件包含也是使用#include指令实现的,它的语法格式有两种,具体如下
格式一
1 |
格式二
1 |
区别
上述两种格式都可以实现文件包含,不同的是,格式一是标准形式,当使用这种格式时,C编译系统在系统指定的路径下搜索尖括号(<>)中的文件;当使用第二种格式时,系统首先会在用户当前工作的目录中搜索双引号(“”)中的文件,如果找不到,再按系统指定的路径进行搜索。
案例实现
foo.h代码如下:
1 |
main.c代码如下:
1 |
|
32还是64-条件编译
案例描述
要求使用条件编译,根据条件输出对应的判定结果:如果系统是32位的,就输出“系统是32位的”;如果系统是64位的,就输出“系统是64位的”。
案例分析
上文提到的“条件编译”也是预处理的一种方式。
一般情况下,C语言程序中的所有代码都要参与编译,但有时出于程序代码优化的考虑,希望源代码中一部分内容只在指定条件下进行编译。这种根据指定条件,只对程序一部分内容编译的情况,称为条件编译。
在C语言中条件编译指令的形式有很多种,接下来将详细讲解一种最常见的条件编译指令:#if/#else/#endif,该指令根据常数表达式来决定某段代码是否执行。
必备知识
#if/#else/#endif指令
通常情况下,#if指令、#else指令和#endif指令是结合在一起使用的,其语法格式如下所示:
1 |
|
在上述语法格式中,编译器只会编译程序段1和程序段2中的一段。当条件为真时,编译器会编译程序段1,否则编译程序段2。
案例实现
案例设计
- 定义两个宏,分别表示Windows32位和64位平台;
- 定义宏SYSTEM表示其中某个平台;
- 使用条件编译指令判断SYSTEM值,并输出结果到屏幕上。
完整代码
1 |
|
#ifdef-神奇的#include<stdio.h>
案例描述
在同一文件中写两遍"#include<stdio.h>",编译器进行编译时为什么没有报错呢?按常理而言,文件"stdio.h"中的函数和数据类型等必然被定义了两次,此时编译器应该报出"重定义"的错误,但实际上编译十分顺利。
案例分析
在上一个案例中我们提到C语言中条件编译指令的形式有很多种,如果现在的你百思不得其解,那是因为你没有学过另一种条件编译指令:#ifdef和#ifndef。下面来讲解。
必备知识
#ifdef指令
如果想判断某个宏是否被定义,可以使用#ifdef指令,通常情况下,该指令需要和#endif一起使用,#ifdef指令的语法格式如下所示:
1 |
|
在上述语法格式中,#ifdef指令用于控制单独的一段源码是否需要编译,它的功能类似于一个单独的#if/#endif
#ifndef指令
和#ifdef相反,#ifdef用来确定某一个宏是否没有被定义,如果宏没有被定义,那么就编译#ifndef和#endif中间的内容,否则就跳过。其语法格式如下所示:
1 |
|
案例实现
如果我们打开"stdio.h"这个文件,便会发现其开头是这样的两行代码。
1 |
在其结尾有这样一行代码:
1 |
这三行代码是三条预处理指令,也就是为什么写两遍"#include<stdio.h>"也不会报错。当然,写更多遍也不会报错。
这三行代码的含义是:如果"_STDIO_H_“没有定义过,那么就定义”_STDIO_H_“。仔细观察后我们会发现**”#define _STDIO_H_“后面什么都没写,其实这也是宏定义的一种写法——并不关注”_STDIO_H_"被定义成了什么,只关注他是否被定义过。**
综上分析可知,初次遇到"_STDIO_H_“的时候,由于宏”_STDIO_H_“尚未定义,因此,#ifndef条件成立,定义”_STDIO_H_“。当再次遇到”_STDIO_H_“的时候,#ifndef的条件不成立,因此它与”#endif"之间的内容就不会被编译了。
利用预定义宏得知程序允许到了何处
下面是<stdio.h>头文件中的五个预定义宏,利用这些宏可以轻松得知程序运行到了何处,有助于编程人员进行程序调试,具体如下表。
预定义宏 | 说明 |
---|---|
_DATE_ | 定义源文件编译日期的宏 |
_FILE_ | 定义源代码文件名的宏 |
_LINE_ | 定义源代码中行号的宏 |
_TIME_ | 定义源代码编译时间的宏 |
_FUNCTION_ | 定义当前所在函数名的宏 |