前言
在C语言中,我们学习过宏的用法。宏通常被用于进行简单的文本替换来执行一系列的操作,比如一些简单的运算。使用宏可以避免函数调用时建立栈帧的开销,提高程序的性能。我们首先来写一个实现加法功能的宏:
cpp
#define ADD(x, y) ((x) + (y))
int main()
{
int a = 10;
int b = 20;
cout << ADD(10, 20) << endl;
return 0;
}
这个宏完美实现了我们的加法需求,但在定义宏时需要格外小心,因为宏可能存在潜在的问题,如副作用和作用域。为确保宏替换后的正确性,可能需要加上多个括号,这样就比较繁琐。而且宏无法进行类型检查,因为它只是完成替换功能。为了解决这些问题,C++引入了内联函数,提供了一种更安全、清晰且性能保持良好的替代方案。
概念
用 inline 修饰的函数叫做内联函数,编译时C++编译器会视情况在调用内联函数的地方展开,此时同样没有函数调用建立栈帧的开销,因此可以提升程序运行的效率。现在我们将上面定义的宏改写成内联函数:
cpp
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int a = 10;
int b = 20;
int ret1 = Add(10, 20);
return 0;
}
写成内联函数后,我们就不需要像定义宏一样注意括号的细节了。我们转到反汇编来看一下:
我们发现它并没有展开,而是和普通的函数调用一样,也建立了栈帧,因为这里依旧有 call 指令,原因是在 debug 模式下需要手动对编译器进行设置才能展开,而 release 模式下则不需要,但是 release 模式下无法调试,我们也无法看到展开的效果,因此接下来我们先设置下编译器。
右键单击箭头指向的这个位置。
如图所示,选择程序数据库这一选项。
内联函数扩展这里选择只适用于 _inline 这一选项,然后点击确定,这样就设置好了。这时候我们在调试一次,转到反汇编来看:
可以看到 call 指令已经没有了,函数确实是直接展开了。并且内联函数下我们依旧可以对程序进行调试。这里有一个小细节,mov 指令是先针对左操作数也就是10的,而对于普通的函数调用,在建立栈帧时先压入栈中的是右操作数,如果不清楚的话可以往上回顾一下没展开的那段汇编代码。我们再来看看宏的汇编代码:
显而易见宏是不一样的,因为它是直接进行替换的。宏替换后只有这样这样一句语句,显然无法进行调试。
特性
-
内联是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段就会用函数体替换函数调用。这样可以省去调用函数的开销,提高程序的运行效率。但是事物总是具有两面性的,内联函数也会存在缺陷,如可能会使目标文件变大,就算内联函数本身很短,但是在调用了很多次的情况下会有很多次的展开,总代码行数就会变长了。
-
内联对于编译器而言只是一个建议,是否采纳这个建议的决定权在于编译器。不同编译器关于内联的实现机制可能不同。一般建议将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现,一般10行左右是一个界限)、不是递归、且频繁调用的函数采用 inline 修饰,否则编译器会忽略内联建议。不可能把一个100行代码的函数当成内联对吧,否则多次调用的情况下目标文件岂不是非常的大。
-
建议不要将内联函数的声明和定义分离,这样可以避免其他源文件在使用该函数时发生链接错误。由于内联函数在调用时会直接被展开,其函数地址不会进入符号表,导致在链接阶段无法找到该函数的地址。因此,如果一定要将内联函数的声明和定义分开,那么该内联函数只能在包含其定义的源文件中使用。
潜在面试问题
宏的优缺点:
优点:
- 提高代码的复用性,通过简单的文本替换实现通用功能。
- 在一些情况下,宏能够提高程序的性能。
缺点:
- 宏不便于调试,因为宏在预编译阶段就进行了替换,因此难以在调试中观察。
- 可能导致代码可读性和可维护性下降,并且容易被误用。
- 缺乏类型安全的检查,可能导致错误的使用。
C++中用什么替代宏:
- 常量的定义可以使用 const 或 enum 来替代宏,提高类型安全和可读性。
- 短小函数的定义可以使用内联函数来替代宏,这样不仅保障了性能,还避免了宏可能引入的潜在问题,并提高了代码的可维护性。