宏定义函数VS普通函数VS内联函数
宏定义函数VS普通函数
- 宏定义函数
-
要点:变量都用括号括起来,防止出错,结尾不需要;。在实际编程中,不推荐把复杂的函数使用宏,不容易调试。多行用\
-
例子:
单行:
#define MAX(a, b) ((a) > (b) ? (a):(b))
多行:#define MALLOC(n, type) \ ((type *) malloc((n)* sizeof(type))
对于第一个函数,如果用普通函数,该怎样写?
int max(int a, int b) { return (a > b ? a : b); }
很显然,我们不会选择用函数来完成这个任务,原因有两个:
- 首先,函数调用会带来额外的开销,它需要开辟一片栈空间,记录返回地址,将形参压栈,从函数返回还要释放堆栈。这种开销会降低代码效率,而使用宏定义则在代码速度方面比函数更胜一筹;
- 其次,函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们如果要比较两个浮点型的大小,就不得不再写一个专门针对浮点型的比较函数。反之,上面的那个宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。
- 和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。
- 还有一些任务根本无法用函数实现,但是用宏定义却很好实现。比如参数类型没法作为参数传递给函数,但是可以把参数类型传递给带参的宏。比如上面的
malloc
的例子.
-
总结:
属性 | #define宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都被插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长。 | 函数代码只出现于一个地方:每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用、返回的额外开销 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能产生不可预料的结果。 | 函数参数只在函数调用时求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
参数求值 | 参数用于宏定义时,每次都将重新求值,由于多次求值,具有副作用的参数可能会产生不可预测的结果。 | 参数在函数调用前只求值一次,在函数中多次使用参数并不会导致多次求值过程,参数的副作用并不会造成任何特殊问题。 |
参数类型 | 宏与类型无关,只要参数的操作是合法的,它可以用于任何参数类型。 | 函数的参数是与类型有关系的,如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。 |
- 补充: 普通函数的调用堆栈情况.
比如:
P( ),Q( ),R( )在经过编译器变为汇编后, 都有自己的代码位置,比如void P(){ phase 1; Q(); phase 2; } void Q() { phase 3; R(); phase 4; }
而对应的栈帧呢:0x0000400A P: xxx #phase1 call Q xxx #phase2 ret 0x0000400B Q: yyy #phase3 call R yyy #phase4 0x0000400C R: zzz #phase zzz #phase
当R函数调用完毕,则R的栈帧出栈,同时将返回地址2pop,返回给rip,这样就继续执行一下条语句了.Q调用完毕也是同理的,所以这就是普通函数的调用情况.--- P的参数入栈 返回地址1(phase2的地址,即函数Q后的一下条语句) --- Q的参数入栈 返回地址2(phas4的地址,即函数R后的一下条语句) --- R的参数 ---
很显然,普通函数调用在汇编后,这个函数只有一份代码量,调用它就入栈出栈它的地址,造成一定的开销.而对于宏定义
来说,只是简单的文本替换,这在预处理时就替换过了,故实际上你可以认为就没有宏定义出来的函数
,所以当然不会有这样的开销.
普通函数VS内联函数 内联函数VS宏
例子:
inline void P() {
phase P1;
phase P2;
}
void Q() {
phase1;
P();
phase2;
}
P
是一个内联函数,对于Q来说编译后得到的汇编是什么样子呢?
Q:
xxx #phase 1
yyy #phase P1
zzz #phase P2
xxx #pahse 2
我们可以发现,Q的汇编代码中并没有对P的调用(call
),而是直接将P的代码拷到Q中,这样的好处就是由于没有call ret
,所以就没有普通函数调用带来的出栈入栈开销,速度更快,但增加代码量,增加内存开销
.
那么内联函数
和宏
又有哪些不同?
内联函数
本质上还是一个函数,只是在调用时不必call ret
罢了.
- 内联函数是一个真正的函数,遵循函数的类型和作用域规则,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确,这样就消除了它的隐患和局限性;
即:inline void func(int i,int j);
调用这个时候,我必须符合参数类型才能正确调用,而宏直接替换就完事了,是类型无关的. - 内联函数可以作为某个类的成员函数,这样就可以在其中使用所在类的保护成员及私有成员;
- 宏是不加任何验证的简单代码替换,除非万不得已,不要使用。
注意:
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间占比很小;如果代码执行时间很短,则内联函数就可以节省函数调用的时间。
- 含有递归调用的函数不能设置为inline;
- 使用了复杂流程控制语句:循环语句和switch语句,不能设置为inline;
- 由于inline增加体积的特性,所以建议inline函数内的代码应很短小。最好不超过5行;
- 内联函数应该在头文件中定义,关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。