C++中的多重继承与虚继承的问题
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,ostream的vptr。而 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++中的多重继承与虚继承的问题的更多相关文章
- 多重继承,虚继承,MI继承中虚继承中构造函数的调用情况
先来测试一些普通的多重继承.其实这个是显而易见的. 测试代码: //测试多重继承中派生类的构造函数的调用顺序何时调用 //Fedora20 gcc version=4.8.2 #include < ...
- C++ Primer 学习笔记_95_用于大型程序的工具 --多重继承与虚继承
用于大型程序的工具 --多重继承与虚继承 引言: 大多数应用程序使用单个基类的公用继承,可是,在某些情况下,单继承是不够用的,由于可能无法为问题域建模,或者会对模型带来不必要的复杂性. 在这些情况下, ...
- C++ Primer 有感(多重继承与虚继承)
1.多重继承的构造次序:基类构造函数按照基类构造函数在类派生列表中的出现次序调用,构造函数调用次序既不受构造函数初始化列表中出现的基类的影响,也不受基类在构造函数初始化列表中的出现次序的影响.2.在单 ...
- C++ Primer 笔记——多重继承与虚继承
1.在多重继承中,基类的构造顺序与派生类列表中基类的出现顺序保持一致,与初始值列表中的顺序无关. 2.在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数.但是如果从多个基类中继承了相同 ...
- 【c++】多重继承与虚继承
派生类的构造函数初始化列表将实参分别传递给每个直接基类,其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始化列表中基类的顺序无关. 类型转换与多个基类 编译器不会在派生类向基 ...
- C++学习之多重继承与虚继承
一.多重继承 我们知道,在单继承中,派生类的对象中包含了基类部分 和 派生类自定义部分.同样的,在多重继承(multiple inheritance)关系中,派生类的对象包含了每个基类的子对象和自定义 ...
- C++继承,多重继承,虚继承的构造函数以及析构函数的调用顺序问题
#include <iostream> using namespace std; class A{ int data_a; public: A(){ data_a = ; cout < ...
- Linux Debugging(四): 使用GDB来理解C++ 对象的内存布局(多重继承,虚继承)
前一段时间再次拜读<Inside the C++ Object Model> 深入探索C++对象模型,有了进一步的理解,因此我也写了四篇博文算是读书笔记: Program Transfor ...
- C++多重继承与虚拟继承
本文只是粗浅讨论一下C++中的多重继承和虚拟继承. 多重继承中的构造函数和析构函数调用次序 我们先来看一下简单的例子: #include <iostream> using namespac ...
随机推荐
- 分页控件(PageControl)的相关属性说明
UIPageControl *pageControl = [[UIPageControl alloc] init]; pageControl.center = CGPointMake(w * ); p ...
- Abp Zero——前端如何新增功能模块
为适应不同开发人员,abp rezo的UI实现了spa和mpa两套: spa--Single-page Application(单页面应用),默认"http://localhost/Acco ...
- MySQL防注入[待续]
1.将输入的参数变成整数 $id = isset[$_GET['tid']]?$_GET['tid']+0:0; 可以防止:"*.php?tid=3 or 1"这样的语句.
- Oracle 学习笔记3:新建数据库没有scott用户解决办法
新建一个数据库,若选择Oracle组件时,没有选择实例方案,完成后进行口令管理,默认列表中是找不到scott用户解锁的.若要解锁scott用户,可以进行如下操作: 使用system或者sys连接数据库 ...
- 三、android中Handle类的用法
当我们在处理下载或是其他需要长时间执行的任务时,如果直接把处理函数放Activity的OnCreate或是OnStart中,会导致执行过程中整个Activity无响应,如果时间过长,程序还会挂掉.Ha ...
- MySQL之建设工程监管信息系统
--创建SelfStudy数据库 CREATE DATABASE ConstructionDB ON PRIMARY --创建主数据库文件 ( NAME=' ConstructionDB', --数据 ...
- raphael画图
// 在坐标(10,50)创建宽320,高200的画布 var paper = Raphael(10, 50, 320, 200); // 在坐标(x = 50, y = 40)绘制半径为 10 的圆 ...
- S(tuple)类及可选(Optional)类型型
元组将多个值组合为单个值.元组内的值可以是任意 类型,各元素不必是相同的类型.元组在作为函数返 回值时尤其有用. 1.定义方法1 let http404Error= (404,"Not Fo ...
- 【学习笔记】【C语言】char类型
1. 存储细节 ASCII单字节表(双字节GBK\GB2312\GB18030\Unicode) 2. 常见错误 char c = A; char c = "A"; char c ...
- "Could not load file or assembly 'DTcms.Web.UI' or one of its dependencies. 拒绝访问。" 的解决办法
出现的问题提示如下: