class Person { public: ... int age() const { return theAge; } // an implicit inline request: age is ... // defined in a class definition
PRivate: int theAge; }; 这样的函数通常是成员函数,不过我们知道友元函数也能被定义在类的内部,假如它们在那里,它们也被隐式地声明为 inline。
显式的声明一个 inline 函数的方法是在它的声明之前加上 inline 要害字。例如,以下就是标准 max 模板(来自 <algorithm>)经常用到的的实现方法:
template<typename T> // an eXPlicit inline inline const T& std::max(const T& a, const T& b) // request: std::max is { return a < b ? b : a; } // preceded by "inline" max 是一个模板的事实引出一个观察结论:inline 函数和模板一般都是定义在头文件中的。这就使得一些程序员得出结论断定函数模板必须是 inline。这个结论是非法的而且有潜在的危害,所以它值得我们考察一下。 inline 函数一般必须在头文件内,因为大多数构建环境在编译期间进行 inline 化。为了用被调用函数的函数本体替换一个函数调用,编译器必须知道函数看起来像什么样子。(有一些构建环境可以在连接期间进行 inline 化,还有少数几个——比如,基于 .NET Common Language InfrastrUCture (CLI) 的控制环境——居然能在运行时 inline 化。然而,这些环境都是例外,并非规则。inline 化在大多数 C++ 程序中是一个编译时行为。)
inline void f() {...} // assume compilers are willing to inline calls to f
void (*pf)() = f; // pf points to f ...
f(); // this call will be inlined, because it’s a "normal" call pf(); // this call probably won’t be, because it’s through // a function pointer 甚至在你从来没有使用函数指针的时候,未 inline 化的 inline 函数的幽灵也会时不时地拜访你,因为程序员并不必然是函数指针的唯一需求者。有时候编译器会生成构造函数和析构函数的 out-of-line 拷贝,以便它们能得到指向这些函数的指针,在对数组中的对象进行构造和析构时使用。
C++ 为对象被创建和被销毁时所发生的事情做出了各种保证。例如,当你使用 new 时,你的动态的被创建对象会被它们的构造函数自动初始化,而当你使用 delete。则相应的析构函数会被调用。当你创建一个对象时,这个对象的每一个基类和每一个数据成员都会自动构造,而当一个对象被销毁时,则发生关于析构的反向过程。假如在一个对象构造期间有一个异常被抛出,这个对象已经完成构造的任何部分都被自动销毁。所有这些情节,C++ 只说什么必须发生,但没有说如何发生。那是编译器的实现者的事,但显然这些事情不会自己发生。在你的程序中必须有一些代码使这些事发生,而这些代码——由编译器写出的代码和在编译期间插入你的程序的代码——必须位于某处。有时它们最终就位于构造函数和析构函数中,所以我们可以设想实现为上面那个声称为空的 Derived 的构造函数生成的代码就相当于下面这样:
Derived::Derived() // conceptual implementation of { // "empty" Derived ctor
Base::Base(); // initialize Base part
try { dm1.std::string::string(); } // try to construct dm1 catch (...) { // if it throws, Base::~Base(); // destroy base class part and throw; // propagate the exception }
try { dm2.std::string::string(); } // try to construct dm2 catch(...) { // if it throws, dm1.std::string::~string(); // destroy dm1, Base::~Base(); // destroy base class part, and throw; // propagate the exception }
try { dm3.std::string::string(); } // construct dm3 catch(...) { // if it throws, dm2.std::string::~string(); // destroy dm2, dm1.std::string::~string(); // destroy dm1, Base::~Base(); // destroy base class part, and throw; // propagate the exception } } 这些代码并不代表真正的编译器所生成的,因为真正的编译器会用更复杂的方法处理异常。尽管如此,它还是准确地反映了 Derived 的“空”构造函数必须提供的行为。不论一个编译器的异常多么复杂,Derived 的构造函数至少必须调用它的数据成员和基类的构造函数,而这些调用(它们自己也可能是 inline 的)会影响它对于 inline 化的吸引力。
库设计者必须评估声明函数为 inline 的影响,因为为库中的客户可见的 inline 函数提供二进制升级版本是不可能的。换句话说,假如 f 是一个库中的一个 inline 函数,库的客户将函数 f 的本体编译到他们的应用程序中。假如一个库的实现者后来决定修改 f,所有使用了 f 的客户都必须重新编译。这经常会令人厌烦。在另一方面,假如 f 是一个非 inline 函数,对 f 的改变只需要客户重新连接。这与重新编译相比显然减轻了很大的负担,而且,假如库中包含的函数是动态链接的,这就是一种对于用户来说完全透明的方法。