函数式宏 vs. 内联函数






3.07/5 (17投票s)
2004年9月25日
8分钟阅读

172270
本文旨在强调 C++ 面向对象领域中这两种特殊特性的一些优缺点。
引言
“计算机能否思考的问题,就像潜艇能否游泳的问题一样。”——艾兹赫·迪杰斯特拉
对我们软件开发者而言,计算机,或者说编译器,并不能完全自主思考。因此,我们有责任为编译器提供最佳信息,以便从中获得最佳结果。一个我们经常出错的常见例子就是使用**函数式宏**和**内联函数**。本文旨在强调 C++ 面向对象领域中这两种特殊特性的一些优缺点。
尽管我们大多数人对函数式宏和内联函数是什么非常了解,但还是让我们回顾一下,以巩固我们的想法。
函数式宏
宏最常用于为程序中反复出现的常量定义名称。定义这些常量更偏好的方法是使用关键字 const
。由于当前重点不在于定义常量的最佳方法,因此我们在此略过。#define
指令功能强大,因此允许宏名称带有参数,从而表现得像函数一样。这种形式的宏被称为函数式宏。每次遇到带有参数的宏名称时,宏定义中的参数都会被替换为实际找到的参数。
例如
#include "iostream.h" #define MAX(a, b) ((a < b) ? b : a) int main( void) { cout << "Maximum of 10 and 20 is " << MAX(10, 20) << endl; return 0; }
Output:
Maximum of 10 and 20 is 20
编译程序时,“a
”和“b
”在宏定义中将被分别替换为 **10** 和 **20**。虽然这看起来很简单,但它有其自身的陷阱,我们将在后面的部分讨论。
内联函数
内联函数是 C++ 的重要特性,常与类一起使用。它们与普通函数无异,只是它们实际上从未被调用。顾名思义,这些函数在每次调用时都会被展开。要启用或使函数在调用点展开,只需在函数定义前加上 inline
关键字即可。
例如
#include "iostream.h" using namespace std; inline int max(int a, int b) { return a < b ? b : a; } int main() { cout << "Maximum of 10 and 20 is " << max(10, 20) << endl; return 0; }
Output:
Maximum of 10 and 20 is 20
编译器看到的上述代码将类似于下方
#include “iostream.h” using namespace std; int main() { cout << "Maximum of 10 and 20 is " << (10> 20 ? 10 : 20) << endl; return 0; }
这看起来与函数式宏非常相似,不是吗?等到我们深入探讨。
函数式宏 vs. 内联函数
宏通常用于定义常量,这些常量可以有效地替代 const 或非 const 变量。由于宏在编译时进行解释,因此它们具有不可替换的优势。
函数式宏带来的惊喜比人们想象的要多,必须非常小心。
当同一块代码需要执行无数次时,函数式宏非常有用。例如,假设需要将条目写入日志文件,函数式宏将大有帮助。
考虑以下示例
#include "iostream.h" #define LOG(X) WriteToFile((X), __FILE__, __LINE__ )
WriteToFile
是一个函数。现在,可以使用此宏将详细信息与消息、文件名和行号一起写入日志文件,如下所示:
int main() { LOG("Inside Main"); return 0; }
虽然不愿显得偏颇,但尽管函数式宏带来了许多优点,人们还是不得不对其有所警惕。函数式宏在被使用时总是会在使用点展开。函数式宏在编译时进行预处理,因此运行时实际上没有宏存在。这就是二进制文件大小增加的原因。
函数式宏的另一个主要缺点是缺乏类型检查,这导致高度的类型不安全性。传递给宏的参数永远不会被预处理。看起来完全无害,不是吗?
这可能会导致很多混乱。
考虑以下示例,它展示了缺乏类型检查的缺点。在此示例中,结果始终是作为第二个参数传入的值。
#include "iostream.h" #define MAX(a, b) ((a < b) ? b : a) int main( void) { cout << "Maximum of 10 and 20 is " << MAX("20", "10") << endl; return 0; }
Output:
Maximum of 10 and 20 is 10
当函数式宏与条件语句结合使用时,需要非常小心地处理。请看以下示例:
#include "iostream.h" #define MAX(a, b) \ if (a < b) \ cout << "Maximum is b:" << b << endl; int main( void) { if (true) MAX(20, 10) else cout << "Macro failed." << endl; return 0; }
Output:
Macro failed.
这总是会给出“Macro failed.”。这是因为编译器会按如下方式查看代码:
#include "iostream.h" int main( void) { if (true) if (20 < 10) cout << "Maximum is b:" << b << endl; else cout << "Macro failed." << endl; return 0; }
一个类似的可能篡改变动结果的组合是函数式宏中的括号。还记得用于说明宏用法的第一个示例吗?现在,当我们移除语句的封闭大括号时,宏的输出将完全失控。它将始终给出条件语句的结果。
#include "iostream.h" #define MAX(a, b) ((a < b) ? b : a) int main( void) { cout << "Maximum of 10 and 20 is " << MAX(10, 20) << endl; return 0; }
Output:
Maximum of 10 and 20 is 1
函数式宏需要额外处理具有副作用的参数。作为参数提供的表达式在进入函数式宏体之前可能不总是被求值。当增量或减量运算符作为参数传递时,其行为将如后续示例所示。
#include "iostream.h" #define MAX(a, b) cout << "The values are - a:" << a << " b:" << b << endl; int main( void) { int a = 10; int b = 10; MAX(a++, b++); return 0; }
Output:
The values are - a:10 b:10
函数式宏尽管存在不足,但仍有其自身的优点,不能完全称之为危险。MFC 中广泛使用函数式宏就是很好的例证。
内联函数相对于函数式宏的主要优势可能在于能够单步调试代码。这也许是导致开发人员偏向内联函数的唯一一个动机。
有效地使用内联函数可以提高性能,但天下没有免费的午餐。提高性能的代价是增加编译时间和可能的代码大小。内联只是对编译器的请求。当发出请求后,优化就取决于编译器了。然后编译器将决定优化方案。因此,使用内联函数时应关注成本和收益,因为它们与展开直接相关。
尽管内联函数被声明为内联,但它们并不总是被内联。不一定非要将函数声明为内联。在类主体内定义的任何函数的函数体都会被隐式视为内联。但这有一个前提。隐式处理取决于函数的复杂度和长度。还记得在内联函数方面谁说了算吗?内联只是一个请求,编译器才是拥有接受或拒绝内联请求权限的人。
在接下来的示例中,有两个函数,其中一个被显式声明为内联,而另一个则不是。在这种情况下,两者都被编译器视为内联,因为它们的定义都在类主体内。
class CInlinesExamples { public: void ImplicitInline() { cout << "This is an implicit inline function." << endl; } inline void ExplicitlyInline() { cout << "This is an explicit inline function." << endl; } };
当使用以下样式编写代码时,任何函数都必须显式声明为内联:
class CInlinesExamples { public: inline void InlineFunction(); }; void CInlinesExamples:: InlineFunction() { cout << " This is an explicit inline function." << endl; }
对于以下函数,inline
指令将完全无效:
- 递归
- long
- 包含循环
在类中,通常构造函数、复制构造函数、析构函数以及赋值运算符重载默认都是内联的。
内联函数总是处于边缘状态。它们可能好,也可能坏,甚至非常坏。让我们看看为什么。
内联函数可能会使进程变小和/或变快。编译器通常会生成大量低级代码来压栈和弹栈寄存器或参数。这通常比内联展开函数体所需的代码量还要多。这种情况会发生在所有函数上,无论其大小如何。优化器可以通过过程集成——即当优化器能够将大函数小型化时——来消除大量冗余代码。这种过程集成将消除一些不必要的指令,这可能会使整个过程运行得更快。
内联函数可能会使进程变大和/或变慢。包含大量内联函数的进程将导致代码尺寸增大。这可能会导致“颠簸”(thrashing)——计算机活动几乎没有进展,因为内存或其他资源耗尽或太少以至于无法执行所需的操作——在按需分页的虚拟内存系统中。换句话说,可执行文件尺寸的增加可能会导致更多的磁盘操作,因为系统最终会花费大部分时间从磁盘获取下一个代码块。
例如,如果一个系统有 100 个内联函数,每个函数展开成 100 字节的可执行代码,并在 100 个地方被调用,那么就会增加 1MB。这 1MB 会有问题吗?谁知道,但有可能,这最后的 1MB 会导致系统“颠簸”,从而减慢速度。
与此相反,内联函数在导致颠簸的同时也可能减少页面错误。工作集大小——即需要同时驻留在内存中的页面数量——即使可执行文件尺寸增加,也可能减小。
考虑以下示例
class CInlinesExamples { public: void OuterFunction() { cout << "This function is the caller function." << endl; } void InnerFunction() { cout << "This function is the called function." << endl; } };
当 InnerFunction
在 OuterFunction
中被调用时,代码通常在两个不同的页面上;当编译器将 InnerFunction
的代码过程化地集成到 OuterFunction
中时,代码通常在同一页面上。这将减少任何页面错误的可能性。
在当今的场景中,大多数开发的系统都不是 CPU 密集型的,而是无意中处于 I/O 密集型、数据库密集型或网络密集型。这意味着系统的整体性能更多地依赖于文件系统、数据库或网络,从而使它们成为瓶颈。这使得内联函数在不用于瓶颈时变得无关紧要。在这种情况下,它们可能对速度或性能没有任何影响。
如果说函数式宏是一堆惊喜,那么内联函数就是终极谜题。关于何时或为何使用它们,根本没有简单的答案。了解它们的最佳方法是动手实践,看看什么最适合。
终章
总而言之,哪一个更好,人们不得不持谨慎态度。尽管函数式宏和内联函数的用法都有很多优缺点,但事实是它们都以自己的方式发挥着作用。