虚继承与虚基类
作用
- 虚继承用于解决多继承条件下的菱形继承问题(浪费存储空间、存在二义性)。
- 假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B–>D 这条路径,还是来自 A–>C–>D 这条路径。
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: public A{
protected:
int m_b;
};
//直接基类C
class C: public A{
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){
m_a = a; } //命名冲突
void setb(int b){
m_b = b; } //正确
void setc(int c){
m_c = c; } //正确
void setd(int d){
m_d = d; } //正确
private:
int m_d;
};
使用
//间接基类A
class A{
protected:
int m_a;
};
//直接基类B
class B: virtual public A{
//虚继承
protected:
int m_b;
};
//直接基类C
class C: virtual public A{
//虚继承
protected:
int m_c;
};
//派生类D
class D: public B, public C{
public:
void seta(int a){
m_a = a; } //正确
void setb(int b){
m_b = b; } //正确
void setc(int c){
m_c = c; } //正确
void setd(int d){
m_d = d; } //正确
private:
int m_d;
};
-
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类,本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
-
-
虚继承的好处:子类中只含有一个间顶层基类的数据,解决数据冗余。
-
建议只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。
虚函数
特征
- 在基类中以virtual关键字开头的成员函数
- 提供了一种接口界面。允许在派生类中对基类的虚函数重新定义。
- 普通函数(非类成员函数)不能是虚函数
- 静态函数(static)不能是虚函数
- 构造函数不能是虚函数(因为在调用构造函数时,虚表指针并没有在对象的内存空间中,必须要构造函数调用完成后才会形成虚表指针)
- 析构函数可以是虚函数
- 内联函数不能是表现多态性时的虚函数,解释见:虚函数(virtual)可以是内联函数(inline)吗?
使用
class Shape // 形状类
{
public:
virtual double calcArea() // 加入vritual关键字
{
...
}
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea(); // virtual在子类函数中非必需
...
};
class Rect : public Shape // 矩形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
Shape * shape2 = new Rect(5.0, 6.0);
// 若无virtual关键字,则调用的是父类的函数
shape1->calcArea(); // 调用圆形类里面的方法
shape2->calcArea(); // 调用矩形类里面的方法
delete shape1;
shape1 = nullptr;
delete shape2;
shape2 = nullptr;
return 0;
}
纯虚函数
作用
- 在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。作为接口而存在。
- 含有纯虚函数的类被称为抽象类。
使用
virtual int A() = 0;
问题
虚函数和纯虚函数的区别
- 类里如果声明了虚函数,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被覆盖,这样的话,这样编译器就可以使用后期绑定来达到多态了。纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。
- 虚函数在子类里面也可以不重载的;但纯虚函数必须在子类去实现。
虚函数指针、虚函数表
- 当子类没有重写父类的虚函数时(没有多态发生时):
class Shape
{
public:
virtual double calcArea() {
return 0;} // 虚函数
protected:
int m_iEdge;
}
class Circlr : public Shape
{
public:
Circle(double r);
private:
double m_dR;
}
-
-
在实例化一个Shape对象的时候,除了有数据成员m_iEdge,还会有另外一个成员vftable_ptr——虚函数表指针,指向虚函数表。
-
虚函数表会与Shape类的定义同时出现,同样会占有一定空间。Shape实例化出的所有对象都只有一个虚函数表。
扫描二维码关注公众号,回复: 12686128 查看本文章 -
虚函数表中含有虚函数指针,指向calcArea()的入口地址。
-
调用Shape的calcArea()函数时,可以先通过虚函数表指针找到虚函数表,再通过位置偏移找到相应的虚函数的入口地址。
-
-
Circle类虽然没有重写父类Shape的虚函数,但却继承了父类Shape的虚函数。
-
在实例化一个Circle对象的时候,仍会产生一个虚函数指针和虚函数表。
-
Circle的虚函数表与父类Shape的虚函数表并不相同,但虚函数表中的虚函数地址却是一样的,都是0x3355。因此,Circle只能访问到父类的calcArea()函数。
-
当子类定义了父类的虚函数时(发生多态):
class Circlr : public Shape
{
public:
Circle(double r);
virtual double calcArea(); // 定义子类虚函数
private:
double m_dR;
}
-
-
由于子类Circle定义了自己的虚函数,因此在自己的虚函数表中虚函数指针覆盖掉了父类Shape的虚函数指针的值。
虚析构函数
作用
- 虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象(释放基类指针时可以释放掉子类的空间,防止内存泄漏)。如下面的代码:
#include<iostream>
using namespace std;
class CShape //基类
{
public:
~CShape() {
cout << "CShape::destrutor" << endl; }
};
class CRectangle : public CShape //派生类
{
public:
~CRectangle() {
cout << "CRectangle::destrutor" << endl; }
};
int main()
{
CShape* p = new CRectangle;
delete p;
return 0;
}
程序输出结果如下:
CShape::destrutor
输出结果说明,delete p;
只引发了 CShape 类的析构函数被调用,没有引发 CRectangle 类的析构函数被调用。这是因为该语句是静态联编的,编译器编译到此时,不可能知道此时 p 到底指向哪个类型的对象,它只根据 p 的类型是 CShape * 来决定应该调用 CShape 类的析构函数。
假设程序需要对 CRetangle 类的对象进行计数,如果此处不调用 CRetangle 类的析构函数,就会导致计数不正确。
改写上面程序中的 CShape 类,在析构函数前加 virtual 关键字,将其声明为虚函数:
class CShape{
public:
virtual ~CShape() {
cout << "CShape::destrutor" << endl; }
};
则程序的输出变为:
CRectangle::destrutor
CShape::destrutor
说明 CRetangle 类的析构函数被调用了。实际上,派生类的析构函数会自动调用基类的析构函数。
-
只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字声明,都自动成为虚析构函数。
-
一般来说,一个类如果定义了虚函数,则最好将析构函数也定义成虚函数。
-
析构函数可以是虚函数,但是构造函数不能是虚函数。
原理
-
- 当父类的析构函数被定义成虚函数时,子类的析构函数会被默认的定义成析构函数。
- 子类的虚函数表中的析构函数的地址就会覆盖掉父类的析构函数的地址,从而实现调用。
- 子类的析构函数执行完后会自动的执行父类的析构函数。