C语言勘破之路-最终篇 —— 预处理(上)

人无完人,持之以恒,方能见真我!!!
共同进步!!

文章目录

一、预定义符号

上一篇讲到编译和连接,这一篇就详细讲讲预处理(预编译)
C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的,如下:

c 复制代码
_ _FILE_ _ 
__LINE__ 
__DATE__ 
__TIME__ 
__STDC__ 

我们需要注意的是,使用这些预定义符号的时候,下面的两个短下划不能少,并且两个短下划线之间是没有间隙的,第一个只是方便我们观察,实际是没有空格的
接下来我们来再详细介绍一下它们:

  1. FILE代表当前进行编译的源文件,在打印时,需要使用占位符%s,它不仅会打印文件名,还会打印文件的完整路径
  2. LINE代表出现了这个预定义符号的行号,比如这个预定义符号出现在第6行时,那么它就代表6,所以需要使用%d进行打印
  3. DATE代表文件被编译时的日期,打印时需要使用占位符%s
  4. TIME代表文件被编译时的具体时间,具体到时分秒,打印时也是使用占位符%s
  5. STDC就与编译文件的编译器有关了,如果编译当前文件的编译器完全遵守了ANSI C标准,那么它将会被定义,并且值为1,打印时需要使用%d,如果该编译器不完全遵守ANSI C标准,那么STDC这个预定义符号就没有被定义过,如果使用它就会报错
    接着我们就来使用一下这几个预定义符号,首先我们来使用前4个预定义符号,来打印我们源文件在编译时的各种信息
c 复制代码
#include <stdio.h>

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

运行结果

接着我们就可以使用STDC这个预定义符号,来判断我们的编译器是否完全遵循ANSI C
可以看到VS2022在运行时报错了,不认识这个标识符,说明我们的VS2022并没有严格遵守ANSI C标准

二、#define定义常量

#define定义常量的基本语法如下:

c 复制代码
#define name stuff

其中的name就是我们定义的常量的名称,stuff就是我们定义的常量的值,可以是整型,可以是字符串,也可以是字符等等
接着我们就使用#define来定义各种类型的常量,我们要注意的一点是,在取名时我们的常量名最好全部大写,这是我们编程的一种习惯,如下:

c 复制代码
#include <stdio.h>

#define MAX 100
#define STR "I am Sam!"
#define CH 'x'

int main()
{
	printf("MAX: %d\n",MAX);
	printf("STR: %s\n",STR);
	printf("CH : %c\n", CH);
	return 0;
}

需要注意的是,我们再define 的时候,最好不要定义分号

c 复制代码
#define MAX 100;

int a = MAX;

替换一下,我们就能发现问题所在

c 复制代码
int a = 100;;

所以,我们要尽量避免上面这种情况,因为这样使用会产生很多不确定性

三.、#define定义宏

#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(definemacro),下面是宏的声明方式:

c 复制代码
#define name( parament-list ) stuff

其中的parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中,要注意的是:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分
是不是有点难懂,我们可以看如下的例子:

c 复制代码
#define SQUARE( x )  x * x

它的形式有点类似于函数,前面就相当于函数名,括号中就是宏的参数,后面是这个宏的计算方式,比如使用SQUARE(5),那么预处理后,就会把这条语句转化成5*5
其中SQUARE和第一个小括号要紧紧贴在一起,如果两者之间有任何空⽩存在,那么(x)就会成为后面的一部分,就会出错
那么我们上面写的这个宏是否就完全正确了呢?其实它还存在一个问题,比如我们来看一个例子:

c 复制代码
#include <stdio.h>

#define SQUARE( x ) x * x

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

但是要注意带入的元素的运算优先级

其中的x会直接被a+1替换,那么SQUARE(x)经过替换过后应该是如下的样子:

c 复制代码
a + 1 * a + 1
//带入a=5
5 + 1 * 5 + 1

但是只要我们给定义的元素加上括号,就能减少错误发生的概率

那么这样是否就一定不会出错了呢?这里就不卖关子了,这样还是不能确保得到我们预期的结果,为什么呢?

c 复制代码
#include <stdio.h>

#define DOUBLE( x ) (x) + (x)

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

按照我们的预期,宏DOUBLE会帮我们计算出一个数的2倍,那么这里5的2倍是10,乘以10过后就变成了100,那么我们来看最后的结果是否是100

c 复制代码
10 * (a) + (a)
//将a替换成5之后
10 * (5) + (5)

这个时候就可以看出来,由于*的优先级更高,所以10和前面那个5结合变成了50,然后+5变成了55,这就是55的由来,所以我们可以看出,光给每个参数加上()还不够,我们还最好把整个式子括起来,表示它们是一个整体

c 复制代码
#define DOUBLE( x ) ((x) + (x))

可以看到最后结果就正确了,所以总结一下,在我们使用宏定义的时候,我们要使用()将每个参数括起来,保证每个参数是一个整体,最后我们还要使用()将整个式子括起来,保证整个式子是一个整体

四、带有副作用的宏参数

宏参数还有副作用,是不是基本上没有听过这种说法,为什么会这么说呢?我们一起来学习一下:
带有副作用的宏参数就是:当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果,其中副作⽤就是表达式求值的时候出现的永久性效果
我们举个栗子来看一下:

c 复制代码
//不带副作⽤
x+1;
//带有副作⽤
x++;

乍一看这两者不是一样的吗?但其实并不一样,因为x++对x造成了永久性的效果,就是对x自增了一个1,而x+1这个表达式对x并没有影响
接着我们来看一个例子来更好的理解,我们来定义一个宏,它的功能就是帮我们找到两个数中的最大数:

c 复制代码
#include <stdio.h>

#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) 

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

这个例子的运行结果是什么呢?我们预期的结果是a变成6,b变成3,ret则是5,因为传参的时候使用的是后置++,所以是先使用a和b的值,也就是把5和2作为参数传过去后,然后a和b再++,所以a变成了6,b变成了3,ret还是5
结果如下:

这里的结果和我们预期还是不一样,本质还是由于在预处理阶段,会将宏直接替换过来

c 复制代码
( (a++) > (b++) ? (a++) : (b++) ) 

在执行这条语句时,首先会执行(a++) > (b++),此时这里是后置++,所以a和b先使用再自增1,由于a是5,b是2,a>b成立了,然后对a和b进行自增1,a就变成了6,b就变成了3
由于(a++) > (b++)的结果为真,所以最后整个三目表达式返回的就是a++的结果,由于这里还是后置++,所以返回的就是6,然后对a自增1变成7,所以最后ret的值就是6,a的值为7,b的值为3
所以我们在使用宏的时候最好不要使用带副作用的宏参数,也就是使用后会对原本的参数造成永久性效果的表达式,例如++和- -操作

五、宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及以下⼏个步骤

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换
  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置,不做任何更改,而对于宏,参数名被它们的值所替换
  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
    注意
  4. 宏参数和#define定义中可以出现其他#define定义的符号,比如先使用#define定义一个常量N,值为100,那么这个N就可以在另一个#define中出现,但是对于宏,不能出现递归
  5. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

六、宏和函数的对比

1.宏的优势

宏通常被应⽤于执⾏简单的运算,而函数则可以应用于较为复杂的场面,⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些

c 复制代码
#define MAX(x,y) ( (x) > (y) ? (x) : (y) ) 

那为什么不⽤函数来完成这个任务?原因有2点:

  1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多,因为函数还要开辟自己的栈帧,进行返回等等操作,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹
  2. 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使⽤,反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于>来比较的类型,宏的参数是类型⽆关的,比如上面我们定义的MAX宏,不仅可以比较整型,同时也可以比较浮点型和长整型等等,而一个函数只能比较单个数据类型

2.函数的优势

对于宏来说,函数也有它的优势,它们没有一定的哪一个好,只有哪一个更适合我们的需求,那么对比宏,函数的优势如下:

  1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中,除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度
  2. 宏是没法调试的,而函数可以一步一步调试,查看bug出现的原因
  3. 宏由于类型⽆关,也就不够严谨,这在上面成为了它的优势,但是在某些场景导致它的不够严谨,这个时候就要使用函数
  4. 宏可能会带来运算符优先级的问题,导致程序容易出错,比如忘记对参数加上(),或者忘了给整个式子加上()都可能出现预期以外的结果

3.宏和函数的命名约定

⼀般来讲函数的宏的使⽤语法很相似,并且语⾔本⾝没法帮我们区分⼆者,所以我们平时就通过命名来简单区分它们,接下来我们来看看它们的命名约定:

  1. 宏名全部大写
  2. 函数名不要全部大写,一般是多个单词中,每个单词的首字母大写
    今天的最终篇上篇就分享到这里,下一篇就是C语言的最后一篇了,C语言的勘破之路马上就要被我们走完了,希望大家多多坚持,马上就要开启我们的数据结构之旅了
相关推荐
hakesashou25 分钟前
python如何打乱list
开发语言·python
网络风云42 分钟前
【魅力golang】之-反射
开发语言·后端·golang
Want5951 小时前
Java圣诞树
开发语言·python·信息可视化
运维小文1 小时前
python之打印、变量、格式化输出
开发语言·python·python基础·hello world
就爱学编程1 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(下)
java·服务器·c语言
坐井观老天2 小时前
使用 C# 测量程序运行消耗的的时间
开发语言·c#
小哈龙2 小时前
c++ 类似与c# 线程 AutoResetEvent 和 ManualResetEvent的实现
c++·c#·多线程
IT猿手2 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解LRMOP1-LRMOP6及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·算法·matlab·智能优化算法·多目标算法
YYY_小后知2 小时前
C# 中 Webclient和Httpclient
开发语言·c#
kittygilr2 小时前
matlab中的cell
开发语言·数据结构·matlab