多重继承

在多重继承中,基类的构造函数的调用次序即不受派生类构造函数初始化列表中出现的基类构造函数的影响,也不受基类在构造函数初始化列表中的出现次序的影响,它按照基类在类派生列表中的出现次序依次调用相应的基类构造函数。析构函数与构造顺序逆序进行。

多重继承中,派生类的指针或引用可以转换为其任意基类的指针或者引用、因此,这种转换更可能遇到二义性的问题。

在多重继承中,成员函数中使用的名字查找首先在函数本身进行,如果不能在本地找到名字,就继续在成员的类中查找,然后同时(并行)查找所有基类继承子树。多重继承的派生类有可能从两个或者多个基类继承同名成员,对该成员不加限定的使用是二义性。

注意:多重继承中首先发生名字查找。你可能会感到吃惊的是,即使两个继承的同名函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公有或者受保护的,也是错误的。或者在一个类给定义了函数,而在另一个类中没有定义,调用仍是错误的。

下面给出实例:

 #include <iostream>
class Base1 {
public:
void print() {}
void display() {}
void show() {}
};
class Base2 {
public:
void print(int) {}
void show(int);
private:
void display(int) {}
};
class Derived: public Base1, public Base2 {
};
int main () {
Derived d;
d.print();
d.display();
d.show();
return 0;
}

  编辑结果错误,这是因为程序中函数print()造成了二义性,程序不知道调用哪一个。解决这种二义性的方法可以是通过制定使用哪个类的版本(即带上类名的前缀)来解决。但最好的解决方法是在解决二义性的派生类中定义函数的一个版本。

虚继承

在多继承中,例如有四个类,类A、B、C、D,其中类B和类C继承类A,类D分别继承类B和类C。这里如果类D采用常规继承,则每个类D对象可能包含两个类A子对象,这个是我们不希望出现的程序希望类B和类C可以共享一个类A对象。

在C++中通过使用虚继承来解决这类问题、虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。

通过在派生类列表中包含关键字virtual设置虚基类,例如:

 class istream : public virtual ios {…};
class ostream : virtual public ios {…};
class iostream : public istream, public ostream {…};

  假定通过对个派生路劲继承名为X的成员,有下面三种可能性:

1)如果在每个路径中X表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例;

2)如果在某个路劲中X是虚基类的成员,而另一路劲中X是后代派生类的成员,也没有二义性,因为特定派生类实例的优先级高于共享虚基类实例。

3)如果沿每个继承路劲X表示后代派生类的不同成员,则该成员的直接访问是二义性的。

例如:

 #include <iostream>
class B {
public:
void print() {
std::cout 《 "B" 《 std::endl;
}
};
class D1: public virtual B {
};
class D2: public virtual B {
public:
void print() {
std::cout 《 "D2" 《 std::endl;
}
};
class DD: public D1, public D2 {
};
int main () {
DD d;
d.print(); // ok: call D2::print
return 0;
}

特殊的初始化语义

通常,每个类只初始化自己的直接基类。在应用与虚基类的时候,这个初始化策略失败。如果使用常规规则,就可能会多次初始化虚基类。类将沿着包含该锡基雷的每个继承路劲初始化。

为了解决这个重复的初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,由最底层派生类的构造函数初始化虚基类。

虽然最底层派生类初始化虚基类,但是任何直接或者间接继承虚基类的类一般也必须为该基类提供自己的初始化式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式只在创建中间类型的对象时使用。

例如,我们四个类:ZooAnimal, Bear, Raccoon和Panda,它们之间构造一个继承层次:Bear和Raccoon继承ZooAnimal,Panda继承Bear和Raccoon.那么,它们的构造函数就形如:

Bear::Bear(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Bear") {}
    Raccoon::Raccoon(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Raccoon") {}
    Panda::Panda(std::string name, bool onExhibit): ZooAnimal(name, onExhibit, "Panda"), Bear(name, onExhibit), Raccoon(name, onExhibit) {}

当创建Panda对象的时候,构造过程如下:

1)首先使用构造函数初始化列表中指定的初始化式构造ZooAnima部分;

2)接下来,构造Bear部分(在派生列表中Bear在前)。忽略Bear初始化列表中用于ZooAnima构造函数的初始化式;

3)然后,构造Raccoon部分,再次忽略ZooAnima初始化式;

4)最后,构造Panda部分。

如果Panda构造函数不是显式初始化ZooAnima基类,就使用ZooAnima默认构造函数;如果ZooAnima没有默认构造函数,则代码出错。

无论虚基类出现在继承层次中的任何地方,总是在构造非虚基类之前构造虚基类。

例如:有下面的继承关系:

class Character { };
    class BookCharater: public Character { };
    class ToyAnimal { };
    class TeddyBear: public BookCharacter, public Bear, public virtual ToyAnimal { };

按声明次序检查直接基类,确定是否存在虚基类。上例中,首先检查BookCharater的继承子树,然后检查Bear的继承子树,最后检查ToyAnimal的继承子树。按照从根类开始向下到最底层派生类的次序检查每个子树。在这里一次检查到ZooAnimal和ToyAnimal为虚基类。

TeddyBear的虚基类的构造次序是先ZooAnimal再ToyAnimal(检查的顺序)。一旦构造了虚基类,就按照声明次序调用非虚基类的构造函数:首先是BookCharacter,它导致调用Character构造函数,然后是Bear。在这里,由最底层派生类TeddBear指定用于ZooAnimal和ToyAnimal的初始化。

当然,对于析构函数的调用顺序与构造函数相反。

示例代码如下:

#include <iostream>
class Character {
public:
Character() {
std::cout 《 "Character Constructor" 《 std::endl;
}
~Character() {
std::cout 《 "Character Destructor" 《 std::endl;
}
};
class BookCharacter: public Character {
public:
BookCharacter() {
std::cout 《 "BookCharacter Constructor" 《 std::endl;
}
~BookCharacter() {
std::cout 《 "BookCharacter Destructor" 《 std::endl;
}
};
class ZooAnimal {
public:
ZooAnimal() {
std::cout 《 "ZooAnimal Constructor" 《 std::endl;
}
~ZooAnimal() {
std::cout 《 "ZooAnimal Destructor" 《 std::endl;
}
};
class Bear: public virtual ZooAnimal {
public:
Bear() {
std::cout 《 "Bear Constructor" 《 std::endl;
}
~Bear() {
std::cout 《 "Bear Destructor" 《 std::endl;
}
};
class ToyAnimal {
public:
ToyAnimal() {
std::cout 《 "ToyAnimal Constructor" 《 std::endl;
}
~ToyAnimal() {
std::cout 《 "ToyAnimal Destructor" 《 std::endl;
}
};
class TeddyBear: public BookCharacter, public Bear, public virtual ToyAnimal {
public:
TeddyBear() {
std::cout 《 "TeddyBear Constructor" 《 std::endl;
}
~TeddyBear() {
std::cout 《 "TeddyBear Destructor" 《 std::endl;
}
};
int main () {
TeddyBear tb;
return 0;
}

  运行结果:

    ZooAnimal Constructor
    ToyAnimal Constructor
    Character Constructor
    BookCharacter Constructor
    Bear Constructor
    TeddyBear Constructor
    TeddyBear Destructor
    Bear Destructor
    BookCharacter Destructor
    Character Destructor
    ToyAnimal Destructor
    ZooAnimal Destructor
    Terminated with return code 0
    Press any key to continue …
 
 

C++中虚拟继承的更多相关文章

  1. 虚拟继承C++

    C++中虚拟继承的概念 为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类.这时从不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数 ...

  2. 关于C++中的虚拟继承的一些总结

    1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念.虚拟基类是为解决多重继承而出现的.如:类D继承自类B1.B2,而类B1.B2都继承自类A,因此在类D中两次出现类A中的变量和函数.为了节省内存 ...

  3. C++继承 派生类中的内存布局(单继承、多继承、虚拟继承)

    今天在网上看到了一篇写得非常好的文章,是有关c++类继承内存布局的.看了之后获益良多,现在转在我自己的博客里面,作为以后复习之用. ——谈VC++对象模型(美)简.格雷程化    译 译者前言 一个C ...

  4. c++面试常用知识(sizeof计算类的大小,虚拟继承,重载,隐藏,覆盖)

    一. sizeof计算结构体 注:本机机器字长为64位 1.最普通的类和普通的继承 #include<iostream> using namespace std; class Parent ...

  5. C++ 虚拟继承

    1.为什么要引入虚拟继承 虚拟继承是多重继承中特有的概念.虚拟基类是为解决多重继承而出现的.如:类D继承自类B1.B2,而类B1.B2都继 承自类A,因此在类D中两次出现类A中的变量和函数.为了节省内 ...

  6. 继承虚函数浅谈 c++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。

    本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...

  7. 关于虚拟继承类的大小问题探索,VC++ 和 G++ 结果是有区别的

    昨天笔试遇到个 关于类占用的空间大小的问题,以前没怎么重视,回来做个试验,还真发现了问题,以后各位笔试考官门,出题时请注明是用什么编译器. vc6/vc8 cl 和 Dev-C 的g++ 来做的测试: ...

  8. c++,为什么要引入虚拟继承

      虚拟基类是为解决多重继承而出现的.   以下面的一个例子为例: #include <iostream.h> #include <memory.h> class CA { i ...

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

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

随机推荐

  1. 基于event 实现的线程安全的优先队列(python实现)

    event 事件是个很不错的线程同步,以及线程通信的机制,在python的许多源代码中都基于event实现了很多的线程安全,支持并发,线程通信的库 对于优先队列的堆实现,请看<python下实现 ...

  2. 将文件内容转化为byte数组返回

    如何将文件内容转化为byte数组并返回呢?对于这个问题,我献上我第一次成功的代码~ package com.succez.task1; import java.io.ByteArrayOutputSt ...

  3. Unity使用C++作为游戏逻辑脚本的研究(二)

    文章申明:本文来自JacksonDunstan的博客系列文章内容摘取和翻译,版权归其所有,附上原文的链接,大家可以有空阅读原文:C++ Scripting( in Unity) 上一篇文章写完,有同学 ...

  4. xml与object 之间的ORM

    xml映射为object对象,同时object对象,以xml来表示: public class Tools { private static XmlNodeList SelectNodes(strin ...

  5. windows SSH Tunnel实施日记

    1.准备条件:SSH跳板服务器一个.软件:Putty,CCProxy 2.putty建立SSH Tunnel:先在session那儿把服务器地址填好,到Tunnel界面上,选Dynamics和Auto ...

  6. FPGA图像处理之行缓存(linebuffer)的设计一

    FPGA图像处理之行缓存(linebuffer)的设计一 作者:OpenS_Lee 1 背景知识 在FPGA数字图像处理中,行缓存的使用非常频繁,例如我们需要图像矩阵操作的时候就需要进行缓存,例如图像 ...

  7. Angular路由——路由守卫

    一.路由守卫 当用户满足一定条件才被允许进入或者离开一个路由. 路由守卫场景: 只有当用户登录并拥有某些权限的时候才能进入某些路由. 一个由多个表单组成的向导,例如注册流程,用户只有在当前路由的组件中 ...

  8. web.config中的configSource

    在大型项目中,可能存在第三方类库的配置如:log4.net,AOP框架Unity,WCF等,或是自定义的配置,造成web.config内容过多,不易维护,影响Config初始化. 这时我们可以使用co ...

  9. Django REST framework+Vue 打造生鲜超市(十三)

    目录 生鲜超市(一)    生鲜超市(二)    生鲜超市(三) 生鲜超市(四)    生鲜超市(五)    生鲜超市(六) 生鲜超市(七)    生鲜超市(八)    生鲜超市(九) 生鲜超市(十) ...

  10. String的substring()用于截取字符串

    substring() 用于返回一个字符串的子字符串,即截取字符串功能. substring()常用的重载方法如下: substring(int beginIndex,int endIndex) 意思 ...