本期介绍🍖
主要介绍:#define如何定义宏,宏替换的规则,为什么宏定义时不要吝啬我们的括号,为什么宏的参数不能带有副作用,宏和函数的区别。还讲解了预处理符号#和##,#undef指令,众多条件编译指令,以及文件包含的方式,与如何解决文件的重复包含👀。
文章目录
- 一、预定义符号🍖
- 二、#define🍖
- 2.1 #define 定义标识符🍖
- 2.2 #define 定义宏🍖
- 2.2.1 宏应尽量多用括号🍖
- 2.2.2 宏替换规则🍖
- 2.2.3 带副作用的宏参数🍖
- 2.2.4 宏与函数的对比🍖
- 2.2.5 命名约定🍖
- 三、预处理操作符#和##🍖
- 3.1 #的用法🍖
- 3.2 ##的用法🍖
- 3.3 嵌套使用宏🍖
- 四、#undef 指令🍖
- 五、命令行定义🍖
- 六、条件编译🍖
- 七、文件包含🍖
- 7.1 头文件被包含的方式🍖
- 7.2 重复头文件包含的解决办法🍖
一、预定义符号🍖
C语言在设计之初就预先定义了下面这些可以使用,但不能修改的宏。
- __FILE__:返回当前正在编译文件的文件名。
- __LINE__:返回当前的行号。
- __DATE__:返回文件编译日期。
- __TIME__:返回文件编译时间。
- __STDC__:如果编译器遵循ANSI C,其值为1,否则未定义。
下面就可以
二、#define🍖
2.1 #define 定义标识符🍖
大多数时候我会像这样#define MAX 100
来使用#define
,也就是定义一个数字常量。但如果你了解过#define
的替换逻辑,就会发现它的用法可不仅于此。就譬如:
- 定义字符串常量:
#define name "xiaoming"
- 简化关键字:
#define reg register
- 替换语句:
#define forever for(i = 0; i < 10; i++)
注意:如果#define
定义的内容过长可以换行继续写,但需要在存了最后一行外,每一行后面加一个反斜杠(续行符),如下所示。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ ,\
__DATE__,__TIME__ )
2.2 #define 定义宏🍖
#define
除了可以定义标识符之外,还有另外一种机制,可以像函数那样传递参数,这种实现被称为:宏或定义宏。
语法:
#define name( parament-list ) stuff
其中name
是宏名,parament-list
是参数列表,需要用逗号隔开,stuff
为宏体,也就是宏的内容。
注意:宏是把参数直接替换到宏内容中去的,与函数传递参数有本质的区别。函数在传递参数前会先把值求出来,然后在传递过去。可宏不是这样,参数是什么样,替换过去还是什么样。下面举个例子:
2.2.1 宏应尽量多用括号🍖
我们在定义宏的时候,应尽量不吝啬括号。为什么呢?要知道宏会把参数原封不动的替换到宏体中,这就导致了一问题的出现:如果参数中具有操作符,并且宏体中也有操作符,那么当参数替换完成后,可能就会因为操作符的优先级,而使得表达式的计算顺序达不到我们的预期。下面来举个例子:
可以见得由于习惯,我们通常会默认参数部分为一个整体,传参后也为一个整体。所以,就会导致上例的错误出现。那该怎么解决呢?既然你认为它是一个整体,那就把它真的变成一个整体。我们只需在定义宏时,给每个参数加上括号即可。如下所示:
既然想到参数与宏体会因为操作符的优先级而导致计算顺序出现偏差,那么也该联想到宏最终会替换到代码中原先调用宏的位置,如若被替换的宏原先就存在于一个表达式中,那么必然也会因为操作符优先级的关系而导致运算顺序的问题出现。所以我们因该把宏体看成一个整体,给它加上一个括号#define POWER(x) ((x) * (x))
。
2.2.2 宏替换规则🍖
- 调用宏时,首先会对参数部分进行检查,若参数部分包含#define定义的符号,则优先进行替换处理。
- 参数替换到文本中,随后插入到程序中原来调用宏的位置。
- 最后,在对结果文件进行扫描,看看是否包含任何#define定义的符号。如果是,就重复上述处理过程。
注意:1. 宏参数与定义中可以出现其他#define定义的符号,但对于宏不能出现递归。
2. 当预处理器搜索#define定义的符号时,字符串常量的内容是不会被搜索的。
2.2.3 带副作用的宏参数🍖
首先,我来解释一下什么叫带副作用。举个例子:我定义了两个参数int a = 1;int b = 0;
,想把a
的值+1后赋给b
,一般会写成b = a + 1;
,但还有一种写法b = a++;
。不可否认,这种写法确实可以达到我们目的,但会让变量a
自增1,可我们并不想让a
自增啊,所以我们就称这种写法为带副作用的。
了解什么是带副作用后,思考一下:如果宏的参数也带有副作用,会引起什么样的后果? 我们知道宏的参数会原封不动的替换到宏体中的,如若此时宏定义中多次包含参数,并且参数还带有副作用,就会重复多次产生副作用,从而导致不可预测的后果。所以我们在使用宏时应该尽量然参数不带有副作用。其实,函数也是可能带有副作用的,这一点需要注意。下面来举个例子:
2.2.4 宏与函数的对比🍖
我们可以通过上面这个例子来比较宏与函数。例子:分别用函数和宏来实现比较两个数的大小。下面是实现:
//宏的实现
#define MAX(x, y) ((x)>(y) ? (x):(y))
//函数的实现
int Max(int x, int y)
{
return (x > y ? x : y);
}
可以看出,两者在代码长度是差不多的。那对于这个例子来说,到底是使用宏好还是函数好呢?在我看来,使用宏比较好。为什么?
首先,使用宏绝对比使用函数的开销要小得多,我们知道函数的调用是非常复杂的,需要传参、压栈、开辟函数栈帧空间、计算、出栈等等一系列操作。反观宏,它只需执行行文本操作和小型的计算工作就行。所以,宏比函数在程序的规模和速度方面更胜一筹。
其次,函数具有严格的类型限定。就算两个函数内容完全一样,仅参数的类型不同,那也是两个完全不同的函数,无法合并成一个的。但宏就比较灵活,在这个例子当中,只要能通过>
符号进行比较的类型,都可以用这个宏。可见,宏比函数灵活,函数比宏严谨。
除此之外,宏还有几个缺点我们也是要知道的:
- 宏定义较长,会大幅增加程序的长度
因为每一次使用宏都会在代码中插入一份宏定义。如果宏定义较长,必然会大幅增长代码的长度。你想啊,如果宏定义了1000行代码,每一次使用该宏,都会向程序中插入一份1000行的代码,必然会严重增加程序的开销。但如果使用函数来定义这1000行的代码,程序中只需要存在一份代码,每次调用这个函数即可。可以看出,这是函数的一个有优点。 - 宏无法调试
我们知道C源代码需要经过预编译、编译、汇编、链接阶段后,才能生成可执行程序,真正的运行起来。调试其实是运行阶段干的事,而早在预编译阶段,宏就已经完成了替换。使得调试时内存中运行的是一套代码,而肉眼所见的又是另一套代码。无法调试,无法调试,不是不能进行调试,而是对于宏来说调试是无意义的,我们无法观测到在内存中真正执行的代码的。
一旦替换完成后的代码与我们所想的逻辑有所出入的话,你就会发现明明代码没什么问题,但调试的结果却不尽人意,这时你就会开始怀疑是不是编译器出bug了啊。我想告诉你的时:想多啦同学。当然函数是可以进行调试的,这是函数的一个优点。 - 宏可能会带来运算符优先级的问题,导致程容易出现错
这个之前讲过,我们需要在宏定义的时候尽量不吝啬括号。
宏有时还可以做到函数无法做到的事,比如:宏的参数可以是类型。举个例子:
#define MALLOC(num, type) (type*)malloc(num * sizeof(type))
int main()
{
//开辟一块4个int型大小的动态内存空间
int* p = MALLOC(4, int);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
return 0;
}
2.2.5 命名约定🍖
一般来说,函数与宏使用的语法很相似,故语言本身无法帮我们区分二者,所以我们一般的使用习惯是:宏名全部大写,函数名不全部大写。
三、预处理操作符#和##🍖
3.1 #的用法🍖
在宏参数前面使用
#
号,则此参数会变成字符串
当我们想要同时打印变量的值与变量名,在只传一个参数的前提下,函数是无法实现的。但宏却可以,我们只需在传过去的参数加上一个#
号就行。如下所示:
3.2 ##的用法🍖
##
可以把位于其两端的符号合成一个符号
3.3 嵌套使用宏🍖
如果一个宏的参数是另一个宏的话,由于宏的替换规则,会先对参数中#define定义的符号进行替换操作。但凡事都有例外,在使用了#
或##
的宏中,如果其参数为另外一个宏,则会阻止另这个宏的展开。如下所示:
为了保证宏的参数优先展开,我们可以使得带有#
或##
的宏多嵌套一层宏定义,这样参数就可以优先展开了。如下所示:
四、#undef 指令🍖
#undef
指令用于移除一个宏定义
#define MAX(x, y) ((x)>(y)?(x):(y))
int main()
{
#undef MAX
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define MAX 30
printf("%d\n", MAX);
return 0;
}
五、命令行定义🍖
许C的编译器提供了一种能力,可以在命令行定义变量。这样就可以使得同一份源文件编译出不同的版本,这样我们就可以根据不同的环境进行适配了。就譬如说:假如程序中定义了一个数组,长度为未定义的标识符。我们就可以使得这个程序,根据不同的不同的设备进行适配。如果这太机子内存比较小,我们就可以使得定义的数组长度更小,反之则数组长度可以更大一点。
六、条件编译🍖
条件编译是指预处理器可以根据条件编译指令,选取源程序中我们想要进行编译的代码进行编译。这样就能使得我们的程序,根据各种不同的情况来进行适配。如此可以看出,条件编译指令能够解决跨平台性问题。
条件编译指令 | 说明 |
---|---|
#if | 如果条件为真,则执行下面的操作 |
#elif | 如果前面的条件为假,而该条件为真,则执行下面的操作 |
#else | 如果前面的条件为假,则执行下面的操作 |
#endif | 用于结束条件编译指令 |
#ifdef | 如果宏已经定义,则执行下面的操作 |
#endif | 如果宏没有定义,则执行下面的操作 |
#if defined( ) | 如果宏已经定义,则执行下面的操作 |
#if !defined( ) | 如果宏没有定义,则执行下面的操作 |
七、文件包含🍖
7.1 头文件被包含的方式🍖
包含头文件的方式有两种#include<stdio.h>
和#include"Add.h"
,一个是用尖括号<>
来包含的,另一个是用双引号""
来包含的。两者的区别在于查找的策略不同:
<>
会直接去编译器的库目录底下查找""
会先去代码所在路径底下去查找,如果没找到,再去库目录底下去查找。
所以如果包含的是库函数,直接用<>
就行了,而包含我们自己写的头文件,用""
来包含。
7.2 重复头文件包含的解决办法🍖
一个团队在开发一款软件的时候,每一个成员都有自己所需负责的模块。实现各自的模块时,必然会引用团队的公共库函数。当最后合并所有模块后,必然多次重复包含公共库函数,使得代码大量冗余。
那怎么解决这个问题呢?其实可以通过条件编译指令来实现。如下所示:
方法一:
#ifndef __TEST_H__//如果没有定义过__TEST_H__,则执行下面这些
#define __TEST_H__//定义__TEST_H__
//……头文件内容
#endif
方法二:
# pragma once
//在头文件开头加上一句这个就不会被重复包含了
这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。