C语言 - 预处理详解(一)#预定义符号 ##define #undef


前言

路漫漫其修远兮,吾将上下而求索;


一、预定义符号

在C语言本身便预定义了一些符号,这些符号是可以直接使用的;

__FILE__       //进行编译的源文件(文件名:路径+主干名+后缀) %s

__LINE__       //文件当前的行号  %d

__DATE__      //文件被编译的日期  %s

__TIME__       //文件被编译的时间  %s

__STDC__      //如果编译器遵循 ANSI C,其值为1,否则未定义 %d

注:这些预定义符号在预处理阶段C语言本就定义好了的(这些预定义符号均是语言内置),是可以直接使用;当然,预定义符号出了上述列出来的几个,还有其他的,只不过这几个最常使用;

这些预定义符号有什么用呢?

例如,下面的代码便是在屏幕上打印出了目标数字:

但是,“打印”究竟是在文件的那个地方、其代码在哪一行、什么时候进行打印的呢?我们是否能记录一下呢?此时便可以用到 __FILE__、__LINE__、__DATE__、__TIME__使用如下:

__STDC__: 如果编译器遵循ANCI C,其返回值为1,否则未定义;故,倘若你想要知道VS编译器是否遵循ANSI C标准便可以用__STDC__ 来测试一下;此预定义符号的具体使用如下:

注:Linux 环境下的 gcc 编译器是遵循ANSI C标准的,这也就是为什么有些语法在测试的时候,在VS下的结果会与在gcc 编译器下的结果不同,当出现这一情况的时候,要以 gcc 编译器的结果为准,因为gcc 编译器才是严格符合ANSI C标准的编译器;

看了上述的注解,你可能会问:DevC++呢?

  • DevC++ 对于标准的支持不严谨,故而你会发现倘若在DevC++ 中写的代码是非常随意的,体现在:你写的语法很糟糕但是该编译器识别不出错误;所以在OJ网站上,一般就是要么使用gcc ,要么使用 clang (苹果公司所维护的编译器);

想必你会有疑问,这些预定义符号有什么用呢有什么用呢?

  • 显然,当我们想知道当前代码在哪个文件哪一行什么时间运行的时候,便可以利用这些预定于符号,故而也不会对获取其行号而发愁;

未来在哪里可以用到这些预定于符号?

  • 记录日志(即将这些信息写入文件之中)

二、#define 

(一)、#define 定义的标识符

语法: #define name stuff

使用如下:

#define MAX 1000     //定义了一个标识符常量MAX

#define reg register     //为register 这个关键字创建一个简短的名字 reg

#define do_forever for(;;)     //用更加形象的符号来替代一种实现(甚至可以是一段代码)

#define CASE break;case    //在写case 语句的时候自动把break 写上

#define DEBUG_PRINT printf("file: %s line=%d  \

                                               date:%s time:%s \n",                                                                                                                  __FILE__,__LINE__,  \

                                                 __DATE__,__TIME__)

注:如果定义的stuff 过长,可以分成几段,除了最后一行外可以在每一行的后面都添一个反斜杠(续行符)并且在此续行符后面不可以再添加其他的东西;

续行符的作用?

  • 相当于转义了回车,让回车不再是回车;如果在续行符后面添加了一个空格,那么此续行符转义的便不再是回车,而是其后的空格 --> 没有转义回车而将一条语句分成了多段--> 报错;

#define 定义的标识符究竟是如何操作的?

  • #define 定义的标识符是在预处理阶段被替换掉,同时会删除该符号;

注:在 test.i 文件中不难发现在我们编写的代码前面有很多行代码,显然这是<stdio.h> 中的文件,故而不要频繁多次地包含头文件

在#define 定义的标识符后面可不可以添加 ?

例如这样:

#define DEBUG_PRINT printf("file: %s line=%d  \

                                               date:%s time:%s \n",                                                                                                                  __FILE__,__LINE__,  \

                                                 __DATE__,__TIME__);

#define MAX 100;

这样写是不推荐的,因为容易写出 bug ;

为什么?

纵使 #define 定义的是一条语句,也不要加 , 因为当你在调用此条语句的时候还会在其后面加 ; (利于代码的可读性)

(二)、#define 定义的宏

#define 机制包括了一个规定,允许把参数替换到文本之中,这种实现通常称为宏(macro)或者定义宏(define macro)

宏的申明方式: #define name(parament-list)  stuff

注:

  • 其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中;
  • 参数左括号必须与name 紧挨;倘若参数左括号与name 之间有任何的空白存在,均会将该参数列表被解释为stuff 的一部分;

使用如下:

#define  SQUARE(X)  X*X

#define 定义的宏与#define 定义的标识符有什么区别:

  • #define 定义的宏是有参数的,而#define 定义的标识符没有参数;

宏的使用如下:

#define 定义的宏仍然是替换;

正式由于宏的本质是替换,那么就极容易出现操作符优先级先后的问题,上述代码存在的问题如下图所示:

特别注意,宏的参数不是计算传入宏体的,而是替换进行的;故而,倘若没有使用 () 来保证宏体中的操作符的优先级,而宏参数不一定只是单单的一个数字,如若也是表达式,那么极易出现操作符优先级带来实际的计算顺序与预期的计算顺序不相符的情况

所以此处就得将宏中的参数当作一个整体,修改如下:

同理,既然宏参数与宏体之间存在操作符的优先级关系,那么宏也不单单是单独使用,即宏体与其外面的数字可能也会存在操作优先级的问题,例如:

所以也要将宏体当作一个整体

核心在于,宏的本质是替换,要考虑到宏参数、宏体以及宏参与计算时与其周围操作符的优先级关系,所以就要利用括号将宏参数与宏体括起来,以保证其计算的顺序;

(三)、#define 替换规则

在程序中扩展 #define 定义的符号和宏时,需要涉及以下三个步骤:

  • 在调用宏时,首先要对其参数进行检查,看看是否包含任何由 #define 定义的符号,如果是,它们首先被替换(首先替换#define 定义的符号)
  • 所要替换的宏体会被插入到程序中原本使用宏的位置,而对于宏、参数名被宏体所替换;(然后替换#define 定义的宏)
  • 再次对结果文件进行扫描,看看它是否包含了任何对 #define 定义的符号,如果是便就重复上述操作;(最后再次检查)

上述步骤图解如下:

注:

1、宏参数和 #define 定义中可以出现其他 #define 定义的符号。但是对于宏,不能出现递归;

2、当预处理器搜索 #define 定义的符号的时候,字符串常量的中内容并不会被搜索

为什么宏不可以递归?

  • 因为宏是完成替换的,它与函数不一样;并不是因为在宏中不能写递归的原因是在于其没有限制条件,而是因为在语法上硬性不支持宏写递归

(四)、# 和 ##

1、 # 的作用

在讲述 # 的作用之前,我们先来了解一下字符串的特性

在C语言中,如若你想打印 "hello world" 可以这样写:,当然你还可以这样写: --> 实际上是在函数printf 中放了两个字符串,但最终会合并为一个字符串,字符串具有自动连接的特点

基于此原理,我们便产生了一个想法:

经过思考,你会发现,此处只能封装成宏,而非函数;

为什么不能封装成函数呢?

  • 因为如若你要封装成一个函数,那么此函数的内部功能要统一才行,而上图中是针对不同的变量而输出不同的对象名称,所以此处不能用函数;

既然如此,变量名如何传入?换句话说,如何将参数插入字符串中?

可能你首先会想到这么写:,但实际上认真思考会发现,N放在字符串中,而宏无法在字符串中被预处理器搜索,故而字符串中的N并不会替换,例子如下:

此时的字符串中的N成了一个普普通通的字符;

联系到,前面将两条字符串放在printf 函数中但最后合并成了一个字符串,你可能会说将"the value of" 与"is %d\n" 分为两条字符串,然后中间放宏参数,具体实现如下:

毫无疑问,这也是不行的,因为只有字符串相邻放在一起才能合成为一条字符串,N 单独放在两字符串中间肯定会出错况且函数printf 也不允许这么操作;

此时便会用到 #

#  --> 将一个宏的参数变成对应的字符串

那么此时 #N 便已然是个字符串,便就可以和字符串相邻放在一起而合并成一条字符串;

使用如下:

还可以利用宏来处理打印不同类型数据的问题;因为不同类型的数据对应着不同的占位符,所以此时的宏会有两个参数:变量以及此数据所对应的占位符;使用如下:

2、## 的作用

## 可以把位于它两边的符号合并成一个符号,并且允许宏定义从分离的文本片段创建标识符

#define ADD_TO_SUM(num , value)  sum##num+=value

ADD_TO_SUM (10,20);  -->  sum10+=20; 即让 sum10 增加20;

注:这样的连接必须产生的是一个合法的标识符否则其结果就是未定义的

注:在预处理之中的 ## 可以将两个符号合并成一个符号;并且允许宏定义从分离的文本片段创建标识符;

## 的使用(在实际的写代码中,## 用得非常少):

当我们利用#define 定义了一个函数,

注:上述代码相当于,利用宏写出了一个“函数模具”,函数的参数具有特定类型,与宏相结合便可以保留函数的大体框架,而利于替换函数的参数类型、返回类型,并利用## 实现函数名的“定制”;

(五)、带副作用的宏参数

宏参数在宏的定义(宏体)中出现超过一次的时候,如若参数带有副作用,那么你在使用这个宏的时候就可能出现危险,而导致不可预测的后果。副作用就是表达式求值的时候出现有永久性效果

什么叫做副作用?

  • 在现实生活中,以生病为例子,倘若一个人生病了,医生给他开的药治疗他的病的同时可能会给他带来副作用(副作用即是产生不良的反应);在代码之中,便体现为,我"帮助"了别人,结果改变了自己,例如 int a  = 2 ; int b = ++a ;  --> 此处的b 确实能得到值3,但是在这过程中 a 的值变了;于是乎此式便带有副作用;存在两个作用:为b 赋值、 更改a 的值;其中更改a 的值便为副作用;

对于宏而言,如下图:

显然,当宏参数有副作用的时候,所得到的结果脱离了我们设计该宏的目的

上述的问题是什么出来的呢?

我们先来回顾一下三目操作符(条件操作符),计算规则:从左到右依次计算, 其具体实现细节如下图所示:

分析上述代码计算的过程:

从上述代码中,你可以发现,像 a++ , b++ 这种带有副作用的宏参数,并不是单单地只执行一次,当 (a++ > b++ )为真的时候,便会让 a++ 执行两次,而当 (a++ > b++ )为假的时候,便会让 b++ 执行两次;这样的代码时十分危险的,因为其结果难以预测;

(六)、宏和函数的对比

宏能完成的任务同样函数也可以,二者究竟有何区别?

例,就上图中求取最大值而言,宏与函数哪个更好?

于此例中,从参数类型的角度来看,宏没有参数类型的检查,可适用于很多的类型,故而显得非常灵活;而函数对参数类型的要求很严格

从执行速度来看,相较于函数,宏的速度更快。为什么呢?

因为宏的本质是替换,还是以上述例子为例;

上述代码利用宏而比函数好的原因:

  • 1、用于调用函数(传参、函数栈帧的开辟)和从函数返回(函数可能会返回数据)的代码可能比实际执行这个小型计算工作所需要的时间长,所以宏比函数在程序的规模和速度方面更胜一筹;
  • 2、函数的参数必须声明为特定的类型宏的参数与类型无关;函数只能在类型合适的表达式上使用,而宏适用于可用于计算该式的所有类型;

但是宏也不是万能的,他也存在缺点:

  • 每次使用宏的时候,一份宏定义的代码插入到,倘若其代码很长,而又多次使用到该宏,那么便会大幅度地增加代码的长度;
  • 宏是不可以进行调试的;
  • 宏的参数与类型无关,显得不严谨
  • 宏可能会带来运算符优先级的问题,而导致程序容易出错;
  • 不可以递归

注:宏的处理是在预处理阶段进行的,而调试调试的是编译、链接产生的可执行程序;

看了上文,你可能会有疑问,宏能实现的,函数也能实现,那么有没有宏能实而函数不能实现的情况呢?

  • 总所周知,函数的参数不能单单是类型,但是介于宏的本质实现是替换,所以宏参数可以是类型;

在前面学习动态开辟的时候,是否有这样的感觉,例如利用malloc 开辟空间,那么就得计算所要开辟的空间的字节数,针对不同类型的数据、存放此数据的个数来计算开辟的空间的大小,其计算过程便容易出现问题,为了减少bug 的出现,那么此时便可以利用宏来实现,代码如下:

#include<stdio.h>

#define MALLOC(num,type) (type *)malloc((num)*sizeof(type))

int main()
{
	short* p1 = MALLOC(10,short);
	if (p1 == NULL)
	{
		perror("malloc short");
		return;
	}
	int* p2 = MALLOC(20, int);
	if (p2 == NULL)
	{
		perror("malloc int");
		return;
	}
	
	return 0;
}

宏与函数的对比:

属性#define 定义宏函数
代码长度每次使用宏的时候,宏代码都会被插入到程序中;倘若宏的代码行很多,多次使用该宏便会使得该程序的代码行大幅度增长函数具体实现的代码只会出现在一个地方;每次使用这个函数的时候,都会那个地方的函数;不会因多次调用函数而大幅度地增加代码行
执行速度宏的本质是替换,且在预处理阶段完成的,只有执行该代码的时间开销,故而其执行速度会更快存在函数的调用返回结果时间上的开销,故而相对来说会慢一些
操作符的优先级宏的本质是替换;宏参数求值是在所有周围表达式的上下文环境里,故而其邻近操作符的优先级可能会影响宏体中的实现,进而导致实际结果会与预期结果不相符的情况;所以在写宏的时候,要为宏参数宏体加上括号,以保证其计算的顺序;函数参数只在函数调用的时候求值一次,它的结果值传递给函数;表达式求值的结果更容易预测;
带有副作用的参数宏参数可能会被替换到宏体中的多个位置,倘若此宏参数带有副作用,那么便会进行多次计算,从而导致所得到的结果会偏离使用的目的,即产生不可预测的结果;函数传参只在传参的时候求值一次,即无论是传址还是传值,本质均是函数针对所传递过来的数据进行操作,所以结果更容易控制;
参数类型宏的参数与类型无关,只要对参数的操作合法,那么此宏便可以适用于任何类型的参数。函数的参数是与类型有关的,如果参数的类型不同,就需要不同的参数,即使他们执行的任务是相同的;
调试宏不能调试函数可以逐语句进行调试
递归宏不能递归函数可以递归

怎么判断在一个条件下该使用宏还是使用函数呢?

  • 如果说该代码足够简单,那我们便可利用宏来写;倘若该代码写出来很复杂、行数多并且容易出错,介于宏不能进行调试,便无法观察执行的细节,所以我们便可以使用函数来解决问题;(注:当然,使用c++ 可以不用纠结于到底使用宏还是函数了; 因为在 c++ 之中提供了概关键字 inline(内联函数)inline 具有了函数的优点和宏的优点;)

(七)、命名约定

如此看来函数与宏在使用的语法上很相似,既然无法利用语言来帮我们区分二者,那么有什么办法能帮助我们区分二者呢?

  • 将宏名全部大写
  • 函数名不全部大写

注:但是也不要以为全是小写的一定是函数,例如: offsetof 全是小写,咋一看以为是函数,其实offsetof 本质上是; 函数getchar 中有部分实现可能利用了宏;

此处只是个约定,总有人不按照套路来走,所以“全大写是宏” 这种判断是宏还是函数的方法只是一种参考,具体靠谱地判断还是得结合代码;

三、#undef

#define 可以用来定义标识符,也可以用来定义宏,那么其定义能否被取消呢?

  • 利用#undef 便可以实现

#undef   用于移除一个宏定义


总结

1、在C语言本身便预定义了一些符号,这些符号是可以直接使用的;

__FILE__       //进行编译的源文件(文件名:路径+主干名+后缀) %s

__LINE__       //文件当前的行号  %d

__DATE__      //文件被编译的日期  %s

__TIME__       //文件被编译的时间  %s

__STDC__      //如果编译器遵循 ANSI C,其值为1,否则未定义 %d

2、#define 定义的标识符

语法: #define name stuff

3、#define 机制包括了一个规定,允许把参数替换到文本之中,这种实现通常称为宏(macro)或者定义宏(define macro)

宏的申明方式: #define name(parament-list)  stuff

注:

  • 其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中;
  • 参数左括号必须与name 紧挨;倘若参数左括号与name 之间有任何的空白存在,均会将该参数列表被解释为stuff 的一部分;

4、#  --> 将一个宏的参数变成对应的字符串

5、## 可以把位于它两边的符号合并成一个符号,并且允许宏定义从分离的文本片段创建标识符

6、#undef   用于移除一个宏定义

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值