带参宏在我们的嵌入式编程中使用得非常多,其定义如下:
代码语言:javascript复制define 标识符(参数列表) 字符序列
其中参数列表中的参数之间用逗号分隔,字符序列中应包含参数表中的参数。在定义带参数的宏时,宏名标识符与左圆括号之间不允许有空白符,应紧接在一起,否则变成了无参数的宏定义。
并且,字符序列与其每一个参数必须用括号扩起来,否则该宏定义可能会产生二义性
。下面举个简单的例子,定义一个求平方的宏函数:
#include <stdio.h>
#define SQUARE(a) a*a // 不严谨的写法
int main(void)
{
int x = 5;
int res = 0;
res = SQUARE(x 2);
printf("res = %dn", res);
return 0;
}
输出结果如下:
res
变量的输出结果为17,与我们期望的res = 49;
相差甚远!这就是因为我们不给字符序列中的宏参数加括号的原因,产生了歧义。程序生成可执行程序之前的预处理过程中把SQUARE(x 2)替换成了x 2*x 2
,因此当x=5
时res的结果为17。我们可以使用命令gcc -E hello.c -o hello.i
进行预处理,然后查看经过预处理得到的文件hello.i
的内容,hello.i
里的内容如下:
hello.i
里的内容与我们上面分析的一致!关于C程序的编译原理可查看往期笔记:【C语言笔记】C语言编译的过程关于windows系统下使用gcc编译器
的方法可参考往期笔记:【C语言笔记】使用notepad 、MinGW来开发C程序
以上程序严谨的求平方的宏函数的定义如下:
代码语言:javascript复制#include <stdio.h>
#define SQUARE(a) ((a)*(a)) // 严谨的做法
int main(void)
{
int x = 5;
int res = 0;
res = SQUARE(x 2);
printf("res = %dn", res);
return 0;
}
程序输出结果如下:
可见,这才是我们要的正确结果。
带参宏到底有多重要,看看TI的一些官方例程就知道,其把很多算法使用带参宏封装起来,用户就可以很方便的使用。
带参宏—— clarke变换算法:
带参宏—— PI调节器算法:
这些.h文件都使用宏来封装各种算法:
同样,ST官方固件库中也大量使用带参宏:
可见带参宏定义的重要性!除此之外,通过以上宏定义,可发现很多宏定义分行时,其行后都加上反斜杠进行分隔,这也是需要注意的细节。
带参宏与函数的区别
查看以上带参宏,我们发现带参宏似乎与函数似乎长得很像,它们之间有什么区别和联系呢?TI为什么要使用宏来对一些算法进行封装呢,难道使用函数来封装不可以吗?答案是可以的:
TI也说了,使用者可以很方便地把这些算法宏转换成一些函数。换句话说就是你可以使用宏定义,也可以使用函数。那么,什么时候封装成宏定义比较好,什么时候封装成函数比较好呢?
以下内容参考文章:
http://blog.sina.com.cn/s/blog_861912cd0100tc94.html
下面,先看一下带参宏与函数的一些区别,举个例子,比较两个数或者表达式大小:
(1)带参宏的方式:
代码语言:javascript复制#define MAX(a,b) ((a)>(b)?(a):(b))
(2)函数封装的方式:
代码语言:javascript复制int max(int a, int b)
{
return (a > b ? a : b);
}
很显然,我们不会选择用函数来完成这个任务,原因有两个:
(1)首先,函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码规模和速度方面都比函数更胜一筹;
(2)其次,函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。
除此之外,宏与函数的不同点还有:宏是在预处理阶段展开,占用的是编译时间,函数实在程序运行时调用的,占用的是程序运行的时间;宏参数没有类型说明,也没有返回值的概念。
和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。
看下面的例子:
代码语言:javascript复制#define MALLOC(n, type)
((type *) malloc((n)* sizeof(type)))
利用这个宏,我们就可以为任何类型分配一段我们指定的空间大小,并返回指向这段空间的指针。我们可以观察一下这个宏确切的工作过程:
代码语言:javascript复制int *ptr;
ptr = MALLOC(5, int);
将这宏展开以后的结果:
代码语言:javascript复制ptr = (int *)malloc((5) * sizeof(int));
这个例子是宏定义的经典应用之一,完成了函数不能完成的功能,但是宏定义也不能滥用,通常,如果相同的代码需要出现在程序的几个地方,更好的方法是把它实现为一个函数。
宏的缺点
- 宏可能会大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于与类型无关,也就不够严谨。
- 宏可能会带来运算符的优先级问题,即二义性。
以上就是关于带参宏的一些总结,如有错误,欢迎指出!