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对象在内存中的存放形式。

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的存储形式:

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

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;

类继承关系图:

上面的代码是来自《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;
};

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

B1::B1()!<

V1::V1()!<

D1::D1()!<

B2::B2()!<

V2::V2()!<

B3::B3()!<

D2::D2()!<

M1::M1()!<

M2::M2()!<

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

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

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

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

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

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

C++中的多重继承与虚继承的问题的更多相关文章

  1. 多重继承,虚继承,MI继承中虚继承中构造函数的调用情况

    先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include < ...

  2. C++ Primer 学习笔记_95_用于大型程序的工具 --多重继承与虚继承

    用于大型程序的工具 --多重继承与虚继承 引言: 大多数应用程序使用单个基类的公用继承,可是,在某些情况下,单继承是不够用的,由于可能无法为问题域建模,或者会对模型带来不必要的复杂性. 在这些情况下, ...

  3. C++ Primer 有感(多重继承与虚继承)

    1.多重继承的构造次序:基类构造函数按照基类构造函数在类派生列表中的出现次序调用,构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响.2.在单 ...

  4. C++ Primer 笔记——多重继承与虚继承

    1.在多重继承中,基类的构造顺序与派生类列表中基类的出现顺序保持一致,与初始值列表中的顺序无关. 2.在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数.但是如果从多个基类中继承了相同 ...

  5. 【c++】多重继承与虚继承

    派生类的构造函数初始化列表将实参分别传递给每个直接基类,其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始化列表中基类的顺序无关. 类型转换与多个基类 编译器不会在派生类向基 ...

  6. C++学习之多重继承与虚继承

    一.多重继承 我们知道,在单继承中,派生类的对象中包含了基类部分 和 派生类自定义部分.同样的,在多重继承(multiple inheritance)关系中,派生类的对象包含了每个基类的子对象和自定义 ...

  7. C++继承,多重继承,虚继承的构造函数以及析构函数的调用顺序问题

    #include <iostream> using namespace std; class A{ int data_a; public: A(){ data_a = ; cout < ...

  8. Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)

    前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transfor ...

  9. C++多重继承与虚拟继承

    本文只是粗浅讨论一下C++中的多重继承和虚拟继承. 多重继承中的构造函数和析构函数调用次序 我们先来看一下简单的例子: #include <iostream> using namespac ...

随机推荐

  1. OpenSSL加解密

    http://www.caole.net/diary/des.html Table of Contents OpenSSL - DES Summary DES使用的例子 另一个带注释的例子 另一段Co ...

  2. Linux 网卡设备驱动程序设计(3)

    三.网络子系统深度分析 用户程序通过网络发送这个网络数据包 通过 SCI 协议无关接口 协议栈 <   UDP的实现  会选择路由 <    IP的实现  会建立这个邻居子系统,建立邻居信 ...

  3. Ajax异步操作集合啦(阿贾克斯)

    /* * Ajax的核心操作对象是xmlHttpRequest * 简化操作步骤:实例化一个xmlHttpRequest对象 ==> 发送请求 ==> 接受响应 ==> 执行回调 * ...

  4. nodejs7-buffer

    buffer:js在后台操作的必须用到二进制,buffer类就是用于帮助我们处理这种情况   创建buffer对象: new Buffer(size):创建buff对象,有length属性 buf.f ...

  5. HTML_创建易用的Web表单

    首先创建一个表单域集合fieldset fieldset元素允许Web开发者将主题相关的表单组合在一起 <fieldset></fieldset> 要说明的是本例子中每个表单都 ...

  6. Jquery 格式化时间

    我们常常会通过datetime得到时间,但是网页前台往往会显示不同的时间 如:2013-12-15 2013年12月23日 2013 12 15 等多种显示效果,这就需要我们把时间格式化一下. 下面是 ...

  7. Android-短信验证

    一.mob.com移动开发者服务平台(ShareSDK)的认识 该平台主要是致力于解决移动开发者的实际需求,同时也致力于一些第三方平台的框架支持,那么这样我们可以更方便的将一些功能集成到我们的App中 ...

  8. Swift数据类型及数据类型转换

    整型  Swift 提供 8.16.32.64 位形式的有符号及无符号整数.这些整数类型遵循 C 语言的命名规 约,如 8 位无符号整数的类型为 UInt8,32 位 有符号整数的类型为 Int32 ...

  9. UI2_视图切换ViewController

    // // SubViewController.h // UI2_视图切换 // // Created by zhangxueming on 15/7/3. // Copyright (c) 2015 ...

  10. gulp基础使用总结

    gulp 安装 1 检测电脑有没有安装node 执行 $ node -v $ npm -v 如果没有安装的话,可以到https://nodejs.org/en/download/下载安装. 2 全局安 ...