【C语言】预处理指令详解

目录

一、预定义符号

[二、#define 定义常量](#define 定义常量)

[三、#define 定义宏](#define 定义宏)

(1)宏定义的使用

(2)带副作用的宏参数

(3)宏替换的规则

(4)宏与函数对比

(5)#和##

① #运算符

② ##运算符

(6)宏的命名规则

(7)#undef

四、命令行定义

五、条件编译

(1)条件编译的使用

(2)常见的条件编译

[① 基础的条件编译](#① 基础的条件编译)

[② 多个分支的条件编译](#② 多个分支的条件编译)

[③ 判断是否被定义](#③ 判断是否被定义)

[④ 嵌套指令](#④ 嵌套指令)

六、头文件的包含

(1)头文件被包含的方式

[① 本地文件包含](#① 本地文件包含)

[② 库文件包含](#② 库文件包含)

(2)嵌套文件包含

[① 嵌套文件包含的概念](#① 嵌套文件包含的概念)

[② 嵌套文件包含的解决方法](#② 嵌套文件包含的解决方法)

七、其它预处理指令


一、预定义符号

预定义符号,会在预处理阶段,被直接替换为它的内容。

预定义符号有:

cpp 复制代码
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

在 VS 环境中演示,预定义符号__STDC__不可使用:

用 gcc 编译器演示,gcc 遵循标准C:

执行命令 gcc -E test.c -o test.i(进行预处理),打开 test.i:

二、#define 定义常量

#define 定义常量,会在预处理阶段,将代码中的名字直接替换为内容。

语法形式:

cpp 复制代码
#define name stuff
// name: 名字
// stuff: 内容
// 举例:
#define M 100

在其它场景下的用法:

cpp 复制代码
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏
符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
 date:%s\ttime:%s\n" ,\
 __FILE__,__LINE__ , \
 __DATE__,__TIME__ ) 

示例代码:

预处理后的 test.i 文件:

有些语言的 switch 语句没有 break,使用这些语言的程序员再使用 C 语言就非常不习惯,老忘记加 break,像示例代码一样使用 #define 定义,编码时就不用写 break 了。

注意:#define 定义标识符,最后不加 ;

如下情况,#define 定义常量加了 ; ,发生错误:

预处理阶段,第 52 行被替换成 max = 100;;,表示两条语句。因为 if 语句没加{},if 只跟 一条语句,所以发生了错误。

三、#define 定义宏

(1)宏定义的使用

#define 定义宏,在预处理阶段,将代码中的 名字(参数),替换为宏的内容,并把参数带入内容中。

语法形式:

cpp 复制代码
#define name( parament-list ) stuff
// name: 名字
// parament-list: 参数列表,由逗号隔开
// stuff: 内容

注意:name后应紧跟(,如果之间加了空格,会被认为 ( parament-list ) stuff 是 stuff, 属于#define 定义标识符。

示例代码1:

预处理后的结果:

将参数改为 x+1(宏的参数是直接替换,而不计算):

预处理后的结果:

改进代码(为了防止替换后,因操作符优先级等,导致运算顺序不是预料的结果,应尽量加小括号):

预处理后的结果:

示例代码2:

改进代码:

(2)带副作用的宏参数

若宏参数带有副作用,并且在宏定义中同一个宏参数不止出现一次,那么这个宏可能会出现不可预料的结果。

赋值符号的右边是示例宏参数:

cpp 复制代码
y = x+1;//执行后,对x不改变,不带副作用
y = x++;//执行后,对x改变,带有副作用

示例代码,期望获得 X、Y 两者最大值,但出现问题:

而定义函数,传入带副作用的参数,却不会出现问题:

结论:应避免使用带副作用的宏参数。

(3)宏替换的规则

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

例如:

  • 再次对结果文件进行扫描,看它是否包含任何由#define定义的符号。如果是,重复上述处理过程。

例如:

注意

  • 宏定义不能出现递归。

例如下面的错误示范:

预处理后的代码:

因为宏只进行一次替换,如果宏定义存在递归,那么替换不完全,会把剩下的宏认为是函数,但这个函数又没被定义,所以出现链接错误。

  • 预处理器搜索#define定义的符号的时,字符串常量的内容并不被搜索。

例如:

(4)宏与函数对比

执行简单的计算 时,宏比函数更有优势

相比函数,宏的优势:

  • 函数,需要函数调用、执行计算、函数返回;宏只需要直接执行计算;因此, 比函数在程序规模和计算速度方面,更优
  • 函数的参数,必须声明特定的类型;宏的参数,类型无关 。因此,宏比函数更灵活

例如:计算两个数中的较大值。

函数的实现:

cpp 复制代码
int Max(int x, int y)
{
	return x > y ? x : y;
}

int main()
{
	int a = 7, b = 5;
	int m = Max(a, b);

	printf("m = %d\n", m);

	return 0;
}

宏定义实现:

cpp 复制代码
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))

int main()
{

	int a = 7, b = 5;
	float m = MAX(a, b);
	//int m = ((a)>(b)?(a):(b));
	printf("m = %f\n", m);

	return 0;
}

调试,查看函数实现的汇编代码:

① 调用函数,执行了19条指令。

② 计算,执行了 9 条指令。

③ 返回函数,执行了10条指令。

调试,查看宏定义实现的汇编代码:

计算,执行了9条指令。

结论:因为函数的实现方法,需要创建函数栈帧,所以多了调用函数、返回函数的指令,在代码规模和计算速度上,明显比宏的实现方法差。

宏参数没有类型的限制,也可以传入浮点数:

相比函数,宏的劣势:

  • 每使用一次宏,都会插入一段宏定义的代码,除非宏很短,否则会大幅增加程序的长度(宏在预处理阶段,会直接被替换成一大段宏的内容;而每次调用的函数,代码只需定义一次)。
  • 无法调试(在预处理阶段,宏就已经被替换掉,而调试是在 .exe 文件生成后执行的操作)。
  • 因为宏类型无关 ,所以不够严谨
  • 可能产生操作符优先级的问题,导致程序运行结果出乎意料(宏参数为表达式、宏参数带有副作用的情况)。

宏有时可以做函数做不到的事,比如宏参数可以是类型,但函数做不到。如下面的代码,只需要给宏传入元素个数、元素类型,就能实现动态开辟空间:

cpp 复制代码
#define MALLOC(N, Type) (Type*)malloc(N * sizeof(Type))

int main()
{
	//int* p = (int*)malloc(10 * sizeof(int));
	int* p = MALLOC(10, int);

	return 0;
}

总结宏和函数的对比:

|----------|----------------------------------------------------------|-------------------------------------|
| 属性 | #define定义宏 | 函数 |
| 代码长度 | 每次使用宏,宏内容被替换到程序中,程序长度会大幅增加。 | 每次使用函数,都调用同一份函数定义的代码。(胜) |
| 执行速度 | 更快。(胜) | 会有调用函数、返回函数的额外开销。 |
| 操作符优先级 | 宏参数求值,在宏内容表达式的上下文环境里,容易产生邻近操作符优先级的问题,导致计算结果不可预料。要多使用圆括号。 | 函数参数求值,只在函数调用时求一次,传给函数,计算结果容易预测。(胜) |
| 带有副作用的参数 | 带有副作用的参数,可能会被替换到宏内容表达式的多个位置,多次求值,产生不可预料的结果。 | 带有副作用的参数,只在函数调用时求值一次,结果容易预测。(胜) |
| 参数类型 | 宏的参数类型无关,更灵活。(胜) | 函数的参数定义了特定类型。 |
| 调试 | 不能调试。 | 可调试。(胜) |
| 递归 | 不能递归。 | 可递归。(胜) |

执行简单计算,使用宏;执行复杂计算,使用函数。当计算复杂时,计算的花销远大于调用函数、返回函数的花销,可以忽略不计。复杂的计算,使用函数,更不易出错。

在C++中引入了内联函数(inline),它既具备了函数的优势,又具备了宏的优势。

(5)#和##

① #运算符

作用:在宏定义的内容表达式中使用,可将宏参数转化为字符串

先了解一个没见过的知识:

示例,我们想打印3句话,但是代码很重复:

红框是3句打印不同的部分,将它们作为宏参数,使用宏(由于预处理器不搜索程序中的字符串常量,所以红框中的v并没有被替换):

此时,在宏定义的表达式中,使用#操作符,将传入的参数转为字符串:

② ##运算符

作用:在宏定义的内容表达式中使用,可将两个参数合成一个标识符(应是合法的标识符)。这被称为记号粘合。

示例,使用函数实现计算两个参数的较大值,但参数类型不同,函数的实现也不同,这样的代码很重复(红框中是不同的部分):

使用宏定义和##,减少编程的繁琐:

预处理后的结果:

(6)宏的命名规则

使用宏和函数的语法非常相似,可以从命名规则角度区分它们。

  • 宏名全部大写(如:MAX)。
  • 函数名不要全部大写(如:Max)。

但这只是一个习惯,并不是定死的,C标准中也有宏定义是小写命名的:

(7)#undef

作用:移除一个#define定义

示例:

四、命令行定义

一些C编译器,允许命令行定义符号。比如,我们有时想要用一个源文件,编译出不同版本程序。

示例,在程序中声明了 SIZE 长度的数组。在内存有限的机器上,我们想要很小的数组;在内存较大的机器上,我们想要较大的数组。这可以通过命令行定义(VS不支持,gcc 支持)实现,源代码如下:

cpp 复制代码
#include <stdio.h>
int main()
{
	int array[SIZE];
	int i = 0;
	for (i = 0; i < SIZE; i++)
	{
		array[i] = i;
	}
	for (i = 0; i < SIZE; i++)
	{
		printf("%d ", array[i]);
	}
	printf("\n");
	return 0;
}

使用如下命令,定义 SIZE:

五、条件编译

(1)条件编译的使用

作用:选择一组语句编译或者不编译

示例,调试性的代码,不想执行调试,但想保留代码,使用条件编译:

因为定义了 DEBUG,所以会编译 printf 语句,在预处理阶段,将 printf 语句保留了下来:

如果不想编译 printf 语句,就注释掉 DEBUG 的定义:

预处理阶段,去掉了 printf 语句:

(2)常见的条件编译

① 基础的条件编译

语法形式:

cpp 复制代码
#if 常量表达式
 //...
#endif

示例(预处理阶段,M被替换成5,常量表达式 5==1 为假,不编译 printf 语句):

cpp 复制代码
#define M 5

int main()
{

#if M==1
	printf("hehe\n");
#endif

	return 0;
}

② 多个分支的条件编译

语法形式:

cpp 复制代码
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif

示例(最终编译 printf("哈哈\n");):

cpp 复制代码
#define M 5

int main()
{

#if M==1
	printf("hehe\n");
#elif M==2
	printf("haha\n");
#elif M==3
	printf("heihei\n");
#else	
	printf("哈哈\n");
#endif

	return 0;
}

③ 判断是否被定义

语法形式1:

cpp 复制代码
// 判断 symbol 是否被定义
#if defined(symbol)
// ...
#ifdef symbol

// 判断 symbol 是否没被定义
#if !defined(symbol)
// ...
#ifndef symbol

语法形式2:

cpp 复制代码
// 判断 symbol 是否被定义
#ifdef symbol
// ...
//#endif

// 判断 symbol 是否没被定义
#ifndef symbol
// ...
#endif

示例代码(最终编译 printf("1hehe\n"); 和 printf("3hehe\n");):

cpp 复制代码
#define M

int main()
{
//判断M是否被定义过,关于值是多少,不关心
// 语法形式1
#if defined(M)
	printf("1hehe\n");
#endif

#if !defined(M)
	printf("2hehe\n");
#endif

// 语法形式2
#ifdef M
	printf("3hehe\n");
#endif

#ifndef M
	printf("4hehe\n");
#endif
	
	return 0;
}

④ 嵌套指令

cpp 复制代码
if defined(OS_UNIX)
	#ifdef OPTION1
		unix_version_option1();
	#endif
	#ifdef OPTION2
		unix_version_option2();
	#endif
#elif defined(OS_MSDOS)
	#ifdef OPTION2
		msdos_version_option2();
	#endif
#endif

很少用到,只有在很大的项目中常用,比如打开 stdio.h 头文件看,就是用了很多条件编译指令(因为代码是跨平台的,根据不同的平台,有不同的代码):

六、头文件的包含

(1)头文件被包含的方式

① 本地文件包含

语法形式:

cpp 复制代码
#include "filename"

查找策略:先在源文件所在目录下找,如果没有,再在标准位置找,如果找不到就提示编译错误。

比如我的项目下的文件目录:

test.c 中包含了本地头文件 add.h,故先在 test.c 所在目录下找:

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

VS2013 环境默认的标准头文件路径:

cpp 复制代码
// 根据自己实际的安装路径找
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

② 库文件包含

语法形式:

cpp 复制代码
#include <filename.h>

查找策略:直接到标准路径下找,找不到就提示编译错误。

库文件包含 也可以用 " " ,但是这种方式会先查找源文件目录,再查找标准路径,会降低查找效率,也不易区分是本地文件还是库文件

(2)嵌套文件包含

① 嵌套文件包含的概念

在预处理阶段,会把头文件包含的所有内容,替换到使用 #include 的文件中。如果同一个头文件被重复包含多次 ,在预处理后的文件中就会有许多重复的代码,使编译效率大大降低

比如,A、B、C 3个文件都包含了头文件 add.h,文件 D 又对A、B、C 进行整合,相当于 D 重复包含了 3 次 add.h 头文件。对于大型工程,会包含3~5万个文件,如果重复包含头文件,而不作处理,其预处理后的文件中重复代码之多,后果不堪设想。

示例,test.c 重复包含头文件 add.h:

cpp 复制代码
// test.c 中的内容
#include "add.h"
#include "add.h"
#include "add.h"
#include "add.h"
#include "add.h"

int main()
{
	return 0;
}

// add.h 中的内容
int Add(int x, int y);

test.c 预处理后的内容:

② 嵌套文件包含的解决方法

方法1:在头文件 add.h 中,使用条件编译

cpp 复制代码
#ifndef __ADD_H__
#define __ADD_H__
//头⽂件的内容
#endif

在源代码 test.c 中,第一次包含头文件 add.h,符号 ADD_H 未被定义,编译 #define ADD_H 和头文件的内容。test.c 后面再包含头文件 add.h,因为已经定义过 ADD_H,不再编译 #define ADD_H 和头文件的内容。

示范:

cpp 复制代码
// 更改后,add.h 中的内容
#ifndef __ADD_H__
#define __ADD_H__
int Add(int x, int y);
#endif

test.c 预处理后的内容:

方法2:在头文件中加以下内容(在 VS 中创建新的头文件,会自动包含这条语句)

cpp 复制代码
#pragma once

注:在**《高质量C/C++编程指南》**中附录的考试试卷,就包含头文件包含相关笔试题目。

七、其它预处理指令

cpp 复制代码
#error
#pragma
#line
...
更多参考《C语言深度解剖》
相关推荐
西猫雷婶2 小时前
python学opencv|读取图像(十九)使用cv2.rectangle()绘制矩形
开发语言·python·opencv
liuxin334455662 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
码农W2 小时前
QT--静态插件、动态插件
开发语言·qt
ke_wu3 小时前
结构型设计模式
开发语言·设计模式·组合模式·简单工厂模式·工厂方法模式·抽象工厂模式·装饰器模式
code04号3 小时前
python脚本:批量提取excel数据
开发语言·python·excel
小王爱吃月亮糖3 小时前
C++的23种设计模式
开发语言·c++·qt·算法·设计模式·ecmascript
hakesashou3 小时前
python如何打乱list
开发语言·python
网络风云4 小时前
【魅力golang】之-反射
开发语言·后端·golang
Want5954 小时前
Java圣诞树
开发语言·python·信息可视化
运维小文4 小时前
python之打印、变量、格式化输出
开发语言·python·python基础·hello world