C++的虚函数常常搞得人晕头转向,很多同学常常混淆不清,今天我们来解析C++的虚函数
基类和派生类的指向
我们来看这样一段代码
#include<iostream>
using namespace std;
class Base
{
public:
Base(int a):ma() {cout << "Base:Basee()"<<endl;}
~Base(){ cout << "Base::~Base()"<<endl;}
protected:
int ma;
};
class Derive : public Base
{
public:
Derive(int b):mb(b),Base(b)
{
cout << "Derive::Derive()"<<endl;
}
~Derive(){cout << "Derive::~Derive()"<<endl;}
priveta:
int mb;
};
int main()
{
Base b(10);
Derive d(20);
// Derive* pd = &b;派生类指针不能指向基类对象
Base* pb = &d;
Base& rb = d;
// Derive &ed = b;
return 0;
}
我们可以知道
- 基类的指针或者引用可以指向或者引用派生类对象
- 派生类的指针或者引用不可以指向或者引用基类对象
内存布局
#include<iostream>
using namespace std;
class A
{
public:
A(int a):ma() {cout << "A()"<<endl;}
~A(){ cout << "~A()"<<endl;}
protected:
int ma;
};
class B : public A
{
public:
B(int b):mb(b),A(b)
{
cout << "B()"<<endl;
}
~B(){cout << "~B()"<<endl;}
protected:
int mb;
};
class C : public A
{
public:
C(int c):mc(c),A(a)
{
cout << "B()"<<endl;
}
~B(){cout << "~B()"<<endl;}
protected:
int mb;
};
class E
{
public:
E(){}
~E(){}
protected:
int me;
};
class D:public B, public C,public E
{
public:
D(int d): md(d),B(d),C(d){cout << "D" <<endl;}
}
int main()
{
D d;
return 0;
}
正常的继承类内存布局是这样的
#include<iostream>
using namespace std;
class A
{
public:
A(int a):ma() {cout << "A()"<<endl;}
~A(){ cout << "~A()"<<endl;}
protected:
int ma;
};
class B : public A
{
public:
B(int b):mb(b),A(b)
{
cout << "B()"<<endl;
}
~B(){cout << "~B()"<<endl;}
protected:
int mb;
};
class C : public A
{
public:
C(int c):mc(c),A(a)
{
cout << "B()"<<endl;
}
~B(){cout << "~B()"<<endl;}
protected:
int mb;
};
class E
{
public:
E(){}
~E(){}
protected:
int me;
};
class D: virtual public B, public C,public E//这里D虚继承B
{
public:
D(int d): md(d),B(d),C(d){cout << "D" <<endl;}
}
int main()
{
D d;
return 0;
}
但是虚继承下内存布局会发生变化
- 布局非虚基类
- 布局虚基类
- 布局派生类
- 将虚基类转移到最下面,原位置填上vbptr
D的内存布局现在就成这个样子,B的内存布局在最下面,在原位置设置了vbptr
构造顺序
虚基类的构造优先级高,所以构造顺序为: A B A C E D
那如果把B类C类改成这样他的内存布局会是怎么样的呢?
class A
{
public:
A(int a):ma() {cout << "A()"<<endl;}
~A(){ cout << "~A()"<<endl;}
protected:
int ma;
};
class B : virtual public A
{
public:
B(int b):mb(b),A(b)
{
cout << "B()"<<endl;
}
~B(){cout << "~B()"<<endl;}
protected:
int mb;
};
class C : virtual public A
{
public:
C(int c):mc(c),A(a)
{
cout << "B()"<<endl;
}
~B(){cout << "~B()"<<endl;}
protected:
int mb;
};
这里会发生虚表合并,A的vbptr会合并到C的vbptr,这是C的vbtable下会有两个虚基类数据
#include<iostream>
using namespace std;
class Base
{
public:
Base(int a):ma() {cout << "Base()"<<endl;}
virtual void Show(cout << "ma" << ma <<endl;)
~Base(){ cout << "~Base()"<<endl;}
protected:
int ma;
};
class Derive : virtual public Base
{
public:
Derive(int b):mb(b),Base(b){}
protected:
int mb;
};
int main()
{
Base* pb = new Derive();
delete pb;
return 0;
}
释放失败
内存布局已经变化 无法找到析构函数
虚函数
为了解决以上的情况,我们要用到虚函数
如果对象有RTTI(运行时类型信息),编译器会使用RTTI,否则推演
虚函数表在编译期间确定,派生类的vfptr会覆盖基类的vfptr,虚表存放在.rodata段,运行时才把vfptr指向虚表,一个虚表属于一个类而不是一个对象。若一个同名函数不是虚函数,那么不存在虚表中。虚函数调用通过寄存器,运行时才得到函数地址,称作动态绑定,实现动多态。所以当我们用基类指针指向派生类对象时,调用派生类的同名虚函数
哪些不能为虚函数?
- 函数能取地址
- 必须依赖对象调用
如何判断动多态?
指针调用时,多为动多态。对象调用时,多为静多态
普通函数在编译期间已经确定地址,存在于符号表,不会加载于内存中。
虚函数存放于.rodata数据段,加载到内存,。通过对象找到虚表,再用寄存器返回入口地址。
指针调用虚函数
1.对象必须完整
继承关系中 析构函数和同名函数的覆盖关系是相同的
这样我们就可以正常的调用基类指针析构了