C++支持三种类型的member functions:static,nonstatic和virtual,每一种类型被调用的方式都不同。
1、Member的各种调用方式
原始的“C with classes”只支持nonstatic member functions。Virtual函数是在20世纪80年代中期被加进来的,并且很显然受到许多质疑。
Static member functions是最后被引入的一种函数类型。在1987年的Usenix C++研讨会的厂商研习营中被正式提议加入C++中,并由cfront2.0实现出来。
Nonstatic Member Functions
C++的设计准则之一就是:nonstatic member function至少必须和一般的nonmember function有相同的效率。也就是说,如果我们在以下两个函数之间作选择:
Float magnitude3d(const Point3d* _this){...}
Float Point3d::mangnitude3d( ) const{...}
那么选择member function不应该带来额外负端。这是因为编译器内部已将“member函数实体”转换为对等的“nonmember函数实体”。
举个例子,下面是magnitude( )的一个nonmember定义:
Float magnitude3d(const Point3d* _this)
{
Return sqrt(_this->_x*_this->_x+
_this->_y*_this->_y+
_this->_z*_this->_z);
}
乍见之下似乎nonmember function比较没有效率,它间接地经由参数取用坐标成员,而member function却是直接取用坐标成员。然而实际上member function被内化为nonmember的形式。下面为转化步骤:
1、改写函数的signature以安插一个额外的参数到member function中,用以提供一个存 取管道,使class object得以调用该函数。
Point3d
Point3d::magnitude(Point3d* const this)
如果member function是const,则变成:
Point3d
Point3d::magnitude(const Point3d* const this)
2、将每一个“对nonstatic data member的存取操作”改为经由this指针来存取:
{
Return sqrt(this->_x*this->_x+
this->_y*this->_y+
this->_z*this->_z);
}
3、将member function重新写成一个外部函数。对函数名称进行“mangling”处理,使 它在程序中独一无二。
Extern magnitude__7Point3dFv(
Register Point3d* const this);
现在这个函数已经被转换好了,而其每一个调用操作也都必须转换。于是:
Obj.magnitude( );
变成了:
Magnitude__7Point3dFv(&obj);
而
Ptr->magnitude( );
变成了:
Magnitude__7Point3dFv(ptr);
名称的特殊处理(Name Mangling)
一般而言,member的名称前面会被加上class的名称,形成独一无二的命令。
class Bar{public:int ival;...};
其中ival有可能变成这样:
Ival__3Bar
为什么编译器要这么做?请考虑这样的派生操作:
Class Foo:public Bar{public:int ival;...};
记住,Foo对象内部结合了base class和derived class两者:
//C++伪码
//Foo的内部描述
Class Foo{
Public:
Int ival_3Bar;
Int ival_3Foo;
...
};
不管你要处理哪一个ival,通过“name mangling”,都可以绝对清楚地指出来。由于member function可以被重载化(overloaded),所以需要更广泛的mangling手法,以提供独一无二的名称。如果把:
Class Point{
Public:
Void x(float newX);
Float x( );
...
};
转换为:
Class Point{
Public:
Void x__5Point(float newX);
Float x__5Point();
...
};
会导致两个被重载化(overloaded)的函数实体拥有相同的名称。为了让它们独一无二,唯有在加上它们的参数链表(可以从函数原型中参考得到)。如果把参数类型也编码进去,就一定可以制造出独一无二的结果,使我们的两个x( )函数有良好的转换(但如果你声明extern “C”,就会压抑nonmember functions的“mangling”效果):
Class Point{
Public:
Void x__5PointFf(float newX);
Float x__5PointFv( );
...
}
以上所示的只是cfront采用的编码方式。我必须承认,目前的编译器并没有统一的编码方法——虽然不断有一些活动企图导引出这方面的一个工业标准。当前C++编译器对name mangling的做法还没有统一。
把函数和函数名称编码在一起,编译器于是在不同的被编译模块之间达成了一种有限形式的类型检验。举个例子,如果一个print函数被这样定义:
void print(const Point3d&){...}
但意外地被这样声明和调用:
Void print(const Point3d);
两个实体如果拥有独一无二的name mangline,那么任何不正确的调用操作在链接时期就因无法决议(resolved)而失败。有时候我们可以乐观地称此为“确保类型安全的链接行为”(type-safe linkage)。之所以说“乐观地”是因为它只可以捕捉函数的标记(signature,亦即函数名称+参数数目+参数类型)错误:如果“返回类型”声明错误,就没办法检查出来!(这句话值得商榷,在VC6.0下,如果“返回类型”声明错误,链接时期会因为无法决议而失败)。
当前的编译系统中,有一种所谓的demangling工具,用来拦截名称并将其转换回去。使用者可以仍然处于“不知道内部名称”的极大幸福之中。然而生命并不是长久以来一直如此轻松,在cfront1.1版,由于该系统(demangling)未经世故,故总是收藏两种名称;编译错误消息用的是程序代码函数名称,而链接器却不,它用的是经过mangled的内部名称。
Virtual Member Functions(虚拟成员函数)
如果normalize( )是一个virtual member function,那么以下的调用:
Ptr->normalize( );
将会被内部转化为:
(*ptr->vptr[1])(ptr);
其中:
n Vptr表示由编译器产生的指针,指向virtual table。它被安插在每一个“声明有(或继承自)一个或多个virtual functions”的class object中。事实上其名称也会被“mangled”,因为一个复杂的class派生体系中,可能存在有多个vptrs。
n 1是virtual table slot的索引值,关联到normalize()函数
n 第二个ptr表示this指针。
此时,由于Point3d::mangnitude( )是在Point3d::normalize( )中被调用,而后者已经由虚拟机制而决议妥当,所以明确地调用“Point3d实体”会比较有效率,并因此压制由于虚拟机制而产生的不必要的重复调用操作。
Register float mag=Point3d::magnitude( );
如果magnitude( )声明为inline函数会更有效率。使用class scope operator明确调用一个virtual function,其决议方式会和nonstatic member function一样:
Register float mag=magnitude__7Point3dFv(this);
对于以下调用:
Obj.normalize( );
如果编译器把它转换为:
(*obj.vptr[1])(&obj);
虽然语意正确,却没有必要。“经由一个class object调用一个virtual function”,这种操作应该总是被编译器像对待一般的nonstatic member function一样地加以决议。
Normalize__7Point3dFv(&obj);
这项优化工程的另一利益是,virtual function的一个inline函数实体可以被扩展开来,因而提供极大的效率利益。
Static Member Functions(静态成员函数)
如果Point3d::normalize()是一个static member function,以下两个调用操作:
Obj.normalize( );
Ptr->normalize( );
将被转换为一般的nonmember函数调用,像这样:
//obj.normalize( );
Normalize__7Point3dSFv( );
//ptr->normalize( );
Normalize__7Point3dSFv( );
在C++中引入static member functions之前,我想你很少看到下面这种怪异写法:
((Point3d*)0)->object_count( );
其中的object_count( )只是简单传回_object_count这个static data member。
在引入static member functions之前,C++语言要求所有的member functions都必须经由该class的object来调用。而实际上,只有当一个或多个nonstatic data members在member function中被直接存取时,才需要class object。Class object提供了this指针给这种形式的函数调用使用。这个this指针把“在member function中存取的nonstatic class members”绑定与“object内对应的members”之上。如果没有任何一个members被直接存取,事实上就不需要this指针,因此也就没有必要通过一个class object来调用一个member function。不过C++语言到目前为止并不能够识别这种情况。
这么一来就在存取static data members时产生了一些不规则性。如果class的设计者把static data member声明为nonpublic(这一直被视为一种好的习惯),那么他就必须提供一个或多个member functions来存取该member。因此,虽然你可以不靠class object来存取一个static member,但其存取函数却得绑定于一个class object之上。
独立于class object之外的存取操作,在某个时候特别重要:当class设计者希望支持“没有class object存在”的情况时,程序方法上的解决之道是很奇特地把0强制转型为一个class指针,因而提供出一个this指针实体:
Object_count((Point3d*)0);
至于语言层面上的解决之道,是由cfront2.0所引入的static member functions。Static member functions的主要特征是它没有this指针。以下的次要特性统统根源于其主要特性:
n 它不能够直接存取其class中的nonstatic members。
n 它不能够被声明为const,volatile或virtual。
n 它不需要经由class object才被调用——虽然大部分时候它都是这样被调用的!
如果取一个static member function的地址,获得的将是其在内存中的位置,也就是其地址。由于static member function没有this指针,所以其地址的类型并不是一个“指向class member function的指针”,而是一个“nonmember 函数指针”。也就是说:
&Point3d::object_count( );
会得到一个数值,类型是:
Unsigned int(*)( );
而不是:
Unsigned int(Point3d::*)( );
Static member function由于缺乏this指针,因此差不多等同于nonmember function。它提供了一个意想不到的好处:成为一个callback函数。
2、Virtual Member Functions(虚拟成员函数)
我们已经看过了virtual function的一般实现模型:每一个class有一个virtual table,内含该class之中有作用的virtual function的地址,然后每个object有一个vptr,指向virtual table的所在。下面,我们来看一组可能的设计,然后根据单一继承、多重继承和虚拟继承等各种情况来探究这个模型。
为了支持virtual function机制,必须首先能够对于多态对象有某种形式的“执行期类型判断法(runtime type resolution)”。也就是说,以下的调用操作将需要ptr在执行期的某些相关信息是,
Ptr->z( );
如此一来才能够找到并调用z( )的适当实体。
第一种可能的实现
或许最直接了当但是成本最高的解决方法就是把必要的信息加在ptr身上。在这样的策略之下,一个指针(或是一个reference)含有两项信息:
n 它所参考的对象的地址(也就是当前它所含有的东西);
n 对象类型的某种编码,或是某个结构(内含某些信息,用以正确决议出z( )函数实例)的地址。
这个方法带来两个问题,第一,它明显增加了空间负担,即使程序并不使用多态(polymorphism);第二,它打断了与C程序间的链接兼容性。
第二种可能的实现
如果这份额外的信息不能够和指针放在一起,下一个可以考虑的地方就是把它放在对象本身。但是哪一个对象真正需要这行信息呢?我们应该把这些信息放进可能被继承的每一个聚合体身上吗?或许吧!但请考虑一下这样的C struct声明:
Struct date{int m,d,y;};
严格地说,这符合上述规范。然而事实上它并不需要那些信息,加上那些信息将使C struct膨胀并且打破链接兼容性,却没有带来任何明显的补偿利益。
也许你会说“只对那些明确使用了class关键词的声明,才应该加上额外的执行期信息。”这么做可以保留语言的兼容性了,不过仍然不是一个够聪明的方法。线面的class 符合新规范:
Class date{public:int m,d,y;};
但实际上他并不需要那份信息。下面的class声明虽不符合新规范,却需要那份信息:
Struct geom{public:virtual ~geom( );...};
是的,我们需要一个更好的规范,一个“以class的使用为基础,而不在乎关键词是class或struct”的规范。如果class真正需要那份信息,它就会存在;不需要,它就不存在。那么,到底何时才需要这份信息呢?很明显是在必须支持某种形式之“执行器多态(runtime polymorphism)”的时候。
在C++中,多态表示“以一个public base class的指针(或引用),寻址出一个derived class object”的意思。如:
Point* ptr;
我们可以指定ptr以寻址出一个Point2d对象:
Ptr=new Point3d;
Ptr的多态机能主要扮演一个输送机制的角色,经由它,我们可以在程序的任何地方采用一组public derived类型。这种多态形式被称为消极的(passive),可以在编译时期完成——virtual base class的情况例外。
当被指出的对象真正被使用时,多态也就变成积极的(active)了。下面对于virtual function的调用,就是一例:
//“积极多态”的常见例子
Ptr->z( );
在runtime type identification(RTTI)性质于1993年被引入C++语言之前,C++对“积极多态”的唯一支持就是对virtual function call的决议操作。有了RTTI,就能够在执行其查询一个多态的pointer或多态的reference了。
//“积极多态”的第二个例子
If(Point3d* p3d=
Dynamic_cast<Point3d*>(ptr))
Return p3d->z;
所以,问题已经被区分出来,那就是:欲鉴定那些classes展现多态特性,我们需要额外的执行期信息。关键词class和struct并不能够帮助我们。由于没有导入如polymorphic之类的新关键词,因此识别一个class是否支持多态,唯一适当的方法就是看看它是否有任何virtual function。只要class拥有一个virtual function,它就需要这份额外的执行期信息。
下一个明显的问题是,什么样的额外信息是我们需要存储起来的?也就是说,如果我们有这样的调用:
Ptr->z( );
其中z( )是一个virtual function,那么什么信息才能让我们在执行期调用正确的z( )实体呢?我们需要知道:
n Ptr所指对象的真实类型,这可以使我们选择正确的z( )实体;
n Z( )实体位置,以便我们能够调用它。
CFRONT的实现
在实现上,首先我们可以在每一个多态的class object身上增加两个members:
1、一个字符串或数字,表示class的类型;
2、一个指针,指向某个表格,表格中带有程序的virtual function的执行期地址。
表格中的virtual functions地址如何被构建起来?在C++中,virtual functions(可经由其class object被调用)可以在编译器获知,此外,这一组地址是固定不变的,执行期不可能新增或替换之。由于程序执行时,表格的大小和内容都不会改变,所以其构建和存取皆可以由编译器完全掌握,不需要执行期的任何介入。(其实所谓的动态执行,也不过是通过在编译期(静态的)引入一层间接性,来表现语言的多态。各种语言中很多地方都使用这种方法。)
然而,执行期备妥那些函数地址,只是解答的一半而已。另一半解答是找到那些地址。以下两个步骤可以完成这项任务:
n 为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格。
n 为了找到函数地址,每一个virtual function被指派一个表格索引值。
这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot中激活virtual function。
一个class只会有一个virtual table。每一个table内含其对应的class object中所有active virtual functions函数实体的地址。这些active virtual functions包括:
n 这个class所定义的函数实体。它会改写(overriding)一个可能存在的base class virtual function函数实体。
n 继承自base class的函数实体。这是在derived class决定不改写virtual function时才会出现的情况。
n 一个pure_virtual_called( )函数实体,它既可以扮演pure_virtual_function的空间保卫者角色,也可以当作执行器异常处理函数(有时候会用到)。
每一个virtual function都被指派一个固定的索引值,这个索引值在整个继承体系中保持与特定的virtual function的关联。
在如下的Point class体系中:
Class Point{
Public:
Virtual ~Point( );
Virtual Point& mult(float)=0;
Float x( ) const{return _x;}
Virtual float y( )const{return 0;}
Virtual float z( 0const{return 0;}
Protected:
Point(float x=0.0};
Float _x;
};
Virtual destructor被赋值slot 1,而mult被赋值slot 2。此例并没有mult( )的函数定义,所以pure_virtual_called( )的函数地址会被放在slot 2中。下图是Point可能的内存布局和其virtual table。
当一个class派生自Point时,会发生什么事呢?例如class Point2d:
Class Point3d:public point{
Public:
Point2d(float x=0.0,float y=0.0)
:Point(x),_y(y){}
~Point2d( );
Point2d& mult(float);
Float y( ) const{return _y;}
//...其它操作
Protected:
Flaot _y;
};
一共有三种可能:
n 它可以继承base class所声明的virtual functions的函数实体。
n 它可以使用自己的函数实体,但它自己的函数实体地址必须放在对应的slot之中。
n 它可以加入一个新的virtual function。这时候virtual table的尺寸会增大一slot,而新的函数实体地址会被放进该slot之中。
现在,如果我们有这样的式子:
Ptr->z( );
那么,我们如何有足够的知识在编译时期设定virtual function的调用呢?
n 一般而言,我们并不知道ptr所指对象的真正类型。然而我们知道,经由ptr可以存取到该对象的virtual table。
n 虽然我们不知道哪一个z( )函数实体会被调用,但我们知道每一个z( )函数地址都被放在slot 4。
这些信息可以使得编译器将该调用转化为:
(*ptr->vptr[4])(ptr);
在一个单一继承体系中,virtual function机制的行为十分良好,不但有效率而且容易塑造出模型来。但是在多重继承和虚拟继承之中,对virtual functions的支持就没有这么简单了。
怪异的函数调用
如上的程序代码,程序运行的结果如右图所示:
在程序中,我们调用pintB->BDisp( ),输出的却是A::Adisp( ),很显然程序调用了A::ADisp( )。同理,pintB->BShow( )调用了A::AShow( )。举这个例子是为了说明,如果用户知道了C++的编译模型,可以通过该模型达到我们自己的任何目的。因为了解了编译模型,我们就可以清楚的知道C++的内存布局。这种对象和代码之间的二进制耦合使得编译器可以产生非常高效的代码。但是,在用C++实现COM组件编程时,这种二进制的紧耦合性导致C++不能支持独立的二进制组件设计,这使得C++与基于.NET平台的C#相比,在组件编程方面有先天的不足。
多重继承下的Virtual Functions
在多重继承中支持virtual functions,其复杂度围绕在第二个及后继的base classes身上,以及“必须在执行期调整this指针”这一点。以下面的class体系为例:
Class Base1{
Public:
Base1( );
Virtual ~Base1( );
Virtual void speakClearly( );
Virtual Base1* clone( ) const;
Protected:
Float data_Base1;
};
Class Base2{
Public:
Base2( );
Virtual ~Base2( );
Virtual void mumble( );
Virtual Base2* clone( ) const;
Protected:
Float data_Base2;
};
Class Derived:public Base1,public Base2{
Public:
Derived( );
Virtual ~Derived( );
Virtual Derived* clone( ) const;
Protected:
Float data_Derived;
};
注:上述代码在VC2008,GCC4.3.2中通过。
“Derived支持virtual functions”的困难度,统统落在Base2 subobject身上。有三个问题需要解决。
n Virtual destructor
n 被继承下来的Base2::mumble( )
n 一组clone函数实体。
首先,我们把一个从heap中配置而得的Derived对象的地址,指定给一个Base2指针:
Base2* pbase2=new Derived;
新的Derived对象的地址必须调整,以指向其Base2 subobject。编译时期会产生以下的代码:
Derived* temp=new Derived;
Base2* pbase2=temp?temp+sizeof(Base1):0;
如果没有这样的调整,指针的任何“非多态运用”(像下面那样)都将失败:
//即使pbase2被指定一个Derived对象,这也应该没有问题
Pbase2->data_Base2;
当程序员要删除pbase2所指的对象时:
//必须首先调用正确的virtual destructor函数实体
//然后施行delete运算符
//pbase2可能需要调整,以指出完整对象的起始点
Delete pbase2;
指针必须被再一次调整,以求再一次指向Derived对象的起始处(推测它还指向Derived对象)。然而上述的offset加法却不能够在编译时期直接设定,因为pbase2所指向的真正对象只有在执行期才能确定。
一般规则是,经由指向“第二或后继之base class”的指针(或reference)来调用derived class virtual function。
//如下例一样
Base2 *pbase2=new Derived;
...
Delete pbase2;
该调用操作所连带的“必要的this指针调整”操作,必须在执行器完成。也就是说,offset的大小,以及把offset加到this指针上头的那一小段程序代码,必须由编译器在某个地方插入。问题是,在那个地方?
Bjame原先实施与cfront编译器中的方法是将virtual table加大,使它容纳此处所需的this指针,调整相关事物。每一个virtual table slot,不再只是一个指针,而是一个聚合体,内含可能的offset以及地址。于是virtual function的调用操作由:
(*pbase2->vptr[1])(pbase2);
改变为:
(*pbase2->vptr[1].faddr)
(pbase2+pbase2->vptr[1].offset);
其中faddr内含virtual function地址,offset内含this执政调整值。
这个做法的缺点是,它相当于连带处罚了所有的virtual function调用操作,不管它们是否需要offset的调整。
THUNK
比较有效的方法是利用所谓的thunk。
所谓thunk是一小段assembly代码,用来(1)以适当的offset值调整this指针,(2)跳到virtual function去。例如,经由一个Base2执政调用Derived destructor,其相关的thunk可能看起来是这个样子:
Pbase2_dtor_thunk:
This+=sizeof(base1);
Derived::~Derived(this);
Bjame并不是不知道thunk技术,问题是thunk只有以assembly代码完成才有效率可言。由于cfront使用C作为其程序代码产生语言,所以无法提供一个有效率的thunk编译器。
Thunk技术允许virtual table slot继续内含一个简单的指针,因此多重继承不需要任何空间上的额外负担。Slots中的地址可以直接指向virtual function,也可以指向一个相关的thunk(如果需要调整this指针的话)。于是,对于那些不需要调整this指针的virtual function而言,也就不需承载效率上的额外负担。
调整this指针的第二个额外负担就是,由于两种不同的可能:(1)经由derived class(或第一个base class)调用,(2)经由第二个(或其后继)base class调用,同一函数在virtual table中可能需要更多笔对应的slots。例如:
Base1* pbase1=new Derived;
Base2* pbase2=new Derived;
Delete pbase1;
Delete pbase2;
虽然两个delete操作导致相同的Derived destructor,但它们需要两个不同的virtual table slots:
1、pbase1不需要调整this指针(因为Base1是最左端base class之故,它已经指向 Derived对象的起始处)。其virtual table slot需放置真正的destructor地址。
2、Pbase2需要调整this指针。其virtual table slot需要相关的thunk地址。
在多重继承之下,一个derived class内含n-1个额外的virtual tables,n表示其上一层base classes的数目(因此,单一继承将不会有额外的virtual tables)。对于本例之Derived而言,会有两个virtual tables被编译器产生出来:
1、一个主要实体,与Base1共享。
2、一个次要实体,与Base2有关。
针对每一个virtual tables,Derived对象中有对应的vptr。Vptrs将在constructor(s)中被设立初值(经由编译器所产生出来的代码)。
用以支持“一个class拥有多个vritual tables”的传统方法是,将每一个tables以外部对象的形式产生出来,并给予独一无二的名称。例如,Derived所关联的两个tables可能有这样的名称:
Vtbl__Derived:
Vtbl__Base2__Derived;
于是当你将一个Derived对象地址指定给一个Base1指针或Derived指针时,被处理的virtual table是主要表格vtbl__Derived。而当你将一个Derived对象地址指定给一个Base2指针时,被处理的virtual table是次要表格vtbl_Base2_Derived。
开节之前,我们曾提到有三种情况会影响对virtual functions的支持。
第一种情况是,通过一个“指向第二个base class”的指针,调用derived class virtual function。例如:
Base2 *ptr=new Derived;
//调用Derived::~Derived
//ptr必须被向后调整sizeof(Base1)个bytes
Delete ptr;
从图4.2之中,你可以看到这个调用操作的重点:ptr指向Derived对象中的Base2 subobject;为了能够正确执行,ptr必须调整指向Derived对象的起始处。
第二种情况是第一种情况的变化,通过一个“指向derived class”的指针,调用第二个base class中一个继承而来的virtual function。在此情况下,derived class指针必须再次调整,以指向第二个base subobject。例如:
Derived* pder=new Derived;
//调用Base2::mumble( )
//pder必须被向前调整sizeof(Base1)个bytes
Pder->mmble( );
第三种情况发生于一个语言扩充性质之下,允许一个virtual function的返回值类型有所变化,可能是base type,也可能是public derived type。这一点可以通过Derived::clone( )函数实体来说明。Clone函数的Derived版本传回一个Derived class指针,默默地改写了它的两个base class函数实体。当我们通过“指向第二个base class”的指针来调用clone( )时,this指针的offset问题于是诞生:
Base2* pb1=new Derived;
//调用Derived* Derived::clone( )
//返回值必须被调整,以指向Base2 subobject
Base2 *pb2=pb1->clone( );
当进行pb1->clone( )时,pb1会被调整指向Derived对象的起始地址,于是clone( )的Derived版会被调用;它会传回一个指针,指向一个新的Derived对象;该对象的地址在被指定给pb2之前,必须先经过调整,以指向Base2 subobject。
虚拟继承下的Virtual Functions
考虑下面的virtual base class派生体系,从Point2d派生出Point3d:
Class Point2d{
Public:
Point2d(float=0.0,float=0.0);
Virtual ~Point2d( );
Virtual void mumble( );
Virtual float z( );
//...
Protected:
Float _x,_y;
};
Class Point3d:public virtual Point2d{
Public:
Point3d(float=0.0,float=0.0,float=0.0);
~Point3d( );
Float z( );
Protected:
Float _z;
};
虽然Point3d有唯一一个base classes,也就是Point2d,但Point3d和Point2d的起始部分并不像“非虚拟的单一继承”情况那样一致。由于Point2d和Point3d的对象不再相符,两者之间的转换也就需要调整this指针。至于在虚拟继承的情况下要清除thunks,一般而言已经被证明是一项高难度技术。
当一个virtual base class从另一个virtual base class派生而来,并且两者都支持virtual functions和nonstatic data members时,编译器对于virtual base class的支持简直就像进入了迷宫一样