目录
🔥个人主页:@草莓熊Lotso的个人主页
🎬作者简介:C++研发方向学习者
📖个人专栏:《C语言》
⭐️人生格言:生活是默默的坚持,毅力是永久的享受。
一.预定义符号
--C语言中设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
1. __FILE__ // 进行编译的源文件2. __LINE__ //文 件当前的行号3. __DATE__ //文 件被编译的日期4. __TIME__ //文 件被编译的时间5. __STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
--我们来使用一下这些预定义符号,观察它们打印出来的结果
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
二.#define定义常量
基本语法形式:
1. #define name stuff
实际举例:
1. # define MAX 10002. # define reg register // 为 register 这个关键字,创建一个简短的名字3. # define do_forever for(;;) //用 更形象的符号来替换⼀种实现,这里是替换死循环4. # define CASE break;case // 在写 case 语句的时候自动把 break 写上。// 如果定义的 stuff 过长,可以分成几行写,除了最后⼀行外,每行的后面都加⼀个反斜杠 ( 续行 符 ) 。5. # define DEBUG_PRINT printf( "file:%s\t line:%d\t date:%s\t time:%s\n" \,__FILE__,__LINE__ , \__DATE__,\__TIME__ )
我们看一下第4个和第5个的使用吧~
演示4:
#include<stdio.h>
#define CASE break;case
int main()
{
int num = 0;
scanf("%d", &num);
switch (num)
{
case 1:
printf("hehe");
//正常来说这里应该加break
// 但是我定义了CASE为 break;case
CASE 2:
printf("haha");
//这里同理
CASE 3:
printf("呵呵");
}
return 0;
}
演示5:
#include<stdio.h>
#define DEBUG_PRINT printf("file:%s\t line:%d\t date:%s\t time:%s\n" \
,__FILE__, \
__LINE__ , \
__DATE__, \
__TIME__ )
int main()
{
DEBUG_PRINT;
return 0;
}
我们再来思考一个问题:在define定义标识符的时候,要不要在最后加上 ; 呢?
比如:
# define MAX 1000;# define MAX 1000
这当然是不行的,很容易导致一些问题,我们直接通过两个错误场景来直观的感受一下吧~
错误场景一:
#include<stdio.h>
#define MAX 1000;
int main()
{
printf("%d", MAX);//实际上会被解析为1000;;会出现两个分号
return 0;
}
错误场景二:
#include<stdio.h>
#define MAX 1000;
int main()
{
int max = 0;
int a = 3;
int b = 4;
if (a>b)
max = MAX;
else
max = 0;
return 0;
}
如果这里加了分号,等到替换之后,if和else之间实际上是两条语句,在没有大括号时,if后边只能控制一条语句。所以这里会出现语法错误。
三.#define定义宏
3.1--定义宏的方法和注意事项
--#define机制包括一个规定,允许将参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
1. #define name( parament-list ) stuff
其中的( parament-list )是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
错误举例:
# define SQUARE( x ) x * x
#include<stdio.h>
#define SQUARE(x) x*x
int main()
{
int a = 10;
int r = SQUARE(a+1);
printf("%d", r);
return 0;
}
这个代码我们简单的来看很可能会认为结果是11*11=121,但其实并不是,它的实际结果是21。
因为替换文本时,实际上是参数x被替换成a+1,所以这条语句应该被解析为:
int r = a+1*a+1 ;//即为10+1*10+1=21;printf ( "%d\n" ,r );
这样我们就可以清晰的看出替换后表达式所求的值了,那么我们该如何解决呢?
其实只需要在宏定义加上两个括号,这个问题就得以解决了
#define SQUARE( x ) (x) * (x)
这样改完后预处理完就可以得到你想要的结果了,变成了(10+1)*(10+1)。
但是呢,这样还不能完全解决所有问题,我们再来看看下面这个宏定义:
# define DOUBLE(x) (x) + (x)
这里我们可以看到定义中我们使用了括号,想避免之前出现的问题,但是这个宏可能会出现新的错误,我们来看如下代码:
#include<stdio.h>
#define DOUBLE(x) (x)+(x)
int main()
{
int a = 10;
printf("%d", 5*DOUBLE(a));
return 0;
}
这串代码打印出什么值呢?看上去好像很可能是100,但是实际上是60。
我们替换完可以发现:
printf ( "%d\n" ,5 * ( 10 ) + ( 10 ));
乘法的优先级先于宏定义后的加法,所有会出现60 ,这个问题我们只需要在宏定义表达式两边加上一对括号就可以了。
# define DOUBLE(x) ( ( x ) + ( x ) )
重要知识点:
所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,所有地方都加上括号避免在使⽤宏时由于参数中的操作符和邻近操作符之间不可预料的相互作⽤。
3.2--带有副作用的宏参数
1. x+1;//不带副作用2. x++;//带有副作用
我们可以通过一个MAX宏来直观体会一下这个错误:
#include<stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++, y++);
printf("x=%d y=%d z=%d", x, y, z);
return 0;
}
我们来分析一下这串代码的结果会是什么吧
1. 先把z换成预处理之后的结果,即为z=( (x++) > (y++) ? (x++) : (y++));
2. 先看第一个表达式,后置加加,先比先x>y为假,所以表达式的结果会表达式3的结果,但是这里的x++后变成6,y++后变成9
3. 计算第三个表达式,此时y再++变成了10,但是因为是后置++所有在++之前把y之前为9的值赋给了z
所以x=6,y=10,z=9;
3.3--宏替换的规则
- 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
- 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
四.宏与函数的对比
--宏通常被应用于执行简单的运算。
比如在两个数中我们想要找较大的一个数时,写成下面的宏的形式,更有优势一些
# define MAX(a, b) ((a)>(b)?(a):(b))
- ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。
宏有时候可以做到一些函数做不到的事,比如:宏的参数可以出现类型,但是函数就做不到
# define MALLOC(num, type)\(type )malloc(num sizeof(type))...// 使⽤MALLOC( 10 , int ); // 类型作为参数// 预处理器替换之后是:( int * ) malloc ( 10 sizeof ( int ));
但和函数相比宏也有它的劣势,如下:
- 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
- 宏是没法调试的。
- 宏由于类型⽆关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错,(比如上面的自增++)
宏和函数的对比关系表:
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏以外,程序的长度都会被大幅增长 | 函数代码只出现于一个地方;每次使用这函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所有相对慢一些 |
操作符优先级 | 宏参数的求值是在周围所有表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议书写宏的时候多写括号 | 函数参数只在函数调用的时候求值一次,其结果值传递给函数,表达式的求值结果容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数和函数类型有关,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数式可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
往期回顾:
【C语言编译与链接】--翻译环境和运行环境,预处理,编译,汇编,链接
【通关文件操作(上)】--文件的意义和概念,二进制文件和文本文件,文件的打开和关闭,文件的顺序读写
【通关文件操作(下)】--文件的顺序读写(续),sprintf和sscanf函数,文件的随机读写,文件缓冲区,更新文件
结语:本篇文章就到此结束了,继前面一篇文章后,在此篇文章中给大家分享了预处理详解中的部分知识点,如预定义符号,#define定义常量,#define定义宏,带有副作用的宏参数,宏替换的规则,宏和函数的对比等,下篇文章会接着为大家分享预处理详解的剩余知识点。如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。