继承是C++的一大特点,我们通过菱形继承和菱形虚继承对继承进行进一步的分析。
【菱形继承】
创建一个基类A让B1和B2公有继承于它,让C公有继承B1和B2。
class A
{
public :
A()
:a(1)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
int a;
};
class B1 : public A
{
public :
B1()
:b1(2)
{
cout << "B1()" << endl;
}
~B1()
{
cout << "~B1()" << endl;
}
int b1;
};
class B2 : public A
{
public:
B2()
:b2(3)
{
cout << "B2()" << endl;
}
~B2()
{
cout << "~B2()" << endl;
}
int b2;
};
class C : public B1,public B2
{
public:
C()
:c(4)
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
int c;
};
我们可以通过内存观察C类对象的内存结构,如下图:
我们可以观察到在C类对象创建成功后,C类对象包含了B1和B2类成员,B1和B2的成员中又包含了A类的成员,那么在对象的构造时具体是怎样的过程呢?
利用反汇编查看汇编代码进行分析,如下图:
去掉了一些其他信息,发现在创建C类对象时,先去调用了B1类的构造函数,在B1的构造函数中又去调用了A类的构造函数,返回C类的构造函数中后接着调用B2的构造函数,在B2的构造函数中又去调用A的构造函数。
析构函数的调用与构造函数的调用顺序相反(先创建后销毁)
我们可以通过打印信息验证进一步验证:
注意:可以通过汇编代码看到,在执行派生类构造函数的初始化列表之前就去调用了基类的构造函数。
问题:
- 当要通过C类对象去访问A类的成员时会出现二义性问题,因为编译器不知道你要访问的是B1类中所包含的A类成员还是B2类中所包含的A类成员。
- 在C类对象构造的构成中,A类的构造函数被执行了两次(B1和B2分别执行一次),会创建出两个完全一样的问题,这就造成了数据冗余问题。
【菱形虚拟继承】
c++中引入了
虚继承来解决上面的二义性问题和数据冗余的问题。虚拟继承的目的是让某个类作出声明,承诺愿意共享它的基类。
创建一个基类A让B1和B2虚继承于它,让C公有继承B1和B2,这时B1和B2就共享了基类A。
class A
{
public :
A()
:a(1)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
int a;
};
class B1 :virtual public A
{
public :
B1()
:b1(2)
{
cout << "B1()" << endl;
}
~B1()
{
cout << "~B1()" << endl;
}
int b1;
};
class B2 : virtual public A
{
public:
B2()
:b2(3)
{
cout << "B2()" << endl;
}
~B2()
{
cout << "~B2()" << endl;
}
int b2;
};
class C : public B1,public B2
{
public:
C()
:c(4)
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
int c;
};
我们可以观察C类对象在内存中的存储结构:
B1和B2都被实现为对A类的虚继承,说明B1和B2共享了基类的成员,但是我们可以通过上图观察到A类成员既没有和B1类连续也没有和B2类连续,那么怎样访问B1和B2中继承于A类的成员呢?这就用到了B1和B2成员中多出来的那个地址,我们打开内存窗口进行观察:
注意:如果一个派生类虚继承于一个基类那么在创建派生类对象时,会在对象前四个字节中加入一个地址,指向一个表,这个表中存放了两个偏移量,相对于自己的偏移量和相对于基类的偏移量,可以通过偏移量来找到共享的基类成员。
进一步对创建C类对象的创建过程进行分析:
我们通过查看汇编代码进行分析
可以通过汇编代码看到,在创建C类对象时,在执行C类构造函数的初始化列表之前分别调用了A类、B1类和B2类的构造函数,并没有在B1和B2的构造函数中调用A类的构造函数,也就保证了只创建出了一份A类成员。
我们可以通过打印信息进一步验证: