前言
可以在前面的系列文章中看到,FreeRTOS 的 头文件中定义了大量的宏定义函数。单单从形式看宏定义的函数和普通函数并无太大的区别,但事实上两者还是有很大不同。
1 宏定义函数与普通函数不同
1.1 编译过程上不同
在编译时,对于宏定义函数而言,预编译时会将这些宏定义函数按展开的规则直接展开成语句,并且宏定义函数在代码中书写多少次,便展开多少次,拷贝相应的代码插入,生成相应的指令,而对于普通函数而言其只会生成一份相应的指令,调用处会生成传参指令和调用指令实现对函数的调用。
1.2 执行过程上不同
在实际执行过程中,宏定义式函数所有的语句都是普通语句执行,而普通函数由于需要调用的缘故,需要进行开辟栈空间、压栈、出栈等操作。
基于以上两点不同使得宏函数和普通函数在使用和设计上有很大差异。
2 批判分析
由于编译时,宏函数是按照宏定义展开规则进行展开的,这一点在提高它的灵活性的同时也会带来许多问题。例如它可以是一些函数的使用变得更为简单。
#define DEF_MALLOC(n, type) \
( (type *) malloc((n)* sizeof(type))) \
int *ptr; \
ptr = (int *) malloc( (5) * sizeof(int) ); \
ptr = DEF_MALLOC( 5, int ); \
显然宏定函数在书写上要比普通函数简洁美观的多。但宏展开的一大弊端是它不会对参数类型进行检查,也不符合函数调用的一些规则,而且在定义时要格外小心其展开的结果是否是我们想要的结果,往往需要加上 {} 或 () 加以限制,例如以下的代码:
#define MAX(x, y)
((x)>(y)?(x):(y))
int x = 1;
int y = 3
MAX(++x,--y);
以上代码的本意可能是比较 ++x 和–y 谁更大,但实际情况却是
((++x)>(--y)?(++x):(--y))
违背了代码原本想要表达的含义。同时宏定义函数实际上被多次展开这一特点使得其在相同代码量和相同插入次数下,编译出的代码量要比普通函数多得多。例如:
// 若函数fun()和DEF_FUN()代码完全一样则,以下普通函数代码编译结果为100byte
fun();
fun();
fun();
fun();
// 那么以下宏定义函数编译结果可能要接近400byte,是普通函数的4倍
DEF_FUN();
DEF_FUN();
DEF_FUN();
DEF_FUN();
另外,在执行过程中由于出入栈等操作开销,相同的普通函数要比宏定义函数的执行效率低。
在实际应用中到底使用宏定义函数还是普通函数需要根据实际需求而定,个人认为当出现小段代码需要多次调用时可以使用宏函数来提高效率,宏函数应该保持短小简洁。
3 内联函数
为结合普通函数定义更加安全,而宏函数较为高效的优点,有些编译器提供 inline 关键字,可以用来声明内连函数。
inline void fun()
{
//代码
}
inline 函数和宏函数一样,它会在代码书写处直接拷贝一份指令,不会像普通函数一样单独生成指令然后调用,这使得其相对普通函数而言其仍然是高效的。但与宏函数不同,它是在编译阶段展开到生成指令中的,而不是预编译阶段展开到代码中,什么时候调用内联函数,就会讲展开后的指令插入进去。内联函数也会进行参数类型的检查,符合函数的一般直觉,由于经常需要进行调用,其每次调用都会被重定义,所以经常在头文件中定义内联函数。
值得说明的是当内联函数具有外部的定义,其他文件只需要采用普通的函数声明,就可以调用它。然而,从别的文件调用函数,将不会被编译成内联函数。另外,在实际运行过程中 inline 函数是可以使用调试器调试的,而宏函数不行。并不是所有函数都适合作为内联函数,内联函数应该满足以下要求,否则编译器可能会忽略 inline 关键字将其作为普通函数处理。
1.不使用循环(for,while)。
2.不使用 switch。
3.不能进行递归调用。
4.代码应短小,不能过长。