在面向对象C++编程中,多态是OO三大特性之一,这种多态称为运行期多态,也称为动态多态;在泛型编程中,多态基于template(模板)的具现化与函数的重载解析,这种多态在编译期进行,因此称为编译期多态或静态多态。
1 运行期多态
运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。
运行期多态的实现依赖于虚函数机制。当某个类声明了虚函数时,告诉编译器在在编译时不得绑定(静态绑定也只能绑定一个),编译器将为该类对象安插一个虚函数表指针,并为类(基类和派生类)设置虚函数表,虚函数表中存放的是该类虚函数地址。同时,改写虚函数调用代码(函数指针调用)。运行期间通过虚函数表指针与虚函数表去确定该类虚函数的真正实现。
#include<iostream>
using namespace std;
class Animal
{
public :
virtual void shout() = 0;
};
class Dog :public Animal
{
public:
virtual void shout(){ cout << "汪汪!"<<endl; }
};
class Cat :public Animal
{
public:
virtual void shout(){ cout << "喵喵~"<<endl; }
};
class Bird : public Animal
{
public:
virtual void shout(){ cout << "叽喳!"<<endl; }
};
// ① 编译器会为有virtual函数成员的类建立一个虚函数表
int main()
{
Animal * animal;
Dog dog;
animal = &dog;
animal->shout();//虚函数的调用会被编译器转换为对虚函数表的访问
Cat cat;
// ② 编译器会为每个新建对象添加一函数指针,指向虚函数表
animal = &cat;
animal->shout();//animal代表this指针,shout是虚函数
// ③ 编译器会改写对象调用成员函数代码,改写成指针调用的形式
Animal * anim3 = new Bird;
anim3->shout();
delete anim3;
system("pause");
return 0;
}
/*
汪汪!
喵喵~
叽喳!
*/
编译器会构建一张虚表( vtable ),每一个类都有自己独特的虚表。同时,在这个继承链上,编译器会为基类插入一个隐式的指针(一般是对象的首地址),指向虚表,称为__vptr。然后,子类继承父类时,会获得继承下来的__vptr,再根据自己的类的情况兼容(修改虚函数表里的值、发生偏移等。于是,当我们构建具体的类时,若是基类类型,__vptr就会指向父类的vtable,若是子类类型,__vptr就会指向子类的vtable。

下面再看一个实例来了解虚函数表,及对象的函数指针是如何指向虚函数表以及调用虚函数代码是如何改写的?
#include <iostream>
using namespace std;
class A
{
public:
A(int _a1 = 1) : a1(_a1) { }
virtual void f() { cout << "A::f" << endl; }
virtual void g() { cout << "A::g" << endl; }
virtual void h() { cout << "A::h" << endl; }
~A() {}
private:
int a1;
};
class C : public A
{
public:
C(int _a1 = 1, int _c = 4) :A(_a1), c(_c) { }
virtual void f() { cout << "C::f" << endl; }
virtual void g() { cout << "C::g" << endl; }
virtual void h() { cout << "C::h" << endl; }
private:
int c;
};
// 通过访问类对象的前4字节(32位编译器)找到虚函数表。
// 虚函数表最后一项用的是0,代表虚函数表结束。
typedef void(*FUNC)(); //重定义函数指针,指向函数的指针
void PrintVTable(long* vTable) //访问虚函数表
{
if (vTable == NULL)
{
return;
}
cout << "vtbl:" << vTable << endl;
int i = 0;
for (; vTable[i] != 0; ++i)
{
printf("function : %d :0X%x->", i, vTable[i]);
FUNC f = (FUNC)vTable[i];
f(); //访问虚函数
}
cout << endl;
}
void main()
{
A a1;
long *p = (long *)(*(long*)&a1);
PrintVTable(p);
C c;
long *p2 = (long *)(*(long*)&c);
PrintVTable(p2);
system("pause");
}
/*
vtbl:00471048
function : 0 :0X40105a->A::f
function : 1 :0X4012c6->A::g
function : 2 :0X4010b9->A::h
vtbl:00471070
function : 0 :0X4010eb->C::f
function : 1 :0X4011d1->C::g
function : 2 :0X401280->C::h
*/

2 编译期多态
对模板参数而言,多态是通过模板具现化和函数重载解析实现的。以不同的模板参数具现化导致调用不同的函数,这就是所谓的编译期多态。
相比较于运行期多态,实现编译期多态的类之间并不需要成为一个继承体系,它们之间可以没有什么关系,但约束是它们都有相同的隐式接口。
#include <iostream>
using namespace std;
class Animal
{
public :
void shout() { cout << "发出动物的叫声" << endl; };
};
class Dog
{
public:
void shout(){ cout << "汪汪!"<<endl; }
};
class Cat
{
public:
void shout(){ cout << "喵喵~"<<endl; }
};
class Bird
{
public:
void shout(){ cout << "叽喳!"<<endl; }
};
template <typename T>
void animalShout(T & t)
{
t.shout();
}
int main()
{
Animal anim;
Dog dog;
Cat cat;
Bird bird;
animalShout(anim);
animalShout(dog);
animalShout(cat);
animalShout(bird);
getchar();getchar();
}
/*
发出动物的叫声
汪汪!
喵喵~
叽喳!
*/
在编译之前,函数模板中t.shout()调用的是哪个接口并不确定。在编译期间,编译器推断出模板参数,因此确定调用的shout是哪个具体类型的接口。不同的推断结果调用不同的函数,这就是编译器多态。这类似于重载函数在编译器进行推导,以确定哪一个函数被调用。
3 运行期多态与编译期多态优缺点分析
3.1 运行期多态优点
OO设计中重要的特性,对客观世界直觉认识。
能够处理同一个继承体系下的异质类集合。
3.2 运行期多态缺点
运行期间进行虚函数绑定,提高了程序运行开销。
庞大的类继承层次,对接口的修改易影响类继承层次。
由于虚函数在运行期在确定,所以编译器无法对虚函数进行优化。
虚表指针增大了对象体积,类也多了一张虚函数表,当然,这是理所应当值得付出的资源消耗,列为缺点有点勉强。
3.3 编译期多态优点
它带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的强大*器武**。
在编译器完成多态,提高运行期效率。
具有很强的适配性与松耦合性,对于特殊类型可由模板偏特化、全特化来处理。
3.4 编译期多态缺点
程序可读性降低,代码调试带来困难。
无法实现模板的分离编译,当工程很大时,编译时间不可小觑。
无法处理异质对象集合。
-End-