C语言预处理

发布于:2024-04-03 ⋅ 阅读:(40) ⋅ 点赞:(0)

目录

什么是预处理

#define定义常量

什么是宏定义常量

宏定义常量的好处 

#define定义宏

带有副作用参数的宏

宏替换的规则

宏函数的对比

#和##

宏的命名

#undef

命令行定义

条件编译

头文件的包含

其他预处理指

什么是预处理

C语言里的预处理,简单来说就是在真正开始编译你的源代码之前,有个特殊的“预备队”先上场,帮你把原始代码整理得更规整、更适应编译器的要求,同时也让你编程时能更方便、更灵活。这个“预备队”就是预处理器,它干的活儿是在编译流程的最前端单独进行的,比检查语法、理解代码含义那些步骤还要早。

#define定义常量

什么是宏定义常量

在编程中我们经常会用到一些常量,比如圆周率的值,最大最小值,长度宽度等,这些变量伴随我们的整个程序,我们就可以用一个宏来替换,例如:

#define PI 3.1415926
#define MAX 100
#define MIN 0
#define ROW 10
#define COL 10

宏定义常量的好处 

在编程实践中,我们时常会遇到一些常数,它们如同程序的脉络一般,贯穿于各个角落,对算法逻辑、数据结构乃至系统行为产生深远影响。然而,当需要调整这些常数时,问题便凸显出来:我们必须逐一排查并手动修改它们在源代码中每一次出现的位置,这无疑是一项既繁琐又易出错的任务,极大地降低了工作效率,且不利于代码维护。

此时,宏定义常量的优越性便显现无遗。借助宏定义,我们只需在一处为这些常数赋予新的名称和值,随后在整个程序中以宏名代替实际数值。一旦宏的值发生变更,预处理器便会以“魔法之手”般的力量,悄无声息地将所有引用宏之处更新为新的值。如此一来,改动一处,全局皆变,省却了在代码汪洋中寻觅与替换的辛劳,极大地提升了调整常数的效率,也确保了改动的一致性与准确性。

总结来说,宏定义常量犹如一把精巧的钥匙,解锁了高效管理贯穿程序全局常数的难题。它以一处定义、处处生效的方式,简化了常数修改的过程,避免了人工逐行排查的困扰,确保了代码的整洁与维护性,堪称提升编程效率与代码质量的得力助手。

#define定义宏

我们学过函数,但你知道宏也可以实现类似函数的功能吗?

例如,我们想要计算一个数的平方,除了用函数以外,我们还可以用宏来完成这个功能,例如:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>

#define SQRT(x) ((x)*(x))

int main()
{
	printf("%d", SQRT(5));
	return 0;
}

像这样我们就可以实现一个数的平方操作,也就是将x等量替换成5

注意事项 

需要注意的是,宏和函数不同,这个在下面我们也会继续讲到

宏仅仅只是简单的替换,他不会根据你想的意思去执行命令,比如如果你在定义宏的时候是这么定义的:

#define SQRT(x) x*x

而如果这时x的值为3+3,那么在宏替换的时候就会变成这样:

3+3*3+3

其结果是 15,这很显然不是我们想要的结果,注意宏替换只是简单的替换,所以我们在使用宏的时候一定要注意多加括号,否则可能会造成不必要的损失

带有副作用参数的宏

我们看看下面两个例子:

#define M(a,b) a>b?a:b

int main()
{
	int a = 4;
	int b = 5;
	printf("%d", M(a++, b++));
	return 0;
}

 这个就是宏定义的副作用

宏替换的规则

当程序中有#define的时候,他的预处理替换会有以下几个阶段:

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

2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。

3.再次重复上述操作,直到被全部替换完成

注意

宏和#define中可以出现其他的#define,但不能出现函数递归

当预处理器搜索宏常量的时候,字符串常量通常不被搜索

宏函数的对比

这么一看,可能会有人觉得宏和函数也没什么区别呀,为什么需要有宏这个东西

但是,当我们预处理的时候,就会有区别了

宏通常被应用于执行简单的运算。 比如在两个数中找出较大的一个时,写成宏,更有优势一些。

宏与函数的区别

1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。

2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 > 来比较的类型。宏的参数是类型无关的。

和函数相比宏的劣势:

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2. 宏是没法调试的。

3. 宏由于类型无关,也就不够严谨。

4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

但宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

宏和函数的对比表:

属性 #define定义宏 函数
代码长度 每次使用时,宏定义都会被插入到代码块中,除了非常小的宏之外,代码长度会大幅增加 代码之出现在一个地方,每次调用时,都调用那个地方
执行速度 更快 存在函数调用和返回开辟空间的时间,更慢一点
操作符优先级 宏参数只是简单的替换,不会根据优先级加上括号,所以在写宏的时候尽量多带括号 根据C语言操作符优先级来
带有副作用的参数 可能被替换到多个宏中去,如果不加以注意,可能该参数的值会不是预期中的 函数传参时值比较容易控制
参数类型 宏的参数与类型无关,不会进行类型检查 不同的参数类型需要一一对应,否则就会报错
调试 宏是不方便调试的 函数可以逐语句调试
递归 宏是不能递归的 函数可以递归

#和##

#

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

#运算符所执行的操作可以理解为”字符串化“。 当我们有一个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .

就可以写:


#define print(n) printf("the value of "#n" is %d",10)

int main()
{
	print(a);
	return 0;
}

##

## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。

## 被称为记号粘合这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

这里我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。 比如:

int int_max(int x, int y)
{
 return x>y?x:y;
}
float float_max(float x, float y)
{
 return x>yx:y;
}

但是通过##我们就可以这样来写:

#define MAX(type) \
type type##_max(type a,type b)\
{\
return a > b ? a : b;\
}

MAX(float)

int main()
{
	printf("%f", float_max(3.4, 6.7));
	return 0;
}

 这样我们就可以很方便得比较各种类型的数据了

宏的命名

平时使用宏的时候一般将名字全部大写

函数名用驼峰法

#undef

这条指令用于移除一个宏定义

#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

条件编译

在编程中,我们有的时候会不需要一些代码,删了又可惜,不删又没法运行,这时候就可以用到条件编译了

#define _DEBUG_

int main()
{
	int arr[10] = { 0 };
	for (int i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef _DEBUG_
		printf("%d ", arr[i]);//判断arr是否被成功赋值
#endif
	}
	return 0;
}

其他的条件编译指令

#if 常量表达式
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif
2.多个分⽀的条件编译
#if 常量表达式
 //...
#elif 常量表达式
 //...
#else
 //...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#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

头文件的包含

本地文件包含

#include "name.h"

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

库文件包含

#include <stdio.h>

查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?

答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件 了。 

其他预处理指令

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。 这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 一个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
 
 return 0;
}

如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。 如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。 如何解决头文件被重复引入的问题?答案:条件编译。 每个头文件的开头写:

 #pragma once

或者

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__