游戏开发工具

C++内联函数也可以用来代替宏

我们在《C语言入门教程》中讲到,宏是可以带参数的,它在形式上和函数非常相似。不过不像函数,宏仅仅是字符串替换,不是按值传递,所以在编写宏时要特别注意,一不小心可能就会踩坑。

使用宏的一个经典例子是求一个数的平方,如下所示:

#include <iostream>
using namespace std;

#define SQ(y) y*y

int main()
{
    int n, sq;
    cin>>n;
    sq = SQ(n);
    cout<<sq<<endl;
    return 0;
}

运行结果:

9↙
81


从表面上看这个宏定义是正确的,但当我们将宏调用SQ(n)换成SQ(n+1)后,就会出现意想不到的状况:

#include <iostream>
using namespace std;

#define SQ(y) y*y

int main()
{
    int n, sq;
    cin>>n;
    sq = SQ(n+1);
    cout<<sq<<endl;
    return 0;
}

运行结果:

9↙
19


我们期望的结果是 100,但这里却是 19,两者大相径庭。这是因为,宏展开仅仅是字符串的替换,不会进行任何计算或传值,上面的sq = SQ(n+1);在宏展开后会变为sq = n+1*n+1;,这显然是没有道理的。

如果希望得到正确的结果,应该将宏定义改为如下的形式:

#define SQ(y) (y)*(y)

这样宏调用sq = SQ(n+1);就会展开为sq = (n+1)*(n+1);,得到的结果就是 100。

如果你认为这样就万事大吉了,那下面的结果会让你觉得考虑不周:

#include <iostream>
using namespace std;

#define SQ(y) (y)*(y)

int main()
{
    int n, sq;
    cin>>n;
    sq = 200 / SQ(n+1);
    cout<<sq<<endl;
    return 0;
}

运行结果:

9↙
200


之所以会出现这么奇怪的结果,是因为宏调用sq = 200 / SQ(n+1);会被展开为sq = 200 / (n+1) * (n+1);,当 n 被赋值 9 后,相当于sq = 200 / 10 * 10,结果显然是 200。

要想得到正确的结果,还应该对宏加以限制,在两边增加( ),如下所示:

#define SQ(y) ( (y)*(y) )

这样宏调用sq = 200 / SQ(n+1);就会展开为sq = 200 / ( (n+1) * (n+1) );,得到的结果就是 2。

说了这么多,我最终想强调的是,宏定义是一项“细思极密”的工作,一不小心就会踩坑,而且不一定在编译和运行时发现,给程序埋下隐患。

如果我们将宏替换为内联函数,情况就没有那么复杂了,程序员就会游刃有余,请看下面的代码:

#include <iostream>
using namespace std;

inline int SQ(int y){ return y*y; }

int main()
{
    int n, sq;
    cin>>n;
    //SQ(n)
    sq = SQ(n);
    cout<<sq<<endl;
    //SQ(n+1)
    sq = SQ(n+1);
    cout<<sq<<endl;
    //200 / SQ(n+1)
    sq = 200 / SQ(n+1);
    cout<<sq<<endl;

    return 0;
}

运行结果:

9↙
81
100
2


看,一切问题迎刃而解!发生函数调用时,编译器会先对实参进行计算,再将计算的结果传递给形参,并且函数执行完毕后会得到一个值,而不是得到一个表达式,这和简单的字符串替换相比省去了很多麻烦,所以在编写C++代码时我推荐使用内联函数来替换带参数的宏。

和宏一样,内联函数可以定义在头文件中(不用加 static 关键字),并且头文件被多次#include后也不会引发重复定义错误。这一点和非内联函数不同,非内联函数是禁止定义在头文件中的,它所在的头文件被多次#include后会引发重复定义错误。

内联函数在编译时会将函数调用处用函数体替换,编译完成后函数就不存在了,所以在链接时不会引发重复定义错误。这一点和宏很像,宏在预处理时被展开,编译时就不存在了。从这个角度讲,内联函数更像是编译期间的宏。

如果读者不了解编译和链接的细节,请阅读《C语言头文件的编写》一章,我们进行了深入详细的说明。

综合本节和上节的内容,可以看到内联函数主要有两个作用,一是消除函数调用时的开销,二是取代带参数的宏。不过我更倾向于后者,取代带参数的宏更能凸显内联函数存在的意义。