多态 & 深入探索C++模型
多态性
- 当在C++中遇到:希望同一个方法在派生类和基类中的行为是不同的。
- 即,方法的行为应取决于调用该方法的对象。
- 又,C++允许子类的成员函数重载基类的成员函数。
- 因此,在C++的继承机制中,用一种称为多态性的技术来解决上面的问题。
- 这种在运行时,能依据其类型确认调用哪个函数的能力,称为多态性,或称迟后连编,又称滞后连编,也称动态联编。
虚函数
- 为了指明某个成员函数具有多态性,用关键字virtual来标志其为虚函数。
- 当在子类的定义了一个与父类完全相同的虚函数时,则称子类的这个函数重写(也称覆盖)了父类的这个虚函数。
class Base
{
public:
virtual void fn()
{
cout << "Base::fn()" << endl;
}
};
class SubClass:public Base
{
public:
virtual void fn()
{
cout << "SunClass::fn()" << endl;
}
};
void call(Base& b)
{
b.fn();
}
void Test()
{
Base b;
call(b);
SubClass s;
call(s);
}
代码实现如下:
注意:
* 子类中的virtual可以省略,但父类中的virtual不能省略。
* 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
* 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
* 只有类的成员函数才能定义为虚函数。
* 静态成员函数不能定义为虚函数。
* 内联函数不能定义为虚函数。
* 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
* 构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要operator=定义为虚函数,因为容易使用时容易引起混淆。
* 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
* 最好把基类的析构函数声明为虚函数。(因为析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,会构成覆盖,将基类的析构函数声明成虚函数可以确保正确的析构函数序列被调用。)
指针和引用类型的兼容性
- 在C++中,动态联编与通过指针和引用调用方法相关。
- 一般情况下,C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型。
- 但,指向基类的引用或指针可以引用派生类对象,而不用进行显式类型转换。
向上强制转换(upcasting)
- 即,将派生类引用或指针转换为基类引用或指针。
- 该规则是is-a关系的一部分。
- 同时,向上强制转换时可传递的。
向下强制转换(downcasting)
- 即,将积累指针或引用转换为派生类指针或引用,
- 一般来说,向下强制转换是不允许的。(is-a关系通常是不可逆的)
- 因为,派生类可以新增数据成员,但这些数据成员的类成员函数不能应用于基类。
虚函数的工作原理
- 一般情况下,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。
- 在这个隐藏成员中,保存了一个指向函数地址数组的指针。
- 而这个数组则被称为,虚函数表,也称虚表。(virtual function table, vbtl)
- 虚函数表中储存了为类对象进行声明的虚函数的地址。
比如,基类对象包含了一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。
如果派生类提供了虚函数的新定义,该虚函数表则将保存新函数的地址;
如果派生类没有重新定义虚函数,该虚表还是将保存函数原始版本的地址;
如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚表中。
无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同。
我们来看一个示例:
class A
{
public:
virtual void fn()
{
cout << "A::fn()" << endl;
}
};
class B : public A
{
public:
virtual void fn()
{
cout << "B::fn()" << endl;
}
};
void Test()
{
A a;
B b;
A* p = &a;
p->fn();
p = &b;
p->fn();
}
代码运行如下:
我们可以通过调试,一窥该代码运行时的原理:
由图易知,B继承了A,重写了fn(),构成了多态,所以此时根据对象的不同,编译器会调不同的指针,指针则指向不同的虚表,虚表内所包含的函数不一,故运行出来所产生的结果也不一。
上述代码也可归纳为,单继承的对象模型。
多继承的对象模型
class A
{
public:
virtual void f1()
{
cout << "A::fn()" << endl;
}
virtual void f2()
{
cout << "A::f2()" << endl;
}
protected:
int _a;
};
class B
{
public:
virtual void f1()
{
cout << "B::fn()" << endl;
}
virtual void f2()
{
cout << "B::f2()" << endl;
}
protected:
int _b;
};
class C : public A, public B
{
public:
virtual void f1()
{
cout << "C::f1()" << endl;
}
virtual void f3()
{
cout << "C::f3()" << endl;
}
protected:
int _c;
};
typedef void(*VFUNC)();
void PrintVTable(void* vTable)
{
printf("虚函数表地址 = 0x%p\n", vTable);
VFUNC* array = (VFUNC*)vTable;
for (size_t i = 0; array[i] != 0; ++i)//虚表内虚函数地址以0作为结束标志
{
printf("第%d个虚函数地址:0x%p->", i, array[i]);
array[i]();
}
cout << "----------------------------" << endl;
}
void Test()
{
A a;
B b;
C c;
PrintVTable(*((int**)&c));
PrintVTable(*((int**)((int*)&c + sizeof(A) / 4)));
}
从监视窗口,我们并没有发现f3的踪迹, 那f3到哪去了呢?
于是,我们便自己打印了虚表,可得:
原来f3并没有自己开辟虚函数表,而是放在了A的虚表中。
由此可得,在多继承中,派生类的虚函数地址会放在虚继承的第一个基类的虚表中。