1. 虚函数指针vfptr与虚函数表vftable
class Base {
public:
Base(int data) :ma(data) { cout << "Base(int data)" << endl; }
~Base() { cout << "~Base()" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
virtual void show(int val) { cout << "Base::show(int)" << endl; }
protected:
int ma;
};
class Deriver :public Base {
public:
Deriver(int data) :mb(data), Base(data) { cout << "Deriver(int data)" << endl; }
~Deriver() { cout << "~Deriver()" << endl; }
void show(){ cout << "Deriver::show()" << endl; }
private:
int mb;
};
1.1 引出虚函数指针(virtual function point)以及虚函数表(virtual function table)
int main() {
Base b(99);
Deriver d(88);
cout << sizeof(Base) << endl;
cout << sizeof(Deriver) << endl;
cout << sizeof(b) << endl;
cout << sizeof(d) << endl;
return 0;
}
打印执行结果:
8
12
8
12
我们不难发现,本应该有4字节大小的Base类,大小却是8,多出来4字节,对象大小也多出4字节。
原因是因为:
- 当一个类含有虚函数时,每个类都会有一张虚函数表,在编译时,由编译器生成,存放在.rodata段的内存里,这样整个类就比没有虚函数的类大了4字节;
- 当一个类有虚函数表时,构造出来的对象的前4字节都会有一根虚函数指针(vfptr具有高优先级,所以在前4字节),用来保存虚函数表的地址,所以构造出来的对象会比没有虚函数时多出4个字节大小
- 注意,虚函数表是属于类的,而不是属于对象的,是该类构造出所有对象所共享的。
1.2 虚函数表中存放的内容
- RTTI 运行时的类型信息,当前属于哪个类的表,保存的就是哪个类的类型;
- 对象中vfptr起始地址在整个对象内存中偏移量,因为vfptr具有最高的优先级,一般放在对象的前4字节,所以偏移量一般为0;
- 该类中存在的虚函数的地址。


注意:
子类继承父类,如果子类含有虚函数,那么也会产生虚函数表,如果子类中的方法重写了父类的虚函数,那么子类中该函数地址将覆盖掉继承来的虚函数表中的那个父类虚函数的地址,即上图中&Base::show()在子类的虚函数表中被&Deriver::show()覆盖,由于子类没有重写&Base::show(int),所以不会发生覆盖
2. 哪些方法可以被写成虚函数?
3. 静态(编译期确定调用哪个函数)绑定(不存在virtual函数时的绑定)
class Base{
public:
Base(int data):ma(data){ cout << "Base(int data)" << endl; }
~Base(){ cout << "~Base()" << endl; }
void show(){ cout <<"Base::show()" <<endl; }
void show(bool val) { cout <<"Base::show(bool)" <<endl; }
private:
int ma;
};
class Deriver:public Base{
public:
Deriver(int data):mb(data),Base(data){ cout << "Deriver(int data)" << endl; }
~Deriver(){ cout << "~Deriver()" << endl; }
void show(){ cout <<"Deriver::show()" <<endl; }
private:
int mb;
};
int main(){
Base b(100);
Deriver d(1000);
b.show(); //Base
d.show(); //Deriver
Base *bp = &d;
Base *dp = &b;
bp->show();// Base
dp->show();// Base
Deriver *bpp = static_cast<Deriver*>(&b);
Deriver *dpp = &d;
bpp->show(); //Deriver
dpp->show(); //Deriver
return 0;
/*
不含虚函数,不会进行运行时的类型检查,只会根据指针或者引用的类型进行判断调用基类还是派生类的方法
*/
}
执行结果:
Base(int data)
Base(int data)
Deriver(int data)
Base::show()
Deriver::show()
Base::show()
Base::show()
Deriver::show()
Deriver::show()
~Deriver()
~Base()
~Base()
show()调用点处的汇编代码:
bp->show();// Base
001E2A58 mov ecx,dword ptr [bp]
001E2A5B call type_info::name (01E14C4h)
dp->show();// Base
001E2A60 mov ecx,dword ptr [dp]
001E2A63 call type_info::name (01E14C4h)
Deriver* bpp = static_cast<Deriver*>(&b);
001E2A68 lea eax,[b]
001E2A6B mov dword ptr [bpp],eax
Deriver* dpp = &d;
001E2A6E lea eax,[d]
001E2A71 mov dword ptr [dpp],eax
bpp->show(); //Deriver
001E2A74 mov ecx,dword ptr [bpp]
001E2A77 call Base::show (01E14C9h)
dpp->show(); //Deriver
001E2A7C mov ecx,dword ptr [dpp]
001E2A7F call Base::show (01E14C9h) //编译就确定了调用哪个函数
总结:
- 正如我们预料之中的,p和*p的类型与Base相关,与后面指向实体类型无关,与指针定义的类型相关,因为指针或者引用指向的对象中不存在vfptr,没有RTTI也就不会访问去RTTI,仅根据指针的类型来判断调用哪个类中的方法,而在编译器就确定了类型;
- 根据汇编我们不难发现,编译时,我们的
point->show()代码被编译为call Base::show和Deriver::show或者根据对象类型来调用相应方法,也就是编译期就已经确定调用的是Base::show还是Deriver::show ,这又被称为静态的绑定。
4. 动态绑定(存在重写时的绑定),深入理解RTTI的作用以及多态的构成原理
class Base {
public:
Base(int data) :ma(data) { cout << "Base(int data)" << endl; }
~Base() { cout << "~Base()" << endl; }
virtual void show() { cout << "Base::show()" << endl; }
virtual void show(bool val) { cout << "Base::show(bool)" << endl; }
private:
int ma;
};
class Deriver :public Base {
public:
Deriver(int data) :mb(data), Base(data) { cout << "Deriver(int data)" << endl; }
~Deriver() { cout << "~Deriver()" << endl; }
void show() { cout << "Deriver::show()" << endl; }
private:
int mb;
};
int main() {
Base b(100);
Deriver d(1000);
b.show(); //Base
d.show(); //Deriver
Base* bp = &d;
Base* dp = &b;
bp->show(); //Deriver
dp->show(); //Base
Deriver* bpp = static_cast<Deriver*>(&b);
Deriver* dpp = &d;
bpp->show(); //Base
dpp->show(); //Deriver
Base& rb = b;
Base& rd = d;
rb.show(); // Base
rd.show(); //Deriver
return 0;
/*
无论前面什么类型的指针或者引用,只需要关心右边实体中存放的RTTI即可,因为继承中有虚函数
然后运行时根据RTTI的类型调用相应类中的虚函数
*/
}
执行结果:
Base(int data)
Base(int data)
Deriver(int data)
Base::show()
Deriver::show()
Deriver::show()
Base::show()
Base::show()
Deriver::show()
Base::show()
Deriver::show()
~Deriver()
~Base()
~Base()
show()调用点处的汇编代码:
b.show(); //Base
001A2BC1 call 4DA82BC5
d.show(); //Deriver
001A2BC6 shr al,1
001A2BC8 call 8E1A2BB1
Base* bp = &d;
001A2BCD inc ebp
001A2BCE ror byte ptr [ecx+458DC445h],1
Base* dp = &b;
001A2BD4 in al,89h
001A2BD6 inc ebp
001A2BD7 mov eax,8BC4458Bh
bp->show(); //Deriver
...
001A2BE7 cmp esi,esp
001A2BE9 call __RTC_CheckEsp (01A12A3h)
dp->show(); //Base
...
001A2BFD cmp esi,esp
001A2BFF call __RTC_CheckEsp (01A12A3h)
Deriver* bpp = static_cast<Deriver*>(&b);
001A2C04 lea eax,[b]
001A2C07 mov dword ptr [bpp],eax
Deriver* dpp = &d;
001A2C0A lea eax,[d]
001A2C0D mov dword ptr [dpp],eax
bpp->show(); //Base
...
001A2C1F cmp esi,esp
001A2C21 call __RTC_CheckEsp (01A12A3h)
dpp->show(); //Deriver
...
001A2C35 cmp esi,esp
001A2C37 call __RTC_CheckEsp (01A12A3h)
Base& rb = b;
001A2C3C lea eax,[b]
001A2C3F mov dword ptr [rb],eax
Base& rd = d;
001A2C42 lea eax,[d]
001A2C45 mov dword ptr [rd],eax
rb.show(); // Base
...
001A2C57 cmp esi,esp
001A2C59 call __RTC_CheckEsp (01A12A3h)
rd.show(); //Deriver
...
001A2C6D cmp esi,esp
001A2C6F call __RTC_CheckEsp (01A12A3h)
注意:__RTC_CheckEsp 为C运行时的类型检查,即就是程序在运行时会查看RTTI的类型,然后根据RTTI的类型调用类中的虚函数。
有一些比较旧的编译器在这里直接回call eax,调用一个寄存器,这就是静态绑定和动态绑定的差别,动态绑定call的内容只有在程序真正运行起来才知道里面是哪个函数,静态绑定在编译阶段就已经确定了。
5. 多态的构成条件
- 类中存在虚函数,不论是当前类自身的,还是从父类继承而来的,只要存在虚函数
- 用指针或者引用去调用该虚函数即可构成。
补充:用对象调用虚函数不可构成多态,一定是指针和引用。
6.多态发生时,如何判断调用的是谁的虚函数?
一般来说,查看指针或者引用所指向的对象的内存中存放的RTTI是哪个类就调用哪个类中的虚函数。
版权声明:本文为KingOfMyHeart原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。