对C语言有一定了解的同学,相信对预处理一定不会陌生。今天我们就来聊一聊一些预处理的相关知识。预处理是在编译之前对源文件进行简单加工的过程,主要是处理以#开头的命令,例如#include <stdio.h>、#define等。预处理是C语言的一个重要功能,在预处理阶段完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
~~~正文开始~~~
预定义符号
什么是预定义符号:预定义符号是由编译器预先设置好的特殊标识符,它们代表了特定的信息,如编译器版本、目标平台信息、编译选项等。在C语言中, 也设置了一些预定符号,可以直接使用。
//常见的C语言预定义符号
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
使用举例:
int main()
{
printf("file:%s line:%d\n", __FILE__, __LINE__);
//输出结果:file:C:\Users\test.c line:269
printf("date:%s time:%s\n", __DATE__, __TIME__);
//输出结果:date:Apr 2 2024 time:17:04:04
return 0;
}
#define
#define定义常量
//基本语法
#define name stuff
使用举例:
//最常见的定义方式
#define MAX 1000
//为register这个关键字,创建⼀个简短的名字。有点类似typedef
#define reg register
//⽤更形象的符号来替换⼀种实现(死循环)
#define do_forever for(;;)
//在写case语句的时候⾃动把 break写上。
#define CASE break;case
//如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__FILE__)
思考一下,为什么在define定义标识符的时候,后面不加分号呢?我们知道define定义的标识符在预处理阶段就会被替换,如果加上分号,就可能导致程序出错。比如:
#define MAX 10;
int main()
{
//替换之后:printf("%d\n", 10;);
//就会有语法错误
printf("%d\n", MAX);
return 0;
}
#define定义宏
#define 机制有⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏(define macro)。
//宏的申明⽅式:
#define name( parament-list ) stuff
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
使用举例:
#define MUL(x) x * x
int main()
{
printf("%d\n", MUL(5));//输出:25
return 0;
}
上面的代码看上去是不是非常完美?实际上存在了一个特别大的bug,请看下面一段代码:
#define MUL(x) x * x
int main()
{
printf("%d\n", MUL(5 + 1));
return 0;
}
这段代码的结果是多少呢?36?no no no,实际上上面的代码会被替换成:
printf("%d\n", 5 + 1 * 5 + 1);//结果为11
所以我们在使用宏的时候一定要注意,应该把上面代码修改为:
#define MUL(x) ((x) * (x))
这样就可以得到我们想要的结果了,所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
宏与函数的对比
宏通常被应用于执行简单的运算。
和函数相比宏的优势
比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。
#define MAX(a, b) ((a)>(b)?(a):(b))
优势有二:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等类型。宏是类型无关的。
和函数相比宏的劣势
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错
宏和函数的对比
命名约定
⼀般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的用个习惯是:
把宏名全部大写
函数名不要全部大写
但是也有例外,offsetof就是一个宏,但它却是全部小写
#undef
这条指令用于移除一个宏定义。
使用举例:
#define MAX 20
int main()
{
printf("%d\n", MAX);
#undef MAX //移除宏定义
//printf("%d\n", MAX); //error
//也可以再次定义宏
#define MIN 10
printf("%d\n",MIN);
return 0;
}
条件编译
在编译⼀个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。比如说:调试性的代码,辛辛苦苦写的删除可惜,保留又碍事,所以我们可以选择性的编译。
使用举例:
#define __DEBUG__
int main()
{
int arr[10] = { 0 };
for (int i = 0; i < 10; i++)
{
arr[i] = i;
#ifdef __DEBUG__ //若为真,则执行printf语句
printf("%d\n", arr[i]);//为了观察数组是否赋值成功。
#endif //__DEBUG__
}
return 0;
}
常见的条件编译指令
- 条件编译
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
//例:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
- 多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
- 判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
使用举例:
#define __DEBUG__
int main()
{
#if defined(__DEBUG__)
printf("haha\n");
#endif
#if !defined(__DEBUG__)
printf("haha\n");
#endif
//打印结果:haha
return 0;
}
- 嵌套指令
#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