前些日子,有个同学问我一个关于虚函数的缺省参数问题。他是从某个论坛上看到的,但是自己没想通,便来找我。现在分享一下这个问题。先看一小段代码:
#include <iostream> using namespace std; class A { public: virtual void Fun(int number = 10) { cout << "A::Fun with number " << number; } }; class B: public A { public: virtual void Fun(int number = 20) { cout << "B::Fun with number " << number << endl; } }; int main() { B b; A &a = b; a.Fun(); return 0; }
问题是,这段代码输出什么?正确答案是:B::Fun with number 10
这个问题并不难,关键要看你对C++了解多少。我了解得不多,但是这个小问题恰好能答上来。很明显,这段代码的输出结果依赖于C++的多态。什么是多态?在C++中,多态表现为指向父类对象的指针(或引用)指向子类对象,然后利用父类指针(或引用)调用它实际指向的子类的成员函数。这些成员函数由virtual关键字定义,也就是所谓的虚函数。
如果你知道C++的多态是怎么回事,那么这道题目你至少能答对前半部分。也就是说,输出的前半部分应该是这样的:B::Fun with number。疑点在于,究竟number等于10还是20?
这就涉及到C++的静态绑定和动态绑定问题。说到静态绑定和动态绑定,就不能不谈“静态类型”和“动态类型”。何为静态类型呢?C++标准(2003)是这么说的:
1.3.11 static type [defns.static.type]
the type of an expression (3.9), which type results from analysis of the program without considering execu-
tion semantics. The static type of an expression depends only on the form of the program in which the
expression appears, and does not change while the program is executing.
什么又是动态类型呢?
1.3.3 dynamic type [defns.dynamic.type]
the type of the most derived object (1.8) to which the lvalue denoted by an lvalue expression refers. [Exam-
ple: if a pointer (8.3.1) p whose static type is “pointer to class B” is pointing to an object of class D, derived
from B (clause 10), the dynamic type of the expression *p is “D.” References (8.3.2) are treated similarly. ]
The dynamic type of an rvalue expression is its static type.
如果你感觉标准写得点深奥,不容易懂,那就来看看C++ Primer(第4版,15.2.4)怎么说的吧:“基类类型引用和指针的关键点在于静态类型(static type,在编译时可知的引用类型或指针类型)和动态类型(dynamic type,指针或引用所绑定的对象的类型,这是仅在运行时可知的)可能不同”。在该书第15章的最后,对静态类型和动态类型做了一个总结:静态类型是指“编译时类型。对象的静态类型与动态类型相同。引用或指针所引用的对象的动态类型可以不同于引用或指针的静态类型”;动态类型是指“运行时类型。基类类型的指针和引用可以绑定到派生类型的对象,在这种情况下,静态类型是基类引用(或指针),但动态类型是派生类引用(或指针)”。
这下就明白多了,静态类型是编译期就能确定的类型,简单地说,当你声明一个变量时,为该变量指定的类型就是它的静态类型。动态类型是在程序运行时才能确定的类型,典型例子就是父类对象指针指向子类对象,这时,父类指针的动态类型就变成了子类指针。正如上述C++标准中所举的例子,假设p原本是一个B类型的指针,如果现在让p指向D对象,而D恰好是B的派生类,那么p的动态类型就是D类型的指针。听上去有点绕,为了方便说明,我还是拿出C++标准上的一个例子来分析:
struct A { virtual void f(int a = 7); }; struct B : public A { void f(int a); }; void m() { B* pb = new B; A* pa = pb; pa->f(); // OK, calls pa->B::f(7) pb->f(); // error: wrong number of arguments for B::f() }这段代码中,pb的静态类型是B类型指针,它的动态类型也是B类型指针。pa的静态类型是A类型指针,而它的的动态类型却是B类型指针。
一旦明白了静态类型和动态类型的概念,静态绑定和动态绑定也就好理解了。按照C++ Primer的说法,动态绑定是指“延迟到运行时才选择运行哪个函数。在C++中,动态绑定指的是在运行时基于引用或指针绑定的对象的基础类型而选择运行哪个virtual函数”。显然,动态绑定与虚函数是息息相关的。与此对应,静态绑定就简单多了:如果一个类型的成员函数不是虚函数,那也就没什么好选择的了,通过指针或引用调用成员函数时,直接绑定到指针或引用的基础类型即可。比如,在上面的代码中,pa->f(),这里调用的实际上是B的成员函数f(),也就是说,被调用的是与pa的动态类型相对应的函数,这就是所谓的“动态绑定”。
说了这么多,来解释本文一开始给出的问题。在C++中,虽然虚函数的调用是通过动态绑定来确定的,但是虚函数的缺省参数却是通过静态绑定确定的。(就这么规定的,据说是为了提高效率)显然,a的静态类型是A的引用,而动态类型是B的引用,因此,当a调用虚函数Fun()时,根据动态绑定规则,它调用的是B的成员函数Fun();而对于虚函数的缺省参数,根据静态绑定规则,它将number确定为A中给出的缺省值10。
再简单说一下本文给出的第二段代码。这是C++标准中给出的一个例子,而且也给了说明:“A virtual function call (10.3) uses the default arguments in the declaration of the virtual function determined by the static type of the pointer or reference denoting the object. An
overriding function in a derived class does not acquire default arguments from the function it overrides.” 我来翻译一下吧:“调用虚函数时使用的缺省参数在虚函数声明中给出,这些缺省参数由指示对象的指针或引用的静态类型确定。派生类中的重写函数无法获得它所重写的函数的缺省参数。”