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

目录

一、预定义符号

二、#define 定义常量

三、#define 定义宏

(1)宏定义的使用

(2)带副作用的宏参数

(3)宏替换的规则

(4)宏与函数对比

(5)#和##

① #运算符

② ##运算符

(6)宏的命名规则

(7)#undef

四、命令行定义

五、条件编译

(1)条件编译的使用

(2)常见的条件编译

① 基础的条件编译

② 多个分支的条件编译

③ 判断是否被定义

④ 嵌套指令

六、头文件的包含

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

① 本地文件包含

② 库文件包含

(2)嵌套文件包含

① 嵌套文件包含的概念

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

七、其它预处理指令


一、预定义符号

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

        预定义符号有:

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

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

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

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

二、#define 定义常量

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

        语法形式:

#define name stuff
// name: 名字
// stuff: 内容
// 举例:
#define M 100

        在其它场景下的用法:

#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 定义宏,在预处理阶段,将代码中的 名字(参数),替换为宏的内容,并把参数带入内容中。

        语法形式:

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

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

        示例代码1:

        预处理后的结果:

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

        预处理后的结果:

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

        预处理后的结果:

        示例代码2:

        改进代码:

(2)带副作用的宏参数

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

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

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

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

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

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

(3)宏替换的规则

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

        例如:

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

        例如:

        注意

  • 宏定义不能出现递归。

        例如下面的错误示范:

        预处理后的代码:

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

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

        例如:

(4)宏与函数对比

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

        相比函数,宏的优势:

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

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

        函数的实现:

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;
}

        宏定义实现:

#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 文件生成后执行的操作)。
  • 因为宏类型无关,所以不够严谨
  • 可能产生操作符优先级的问题,导致程序运行结果出乎意料(宏参数为表达式、宏参数带有副作用的情况)。

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

#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 支持)实现,源代码如下:

#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)常见的条件编译

① 基础的条件编译

        语法形式:

#if 常量表达式
 //...
#endif

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

#define M 5

int main()
{

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

	return 0;
}

② 多个分支的条件编译

        语法形式:

#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif

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

#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:

// 判断 symbol 是否被定义
#if defined(symbol)
// ...
#ifdef symbol

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

        语法形式2:

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

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

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

#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;
}

④ 嵌套指令

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)头文件被包含的方式

① 本地文件包含

        语法形式:

#include "filename"

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

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

                                

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

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

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

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

② 库文件包含

        语法形式:

#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:

// 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 中,使用条件编译

#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__ 和头文件的内容。

        示范:

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

        test.c 预处理后的内容:

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

#pragma once

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

七、其它预处理指令

#error
#pragma
#line
...
更多参考《C语言深度解剖》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值