65.9K
CodeProject 正在变化。 阅读更多。
Home

函数式宏 vs. 内联函数

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.07/5 (17投票s)

2004年9月25日

8分钟阅读

viewsIcon

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

InnerFunctionOuterFunction 中被调用时,代码通常在两个不同的页面上;当编译器将 InnerFunction 的代码过程化地集成到 OuterFunction 中时,代码通常在同一页面上。这将减少任何页面错误的可能性。

在当今的场景中,大多数开发的系统都不是 CPU 密集型的,而是无意中处于 I/O 密集型、数据库密集型或网络密集型。这意味着系统的整体性能更多地依赖于文件系统、数据库或网络,从而使它们成为瓶颈。这使得内联函数在不用于瓶颈时变得无关紧要。在这种情况下,它们可能对速度或性能没有任何影响。

如果说函数式宏是一堆惊喜,那么内联函数就是终极谜题。关于何时或为何使用它们,根本没有简单的答案。了解它们的最佳方法是动手实践,看看什么最适合。

终章

总而言之,哪一个更好,人们不得不持谨慎态度。尽管函数式宏和内联函数的用法都有很多优缺点,但事实是它们都以自己的方式发挥着作用。

© . All rights reserved.