【C语言笔记】带参宏定义(二)

2019-06-26 16:42:27 浏览数 (1)

带参宏在我们的嵌入式编程中使用得非常多,其定义如下:

代码语言:javascript复制
define 标识符(参数列表) 字符序列

其中参数列表中的参数之间用逗号分隔,字符序列中应包含参数表中的参数。在定义带参数的宏时,宏名标识符与左圆括号之间不允许有空白符,应紧接在一起,否则变成了无参数的宏定义。

并且,字符序列与其每一个参数必须用括号扩起来,否则该宏定义可能会产生二义性 。下面举个简单的例子,定义一个求平方的宏函数:

代码语言: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;
}

输出结果如下:

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));

这个例子是宏定义的经典应用之一,完成了函数不能完成的功能,但是宏定义也不能滥用,通常,如果相同的代码需要出现在程序的几个地方,更好的方法是把它实现为一个函数。

宏的缺点

  • 宏可能会大幅度增加程序的长度。
  • 宏是没法调试的。
  • 宏由于与类型无关,也就不够严谨。
  • 宏可能会带来运算符的优先级问题,即二义性。

以上就是关于带参宏的一些总结,如有错误,欢迎指出!

0 人点赞