Q1:C++多态形式:静态多态性、动态多态性
• 多态:以一个 “public base class” 的指针寻址出一个 “derived class object”(深入探索C++对象模型定义)
• 静态多态性通常称为编译时多态,到底模板是不是多态???我个人认为不是
• 动态多态性通常称为运行时多态,通过虚函数来实现
• 动态多态性的两个条件:
○ 在基类中必须使用虚函数或纯虚函数 ○ 调用函数时使用基类的指针或引用
Q2:消极多态与积极多态(深入探索C++对象模型中定义)
• 消极多态:指针的多态机能主要扮演一个输送机制的角色,经过这个指针,我们可以在程序的任何地方采用一组派生类类型。这样的传输机制的多态形式称为消极多态
Eg:
point2d p2d;
point * ptr = &p2d; //此时,ptr带来的多态性体现为传输机制,通过基类指针传输派生类对象
• 积极多态:当指针所指对象被真正使用时,多态也就变为积极的了
Eg:
ptr->f(); //ptr 所指对象被真正使用
Q3:对虚函数调用的深入探究
• 对如下调用,若z()是一个虚函数,那需要那些信息才能在执行期调用正确的z()实例:
ptr->z()
ptr所指对象的真实类型。(用以选择正确的z()实例)
z()实例的位置
因此,需要在每一个多态的类对象上增加两个成员:
1. 一个字符串或数字,表示 class 的类型;(vptr所指表格的第一项即为类型说明,用以支持RTTI) 2. 一个指针,指向某个表格,表格中持有程序的虚函数的执行期地址(vptr)
• 虚函数表的构建与存取皆有编译器掌控,不需要任何执行期的介入
Q4:单一继承下的虚函数
• 在单一继承情况下,一个类对象中,可能包含多个vptr,但只会有一个 virtual table。每一个 table 中内含其对应类对象中所有 active virtual functions 函数实例的地址,这些函数包括:
• 这一个类所定义的函数实例,包括改写的继承自 base class 的函数实例 • 继承自 base class 的函数实例,指该类并不会改写的继承实例 • 一个 pure_virtual_called()的实例。既可以扮演纯虚函数的空间保卫 角色,又可以当作执行期异常处理函数
• 每一个虚函数都被指派一个固定的索引,这个索引在整个继承体系中保持与特定的虚函数的关系。
Eg:
class Point
{
public:
virtual ~Point();
virtual Point& mult(float) = 0;
virtual float y()const{ return 0; }
virtual float z()const { return 0; }
protected:
float _x;
};
class Point2d : public Point
{
public:
virtual ~Point2d();
Point2d& mult(float);
float y() const { return _y; }
protected:
float _y;
};
其虚函数表中的取值情况如下所示:
Point 的虚函数表如下:
Point2d 的虚函数表如下:
Q5:多重继承下的虚函数
• 多重继承中支持虚函数的两个难点在于:
1. 对第二个以及后继的 base classes 的虚函数的处理 2. 必须在执行期调整 this 指针
*备注:对于虚函数改写(覆盖)而言,允许一个虚函数的返回值类型有所变化,可能是 base type,也可能是 publicly derived type(仅限指向类类型的指针与引用)。但函数名称,参数列必须与被改写的函数完全相同
附加知识点:定义一个基类指针指向一个派生类对象,通过该指针调用改写函数时,有以下两种情况:
1. 若改写函数不是虚函数,则调用时调用的为基类的函数实例 2. 若改写的函数是虚函数,则调用时调用的为派生类的函数实例
*备注:非虚函数并不在类实例中,不影响类实例的大小。而虚函数则影响类实例的大小,因为会造成附加的 vptr 指针出现在类实例中
• 对多重继承的讨论以如下例子来进行:
class Base1
{
public:
virtual ~Base1();
virtual void speakClearly();
virtual Base1 * clone()const;
};
class Base2
{
public:
virtual ~Base2();
virtual void mumble();
virtual Base2 * clone()const;
void test();
};
class Derived : public Base1,public Base2
{
public:
virtual ~Derived();
virtual Derived * clone()const;
};
此问题中派生类 Derived 对虚函数的支持的困难度体现 Base2 子对象上。即有以下三个问题需要解决:
1. 虚析构函数 2. 被继承下来的 Base2::mumble(); 3. 一组clone()函数的实例
• 对于第一个问题:虚析构函数的分析:
• Eg1:
Base2 * pbase2 = new Derived;
该操作需要对新的 Derived 对象的地址进行调整以指向其 Base2 子对象,即编译时产生如下代码:
Derived * temp = new Derived;
Base2 * pbase2 = temp ? temp + sizeof( Base1 ) : 0;
//事实上此处的加法操作并不能够在编译期直接设定,因为pbase2所指向的真正对象类型在编译期未知
即将 pbase2 的值调整为该 Derived 对象的 Base2 子对象的位置,否则,任何非多态的运用都会失败。如:
pbase2->test(this); //因为 this 指针不指向 Base2 子对象
• Eg2:
delete pbase2;
对于这种情况,指针需要被再一次调整,使其指向 Derived 对象的起始处,调用正确的虚析构函数实例,然后施行 delete 运算符
• 对于调整 this 指针时不知道偏移的两种处理方法:
@H_502_299@(*pbase2->vptr[1])@H_502_299@(pbase2); (*pbase2->vptr[1].faddr)(pbase2 + pbase2->vptr[1].offset);
这种情况下,连坐处罚了所有的虚函数调用操作,不管其是否需要 offset 调整
thunk方法
所谓的 thunk 方法是一小段代码,用来以适当的 offset 调整 this 指针, 并跳转到虚函数处。此时,虚函数表中存放的仍然是指针
• 若不需要调整this指针的虚函数,此时 slot 中存放的就是虚函数的地址
• 若需要调整 this 指针的虚函数,此时 slot 中存放的是一个相关的 thunk 的地址
• 对于第二个问题:被继承下来的 Base2::mumble():
• Eg1:
Derived * pder = new Derived;
pder->mumble();
对于这种情况,Derived 的指针必须再次调整以指向第二个基类子对象,用以调用该子对象的 mumble() 函数
• 对于第三个问题:clone()函数的实例
• Eg:
Base2 * pb1 = new Derived;
Base2 * pb2 = pb1->clone();
在这个过程中,pb1->clone()执行时,pb1会被调整到指向 Derived 对象的起始地址,调用 Derived::clone(),传回一个指向新的 Derived 对象的指针,该对象地址再次进行调整后指向 Base2 子对象,并传送给 pb2
• 如果虚函数够小(平均大小是8行),则将使用 sun 编译器提供的 “split functions” 的技术: 以相同算法产生出两个函数,其中第二个在返回之前,为指针加上必要的 offset
*此时,通过 Derived 指针或 Base1 指针调用函数都不需要调整返回值,而通过 Base2 指针所调用的,则是另一个调整返回值的函数
• 在多重继承中,一个派生类内含 n-1 个额外的虚函数表,其中 n 表示其上一层的基类的个数。(因此,单一继承的派生类只有一个虚函数表)
Q6:虚拟继承下的虚函数
• 虚基类不同于普通基类,虚基类位于实例对象的底部(即实例对象地址的最高处),因此,在此情况下,虚基类与派生类之间的转换必须要调整 this 指针
Eg:
class A{};
class B : public virtual A{};
此时虽然类 B 仅有一个基类 A,但由于基类 A 是虚基类,在这种情况下,虚基类 A 位于类 B 的底部,因此,在类 A 与类 B 之间的转换需要调整 this 指针
• 更复杂的情况不予讨论。建议:不要在一个虚基类中声明非静态数据成员