第8章 编绎和预处理
作业点评 请简述在使用puts()和strcat()函数时需要注意哪些题。 请叙述一下,你都掌握了哪些字符串操作函数。
预习检查 常用的预处理指令有哪些
学习目标 重点 了解 掌握 宏定义 1 3 条件编绎指令 2 文件包含
【案例1】-案例描述 本案例要求通过编程求出矩形的面积。大家可能疑惑,经历了前面 编程的“风风雨雨”为什么要在这里安排如此简单的案例?这是为了 引入“预处理”这个概念。本案例要求将矩形的长和宽设置为宏,然 后再求出矩形的面积。当然这是最简单的预处理。
【案例1】-案例分析 宏定义是预处理最常用的功能之一,它用于将一个标识符定义为一个字符串,这样,在源程序被编译器处理之前,预处理器会将标识符替换成所定义的字符串。根据是否带参数,可以将宏定义分为无参数宏定义和带参数宏定义。本案例要定义的是不带参数的宏定义。
【案例1】-必备知识 不带参数的宏定义 1
【案例1】-必备知识 宏定义是最常用的预处理功能之一,它用于将一个标识符定义为一个字符串。这样,在源程序被编译器处理之前,预处理器会将标识符替换成所定义的字符串。根据是否带参数,可以将宏定义分为不带参数的宏定义和带参数的宏定义。
【案例1】-必备知识 不带参数的宏定义 在程序中,经常会定义一些常量,比如,3.14、“ABC”。如果这些常量在程序中被频繁使用,难免会出现书写错误的情况。为了避免程序书写错误,可以使用不带参数的宏定义来表示这些常量。 #define 标识符 字符串 #define PI 3.14
【案例1】-案例实现 案例设计 1 2 案例代码(详见教材代码实现) 使用不带参数的宏定义分别表示矩形的长和宽; 计算矩形的面积并输出到屏幕上。 案例代码(详见教材代码实现)
多学一招 #undef指令取消宏定义 除了#define之外,相应地还有#undef指令,#undef指令用于取消宏定义。在#define定义了一个宏之后,如果预处理器在接下来的源代码中看到了#undef指令,那么#undef后面这个宏就都不存在了。 #define PI 3.14 int main() { printf("%f\n", PI); #undef PI return 0; } 这句代码调用错误 取消宏定义
【案例2】-案例描述 在之前的章节中我们已经学过简单的数据交换,想必大家对基础数 据的交换方法早已了如指掌。本案例要求使用宏定义,依次交换两个 一维数组中的元素。
【案例2】-案例分析 本案例要实现两个一维数组中元素的依次交换,整个交换过程包含多次数组元素的交换。结合之前学习的知识,可以使用函数实现简单的数据交换功能,在使用循环遍历数组的同时,调用交换函数,实现数组元素的交换。本案例要求使用宏定义实现此功能,案例1最简单的预处理中学习的不带参宏定义无法实现该功能,因为数组遍历的过程中数据在不断改变,而不带参宏定义中只能定义固定的内容。这里可以使用第二简单的预处理方法——带参宏定义来完成本案例。 下面请先学习其使用方法。
【案例2】-必备知识 带参数的宏定义 1
【案例2】-必备知识 带参数的宏定义 不带参数的宏定义只能完成一些简单的替换操作。如果希望程序在完成替换过程中,能够进行一些更加灵活的操作,例如,根据不同的半径计算圆的面积,这时可以使用带参数的宏定义。 #define 标识符(形参表) 字符串
【案例2】-必备知识 带参数的宏定义 带参宏定义和带参函数有时可以实现同样的功能,但两者却有着本质上的不同。 基本操作 带参数的宏定义 带参数的函数 处理时间 预处理 程序运行时 参数类型 无 需定义参数类型 参数传递 不分配内存,无值传递的问题 分配内存,将实参值带入形参 运行速度 快 相对较慢,因为函数的调用会涉及到参数的传递、压栈、出栈等操作
【案例2】-必备知识 带参数的宏定义 #define ABS(x) ((x) >= 0 ? (x) : -(x)) double x = 12; ABS(++x); 结果为14 原因 在预处理时,表达式“ABS(++x)”会被替换成“((++x) >= 0 ? (++x) : (-(++x)))”,因此结果是14。 宏定义中的参数替换是“整体”替换,如用“++x”替换“x”,而不像函数中只是参数之间的值传递。
【案例2】-案例实现 案例设计 1 2 3 案例代码(详见教材代码实现) 定义带参数的宏定义; 定义两个数组,给它们的元素赋值; 利用带参宏定义,交换数组元素的值并打印到屏幕上。 案例代码(详见教材代码实现)
脚下留心 宏定义中参数的替换 1、如果宏定义中的字符串出现运算符,需要在合适的位置加上括号,如果不添加括号可能会出现错误。 2、宏定义的末尾不用加分号,如果加了分号,将被视为被替换的字符串的一部分。由于宏定义只是简单的字符串替换,并不进行语法检查,因此,宏替换的错误要等到系统编译时才能被发现。 3、宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在替换时由预处理程序嵌套替换。
【案例3】-案例描述 在程序设计时需要很多输出格式,如整型、实型和字符型等等,在 编写程序时会经常使用这些输出格式,如果多次书写这些格式会很繁琐, 要求设计一个头文件,将经常使用的输出模式都写进头文件中,方便编 写代码。
【案例3】-案例分析 本案例要实现两个一维数组中元素的依次交换,整个交换过程包含多次数组元素的交换。结合之前学习的知识,可以使用函数实现简单的数据交换功能,在使用循环遍历数组的同时,调用交换函数,实现数组元素的交换。本案例要求使用宏定义实现此功能,案例1最简单的预处理中学习的不带参宏定义无法实现该功能,因为数组遍历的过程中数据在不断改变,而不带参宏定义中只能定义固定的内容。这里可以使用第二简单的预处理方法——带参宏定义来完成本案例。 下面请先学习其使用方法。
【案例3】-必备知识 文件包含命令的形式 1
【案例3】-必备知识 除宏定义外,文件包含也是一种预处理语句,它的作用就是将一个源程序文件包含到另外一个源程序文件中。
【案例3】-必备知识 文件包含命令的形式 同引入头文件一样,文件包含也是使用#include指令实现的,它的语法格式有两种,具体如下: 当使用双引号(“”)时,系统首先会在用户当前工作的目录中搜索双引号("")中的文件,如果找不到,再按系统指定的路径进行搜索。
【案例3】-案例实现 案例设计 1 将整型数据的输出写入到头文件中; 2 调用头文件输出整型数据; 案例代码(详见教材代码实现)
【案例4】-案例描述 此处的32和64指的是系统架构。案例要求使用条件编译,根据条件 输出对应的判定结果:如果系统是32位的,就输出“系统是32位的”, 如果系统是64位的,就输出“系统是64位的”。
【案例4】-案例分析 上文提到的“条件编译”也是预处理的一种方式。 一般情况下,C语言程序中的所有代码都要参与编译,但有时出于程序代码优化的考虑,希望源代码中一部分内容只在指定条件下进行编译。这种根据指定条件,只对程序一部分内容编译的情况,称为条件编译。在C语言中条件编译指令的形式有很多种,接下来将详细讲解一种最常见的条件编译指令:#if/#else/#endif,该指令根据常数表达式来决定某段代码是否执行。
【案例4】-必备知识 #if/#else/#endif指令 1
【案例4】-必备知识 #if/#else/#endif指令 在C语言中,最常见的条件编译指令是#if/#else/#endif指令,该指令根据常数表达式来决定某段代码是否执行。通常情况下,#if指令,#else指令和#endif指令是结合在一起使用的,其语法格式如下所示: #if 常数表达式 程序段1 #else 程序段2 #endif
【案例4】-案例实现 案例设计 1 2 3 案例代码(详见教材代码实现) 定义两个宏分别表示Windows32位和64位平台; 定义宏SYSTEM表示其中某个平台; 3 使用条件编译指令判断SYSTEM值,并输出结果到屏幕上; 案例代码(详见教材代码实现)
【案例5】-案例描述 学过文件包含之后,不免有同学提出这样的问题:在同一文件中写两 遍“#include <stdio.h>”,编译器进行编译时为什么没有报错呢?按常理 而言,文件“stdio.h”中的函数和数据类型等必然被定义了两次,此时 编译器应该报出“重定义”的错误,但实际上编译十分顺利,是不是很 神奇?
【案例5】-案例分析 在上一个案例中我们提到“C语言中条件编译指令的形式有很多种”,如果现在的你百思不得其解,那是因为你没有学过另一种条件编译指令:#ifdef和#ifndef。下面来讲解这条编译指令的神奇之处。
【案例5】-必备知识 #ifdef指令 1 #ifndef指令 2
【案例5】-必备知识 #ifdef指令 在C语言中,如果想判断某个宏是否被定义,可以使用#ifdef指令,通常情况下,该指令需要和#endif一起使用,ifdef指令的语法格式如下所示: #ifdef 宏名 程序段1 #else 程序段2 #endif
【案例5】-必备知识 #ifdnef指令 和#ifdef相反,#ifndef用来确定某一个宏是否被定义,如果宏没有被定义,那么就编译#ifndef到#endif中间的内容,否则就跳过,其语法格式如下所示: #ifndef 宏名 程序段1 #else 程序段2 #endif
【案例5】-案例实现 核心代码 #ifndef _STDIO_H_ #define _STDIO_H_ ……. 预处理指令 #ifndef _STDIO_H_ #define _STDIO_H_ ……. #endif /* _STDIO_H_ */ 这三行代码是三条预处理命令,它们便是写两遍“#include <stdio.h>”也不会报错的原因。当然,写更多遍也不会报错。通过刚刚的学习,我们知道这三行代码的含义是:如果“_STDIO_H_”没有被定义过,那么就定义“_STDIO_H_”。仔细观察我们会发现“#define _STDIO_H_”的后面什么都没写,其实这也是宏定义的一种写法。并不关注“_STDIO_H_”被定义成了什么,只关注它是否被定义过。
多学一招 预定义宏 学习了宏定义,下面来看下<stdio.h>头文件中的五个预定义宏,利用这些宏可以轻松获得程序运行到了何处,有助于编程人员进行程序调试。 预定义宏 说明 __DATE__ 定义源文件编译日期的宏 __FILE__ 定义源代码文件名的宏 __LINE__ 定义源代码中行号的宏 __TIME__ 定义源文件编译时间的宏 __FUNCTION__ 定义当前所在函数名的宏
本章小结 本章主要讲解了预处理的三种方式,分别是宏定义、文件包含和条件编译。其中,宏定义是最常用的一种预处理方式;文件包含对于程序功能的扩充很有帮助;条件编译可以优化程序代码。熟练掌握这三种预处理方式,将对以后的程序设计大有帮助。