在运行状态下进行称之为动态。
1.虚函数表
(1)包含虚函数的类
class B {
virtual int f1 (void);
virtual void f2 (int);
virtual int f3 (int);
};
编译器会为每个包含虚函数的类生成一张虚函数表,即存放每个虚函数地址的函数指针数组,简称虚表(vtbl),每个虚函数对应一个虚函数表中的下标。
+---------+---------+---------+
vptr1-> | B::f1 | B::f2 | B::f3 |
+---------+---------+---------+
0 1 2
除了为包含虚函数的类生成虚函数表以外,编译器还会为该类增加一个隐式成员变量,通常在该类实例化对象的起始位置,用于存放虚函数表的首地址,
该变量被称为虚函数表指针,简称虚指针(vptr)。例如:
B* pb = new B;
pb->f3 (12);
被编译为
pb->vptr[2] (pb, 12); // B::f3 参数pb是this指针
注意:虚表是一个类一张,而不是一个对象一张,同一个类的多个对象,通过各自的虚指针,共享同一张虚表。
+---------+---------+---------+
vptr-> | vptr1 | vptr2 | vptr3 |
+---------+---------+---------+
(2)继承自B的子类
class D : public B {
int f1 (void);
int f3 (int);
virtual void f4 (void);
};
子类覆盖了基类的f1和f3,继承了基类的f2,增加了自己的f4,编译器同样会为子类生成一张专属于它的虚表。
+---------+---------+---------+---------+
vptr(子类)->| D::f1 | B::f2 | D::f3 | D::f4 |
+---------+---------+---------+---------+
0 1 2 3
指向子类虚表的虚指针就存放在子类对象的基类子对象中。例如:
B* pb = new D;
pb->f3 (12);
被编译为
pb->vptr[2] (pb, 12); // D::f3
这就是多态的工作原理。
用一个函数做一个测试:
class A{
public:
A():m_ch('A'){}
virtual void foo() {
cout << m_ch << "::foo()" << endl ;
}
virtual void bar(){
cout << m_ch << "::bar()" << endl ;
}
private:
char m_ch ;
} ;
class B:public A{
public:
B():m_ch('B'){}
void foo(){
cout << "B::foo()" <<endl ;
}
private:
char m_ch ;
} ;
int main(){
A a ;
void(**vptr_a)(A*) = *(void(***)(A*))&a ;
cout << (void *)vptr_a <<endl ; //0x8048bb0
cout << "foo():"<<(void *)vptr_a[0] <<endl ; //foo():0x8048992
cout << "bar():" <<(void *)vptr_a[1] <<endl ; //bar():0x80489d4
vptr_a[0](&a) ; //A::foo()
vptr_a[1](&a) ; //A::bar()
cout << "-----------------------------------------" <<endl ;
B b ;
void(**vptr_b)(B*) = *(void(***)(B*))&b ;
cout << (void *)vptr_b <<endl ; //0x8048ba0
cout << "foo():"<<(void *)vptr_b[0] <<endl ; //foo():0x8048a3a
cout << "bar():" <<(void *)vptr_b[1] <<endl ; //bar():0x80489d4
vptr_b[0](&b) ; //B::foo()
vptr_b[1](&b) ; //A::bar()
}
上述程序说明了虚函数表是真实存在的:
(1)void(**vptr_a)(A*) = *(void(***)(A*))&a ;建立一个vptr_a的虚函数表,如下图:
对象a的起始地址就是虚指针起始的所指向的地址。
(2)通过基类和子类的虚函数指针指向的地址对比可以看出,基类和子类的虚函数表不是一个
(3)所谓的多态,就是子类中的函数原型(返回类型+函数名+形参表+常属性)相同的基类虚函数进行一个替换,编译器是通过this指针来区分对象。
2.动态绑定
当编译器“看到”通过指针或者引用调用基类中的虚函数时,并不急于生成有关函数调用的指令,相反它会用一段代码替代该调用语句,这段代码在运行时被执行,完成如下操作:
1)根据调用指针或引用的目标对象找到其内部的虚表指针;
2)根据虚表指针找到其所指向的虚函数表;
3)根据虚函数名和函数指针在虚函数表中的索引,找到所调用虚函数的入口地址;
4)在完成函数调用的准备工作以后,直接跳转到虚函数入口地址处顺序执行函数体指令序列,直到从函数中返回。
3.动态绑定对性能的影响
1)虚函数表和虚指针的存在势必要增加内存空间的开销。
2)和普通函数调用相比,虚函数调用要多出一个步骤,增加运行时间的开销。
3)动态绑定会妨碍编译器通过内联优化代码,虚函数不能内联。