一、虚函数
类中用virtual关键字修饰的函数。
作用:主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
};
二、虚函数表
1、虚函数表格大小
先看空类的大小
//空类
class Vfptr_classA{
};
int main(){
Vfptr_classA Vfptr_A;
cout << "sizeof Vfptr_A :" << sizeof(Vfptr_A) <<endl;
return 0;
}
##打印
sizeof Vfptr_A :1
2、有一个虚函数的类
//有一个虚函数的类
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
};
int main(){
Vfptr_classA Vfptr_A;
cout << "sizeof Vfptr_A :" << sizeof(Vfptr_A) <<endl;
return 0;
}
##打印
sizeof Vfptr_A :4
3、有2个虚函数的类
//虚函数表 Vfptr
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
};
int main(){
Vfptr_classA Vfptr_A;
cout << "sizeof Vfptr_A :" << sizeof(Vfptr_A) <<endl;
return 0;
}
##打印
sizeof Vfptr_A :4
由此可以正式,类在内存中记录虚函数是以一个指针记录的,并且该指针指向一个数组,数组中装着的是虚函数的地址。同时,经过实验,发现32bit的编译器下,虚函数表的指针大小是4字节。64bit的编译器下,虚函数表的指针大小是8字节。
2、虚函数表在类内存中的位置
首先我们通过代码分析一下:
//虚函数表 Vfptr
class Vfptr_classA{
public:
int a;
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
void show(){
cout<<"show"<<endl;
}
};
int main(){
//虚函数表
Vfptr_classA Vfptr_A;
cout << "address Vfptr_A :" << &Vfptr_A <<endl;
cout << "address Vfptr_A.a :" << &Vfptr_A.a <<endl;
return 0;
}
##打印
address Vfptr_A :00EFF9F8
address Vfptr_A.a :00EFF9FC
可以看到,成员变量到类的首地址有4个字节(虚函数表指针),成员变量的地址在虚函数表后,由此可以通过取类变量地址的方式获取虚函数表地址。
会有同学问,如果有成员函数呢??成员函数不占用对象的内存。这是因为所有的函数都是存放在代码区的,不管是全局函数,还是成员函数。
3、如何利用虚函数表调用虚函数
直接上代码
//虚函数表 Vfptr
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
};
typedef void (*fun)(); //定义函数指针
int main(){
//虚函数表
Vfptr_classA Vfptr_A;
int * Vfptr_As = (int *)(*((int *)&Vfptr_A)); //获取虚函数表
//实际上是一个数组
for(int i = 0 ; Vfptr_As[i] != 0 ; ++i){
cout<< "fptr "<< i <<"address :"<<Vfptr_As[i] <<endl;
fun f = (fun)Vfptr_As[i];
f();
}
return 0;
}
##打印
fptr 0address :11080325
function1
fptr 1address :11080335
function2
注意:以上代码为32位,虚函数表指针为4字节。而若用64为编译器运行,则会获取地址失败,原因是64位编译器时,虚函数表指针为8字节,需要用long long类型指针获取。
Vfptr_classA Vfptr_A;
long long * Vfptr_As = (long long *)(*((long long *)&Vfptr_A));
cout<< "Vfptr :"<<Vfptr_As<<endl;
for(int i = 0 ; Vfptr_As[i] != 0 ; ++i){
cout<< "fptr "<< i <<"address :"<<Vfptr_As[i] <<endl;
fun f = (fun)Vfptr_As[i];
f();
}
##打印
fptr 0address :140695608234229
function1
fptr 1address :140695608233989
function2
4、继承类多态(虚函数重写)
1、继承
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
};
class Vfptr_classB : public Vfptr_classA{
public:
virtual void function3(){
cout<<"function3"<<endl;
}
int b;
};
##主函数执行
Vfptr_classB Vfptr_B;
cout << "sizeof Vfptr_B :" << sizeof(Vfptr_B) <<endl;
cout << "address Vfptr_B :" << &Vfptr_B <<endl;
cout << "address Vfptr_B.b :" << &Vfptr_B.b <<endl;
##打印
sizeof Vfptr_B :8
address Vfptr_B :009DFB74
address Vfptr_B.b :009DFB78
由上可知,派生类B在头部也有一张虚函数表,大小为4字节(32bit),成员变量在后。而通过调试看
发现,派生类头部共用基类的虚函数列表,而派生类新增加的虚函数会保存在基类的虚函数后面。
2、重写
class Vfptr_classB : public Vfptr_classA{
public:
virtual void function3(){
cout<<"function3"<<endl;
}
void function1(){
cout<<"ClassB function1"<<endl;
}
int b;
};
##主函数执行
int main(){
Vfptr_classB Vfptr_B;
int * Vfptr_Bs = (int *)(*((int *)&Vfptr_B));
cout<< "Vfptr :"<<Vfptr_Bs<<endl;
for(int i = 0 ; Vfptr_Bs[i] != 0 ; ++i){
cout<< "fptr "<< i <<"address :"<<Vfptr_Bs[i] <<endl;
fun f = (fun)Vfptr_Bs[i];
f();
}
return 0;
}
##打印
fptr 0address :8589982
ClassB function1
fptr 1address :8589957
function2
fptr 2address :8589637
function3
发现变化了,由打印顺序看,由于派生类B重写了基类A的function1,重写后的函数会被替换到虚函数列表中基类A的function1的位置。看一下调试
证实了我们的猜测。
3、多重继承
1、重写一个基类的虚函数
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
};
class Vfptr_classC{
public:
virtual void function4(){
cout<<"function4"<<endl;
}
};
class Vfptr_classB : public Vfptr_classA , public Vfptr_classC{
public:
virtual void function3(){
cout<<"function3"<<endl;
}
void function1(){
cout<<"ClassB function1"<<endl;
}
int b;
};
int main(){
Vfptr_classB Vfptr_B;
cout << "sizeof Vfptr_B :" << sizeof(Vfptr_B) <<endl;
cout << "address Vfptr_B :" << &Vfptr_B <<endl;
cout << "address Vfptr_B.b :" << &Vfptr_B.b <<endl;
return 0;
}
##打印
sizeof Vfptr_B :12
address Vfptr_B :00B3F74C
address Vfptr_B.b :00B3F754
打开调试内存
可以看到,此时在B的内存中,头部是基类A的虚函数表,跟着的是基类B的虚函数表,它们各占用4个字节。同时,重写基类A的function1函数,会覆盖原来基类A的function1函数在虚函数表的位置。
猜想:在B中重写基类C的function4,是否也会覆盖基类C中的funciton4在虚函数表的位置??好,我们继续做实验。
2、重写两个基类中的虚函数
class Vfptr_classA{
public:
virtual void function1(){
cout<<"function1"<<endl;
}
virtual void function2(){
cout<<"function2"<<endl;
}
};
class Vfptr_classC{
public:
virtual void function4(){
cout<<"function4"<<endl;
}
};
class Vfptr_classB : public Vfptr_classA , public Vfptr_classC{
public:
virtual void function3(){
cout<<"function3"<<endl;
}
void function1(){
cout<<"ClassB function1"<<endl;
}
void function4(){
cout<<"ClassB function4"<<endl;
}
int b;
};
int main(){
Vfptr_classB Vfptr_B;
cout << "sizeof Vfptr_B :" << sizeof(Vfptr_B) <<endl;
cout << "address Vfptr_B :" << &Vfptr_B <<endl;
cout << "address Vfptr_B.b :" << &Vfptr_B.b <<endl;
return 0;
}
##打印
sizeof Vfptr_B :12
address Vfptr_B :00B3F74C
address Vfptr_B.b :00B3F754
猜想没错。
总结:
虚函数是动态多态(程序运行时多态,重载为静态多态)。实现方式为:
1、当类自身有虚函数,则会创建一张虚函数表,放置于类的开头,占用4个字节内存(64位为8bit)。
2、当类的父类中有虚函数,则派生类会继承父类的虚函数表,同时若派生类有新的虚函数,则会在父类的虚函数表后面追加。
3、当派生类为多重继承,并且多个父类都有虚函数,则会全部继承父类的虚函数表,顺序以继承顺序,并且派生类中新增的虚函数只会添加在第一个父类虚函数表中。
4、当派生类重写父类的虚函数,则重写后的函数会取缔原来虚函数在虚函数表中的位置。
4、虚函数运用时的理解
接着上,3.2(重写两个基类中的虚函数)中的例子
主程序中执行
##主程序中执行
Vfptr_classA * ptrA = new Vfptr_classB();
ptrA ->function1();
##打印
ClassB function1
猜想:
声明一个基类A的指针ptrA ,实例化出派生类B的对象。
而ptrA 指针类型是基类A,则只能索引到派生类B中的基类A的部分:
即此时,ptrA 应该指向的是派生类B中函数列表
但是
当我们调试的时候发现
ptrA指针指向B的整个内存空间,只是不能通过
这种方式调用其他派生类的函数。虽然我们依然可以通过地址偏移,找到B空间的第二张表(基类C的虚函数表),但不建议这样用,代码可读性变差。
Vfptr_classA * ptrA = new Vfptr_classB();
int * Vfptr_Bs = (int *)(*((int *)ptrA + 1)); //通过把(int *)ptrA + 1来获取下一张表
for(int i = 0 ; Vfptr_Bs[i] != 0 ; ++i){
cout<< "fptr "<< i <<"address :"<<Vfptr_Bs[i] <<endl;
fun f = (fun)Vfptr_Bs[i];
f();
}
三、虚继承
虚继承和虚函数不相关!!!
虚继承是喂了解决菱形继承问题的一种手段。
1、菱形继承问题
class Vbptr_classA{
public:
int a;
};
class Vbptr_classB : public Vbptr_classA{
public:
int b;
};
class Vbptr_classC : public Vbptr_classA{
public:
int c;
};
class Vbptr_classD : public Vbptr_classB , Vbptr_classC{
public:
int d;
};
int main()
{
Vbptr_classD D;
D.a = 1;
return 0;
}
当D调用基类A中的a成员,则会报错
原因是:D实例化时会先实例化B,C 。B,C实例化又会先实例化基类A,则最终,D中会存在两个int a;
2、虚继承
class Vbptr_classA{
public:
int a;
};
class Vbptr_classB : virtual public Vbptr_classA{
public:
int b;
};
class Vbptr_classC : virtual public Vbptr_classA{
public:
int c;
};
class Vbptr_classD : public Vbptr_classB , Vbptr_classC{
public:
int d;
};
int main()
{
Vbptr_classD D;
return 0;
}
观察内存
一看,同样是B里面有A,C里面也有A,后面多了一个基类A。但是细心的就可以发现,B中的A的地址与C中A的地址是与最后面的基类A的地址是一致的。所以我们可以得出结论,以这种方式的虚继承时,B和C中会创建一张虚基表,记录的是基类A的位置,记录方式是以地址+偏移量的方式记录,而真正A的实例化部分会放在最后。则由此就可以解决空间拷贝的问题。
3、虚基表(虚表)的理解
未写完。。