#预处理和函数的对比以及条件编译
预处理详解
- 预定义符号
- #define
- #define定义宏
- #和##
- 宏和函数的对比
- #undef和条件编译
前言:本来不打算写着一篇博客的,总觉得这一部分的知识以后用的话直接再来查一下就好了,没有看的太重,但是后来又想了想,初学者的路上还是要养成善于总结和归纳的习惯比较好,因此写下了这一关于预处理的总结博客,将预处理和函数做了一个对比(举例说明),此后还介绍了条件编译的相关内容,希望能对初学者有一定的帮助
预定义符号
大家一提到预处理符号可能第一时间想到的就是#define关键字,在介绍#define的内容之前我们先来介绍一下C语言内置的预定义符号作为某些同学的扩展。
FILE //进行编译的源文件
LINE //文件当前的行号
DATE //文件被编译的日期
TIME //文件被编译的时间
STDC //如果编译器遵循ANSI C,其值为1,否则未定义
既然是C语言内置的,那么就是可以直接使用的,接下来我给大家写段代码演示一下,大家跟着注释走一遍这里就可以过去了,没什么要说的。
#include <stdio.h>
int main()
{
printf("file name:%s\nline:%d\n", __FILE__, __LINE__);
//注意这里的时间是文件编译的时间,不是文件运行的时间
printf("TIME:%s\n", __TIME__);
//__STDC__//VS并不是完全支持ACSI C的,所以这里是未定义
printf("holle world!\n");
return 0;
}
#define
#define在我们日常写代码时候可能用的还是比较少的,但是我在翻看一些书籍的时候,发现大佬们经常使用#define来表示某种状态或者用来提高代码的逻辑可读性,这一点还是需要我们借鉴和学习的。下面我们就来介绍#define和#define有关的一系列知识点。
语法:
#define MAX 100
#define FOR for( ; ; )
#define DOU double //为doube类型重新命名
#define MAX 100
#define FOR for (; ; ) //死循环
#define DOU double //为doube类型重新命名
int main()
{
int a = MAX;
DOU f = 3.14;
printf("%d %lf\n", a, f);
return 0;
}
这是#define最简单的一种基本用法,很简单,但是仍然有需要我们注意的点。
#define的最后我们一般是不用加上 ; 的,这可能会造成我们在使用时的预防错误
关于#define的功能还有很多,并不只有上面简单的用法。
#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
注意:
( parament-list )与name之间是没有空格的,与stuff之间可以存在一个或者多个空格。
比如如下的宏定义:
这是不是和我们的函数有一些相似呢,但是函数和宏哪个更好呢?我们之后再给大家介绍宏和函数对比的相关内。
大家再是思考一个问题,如果我在这里传入的是以下的内容呢?
SUM(1+1, 2+2);
此时ret的结果是什么呢?
有同学可能认为简单嘛,1+1=2 2+2 = 4 24 = 8,结果ret=8嘛。
最终的结果却是5!!
其实是这样的,宏在传参的时候,并不会像函数那样,会先对实参表达式进行运算,再传给形参,这里传过去的是1+1和2+2,那么此时的SUM就是1+12+2
即1+2+2 = 5。
我们只需要在宏体部分做一些修改就好了。
#define SUM(a, b) (a)*(b)
这样就能达到我们想要的结果了。
所以
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中
的操作符或邻近操作符之间不可预料的相互作用。
宏的替换规则:
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
我们以下面的代码为例,解释一下上面的规则:
#define MAX 10
#define PRI printf("%d\n", MAX)
int main()
{
PRI;
return 0;
}
在程序预编译阶段会扫描宏体中是否含有#define定义的符号,如果有,会先将宏体中符号发生替换,即:
#define PRI printf(“%d\n”, 10)
一个源文件在变成一个.exe的可执行文件时,会经历几个阶段,第一个就是预编译阶段,然后就是编译,汇编,链接这几几个过程,大家对这一方面感兴趣的话可以去查一下相关的优质博客,了解一下。
#和##
了解完#define和宏之后我们接着就来介绍一下#和##的作用。
首先我们看看这样的代码:
char* p = “hello ““bit\n”;
printf(“hello”” bit\n”);
printf(“%s”, p);
这里的输出是hello bit
我们发现字符串是有自动连接的特点的!!
在知道这个前提条件之后我们来看下段代码
我们发现将#将FORMAT转换成了一个字符串,上面的代码就相当于
printf(“the value is ““FORMAT””\n”, VALUE)
所以#可以把一个宏参数变成对应的字符串
接下来我们再来看一下##的作用,同样还是以一段代码为例
我们发现##有以下特性
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符
关于##的实际运行可能不多,但是这写知识点我们还是要知道的,正所谓“存在即合理”。
宏和函数的对比
学习完有关#define定义符号和宏的有关知识点后,我们发现在有时候函数能做到的事情,宏同样也可以做的到,那么宏和函数对比它们各自的优缺点是什么呢?什么时候用宏什么时候使用函数呢?
下面我们就来一一介绍。
我们这里分别给出使用宏和函数实现取两个数中的较大值的代码。
显然这两者都能实现相应的功能,那么如果让你选择,你会选择那一种方法呢?
要想看出两者的区别,我们直接观察是看不出来的,我们通过反汇编的角度再来看一下这段代码的情况
main函数内部
Max函数内部
我们可以很清楚的看到如果用函数的方式来实现这一小功能的话,远不如使用宏来的更简单一些(反汇编代码越多代表着消耗的时间越长)。
所以我们在实现的一些简单的功能时,比如比较大小,或则取较大值可以使用宏来代替函数,会提升我们代码的运行效率。
还有一点就是大家有没有注意到,宏在比较大小的时候,是不需要指定格式的,它并不像函数那样,需要指定是int类型还是float类型,这一点还是比较好的。
那是不是宏就是比函数要好呢?
当然不是。
我们知道#define定义的宏是在预编译阶段对文本中的内容进行替换的
#define STR(a, b) #a#b
int main()
{
char* p = STR(holle, world);
//预编译之后的结果:char* p = "holle""world";
printf("%s\n", p);
return 0;
}
那么假设我们的宏体很长,有几十行代码,那么我们在预编译的阶段就会多出很多行的代码,会占用较多的内存,并不好。
再者,宏能实现的功能是很有限的,它并不像函数那样灵活,可以处理一些错误情况和复杂功能。
所以综合各种情况,给出一下表格
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的 |
调式 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
因为宏在预编译阶段就已经完替换,而调试代码是在完成链接之后的,所以#define定义的符号和宏是无法调试管查的。
#undef和条件编译
最后是关于#undef和条件编译的介绍,两者基本上都是成对出先的。
先来介绍#undef的用法。
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
假设我们想要取消对NAME的定义,就可以这样做
条件编译,从字面意思上也就是满足条件就进行编译,不满足就不进行编译
比如说
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
#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 //__DEBUG__
}
return 0;
}
如果__BEBUG被定义了,那么就编译printf(“%d\n”, arr[i]);,反之不进行编译。
条件编译的方式有很多种
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
大家多做做练习这些内容就能很好的掌握住了。