一、虚基类和虚继承
虚基类:被虚继承的类,就称为虚基类。
virtual作用:
1.virtual修饰了成员方法是虚函数。
2.可以修饰继承方式,是虚继承。被虚继承的类就称为虚基类。
vfptr:一个类有虚函数,这个类生成的对象就有vfptr,指向vftable。
vbptr:在派生类中从基类虚继承而来。
vftable:第一行为向上偏移量,第二行为虚基类指针离虚基类内存的偏移量。
vbtable:存放的RTTI指针,指向运行时RTTI信息与虚函数地址。
我们来看一个例子:
class A
{
public:
private:
int ma;
};
class B : public A
{
public:
private:
int mb;
};
//A a; 4个字节
//B b; 8个字节
这里我们的对象a占4个字节,对象8占8个字节。但如果我们给B的继承方式访问限定符前面加了一个virtual关键字。
我们使用命令:cl –d1reportSingleClassLayout[classname] xxx.cpp查看此时的内存布局。
类A被虚继承了,但内存布局没有变化。
我们再看一下类B,不是之前的8个字节,变为12个字节,多了一个vbptr指针。原来最上面应该为ma与mb,但是现在多了一个vbptr(虚基类指针),ma跑到派生类最后面去了。vbptr指向的是vbtable,vbtable第一行为0,第二行为虚基类指针到虚基类数据的偏移量。
当我们遇到虚继承时候。要考虑派生类B的内存布局时,首先我们先不考虑虚继承。类B继承了基类的ma,还有自己的mb;当我们基类被虚继承后,基类变为虚基类,虚基类的数据一定要在派生类数据最后面,再在最上面添加一个vbptr。派生类的内存就由这几部分来构成。
虚基类指针(vbptr)指向虚基类表(vbtable),vbtable第一行为向上的偏移量,因为vbptr在该派生类内存起始部分,因此向上的偏移量为0;第二行为向下的偏移量(vbptr离虚基类数据的偏移量),原来基类的数据放到最后,找ma的时候还是在最开始找,但ma被移动,根据偏移的字节就可以找到。
二、虚基类和虚继承出错情况分析
那么当我们虚基类指针与虚函数指针在一起出现的时候会发生什么呢?
调用是没有问题的,但是delete会出错。
class A
{
public:
virtual void func()
{
cout << "call A::func" << endl;
}
private:
int ma;
};
class B : virtual public A
{
public:
void func()
{
cout << "call B::func()" << endl;
}
private:
int mb;
};
int main()
{
//基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。
A *p = new B();//B::vftable
p->func();
delete p;
return 0;
}
如图:调用成功,但delete时会出错。
我们分析一下:
B的内存布局:B首先从A中获取vfptr与ma,B中还有自己的mb;此时A被虚继承,A中所有的东西都移动到派生类最后面,最上面补一个vbptr,vbptr指向vbtable,vfptr指向vftable;我们基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。普通情况下,派生类内存布局先是基类,在是派生类,基类指针指向派生类对象时,基类指针指向的就是派生类内存的起始部分。但是虚继承下,基类称为虚基类,虚基类的数据在派生类最后面,原地方补上vbptr,此时再用基类指针指向派生类对象时候,基类指针永远指向派生类基类部分的起始地址。虚基类一开始就是vfptr,能够用p指向的对象访问vfptr与vftable的原因。释放内存时候出错,因为对象开辟是在最上面即绿色部分,但是p所持有的是虚基类的地址,delete时从虚基类起始地址delete,因此出错。
命令验证如下:
我们验证一下内存地址:
class A
{
public:
virtual void func()
{
cout << "call A::func" << endl;
}
void operator delete(void *ptr)
{
cout << "operator delete p:" << ptr << endl;
free(ptr);
}
private:
int ma;
};
class B : virtual public A
{
public:
void func()
{
cout << "call B::func()" << endl;
}
void* operator new(size_t size)
{
void *p = malloc(size);
cout << "operator new p:" << p << endl;
return p;
}
private:
int mb;
};
//A a; 4个字节
//B b; 8个字节
int main()
{
A *p = new B();//B::vftable
cout << "main p:" << p << endl;
p->func();
delete p;
return 0;
}
0115D9B8为分配的内存的起始地址,我们用基类指针指向派生类对象一定是指向派生类内存基类的起始部分:0115D9C0刚好比0115D9B8多了8个字节,是vbptr与mb,但是delete时候从0115D9C0开始释放,因此崩溃。
Windows的VS下这样写会出错,但是Linux下的g++delete时会自动偏移到new内存的起始部分,进行内存free(),不会出错。
我们如果在栈上开辟内存,基类指针指向派生类对象,出了作用域自己进行析构,这样是没有问题的。
B b;
A *p = &b;//B::vftable
cout << "main p:" << p << endl;
p->func();
三、菱形继承问题
多重继承:可以复用多个基类的代码到派生类中。
但是多重继承中也会出现问题:我们来看一个经典的问题:
菱形继承问题:会导致派生类有多份间接基类的数据,可以采用虚继承来解决。 A为B、C的基类,B从A单继承而来,C从A也是单继承而来;D是B和C多继承而来,D有两个基类分别为B和C。A称为D的间接基类,D也有A的数据。
假设A中有ma变量,B从A继承来并且有自己的mb,C从A继承来并且有自己的mc,D从B与C多继承而来,从B继承来了ma与mb,从C继承来了ma与mc,D也有自己的属性md;那么就出现了问题,我们的间接基类D有多份ma属性,这就是菱形继承问题。
当然,我们多重继承还会出现别的问题:
半圆形继承问题: B从A单一继承而来,C有一个基类B而且同时还从A继承而来。A到B为单继承,C为多继承。
假设ma为类A的属性,B从A继承而来,B有自己的mb;C从B与A继承而来,C有自己的mc,又从A继承来了ma,从B继承来了mb与mc;产生了菱形继承同样的问题,C中出现了两个ma属性。
多重继承虽然可以复用多个基类的代码到派生类中,但是会出现许多问题,因此C++开源代码上很少见到多重继承。
我们用代码来实现一下菱形继承问题:
class A
{
public:
A(int data):ma(data)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int ma;
};
class B : public A
{
public:
B(int data):A(data), mb(data)
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
protected:
int mb;
};
class C : public A
{
public:
C(int data):A(data), mc(data)
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
protected:
int mc;
};
class D : public B , public C
{
public:
D(int data):B(data), C(data), md(data)
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
protected:
int md;
};
int main()
{
D d(10);
return 0;
}
我们画一下d对象的内存布局。
D能看见B,C与md,所以D在构造时调用B,C的构造及ma的初始化。ma的初始化在B与C的构造函数中进行,因此D内存为20个字节。打印一下:
先是A的构造,B的构造,又是A的构造,C的构造,最后是D的构造;析构顺序与其相反。我们发现,D这个派生类中调用了两次A的构造,数据重复,浪费内存,这种情况必须被杜绝。
那么如何处理这种问题呢?就需要虚继承来处理了。
所有从A继承而来的地方都采用虚继承,A就为虚基类。
此时:B从A虚继承而来,A为虚基类,A::ma移动到派生类最后面,在A::ma位置上补一个vbptr;C也是从A虚继承而来,A::ma移动到派生类最后面,但发现已经有一份同样的虚基类的数据,那么C的A::ma丢弃,在A::ma位置存放vbptr。此时派生类中只有一份基类A::ma的数据,以后访问都是同一个ma;同时ma的初始化由D来负责。虚继承就可以解决多重继承中的菱形继承与半圆形继承出现的问题了。
代码修改为如下:
class A
{
public:
A(int data):ma(data)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
protected:
int ma;
};
class B : virtual public A
{
public:
B(int data):A(data), mb(data)
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
protected:
int mb;
};
class C : virtual public A
{
public:
C(int data):A(data), mc(data)
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
protected:
int mc;
};
class D : public B , public C
{
public:
D(int data):A(data), B(data), C(data), md(data)
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
protected:
int md;
};
int main()
{
D d(10);
return 0;
}
打印结果:修改成功;A、B、C、D各初始化与析构一次。