个人主页点这里~
预处理
- 一、预处理符号
- 二、#define定义常量
- 三、#define定义宏
- 四、带有副作用的宏参数
- 五、宏替换的规则
- 六、宏与函数的对比
- (一)、宏的优势
- (二)、宏的劣势
- (三)、宏和函数的对比
- 七、#和##
- 1、#运算符
- 2、##运算符
- 八、命名约定
- 九、#undef
- 十、命令行定义
- 十一、条件编译
- 十二、头文件的包含
- 1、头文件被包含的方式
- ①本地文件包含
- ②嵌套文件包含
- 十三、其他预处理指令
一、预处理符号
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
以上是C语言设置的一些预定义符号,是可以直接使用的,预定义符号在预处理阶段处理
二、#define定义常量
基本语法:
#define name stuff
例子:
#define MAX 1000 //将MAX赋值为1000
#define reg register //为 register这个关键字,创建⼀个简短的名字reg
#define do_forever for(;;) //⽤更形象的符号来替换⼀种实现
//for(;;)是无条件的for循环,是一个死循环
#define CASE break;case //在写case语句的时候⾃动把 break写上。
当我们在使用#define的时候,变量有两项,一是name,二是stuff,而stuff中的值将会代替代码中所有的name,相当于是办公软件word里边的替换,所以我们遇到以下的问题,就可以一下解决出来:
#define MAX 1000;
#define MAX 1000
我们说,这两种被定义的方式是不同的,上边的第一个定义可以用来完成以下代码:
#define MAX 1000;
#include <stdio.h>
int main()
{
int a = MAX
return 0;
}
第二个可以完成以下代码:
#define MAX 1000
#include <stdio.h>
int main()
{
int a = MAX;
return 0;
}
都是可以的,但是我们要注意,当我们想要直接用printf输出MAX的值时,用第一个是不可以的
这是使用第二个定义,正确的做法:
#define MAX 1000
#include <stdio.h>
int main()
{
printf("%d",MAX);
return 0;
}
使用第一个定义:
#define MAX 1000;
#include <stdio.h>
int main()
{
printf("%d",MAX);
return 0;
}
我们在这里发现,MAX被替换成了1000;
1000;是不能通过printf打印的
所以我们得出一个结论:在使用#define定义数字时,尽量不要加入符号
三、#define定义宏
#define机制包括了一个机制,就是允许把参数替换到文本中,这种实现通常称为宏或者定义宏
宏的声明方式:
#define name( parament-list ) stuff
parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中
name与旁边的参数列表的左圆括号必须紧邻,如果二者之间有任何空白存在,参数列表就会被认为是stuff的一部分
举一个求平方的例子:
#define SQUARE( x ) x * x
当我们使用SQUARE( 9 )时,编译器就会将它替换成9*9
注意:
在使用宏定义的时候,我们为了不让我们所定义的量出现错误,最好给每个变量都加上括号,不然就会出现错误
例子:
#define SQUARE( x ) x * x
#include <stdio.h>
int main()
{
int a = 5;
printf("%d\n" ,SQUARE(a + 1) );
return 0;
}
这会发现一个错误,替换之后变成5+1*5+1,最终的答案是11而不是36
所以最好给每个量都套一个括号
修改后:
#define SQUARE( x ) ((x) * (x))
#include <stdio.h>
int main()
{
int a = 5;
printf("%d\n" ,SQUARE(a + 1) );
return 0;
}
四、带有副作用的宏参数
#define x++;
上面这个宏就是一个副作用宏,因为替换之后会持续造成作用,这样就可能会导致危险,简单来说,副作用就是表达式求值的时候会出现的永久性效果
我们来举一个例子
#define MIN(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 6;
y = 9;
z = MIN(x++, y++);//z = ((x++) < (y++) ? (x++) : (y++))
printf("x=%d y=%d z=%d\n", x, y, z);
结果是x=8 y=10 z = 7
我们先来计算<左右两边的值,x++是先赋值再++,y++也是先赋值再++,x<y,所以执行问号后边的语句,即x++先赋值再++,此时x=7,y=10,所以z=7,然后++,x=8,最终得到答案就是上述答案
五、宏替换的规则
1、调用宏的时候,首先对参数进行检查,看是否包含任何由#define定义的符号,如果是,他们首先被替换,也就是首先替换参数
2、然后替换文本随后被插入到程序中原本文本的位置,对于宏,参数名被它们的值所替换,也就是把宏定义的值替换被替换的值
3、最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是,就重复上述处理过程,也就是再次扫描然后重复上述过程
4、宏参数和#define定义中可以出现其他#define定义的符号,但是宏是不能够递归的
5、在字符串中的#define定义的符号不能被替换
六、宏与函数的对比
(一)、宏的优势
当我们要进行一些简单的计算时,使用宏替换比函数更有优势一些
1、因为不管是简单的还是复杂的计算,使用函数都会在栈中开辟一块空间(在我们之前的博文函数栈帧的创建和销毁一文中有详细的内容,大家有兴趣可以看一下),然后还有销毁空间,在开辟空间之前会有开辟空间之前的指令,这会增长运行时间,而反观用宏替换的方式,直接将代码替换,省去了开辟空间的时间,速度更快
2、使用函数要声明数据类型,所以一个函数只能由特定数据类型的数据使用,但是宏可以使用任意的数据,宏的参数与类型无关,只是一个简单的替换
3、宏的参数可以是类型,函数不行
例子:
( \ 这个符号是连字符,如果代码内容较长,可以用连字符来连接,程序生成的过程中会自动将 \ 去掉并连接上下)
#define MALLOC(num, type)\
(type*)malloc(num*sizeof(type))
int main()
{
MALLOC(5,int);//(int*)malloc(5*sizeof(int));
return 0
}
(二)、宏的劣势
再进行复杂计算时,使用函数会更有优势一些
1、每次使用宏的时候,宏定义的代码会插入到程序中,在宏较长的情况下可能会导致大幅度增加程序的长度
2、宏无法调试
3、宏与类型无关,这虽然是它的一个优点,也是一个缺点,因为这导致它不够严谨
4、宏可能会带来运算优先级问题,如上面第三条所说,容易导致程序出错
(三)、宏和函数的对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序当中,除了非常小的宏之外,程序的长度会大幅度增长 | 每次使用函数时,都调用同一个地方的同一份代码 |
执行速度 | 更快 | 存在函数栈帧的创建和销毁,相对于宏会慢一些 |
操作符优先级 | 宏在书写的时候要多加括号,否则会因为临近操作符优先级不同,使目的与代码不匹配的问题 | 表达式的求值容易被预测,不会出现宏一样的前后操作符优先级问题 |
带有副作用的参数 | 参数可能会被替换到程序中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会造成风险 | 函数参数只在传参的时候求值一次,结果容易被控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作合法,就可以使用任何数据类型 | 函数的参数与类型有关,必须严格按照参数类型来进行使用,不同参数类型不同,所需要的函数就不同,尽管它们的任务相同 |
调试 | 不能调试 | 可逐句调试 |
递归 | 不能递归 | 可以递归 |
七、#和##
1、#运算符
#运算符可以将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中
简单来说它的功能就是字符串化
例子:
当我们想打印出来一个数字的大小:the value of number is 6
我们可以这样做:
#define PRINT(n) printf("the value of "#n " is %d", n);
int main()
{
int number = 6;
PRINT(number);
return 0;
}
它在使用时,#n就是#number,#number就是转换成"number",这时字符串内的代码就会被预处理为:
printf("the value of ""number" " is %d", number);
然后正常执行代码,就得到了the value of number is 6
2、##运算符
##被称为记号粘合,它可以把左右两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符,当然这样的连接是要产生一个合法的标识符的,否则其结果就是未定义的,我们可以用这个运算符来写一个类似于宏的函数,这样的函数是可以定义自由定义数据类型的,使用起来是非常方便的
#define GENERIC_MIN(type) \
type type##_min(type x, type y)\
{ \
return (x<y?x:y); \
}
GENERIC_MIN(int) //替换到宏体内后int##_min ⽣成了新的符号 int_min做函数名
GENERIC_MIN(float) //替换到宏体内后float##_min ⽣成了新的符号 float_min做函数名
int main()
{
//调⽤函数
int m = int_min(6, 3);
printf("%d\n", m);
float b = float_min(1.5f, 4.5f);
printf("%f\n", b);
return 0;
}
八、命名约定
函数与宏的使用比较相似,我们这里约定俗成的规则就是将宏全部大写,函数部分大写,然后其他代码使用小写,这样可以很好的区分宏、函数以及其他代码
九、#undef
#undef 可以移除一个宏定义,如果现存的一个名字需要被重新定义,那么就使用它进行移除
#undef NAME
十、命令行定义
许多C的编译器提供了在命令行中定义符号的能力,用于启动编译过程
在这里我们可以调节数组的大小,或者循环次数的大小等
十一、条件编译
我们平常写代码的时候,我们不清楚所写的代码是否能够实现目标时,我们往往会对某一个某块进行调试,但有一些代码是专门用来调试时加上的,删了有些可惜,保留又碍事,这时我们就可以选择性的编译,使用条件编译指令
例子:
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);
#endif
}
return 0;
}
这里的printf函数用来检查赋值是否成功,#ifdef用来检查后边的指令是否被定义,如果被定义了,那么就进行编译,如果未被定义则编译,调试结束之后将#define语句注释掉就行了
常见的条件编译指令
//1.
#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
十二、头文件的包含
1、头文件被包含的方式
①本地文件包含
#include "filename"
查找策略:先在源文件所在目录下查找,如果未找到,就在标准位置查找,即库函数所在的位置,如果找不到就提示编译错误
②嵌套文件包含
我们知道,每一条代码就可能使用一块空间,如果我们在一个大的程序里边写代码时,我们可能多次包含同一个头文件,那么包含了几次,这条代码就编译几次,极大的影响效率,我们可以通过使用条件编译避免头文件的重复引入
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif
或
#pragma once
十三、其他预处理指令
c语言给我们很多预处理指令,我们工作的过程中可能会用到,大家自行查找学习
今日分享就到这里了~