C语言进阶 预处理详解(1)

在上一篇博客,我们了解了一个程序是是怎么运行起来的,一个程序的运行过程,是由编译环境和运行环境组成的,而编译环境分为编译和链接两个阶段,编译的第一步就是预处理,这个过程其实还有很多我们需要了解的知识,本篇博客将会对此进行讲解。


1.预定义符号

其实在C语言中给我们提供了很多预定义的符号,这些符号代表着某些值,这里举出几个例子

             __FILE__//进行编译的源文件
             __LINE__//文件当前的行号
             __DATE__//文件被编译的日期
             __TIME__//文件被编译的时间

这些符号是语言内置的,我们直接可以使用的。

int main()
{
	int i = 0;
	
	printf("file:%s line:%d date:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
	
}

在这里插入图片描述
可以看到我们的源文件绝对路径,行号,以及日期时间。
这只是预定义符号的一部分,感兴趣可以百度完整的。
在这里提供一个思路,就是我们可以利用这些预处理符号和文件操作弄一个日志,把重要的状态信息保存下来,方便查看:

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>

int main()
{
	int i = 0;
	FILE* pf = fopen("sts.txt", "a");
	if (pf == NULL)
	{
		return 1;
	}
	for (i = 0; i < 10; ++i)
	{
		fprintf(pf,"file:%s line:%d date:%s time:%s i=%d\n", __FILE__, __LINE__, __DATE__, __TIME__, i);
	}
	fclose(pf);
	pf = NULL;

	return 0;
}

这是sts中的文本信息
在这里插入图片描述

#define

#define是预处理中很重要的一个指令,而且我们平时也很常用,常用的功能主要有两个,在这里详细的介绍一下:
1.#define定义标识符
这也是我们在初学阶段经常使用的功能,语法是这样的:

#define name stuff

#define 后一个空格 再加上标识符的名字 后再加一个表达式,系统就会在预处理阶段把标识符替换成表达式,要注意的是,由于替换的时候只替换了表达式,所以容易出现各种问题,下面我们先看看标识符的使用:

#include<stdio.h>
#define N 10
int main()
{
	int a = N;
	return 0;
}

之前我们说啊,预处理之后,这#define定义的标识符都会被替换,现在我们有办法查看预处理之后的文件了:
在这里插入图片描述
只需把预处理器的预处理到文件改成是,我们就可以看到test.i文件了
在这里插入图片描述
能看到啊,经过预处理之后直接多了一万行代码,那都是stdio提供的,我们不用管,主要看从 int main开始的这几行,之前的#define已经消失了,a=N,N也被替换成10了。当然,标识符的表达式可以不是一个值,也可以是一个算式,一个字符串,反正你放进去什么,最后你的标识符都会原封不动的被替换成表达式。

2.#define定义宏
#define定义宏,这个有点类似函数啊,先看看他的语法:

#define name(parament-list) stuff

在这里解释一下啊,parament-list 是一个由逗号分隔开的符号表,这个符号表可以应用在stuff这个表达中,例如

#define sqare(a) ((a)*(a))

这时候a就类似于一个函数,能求平方的函数:
在这里插入图片描述
大家可能有点疑惑啊,就是我为什么要加这么多括号,有啥用呢?
在这里提醒大家,在宏定义中千万不要吝括号,因为#define是直接把你给的表达式打印出来,那在遇到乘法时就有可能出现问题,举个例子:
在这里插入图片描述
如果我们现在传的是N,进去就是b+1,原封不动的放到sqare中去,
((b+1)*(b+1)) = 121,这是正确的对吧,那如果我们去掉括号呢?
那就是b+1*b+1 = 21
在这里插入图片描述
所以啊,无论是宏定义,或者是定义标识符,都不要吝啬括号,不然会出现我们预料不到的问题。

#define的替换规则

1.在调用宏的是,先检查参数,看看参数有没有标识符,有标识符先替换。
2.替换文本(也就是stuff中的表达式)在参数检查之后被插入到宏或者标识符被使用的位置,原封不动的替换。
3.再次检查,看看有没有遗漏的宏和标识符,如果有,重复流程。

这三条规则都挺好理解的,这里就不多说了,主要说一些需要注意的地方:
1)宏不能出现递归调用,虽然宏中可以出现标识符,但是宏中不能有宏。
2)当预处理器搜索#define定义的符号时,字符串常量并不被搜索。
在这里解释一下第二个,举个例子大家就明白了。
在这里插入图片描述
看这样一段代码,N是我们用#define定义的标识符,我们在printf中也打印N,如果他能被检测那就会出现 11 11 11 11 11 21,不能就是 N N N N N 21

在这里插入图片描述
显然,字符串常量中的标识符并不能被检测。

#和##的使用

在讲这个之前,我们需要先补充一些知识储备:字符串是有自动连接的特点的,这是什么意思呢?

printf("hello world!");
printf("hello"" world!");

这两行代码,打印的结果都是一样的:
在这里插入图片描述
这就证明了,字符串是有自动连接的特点的,无论我们分成多少段给他,他都会整合成一个字符串。
那我们现在就可以玩一些比较有意思的了,在这里向大家提出一个问题:
我想封装一个函数,传入a就打印 a = a的值,传入b就打印b = b的值,看起来似乎很简单:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

void print(int a)
{
	printf("a = %d\n", a);
}
int main()
{
	int b=10;
	int a = 22;
	print(a);
	print(b);
	return 0;
}

在这里插入图片描述
但是这个函数有一个问题,无论我们传入的参数是谁,他都显示的是 a = 多少,那有没有办法让显示的跟着传入的参数改变呢?当然是有办法的,这里我们要介绍#的用法

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>

#define print(format,value) printf(#value " is " format"\n",value)
int main()
{
	int b=10;
	int a = 22;
	print("%d",a);
	print("%d",b);
	return 0;
}

看一下这串代码,我们用宏定义的方式写了一个输出,format是格式,表示你传入的是什么类型的数据,value是你传入数据的值,#value,这个放在宏定义里面,把一个宏定义的参数,变成其对应的字符串,而不是该参数对应的值,所以他能实现一件事,就是传入a就打印a,传入b就打印b。
那把这个printf中的语句翻译一下,就是 “参数名”" is "参数类型 “\n” ,注意这个参数类型我没有给双引号,因为给了双引号他就是一个字符串了,不能被宏识别,所以我们要自己输入对应的格式。
在这里插入图片描述
还有一个##符号,他的作用是把位于他两边的符号构成一个符号,例如

sum##num,num是我们传入的可变的参数,这样num是1传入的就是sum1
2就是sum2,这里再举个例子:
在这里插入图片描述

sum会和num连接在一起形成一个符号,传入1自然就是sum1。

带副作用的宏参数

先说说副作用是啥意思,就是你在调用这个参数的时候,参数发生了不可逆的改变,例如a++,++a这样的,这时候可能引起一些出乎意料的结果,举个例子:

#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
	int a = 1;
	int b = 2;
	int c = MAX(a++, b++);
	printf("%d %d %d\n", a, b, c);
	return 0;
}

就像这样的代码,我们原本是为了求a,b中的最大值,然后给c,a,b,c的值应该是1 2 2,可是我们传入的不是a和b是a++和b++,这是带有副作用的参数,如果预处理完,表达式就应该是这样的:((a++)>(b++)?(a++):(b++)),首先判断的时候,a,b ++就已经执行了依次,然后由于a<b,我们又执行了一次b表达式,此时 a b c的值已经变成了 2 4 3 ,显然给a和b带来了永久性的改变,这就是宏参数的副作用,如果这个求最大值是用函数传值进行的,就不会有副作用,因为这时的形参只是实参的拷贝。
既然提到了函数,那我们就说一说函数与宏的区别:

宏与函数对比

先拿个例子说,就像刚才我们写的求最大值的宏函数,为什么用宏不用函数呢?
原因有二:
第一点,调用函数,返回值,其中还要占用空间,这比直接使用都复杂,所以第一点就是宏更快,更少的占用空间。
第二点,我们可以发现宏只是负责打印,和我们传入什么没有关系,所以所以宏并不限定类型,这点是十分重要的。
在这里插入图片描述
在这里总结了一些宏和函数的区别,大家有兴趣可以看看。

总结

#define是一个很方便且拥有很多功能的指令,希望大家能够熟练运用,并且区分宏和函数的区别,什么时候用宏,什么时候用函数,要有感觉。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值