#include<iostream>
using namespace std;
class Base {
public:
Base(int data)
:ma(data)
{}
void show() {
cout << "Base::show()" << endl;
}
void show(int) {
cout << "Base::show(int)" << endl;
}
protected:
int ma;
};
class Derive :public Base {
public:
Derive(int data)
:Base(data)
, mb(data)
{}
void show() {
cout << "Derive::show()" << endl;
}
protected:
int mb;
};
int main() {
Derive d(50);
Base* pb = &d;
pb->show();//call Base::show (0FF1037h)
pb->show(5);//call Base::show (0FF12F3h)
//静态绑定,编译时期就确定好调用函数
cout << sizeof(Base) << endl;
cout << sizeof(Derive) << endl;
cout << typeid(pb).name() << endl;
cout << typeid(*pb).name() << endl;
return 0;
}
虚函数
如果类里定义了一个虚函数,那么编译阶段,编译器给这个类类型产生一个唯一的vftable虚函数表,其中存储的内容主要是RTTI指针和虚函数的地址
Base vftable
RTTI指针:run-time type information 运行时的类型信息,指向一个保存类型的字符串"Base"
偏移量:大部分情况下是0
虚函数的地址:&Base::show() &Base::show(int)
总结:
1.当程序运行时,每一张虚函数表都会加载到.rodata(只读数据段)区
2.拥有虚函数类型的类大小和对象大小不再仅仅是成员变量的大小,其内存开始部分还要加上一个vfptr指针指向该类类型vftable虚函数表中,虚函数地址的起始部分
该类定义的所有对象,它们的vfptr都指向同一张vftable虚函数表中,虚函数地址的起始部分
3.一个类中只要有一个虚函数,就会生成vfptr,拥有再多的虚函数不会增加对象内存大小,但会增加虚函数表的大小
4.如果派生类中的方法,和基类继承来的某个方法,返回值、函数名、参数列表都相同,并且基类中该方法是virtual虚函数,那么派生类中的该方法自动处理成虚函数。这两个成员方法间的关系是『覆盖』
所以派生类Derive也会生成虚函数表
Derive vftable
RTTI指针:指向"Derive"(实际是一个RTTI类型的对象)
0
虚函数地址:&Base::show(int) &Derive::show(),Derive::show()将Base::show()覆盖了
覆盖:指虚函数表中虚函数地址的覆盖
pb->show();//Base::show()->Derive::show()
如果发现Base::show()是普通函数,那么调用的一定是基类的成员,进行静态绑定call Base::show (函数地址)
如果发现Base::show()是虚函数,那么进行动态绑定
mov eax,dword ptr[pb]//取pb内存的前四个字节,即vfptr指向虚函数地址
mov ecx,dword ptr[eax]
call ecx//虚函数的地址
pb->show(int);//虚函数,依然是动态绑定,Base::show(int)
cout << sizeof(Base) << endl;//4->8
cout << sizeof(Derive) << endl;//8->12
cout << typeid(pb).name() << endl;//class Base* 不变(静态类型语言)
cout << typeid(*pb).name() << endl;//class Base->class Derive
如果Base没有虚函数,*pb识别编译时期的类型Base
如果Base有虚函数,*pb识别运行时期的类型。访问对象中的vfptr,即派生类虚函数表中RTTI中保存的类型
偏移量
指vfptr在对象内存中的偏移量,在起始部分4个字节那么偏移量就是0
实现虚函数的条件?
1.虚函数会产生函数地址保存在vftable中,因此虚函数依赖对象,对象必须存在(vfptr->vftable->虚函数地址)
2.构造函数不能为虚函数(构造函数完成才有对象)。构造函数中调用虚函数也不会发生动态绑定(调用任何函数都是静态绑定),因为一个派生类构造过程中,先调用基类的构造函数,再调用派生类构造函数
3.static静态成员方法不能为虚函数,因为它不需要对象
4.析构函数可以成为虚函数
#include<iostream>
using namespace std;
class Base {
public:
Base(int data)
:ma(data)
{
cout << "Base()" << endl;
}
~Base() {
cout << "~Base()" << endl;
}
void show1() {
cout << "call Base::show()" << endl;
}
virtual void show2() {
cout << "call Base::show()" << endl;
}
protected:
int ma;
};
class Derive :public Base {
public:
Derive(int data)
:Base(data)
, mb(data)
{
cout << "Derive()" << endl;
}
~Derive() {
cout << "Derive()" << endl;
}
protected:
int mb;
};
int main() {
Base* pb = new Derive(20);
pb->show1();//静态绑定
pb->show2();//动态绑定
delete pb;//打印发现派生类的析构函数没有被调用!
/*pb->~Base();基类析构函数是普通函数,所以进行了静态绑定
call Base::~Base()
*/
/*Derive d(20);
Base* pb = &d;
pb->show();*/
return 0;
}
所以将基类析构函数写为虚函数
基类析构函数是虚函数,派生类析构函数自动成为虚函数
virtual ~Base() {
cout << "~Base()" << endl;
}
/*将基类析构函数写为虚函数后,派生类析构函数自动成为虚函数
派生类和基类的析构函数视为同名函数,派生类析构函数会覆盖基类析构函数,但是释放完派生类内存后还是会再去释放基类内存
*/
基类析构函数必须写为虚函数的情况:
基类的指针/引用指向堆上new出来的派生类对象,delete(基类指针)调用析构函数必须发生动态绑定,否则会使派生类析构函数没有被调用到