一、虚函数与静态绑定、动态绑定问题
虚函数: 在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数。
注意:
1.一个类里面定义了虚函数,那么编译阶段,编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata区(只读数据区)。
2.一个类里面定义了虚函数,那么这个类定义的对象,其运行时,内存中开始部分,多存储一个vfptr虚函数指针,指向相应类型的虚函数表vftable。一个类型定义的n个对象它们的vfptr指向都是同一张虚函数表。
3.一个类里面虚函数的个数不影响的对象内存大小,影响的是虚函数表的大小。
4.如果派生类中的方法和基类继承来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是virtual虚函数,那么派生类的这个方法自动处理成虚函数,即覆盖关系。
静态绑定(函数调用):编译时期的函数的调用,绑定的是对普通函数的调用。
动态绑定(函数调用):运行时期的函数的调用,绑定的是对虚函数的调用。
覆盖: 基类和派生类方法,返回值,函数模已经参数列表都相同,而且基类的方法是虚函数。那么派生类的方法自动处理成虚函数,它们之间称为覆盖关系。
案例1:静态绑定:我们先来看一个简单例子。
//Author:Mr.Rain
#include <iostream>
#include <typeinfo>
using namespace std;
class Base
{
public:
Base(int data = 10): 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 = 20):Base(data),mb(data){}
void show()
{
cout << "Derive::show()" << endl;
}
private:
int mb;
};
int main()
{
Derive d(50);
Base *pb = &d;
pb->show();
pb->show(10);
cout << sizeof(Base) << endl;//4
cout << sizeof(Derive) << endl;//8
cout << typeid(pb).name() << endl;//class Base *
cout << typeid(*pb).name() << endl;//class Base
return 0;
}
我们来看一看汇编指令:
pb->show();//静态绑定 call Base::show(01612DAh)
pb->show(10);//静态绑定 call Base::show(01612B2h)
编译期间将高级源代码编译为汇编码,指定了call Base::show(01612DAh)与call Base::show(01612B2h),即编译期间指定了函数的调用,这就是是静态绑定。
打印结果为:
案例2:动态绑定:我们向基类中的成员方法添加virtual关键字。
class Base
{
public:
Base(int data = 10): ma(data){}
virtual void show()//虚函数
{
cout << "Base::show()" << endl;
}
virtual void show(int)//虚函数
{
cout << "Base::show(int)" << endl;
}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 20):Base(data),mb(data){}
void show()
{
cout << "Derive::show()" << endl;
}
private:
int mb;
};
添加了virtual关键字,将其变为虚函数,那么到底发生了什么事情?
如果一个类里面定义了虚函数,那么编译阶段,编译器需给这个类类型产生一个唯一的vftable虚函数表。虚函数表中主要存储的内容就是RTTI指针和虚函数的地址。 基类与派生类中vftable表如图所示:
我们再次来打印一下:
我们看下上面动态绑定与静态绑定执行流程:
分析一下:pb->show();
pb->show();编译阶段发现show()为Base类型,到基类作用域查看Base::show(),若show()为普通函数,就进行静态绑定call Base::show();若编译阶段指针为Base类型,到基类作用域查看Base::show(),发现show()为虚函数,就进行动态绑定。将虚函数表的地址vfptr放入eax寄存器,将vfptr存的地址的4字节内存&Derive::show()地址放入ecx寄存器,寄存器的地址我们不知道。
1.mov eax,dword ptr[pb] 将虚函数表的地址vfptr放入eax寄存器
2.mov ecx,dword ptr[eax] 将vfptr存的地址的4字节内存&Derive::show()地址放入ecx
3.call ecx 调用ecx,取虚函数地址
只有在运行时候才知道寄存器的地址,找到哪个地址调用哪个函数,这就是静态绑定。pb->show(int)同理,是动态绑定。
sizeof变化的原因:
多了virtual,即虚函数会多vfptr指针,因此sizeof()大小也会变。
pb的类型:Base->有没有虚函数
如果Base没有虚函数,*pb识别的就是编译时期的类型。 *pb就是Base类型;
如果Base有虚函数,*pb识别的就是运行时期的类型:RTTI类型,即Derive类型;
我们也可以通过VS命令来查看:
cl -d1reportSingleClassLayout(输出对象内存布局信息)
那么我们了解了虚函数,哪些函数不能实现成虚函数呢?
1.要成为虚函数,函数地址就要记录在虚函数表中,即虚函数能产生函数地址,存储在vftable中。
2.vfptr指针需要依赖对象,对象必须存在。找到虚函数表才能找到虚函数地址,虚函数表存储在虚函数指针vfptr中,虚函数指针vfpte在对象的内存中存放。
构造函数:(调用任何函数都是静态绑定的)
1.构造函数不能称为虚函数
2.构造函数中调用虚函数也不会发生静态绑定。
static静态成员方法:静态成员方法调用不依赖对象,因此也不能成为虚函数。
二、虚析构函数
析构函数:可以成为虚函数,调用时候对象存在。
虚析构函数:在析构函数前加上virtual关键字。
什么时候需要把基类的析构函数必须实现成虚函数?
基类的指针(引用)指向堆上new出来的派生类对象的时候,delete调用析构函数的时候,必须发生动态绑定,否则会导致派生类的析构函数无法调用。
来看一下这段代码:
class Base
{
public:
Base(int data) :ma(data)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
virtual void show()
{
cout << "call Base::show()" << endl;
}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data):Base(data), mb(data),ptr(new int(data))
{
cout << "Derive()" << endl;
}
~Derive()
{
delete ptr;
cout << "~Derive() " << endl;
}
private:
int mb;
int *ptr;
};
int main()
{
Base *pb = new Derive(10);
pb->show();//静态绑定pb Base* *pb Derive
delete pb;
return 0;
}
执行结果:执行出现问题
执行出现问题:派生类的析构函数没有被调用到,内存泄露。
我们来分析一下问题: pb的类型是Base类型,因此delete调用析构函数先去Base中找Base::~Base(),对于析构函数的调用就是静态绑定,之间编译,没有机会调用派生类的析构函数,最后发生内存泄露。
解决方案: 将基类的析构函数定义为虚析构函数,派生类的析构函数自动成为虚函数。 pb的类型是Base类型,调用析构时去Base中找Base::~Base发现它为虚函数,发生动态绑定。派生类的虚函数表中:&Derive:: ~derive,用派生类析构函数将自己部分进行析构,再调用基类的析构函数将基类部分析构。
class Base
{
public:
Base(int data) :ma(data)
{
cout << "Base()" << endl;
}
virtual~Base()
{
cout << "~Base()" << endl;
}
virtual void show()
{
cout << "call Base::show()" << endl;
}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data):Base(data), mb(data),ptr(new int(data))
{
cout << "Derive()" << endl;
}
~Derive()
{
delete ptr;
cout << "~Derive() " << endl;
}
private:
int mb;
int *ptr;
};
执行结果:成功析构。
三、深入动态绑定问题
虚函数的调用一定就是动态绑定吗?
在类的构造函数当中,调用虚函数,也是静态绑定。构造函数中调用其他函数,包括虚函数,不会发生动态绑定。
注意:
1.用对象本身调用虚函数,是静态绑定。
2.动态绑定:虚函数前面必须是指针或引用调用才能发生动态绑定:基类指针指向基类对象,基类指针指向派生类对象,都是动态绑定。
3.如果不是通过指针或者引用来调用虚函数,那就是静态绑定。
案例1:我们来看个例子:发生的是静态绑定
class Base
{
public:
Base(int data = 0):ma(data){}
virtual void show()
{
cout << "Base::show()" << endl;
}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int data = 0):Base(data), mb(data){}
void show()
{
cout << "Derive::show()" << endl;
}
private:
int mb;
};
int main()
{
Base b;
Derive d;
//静态绑定
b.show();//虚函数 call Base::show();
d.show();//虚函数 call Derive::show();
return 0;
}
转到反汇编:发现其为静态绑定
得出结论:用对象本身调用虚函数,是静态绑定。
案例2:基类指针指向基类对象
Base b;
Derive d;
Base *pb1 = &b;//基类指针指向基类对象
pb1->show();
转到反汇编:为动态绑定
案例3:基类指针指向派生类对象
Base b;
Derive d;
Base *pb2 = &d;//基类指针指向派生类对象
pb2->show();
转到反汇编:为动态绑定
案例4:基类指针指向基类对象,基类指针引用派生类对象
Base &rb1 = b;
rb1.show();
Base &rb2 = d;
rb2.show();
与指针一样:都是动态绑定。
案例5:派生类指针调用派生类对象,派生类引用调用派生类对象
Derive *pd1 = &d;
pd1->show();
Derive &rd1 = d;
rd1.show();
转到反汇编:都是动态绑定。
案例6:强制类型转换
Derive *pd2 = (Derive*)&b;
pd2->show();
动态绑定:但最终调用的是Base::show();