文章目录
一、预定义符号
学习本篇文章的内容推荐先去看前面的编译和链接,才能更好地理解和吸收,文章链接:【C语言】编译和链接(编译环境和运行环境)
C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的,如下:
_ _FILE_ _
_ _LINE_ _
_ _DATE_ _
_ _TIME_ _
_ _STDC_ _
我们需要注意的是,使用这些预定义符号的时候,下面的两个短下划不能少,并且两个短下划线之间是没有间隙的,由于写文章时两个短下划线会进行合并,所以这里我加上了空格,但是在实际使用时,下面的两个短下划线之间没有间隙
接下来我们来再详细介绍一下它们(在描述时我会省略短下划线,但是我们要注意,那些下划线也属于预定义符号的一部分,使用时必须加上):
- FILE代表当前进行编译的源文件,在打印时,需要使用占位符%s,它不仅会打印文件名,还会打印文件的完整路径
- LINE代表出现了这个预定义符号的行号,比如这个预定义符号出现在第6行时,那么它就代表6,所以需要使用%d进行打印
- DATE代表文件被编译时的日期,打印时需要使用占位符%s
- TIME代表文件被编译时的具体时间,具体到时分秒,打印时也是使用占位符%s
- STDC就与编译文件的编译器有关了,如果编译当前文件的编译器完全遵守了ANSI C标准,那么它将会被定义,并且值为1,打印时需要使用%d,如果该编译器不完全遵守ANSI C标准,那么STDC这个预定义符号就没有被定义过,如果使用它就会报错
接着我们就来使用一下这几个预定义符号,首先我们来使用前4个预定义符号,来打印我们源文件在编译时的各种信息,如下:
#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;
}
我们来看看代码运行结果:
我们来看看运行结果是否如同我们上面说的那样,首先FILE会打印源文件的完整路径,LINE会打印它出现时的行号,可以看到LINE确实是在第8行出现,DATE打印的就是文件被编译的日期,是2024年10月26日,也没有问题,最后就是TIME,也确实打印了文件编译时的时分秒
接着我们就可以使用STDC这个预定义符号,来判断我们的编译器是否完全遵循ANSI C,如下:
#include <stdio.h>
int main()
{
printf("STDC: %d", __STDC__);
return 0;
}
接着我们在VS2022这个IDE上面运行一下,结果如图:
可以看到VS2022在运行时报错了,不认识这个标识符,说明我们的VS2022并没有严格遵守ANSI C标准,可能遵守了%99,但是就是没有完全遵守
二、#define定义常量
#define定义常量的基本语法如下:
#define name stuff
其中的name就是我们定义的常量的名称,stuff就是我们定义的常量的值,可以是整型,可以是字符串,也可以是字符等等
接着我们就使用#define来定义各种类型的常量,我们要注意的一点是,在取名时我们的常量名最好全部大写,这是我们编程的一种习惯,如下:
#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定义常量时,后面是否要加分号?比如:
#define MAX 100;
#define MAX 100
我们首先要知道#define定义常量时是怎么工作的,它会直接把常量名替换为对应的值,在第一条语句中,MAX就代表了100; ,而在第二条语句中,MAX就只代表100
所以很明显我们在使用#define定义常量时,最好不要在后面加上分号,那么为什么有时候加上分号也没有问题呢?如下:
#define MAX 100;
int a = MAX;
当我们运行这条语句时,发现不会出错,这是为什么呢?我们只需要把它替换一下就知道了:
int a = 100;;
现在相当于就是语句后多了一条分号,前一个分号就是这条语句的结束标志了,第二个分号相当于是一个空语句,什么也没有做,所以这句话就相当于了两条语句,第二条语句是空语句,什么也没有做,所以执行起来没有问题
但是这种情况也不是我们使用#define定义常量的初衷,我们只是想要使用MAX表示100而已,并不想要带上那个分号,并且加上分号后,有很多情况会出错,所以我们使用时就最好不要在#define定义常量时在后面加上分号
三.、#define定义宏
#define 机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(definemacro),下面是宏的声明方式:
#define name( parament-list ) stuff
其中的parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中,要注意的是:参数列表的左括号必须与name紧邻,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分
是不是有点难懂,我们可以看如下的例子:
#define SQUARE( x ) x * x
它的形式有点类似于函数,前面就相当于函数名,括号中就是宏的参数,后面是这个宏的计算方式,比如使用SQUARE(5),那么预处理后,就会把这条语句转化成5*5
其中SQUARE和第一个小括号要紧紧贴在一起,如果两者之间有任何空⽩存在,那么(x)就会成为后面的一部分,就会出错
那么我们上面写的这个宏是否就完全正确了呢?其实它还存在一个问题,比如我们来看一个例子:
#include <stdio.h>
#define SQUARE( x ) x * x
int main()
{
int a = 5;
printf("%d\n", SQUARE(a + 1));
return 0;
}
我们预期的结果是它帮我们算出6的平方36,那么它最后能否得到这个结果呢?我们来看看它的运行结果:
我们可以惊奇的发现,程序运行的结果不应该是a+1,也就是6的平方36吗?为什么结果变成了11?这就要涉及到我们上面谈到过的,#define定义的内容是直接替换的,不会有任何的变化
其中的x会直接被a+1替换,那么SQUARE(x)经过替换过后应该是如下的样子:
a + 1 * a + 1
//带入a=5
5 + 1 * 5 + 1
这个时候就可以发现问题了,由于运算符的优先级,中间的1 * 5会优先计算,变成5,然后就是5+5+1,最后结果为11
那么怎么解决运算符导致的错误呢?我们可以在定义宏的时候,把参数使用小括号括起来,让每个参数成为一个整体,无论怎么样都是参数内部先计算,最后再进行宏定义的运算,如下:
#define SQUARE( x ) (x) * (x)
我们将宏定义改成这样再来看看代码运行结果:
那么这样是否就一定不会出错了呢?这里就不卖关子了,这样还是不能确保得到我们预期的结果,为什么呢?我们接着看一个例子:
#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:
可以看到结果又与我们预期的不一样了,这还是我们在预处理阶段出现的问题,还是因为#define使用宏的时候,会直接替换内容,上面的那条语句经过替换后如下:
10 * (a) + (a)
//将a替换成5之后
10 * (5) + (5)
这个时候就可以看出来,由于*的优先级更高,所以10和前面那个5结合变成了50,然后+5变成了55,这就是55的由来,所以我们可以看出,光给每个参数加上()还不够,我们还最好把整个式子括起来,表示它们是一个整体,如下:
#define DOUBLE( x ) ((x) + (x))
接着我们就拿这个宏定义来试试答案是否会变成我们预想的100,如图:
可以看到最后结果就正确了,所以总结一下,在我们使用宏定义的时候,我们要使用()将每个参数括起来,保证每个参数是一个整体,最后我们还要使用()将整个式子括起来,保证整个式子是一个整体
四、带有副作用的宏参数
宏参数还有副作用,是不是基本上没有听过这种说法,为什么会这么说呢?我们一起来学习一下:
带有副作用的宏参数就是:当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果,其中副作⽤就是表达式求值的时候出现的永久性效果
我们可以举一个例子,如下:
//不带副作⽤
x+1;
//带有副作⽤
x++;
乍一看这两者不是一样的吗?但其实并不一样,因为x++对x造成了永久性的效果,就是对x自增了一个1,而x+1这个表达式对x并没有影响
接着我们来看一个例子来更好的理解,我们来定义一个宏,它的功能就是帮我们找到两个数中的最大数:
#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
那么最后结果是否是我们预期的结果呢?如图:
可以看到,最后结果和我们的预期又不一样,而且还相差的很远,这是为什么呢?这其实就是我们所说的带副作用的宏参数,那么引起它的本质是什么呢?没错,还是因为宏定义时的#define替换规则
由于在预处理阶段,会将宏直接替换过来,所以上面的语句就变成如下语句:
( (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定义符号和宏时,需要涉及以下⼏个步骤,我们简单地了解一下:
- 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换
- 替换⽂本随后被插⼊到程序中原来⽂本的位置,不做任何更改,而对于宏,参数名被它们的值所替换
- 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
注意:
- 宏参数和#define定义中可以出现其他#define定义的符号,比如先使用#define定义一个常量N,值为100,那么这个N就可以在另一个#define中出现,但是对于宏,不能出现递归
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
六、宏和函数的对比
1.宏的优势
宏通常被应⽤于执⾏简单的运算,而函数则可以应用于较为复杂的场面,⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些
#define MAX(x,y) ( (x) > (y) ? (x) : (y) )
那为什么不⽤函数来完成这个任务?原因有2点:
- ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多,因为函数还要开辟自己的栈帧,进行返回等等操作,所以宏⽐函数在程序的规模和速度⽅⾯更胜⼀筹
- 更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使⽤,反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于>来比较的类型,宏的参数是类型⽆关的,比如上面我们定义的MAX宏,不仅可以比较整型,同时也可以比较浮点型和长整型等等,而一个函数只能比较单个数据类型
2.函数的优势
对于宏来说,函数也有它的优势,它们没有一定的哪一个好,只有哪一个更适合我们的需求,那么对比宏,函数的优势如下:
- 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中,除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度
- 宏是没法调试的,而函数可以一步一步调试,查看bug出现的原因
- 宏由于类型⽆关,也就不够严谨,这在上面成为了它的优势,但是在某些场景导致它的不够严谨,这个时候就要使用函数
- 宏可能会带来运算符优先级的问题,导致程序容易出错,比如忘记对参数加上(),或者忘了给整个式子加上()都可能出现预期以外的结果
3.宏和函数的命名约定
⼀般来讲函数的宏的使⽤语法很相似,并且语⾔本⾝没法帮我们区分⼆者,所以我们平时就通过命名来简单区分它们,接下来我们来看看它们的命名约定:
- 宏名全部大写
- 函数名不要全部大写,一般是多个单词中,每个单词的首字母大写
今天的C语言知识分享就到这里啦,也是我们的最终篇(上),下一篇我们会讲到条件编译,又是一个硬知识,最后希望大家能在我的博客能够学习到知识,如果有疑问欢迎提出来
bye~