C++中的多重继承与虚继承的问题

时间:2023-05-12 20:52:14

1.C++支持多重继承,但是一般情况下,建议使用单一继承.

类D继承自B类和C类,而B类和C类都继承自类A,因此出现下图所示情况:

A          A

\          /

B     C

\  /

D

而类D中会出现两次A。为节省内存空间,可以将B、C对A的继承定义为虚拟继承,而A就成了虚拟基类。又叫钻石继承,菱形继承,最后形成如下图所示情况:

A

/      \

B       C

\     /

D

2.在标准I/O库中的类都继承了一个共同的抽象基类ios,那个抽象基类管理流的条件状态并保存流所读写的缓冲区。istream和ostream类直接继承这个公共基类,库定义了另一个名为isotream的类,它同时继承istream和ostream,iostream类既可以对流进行读又可以对流进行写。如果I/O类型使用常规继承,则每个iostream对象可能包含两个ios子对象:一个包含在它的istream子对象中,另一个包含在它的 ostream子对象中。从设计角度讲,这个实现是错误的:iostream类想要对单个缓冲区进行读和写,它希望跨越输入和输出操作符共享条件状态。

3.多重继承遇到的问题:

//diamond.cpp
#include<iostream>
using namespace std;
class A{
public:
A (int x) : m_x(x) {}
int m_x;
};
class B : public A {
public:
B (int x) : A(x) {}
void set(int x) {
this -> m_x = x;
}
};
class C : public A {
public:
C (int x) : A(x) {}
int get(void) {
return this -> m_x;
}
};
class D : public B,public C {
public:
D (int x) : B(x),C(x) {}
};
int main(void) {
D d();
d.set();
cout << d.get() << endl;
return ;
}

这样的运行结果是10?还是20呢?结果是10,为什么?明明sets的是20,为什么get的还是10呢?

要解释这个问题那酒必须要先搞清楚,d对象在内存中是如何存放的,是怎样布局的。每一个子类都会有一个内存视图,在子类里都包含了它的基类子对象,下面是创建是d对象时,d对象在内存中的存放形式。

C++中的多重继承与虚继承的问题

D中,包含一个B类的基类子对象和一个C类型基类子对象,而B和C里各自有一个A类型基类子对象,所以可以看到,在d的内存布局中有两个A类型基类子对象。

set函数是类B的成员函数,在执行set函数时,this指针指向B(其实也是指向A,B从A继承,A存在B中的首地址),所以set执行后,改变的是B里的A类基类子对象的数据成员的值。同理,get函数得到的是C里A类基类子对象的数据成员的值。这样就可以理解这样的运行结果了。所谓钻石继承问题,就是公共基类对象在我们最终的子类对象中有多个副本,多份拷贝,当我们沿着不同的继承路径去访问公共基类子对象时结果会出现不一致。

而我们应该怎样解决这样的问题呢?采用虚继承。我们所期望的d的存储形式:

C++中的多重继承与虚继承的问题

我们需要按如下方式修改代码:

class B : virtual public A //虚继承

class C : virtual public A //虚继承

D(int x) : B(x),C(x),A(x) {}

在这个过程中,A对象只在D的初始化表中A(x)进行构造(虚基类最先被构造),而在B和C的初始化表中不再对A进行构造(实际上是都有一个指针指向了D中的A(x),来对A进行构造)。

4.虚继承与普通继承的区别:

假设derived 继承自base类,那么derived与base是一种“is a”的关系,即derived类是base类,而反之错误;

假设derived 虚继承自base类,那么derivd与base是一种“has a”的关系,即derived类有一个指向base类的vptr。

5.相关的面试题:

class stream
{
public:
stream(){cout<<"stream::stream()!"<<endl;}
}; class iistream:virtual stream
{
public:
iistream(){cout<<"istream::istream()!"<<endl;}
}; class oostream:virtual stream
{
public:
oostream(){cout<<"ostream::ostream()!"<<endl;}
}; class iiostream:public iistream,public oostream
{
public:
iiostream(){cout<<"iiostream::iiostream()!"<<endl;}
}; int main(int argc, const char * argv[])
{
iiostream oo;

输出结果:

程序运行的输出结果为:

stream::stream()!

istream::istream()!

ostream::ostream()!

iiostream::iiostream()!   

输出这样的结果是毫无悬念的!本来虚拟继承的目的就是当多重继承出现重复的基类时,其只保存一份基类。减少内存开销。其继承结构为:

stream

/               \

istream           ostream

\                 /

iiostream

这样子的菱形结构,使公共基类只产生一个拷贝。

2)而现在我们换种方式使用虚继承:

class stream
{
public:
stream(){cout<<"stream::stream()!"<<endl;}
}; class iistream:public stream
{
public:
iistream(){cout<<"istream::istream()!"<<endl;}
}; class oostream:public stream
{
public:
oostream(){cout<<"ostream::ostream()!"<<endl;}
}; class iiostream:virtual iistream,virtual oostream
{
public:
iiostream(){cout<<"iiostream::iiostream()!"<<endl;}
}; int main(int argc, const char * argv[])
{
iiostream oo;

其输出结果为:

stream::stream()!

istream::istream()!

stream::stream()!

ostream::ostream()!

iiostream::iiostream()!

从结果可以看到,其构造过程中重复出现基类stream的构造过程。这样就完全没有达到虚拟继承的目的。其继承结构为:

     stream                             stream

\                     /

istream    ostream

\                   /

iiostream

从继承结构可以看出,如果iiostream对象调用基类stream中的成员方法,会导致方法的二义性。因为iiostream含有指向其虚继承基类 istream,ostreamvptr。而 istream,ostream包含了stream的空间,所以导致iiostream不知道导致是调用那个stream的方法要解决改问题,可以指定vptr,即在调用成员方法是需要加上作用域,例如:

iiostream ii;
ii.f();//报错,二义性

ii.istream::f();//正确,显示前缀,加上了作用域

3)终极boss

class B1
{
public:
B1(){cout<<"B1::B1()!<"<<endl;}
void f() {cout<<"i'm here!"<<endl;}
}; class V1: public B1
{
public:
V1(){cout<<"V1::V1()!<"<<endl;}
}; class D1: virtual public V1
{
public:
D1(){cout<<"D1::D1()!<"<<endl;}
}; class B2
{
public:
B2(){cout<<"B2::B2()!<"<<endl;}
}; class B3
{
public:
B3(){cout<<"B3::B3()!<"<<endl;}
}; class V2:public B1, public B2
{
public:
V2(){cout<<"V2::V2()!<"<<endl;}
}; class D2:virtual public V2, public B3
{
public:
D2(){cout<<"D2::D2()!<"<<endl;}
}; class M1
{
public:
M1(){cout<<"M1::M1()!<"<<endl;}
}; class M2
{
public:
M2(){cout<<"M2::M2()!<"<<endl;}
}; class X:public D1, public D2
{
M1 m1;
M2 m2;
};
int main(int argc, const char * argv[])
{
X x;

类继承关系图:

C++中的多重继承与虚继承的问题

上面的代码是来自《Exceptional C++ Style》中关于继承顺序的一段代码。可以看到,上面的代码继承关系非常复杂,而且层次不是特别的清楚。而虚继承的加入更是让继承结构更加无序。不管怎么样,我们还是可以根据c++的标准来分析上面代码的构造顺序。c++对于创建一个类类型的初始化顺序是这样子的:

1.最上层派生类的构造函数负责调用虚基类子对象的构造函数。所有虚基类子对象会按照深度优先、从左到右的顺序进行初始化;

2.直接基类子对象按照它们在类定义中声明的顺序被一一构造起来;

3.非静态成员子对象按照它们在类定义体中的声明的顺序被一一构造起来;

4.最上层派生类的构造函数体被执行。

根据上面的规则,可以看出,最先构造的是虚继承基类的构造函数,并且是按照深度优先,从左往右构造。因此,我们需要将继承结构划分层次。显然上面的代码可以认为是4层继承结构。其中最顶层的是B1,B2类。第二层是V1,V2,V3。第三层是D1,D2.最底层是X。而D1虚继承V1,D2虚继承V2,且D1和D2在同一层。所以V1最先构造,其次是V2.在V2构造顺序中,B1先于B2.虚基类构造完成后,接着是直接基类子对象构造,其顺序为D1,D2.最后为成员子对象的构造,顺序为声明的顺序。构造完毕后,开始按照构造顺序执行构造函数体了。所以其最终的输出结果为:

B1::B1()!<

V1::V1()!<

B1::B1()!<

B2::B2()!<

V2::V2()!<

D1::D1()!<

B3::B3()!<

D2::D2()!<

M1::M1()!<

M2::M2()!<

从结果也可以看出其构造顺序完全符合上面的标准。而在结果中,可以看到B1重复构造。还是因为没有按照要求使用virtual继承导致的结果。要想只构造B1一次,可以将virtual全部改在B1上,如下面的代码:

class B1
{
public:
B1(){cout<<"B1::B1()!<"<<endl;}
void f() {cout<<"i'm here!"<<endl;}
}; class V1: virtual public B1 //public修改为virtual
{
public:
V1(){cout<<"V1::V1()!<"<<endl;}
}; class D1: public V1
{
public:
D1(){cout<<"D1::D1()!<"<<endl;}
}; class B2
{
public:
B2(){cout<<"B2::B2()!<"<<endl;}
}; class B3
{
public:
B3(){cout<<"B3::B3()!<"<<endl;}
}; class V2:virtual public B1, public B2 //public B1修改为virtual public B1
{
public:
V2(){cout<<"V2::V2()!<"<<endl;}
}; class D2: public V2, public B3
{
public:
D2(){cout<<"D2::D2()!<"<<endl;}
}; class M1
{
public:
M1(){cout<<"M1::M1()!<"<<endl;}
}; class M2
{
public:
M2(){cout<<"M2::M2()!<"<<endl;}
}; class X:public D1, public D2
{
M1 m1;
M2 m2;
};

C++中的多重继承与虚继承的问题

根据上面的代码,其输出结果为:

B1::B1()!<

V1::V1()!<

D1::D1()!<

B2::B2()!<

V2::V2()!<

B3::B3()!<

D2::D2()!<

M1::M1()!<

M2::M2()!<

由于虚继承导致其构造顺序发生比较大的变化。不管怎么,分析的规则还是一样。

上面分析了这么多,我们知道了虚继承有一定的好处,但是虚继承会增大占用的空间。这是因为每一次虚继承会产生一个vptr指针。空间因素在编程过程中,我们很少考虑,而构造顺序却需要小心,因此使用未构造对象的危害是相当大的。因此,我们需要小心的使用继承,更要确保在使用继承的时候保证构造顺序不会出错。下面我再着重强调一下基类的构造顺序规则:

1.最上层派生类的构造函数负责调用虚基类子对象的构造函数。所有虚基类子对象会按照深度优先、从左到右的顺序进行初始化;

2.直接基类子对象按照它们在类定义中声明的顺序被一一构造起来;

3.非静态成员子对象按照它们在类定义体中的声明的顺序被一一构造起来;

4.最上层派生类的构造函数体被执行。