C语言 预处理详解


正文开始

1. 何为预处理?

当我们运行一段代码的时候,它会经历以下过程:预处理 -> 编译 -> 汇编 -> 链接 -> 生成可执行程序。只有经过这些操作,才能将开发者写的文字代码,转换为计算机能看懂的机器语言。而今天我们所学的就是第一个阶段:预处理阶段

在预处理阶段,源文件和头文件会被处理成后缀为.i的文件。预处理阶段主要处理那些源文件中以#开始的预编译指令。例如:#include、#define,处理规则如下:

  • 将所有#define删除,并展开所有的宏定义。就是将定义的常量和宏替换到对应位置
  • 处理所有的条件编译指令,如:#if、#ifdef、#elif、#else、#endif
  • 处理#include预编译指令,将包含的头文件的內容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件
  • 删除所有注释
  • 添加行号和文件名标识,方便后续编译器生成调试信息
  • 保留所有的#pragma的编译器指令,方便编译器后续使用。

这些內容我们接下来会详细讲解,看完文章不要忘记回来复习呦

2. 预定义符号

C语言预设了一些预定义符号,这些预定义符号也是在预处理期间处理的:

__FILE__	//源文件地址
__LINE__	//当前的行号
__DATE__	//文件被编译的日期
__TIME__	//文件被编译的时间
__STDC__	//如果编译器遵循ANSR C则为1,否则未定义

我们打印一下看看:

#include <stdio.h>

int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	return 0;
}

运行结果:
在这里插入图片描述

3. #define 定义常量

我们可以通过#define来给对象重命名:

#define name stuff

#define 用法:

  • stuff重命名为name
  • 重命名的实现方式是在预处理阶段,将所有的name原封不动的替换为stuff

例如:

//定义常量
#define MAX 1000
//为 register 关键字起一个简短的名字
#define reg register	
//用更形象的符号来替换一种实现
#define do_forever for(;;)  
//若定义的 stuff 过长,可将內容分
//为几行写,每行最后通过续行符\连接
#define DEBUG_PRINT printf("file:%s\nline:%d \
							\ndate:%s\ntime:%s\n",\
							__FILE__,__LINE__,\
							__DATE__,__TIME__)
#include <stdio.h>

int main()
{
	printf("%d\n", MAX);
	DEBUG_PRINT;
	return 0;
}

运行结果:
在这里插入图片描述

4. #define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)

宏的申明方式:

#define name(parament-list) stuff

宏的用法:

  • name为宏的名字
  • parament-list是一个由逗号隔开的符号表,它们可能出现在 stuff 中
  • stuff是替换进 parament-list 中的表达式

例如:

#include <stdio.h>
#define SQUARE( x ) x * x

int main()
{
	int a = SQUARE(5);
	printf("%d\n", a);
	return 0;
}

上述代码中调用宏的时候,首先将5传递进SQUARE( x )中去,然后将5代入表达式x * x中计算结果。程序中的宏,预处理器会将把表达式直接替换为5 * 5

但这样的书写方法存在一些问题,比如:

#include <stdio.h>
#define SQUARE( x ) x * x

int main()
{
	int a = 2;
	int b = SQUARE(a + 1);
	printf("%d\n", b);
	return 0;
}

运行结果:
在这里插入图片描述
上述代码的目的是计算a + 1的平方,但结果并不是我们预期的9。这是因为,宏定义是直接将参数替换掉的,也就是说,实际上计算的是a + 1 * a + 1结果自然就是5了。

所以为了避免产生上述代码的问题,我们在定义宏时,要加上括号,来规整优先级的问题,上述代码可改为:

#include <stdio.h>
#define SQUARE( x ) ( ( x ) * ( x ) )

int main()
{
	int a = 2;
	int b = SQUARE(a + 1);
	printf("%d\n", b);
	return 0;
}

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用

5. 宏的副作用

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么在使用宏的时候就可能出现不可预测的后果。副作用就是表达式求值的时候出现自身改变的情况

例如:

x + 1;//不带副作用
x++;//带有副作用

可能会产生的问题:

#include <stdio.h>
#define MAX(a,b) ( (a) > (b) ? (a) : (b) )

int main()
{
	int x = 5;
	int y = 7;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);
	return 0;
}

运行结果:
在这里插入图片描述
上述代码中,使用宏时,首先将参数传递进表达式(x++) > (y++) ? (x++) : (y++),首先进行 x 与 y 的比较,比较完成后,x 与 y 都加一,此时 x=6,y=8,而后返回表达式y++,也就是8,返回后 y 再加一。所以最后的结果就是 x=6,y=9,z=8。

上述代码中的宏参数在定义的时候出现超过了一次,并且参数带有副作用,这就导致了最终结果的偏差。

6. 宏替换的规则

在程序中拓展 #define 定义符号和宏时,有以下步骤:

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果有,他们首先被替换
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,则重复上述处理过程

注:

  • 宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归
  • 当预处理器搜索 #define 定义的符号时,字符串常量的內容并不被搜索。例如:定义#define MAX 10,并有printf(“MAX is ten.”),那么字符串常量中的MAX并不会被搜索和替换。

7. 宏与函数

从刚才的学习中我们了解到,宏的作用其实就是能够接收参数,并按照指定表达式输出。这与函数的作用似乎很是相似。
宏通常用于执行简单的运算

宏相对于函数的优势

  • 调用函数需要创建并销毁栈帧,还要涉及到传参、函数返回等步骤,而宏则是直接替换,所以宏所需时间更短
  • 更为重要的是,函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用。但因为宏的直接替换的特点,所以宏的参数类型是更为灵活的

例如:

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

//函数
int Max(int a , int b)
{
	if(a > b)
		return a;
	else
		return b;
}

上述代码中,宏和函数的功能是相同的,都是求出两个数的较大值,但这个函数只能用于比较整型元素,而宏可以适用于整型、长整型、浮点型等等。

宏相对于函数的劣势

  • 预处理阶段会将宏直接替换掉,所以每次使用宏的时候,一份宏定义的代码将会插入到程序中,若宏比较长,那就会大幅增加程序的长度;而函数只出现在一个地方,每次使用都去那个地方调用
  • 调试是无法进入到宏的内部的,所以宏是无法调试的
  • 宏由于类型无关,所以不够严谨
  • 宏可能会带来运算符优先级的问题,导致程序出现不可预料的错误
  • 宏不能递归,而函数可以递归

一般来说,函数和宏的使用语法很相似,所以我们通过一个小习惯来区分宏和函数:

  • 宏名全部大写
  • 函数名不要全部大写

当然这只是一种约定俗成的规则,并不具有硬性要求

8. #和##

8.1 # 运算符

#运算符可以将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执行的操作可以理解为“字符串化

比如,我们有一个变量int a = 2我们想打印the value of a is 2,那么我们可以这样实现:

#include<stdio.h>
#define PRINT(x) printf("the value of "#x " is %d", x)

int main()
{
	int a = 2;
	PRINT(a);
	return 0;
}

运行结果:
在这里插入图片描述
简单来说,#所产生的效果就是,将宏参数的名字原封不动的替换进表达式,而不是将宏参数的值替换进表达式

8.2 ## 运算符

##运算符可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。所以它又被称为记号粘合

这样的连接必须产生一个合法的标识符,否则结果未定义

例如:

#include <stdio.h>
#define X(n) xn
#define Y(n) y##n //将符号 y 和 n 合并为一个记号
int X(1) = 10; 
int Y(1) = 10; 
int main()
{
	return 0;
}

预处理后结果:
在这里插入图片描述

9. #undef

使用#undef这条预处理指令可以移除一个宏定义,例如:

#define MAX 10 //定义
//...
#undef MAX //移除定义
//...
int a = MAX;//error未定义

10. 条件编译

我们可以通过预处理指令来进行条件编译,它的逻辑与之前所学的分支语句基本相同,下面我们学习一下常见的条件编译指令:

//单个分支的条件编译
#if 常量表达式	//该表达式由预处理器求值
	//...
#endif
___________________________________
//多个分支的条件编译
#if 常量表达式1
	//...
#elif 常量表达式2
	//...
#else
	//...
#endif
___________________________________
//判断是否被定义
#if defined(判断对象)
	//...
#endif

//或

#ifdef 判断对象
	//...
#endif
___________________________________
//判断是否没被定义
#if !defined(判断对象)
	//...
#endif

//或

#ifndef 判断对象
	//...
#endif

需要注意的是,上述一些条件编译的判断式都为常量表达式,不能使用变量,例如:

#include <stdio.h>
#define MAX 10
int main()
{
	//int MAX = 10; error
	#if MAX > 5
		printf("hello");
	#elif MAX == 5
		printf("world");
	#else
		printf("haha");
	#endif
	return 0;
}

11. 头文件的包含

11.1 本地文件包含

#include "filename.h"

查找文件策略:先在源文件所在目录下查找,如果未找到,则编译器就像查找库函数头文件一样在标准位置查找头文件。若仍找不到则编译错误。

Linux 环境的标准头文件的路径:

/usr/include

VS环境下标准头文件的路径:

C:\Program Files (x86)\Microsfot Visual Studio 12.0\VC\include

标准头文件路径与用户安装路径有关,这里仅供参考

11.2 库文件包含

#include <filename.h>

查找文件策略:直接去标准路径下去查找,若找不到则提示编译错误。

也就是说使用“”查找文件的范围更为广泛,所以可以使用“”替换<>,但这样做的效率就会变低,因为“”的会查找两个位置的文件。而且这样也不易区分库文件和本地文件,所以不建议用“”替换<>

11.3 嵌套文件的包含

我们使用#include引用文件时,在预处理阶段就会将这个文件的所有内容替换到对应位置,那如果重复编译了同一个文件,这样就降低了效率,所以我们可以通过如下方式确保一个文件只被编译一次:

#ifndef __TEST_H__
#define __TEST_H__
//头文件內容...
#endif

上述代码所在文件第一次被引用后会定义常量TEST_H,并编译头文件內容,当第二次被引用时,并不会编译头文件內容,这样就确保了一个文件只被编译一次。

我们还可以通过以下指令实现上述功能:

#pragma once


  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
完整版:https://download.csdn.net/download/qq_27595745/89522468 【课程大纲】 1-1 什么是java 1-2 认识java语言 1-3 java平台的体系结构 1-4 java SE环境安装和配置 2-1 java程序简介 2-2 计算机中的程序 2-3 java程序 2-4 java类库组织结构和文档 2-5 java虚拟机简介 2-6 java的垃圾回收器 2-7 java上机练习 3-1 java语言基础入门 3-2 数据的分类 3-3 标识符、关键字和常量 3-4 运算符 3-5 表达式 3-6 顺序结构和选择结构 3-7 循环语句 3-8 跳转语句 3-9 MyEclipse工具介绍 3-10 java基础知识章节练习 4-1 一维数组 4-2 数组应用 4-3 多维数组 4-4 排序算法 4-5 增强for循环 4-6 数组和排序算法章节练习 5-0 抽象和封装 5-1 面向过程的设计思想 5-2 面向对象的设计思想 5-3 抽象 5-4 封装 5-5 属性 5-6 方法的定义 5-7 this关键字 5-8 javaBean 5-9 包 package 5-10 抽象和封装章节练习 6-0 继承和多态 6-1 继承 6-2 object类 6-3 多态 6-4 访问修饰符 6-5 static修饰符 6-6 final修饰符 6-7 abstract修饰符 6-8 接口 6-9 继承和多态 章节练习 7-1 面向对象的分析与设计简介 7-2 对象模型建立 7-3 类之间的关系 7-4 软件的可维护与复用设计原则 7-5 面向对象的设计与分析 章节练习 8-1 内部类与包装器 8-2 对象包装器 8-3 装箱和拆箱 8-4 练习题 9-1 常用类介绍 9-2 StringBuffer和String Builder类 9-3 Rintime类的使用 9-4 日期类简介 9-5 java程序国际化的实现 9-6 Random类和Math类 9-7 枚举 9-8 练习题 10-1 java异常处理 10-2 认识异常 10-3 使用try和catch捕获异常 10-4 使用throw和throws引发异常 10-5 finally关键字 10-6 getMessage和printStackTrace方法 10-7 异常分类 10-8 自定义异常类 10-9 练习题 11-1 Java集合框架和泛型机制 11-2 Collection接口 11-3 Set接口实现类 11-4 List接口实现类 11-5 Map接口 11-6 Collections类 11-7 泛型概述 11-8 练习题 12-1 多线程 12-2 线程的生命周期 12-3 线程的调度和优先级 12-4 线程的同步 12-5 集合类的同步问题 12-6 用Timer类调度任务 12-7 练习题 13-1 Java IO 13-2 Java IO原理 13-3 流类的结构 13-4 文件流 13-5 缓冲流 13-6 转换流 13-7 数据流 13-8 打印流 13-9 对象流 13-10 随机存取文件流 13-11 zip文件流 13-12 练习题 14-1 图形用户界面设计 14-2 事件处理机制 14-3 AWT常用组件 14-4 swing简介 14-5 可视化开发swing组件 14-6 声音的播放和处理 14-7 2D图形的绘制 14-8 练习题 15-1 反射 15-2 使用Java反射机制 15-3 反射与动态代理 15-4 练习题 16-1 Java标注 16-2 JDK内置的基本标注类型 16-3 自定义标注类型 16-4 对标注进行标注 16-5 利用反射获取标注信息 16-6 练习题 17-1 顶目实战1-单机版五子棋游戏 17-2 总体设计 17-3 代码实现 17-4 程序的运行与发布 17-5 手动生成可执行JAR文件 17-6 练习题 18-1 Java数据库编程 18-2 JDBC类和接口 18-3 JDBC操作SQL 18-4 JDBC基本示例 18-5 JDBC应用示例 18-6 练习题 19-1 。。。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值