深入解析C++编程中基类与基类的继承的相关知识

基类
继承过程将创建一个新的派生类,它由基类的成员加上派生类添加的任何新成员组成。在多重继承中,可以构建一个继承关系图,其中相同的基类是多个派生类的一部分。下图显示了此类关系图。

单个基类的多个实例
在该图中,显示了 CollectibleString 和 CollectibleSortable 的组件的图形化表示形式。但是,基类 Collectible 位于通过 CollectibleSortableString 路径和 CollectibleString 路径的 CollectibleSortable 中。若要消除此冗余,可以在继承此类类时将其声明为虚拟基类。

多个基类
如多重继承中所述,类可以从多个基类派生。在多重继承模型中(其中,类派生自多个基类),使用 base-list 语法元素指定基类(请参阅概述中的“语法”一节)。例如,可以指定派生自 CollectionOfBook 和 Collection 的 Book 的类声明:

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {
};
class Book {};
class CollectionOfBook : public Book, public Collection {
 // New members
};

指定基类的顺序并不重要,只不过在某些情况下,将调用构造函数和析构函数。在这些情况下,指定基类的顺序将影响:
构造函数进行初始化的顺序。如果您的代码依赖要在 Book 部分之前初始化的 CollectionOfBook 的 Collection 部分,则规范的顺序很重要。按照 base-list 中指定类的顺序执行初始化。
调用析构函数以进行清理的顺序。同样,如果在销毁另一部分时必须呈现类的特定“部分”,则顺序非常重要。按照与 base-list 中指定类的顺序相反的顺序调用析构函数。
注意
基类的规范顺序会影响类的内存布局。不要基于内存中基成员的顺序做出任何编程决策。
当指定 base-list 时,不能多次指定同一类名。但是,可以将类多次作为派生类的间接基。

虚拟基类
由于一个类可能多次成为派生类的间接基类,因此 C++ 提供了一种优化这种基类的工作方式的方法。虚拟基类提供了一种节省空间和避免使用多重继承的类层次结构中出现多义性的方法。
每个非虚拟对象包含在基类中定义的数据成员的一个副本。这种重复浪费了空间,并要求您在每次访问基类成员时都必须指定所需的基类成员的副本。
当将某个基类指定为虚拟基时,该基类可以多次作为间接基而无需复制其数据成员。基类的数据成员的单个副本由将其用作虚拟基的所有基类共享。
当声明虚拟基类时,virtual 关键字将显示在派生类的基列表中。
请考虑下图中的类层次结构,它演示了模拟的午餐排队。

模拟午餐排队图
在该图中,Queue 是 CashierQueue 和 LunchQueue 的基类。但是,当将这两个类组合成 LunchCashierQueue 时,会出现以下问题:新类包含类型 Queue 的两个子对象,一个来自 CashierQueue,另一个来自 LunchQueue。下图显示了概念上的内存布局(实际物理内存布局可能会进行优化)。

模拟午餐排队对象
请注意,Queue 对象中有两个 LunchCashierQueue 子对象。以下代码将 Queue 声明为虚拟基类:

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

virtual 关键字可确保只包含子对象 Queue 的一个副本(请参阅下图)。

使用虚拟基类模拟午餐排队对象
一个类可以同时具有一个给定类型的虚拟组件和非虚拟组件。下图演示了这种情况。

同一个类的虚拟组件与非虚拟组件
在图中,CashierQueue 和 LunchQueue 将 Queue 用作虚拟基类。但是,TakeoutQueue 将 Queue 指定为基类而不是虚拟基类。因此,LunchTakeoutCashierQueue 具有类型 Queue 的两个子对象:一个来自包含 LunchCashierQueue 的继承路径,另一个来自包含 TakeoutQueue 的路径。下图对此进行了演示。

带虚拟和非虚拟继承的对象布局
注意
与非虚拟继承相比较,虚拟继承提供了显著的大小优势。但是,它可能会引入额外的处理开销。
如果派生类重写它从虚拟基类继承的虚函数,并且派生基类的构造函数或析构函数使用指向虚拟基类的指针调用该虚函数,则编译器可能会将其他隐藏的“vtordisp”字段引入到具有虚拟基的类中。/vd0 编译器选项将禁止添加隐藏的 vtordisp 构造函数/析构函数置换成员。默认的 /vd1 编译器选项会在必要时启用它们。仅当确定所有类构造函数和析构函数以虚拟方式调用虚函数时才关闭 vtordisps。
/vd 编译器选项会影响整个编译模块。使用 vtordisp 杂注可以逐个类地禁用 vtordisp 字段,然后重新启用这些字段:

#pragma vtordisp( off )
class GetReal : virtual public { ... };
#pragma vtordisp( on )

对于前面的类声明,如下所示的代码是不明确的,因为 b 所指的 b 是在 A 中还是在 B 中并不清楚:

C *pc = new C;

pc->b();

名称多义性
多重继承使得沿多个路径继承名称成为可能。沿这些路径的类成员名称不一定是唯一的。这些名称冲突称为“多义性”。
任何引用类成员的表达式必须采用明确的引用。以下示例说明如何产生多义性:

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
 unsigned a;
 unsigned b();
};

class B {
public:
 unsigned a(); // Note that class A also has a member "a"
 int b();  // and a member "b".
 char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

请看前面的示例。由于名称 a 是类 A 和类 B 的成员,因此编译器无法辩明哪个 a 指定将调用函数。如果成员可以引用多个函数、对象、类型或枚举数,则对该成员的访问是不明确的。
编译器通过按此顺序执行测试来检测多义性:
如果对名称的访问是不明确的(如上所述),则会生成错误消息。
如果重载函数是明确的,则将解析它们。(有关函数重载多义性的详细信息,请参阅参数匹配。)
如果对名称的访问违背了成员访问权限,则会生成错误消息。(有关详细信息,请参阅成员访问控制。)
在表达式通过继承产生多义性时,您可以通过限定考虑中的名称及其类名来手动消除该多义性。若要适当编译上面的示例而不产生多义性,请使用如下代码:

C *pc = new C;

pc->B::a();

注意
在声明 C 时,如果在 B 的范围内引用 C,则可能会导致出现错误。但不会发出任何错误,直到在 B 的范围内实际创建对 C 的非限定引用。
主导
通过一个继承关系图到达多个名称(函数、对象或枚举器)是可能的。这种情况被视为与非虚拟基类一起使用时目的不明确。这些名称与虚拟基类一起使用时目的不明确,除非其中一个名称“决定”其他名称。
如果某个名称在两个类中定义并且一个类派生自另一个类,则该名称可控制另一个名称。基准名称是派生类中的名称;此名称在本应出现多义性时使用,如以下示例所示:

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
 int a;
};

class B : public virtual A {
public:
 int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
 D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

不明确的转换
从指向类类型的指针或对类类型的引用的显式或隐式转换可能会导致多义性。下图(指向基类的指针的不明确转换)显示如下内容:
D 类型的对象的声明。
将 address-of 运算符 (&) 应用于该对象的效果。请注意,address-of 运算符总是提供该对象的基址。
将使用 address-of 运算符获取的指针显式转换为基类类型 A 的效果。请注意,将该对象的地址强制转换为 A* 类型并不总是为编译器提供足够的信息,以供 A 类型的子对象进行选择;在这种情况下,将存在两个子对象。

指针到基类的不明确转换
到类型 A*(指向 A 的指针)的转换是不明确的,因为无法辩明 A 类型的哪个子对象是正确的。请注意,您可以通过显式指定要使用的子对象来避免多义性,如下所示:

(A *)(B *)&d  // Use B subobject.
(A *)(C *)&d  // Use C subobject.

多义性和虚拟基类
如果使用虚拟基类,则函数、对象、类型和枚举数可通过多重继承路径到达。因为仅有一个基类实例,因此在访问这些名称时不存在二义性。
下图显示如何使用虚拟和非虚拟继承构成对象。

虚拟和非虚拟派生
在该图中,通过非虚拟基类访问类 A 的任何成员都将导致二义性;编译器没有解释是使用与 B 关联的子对象还是与 C 关联的子对象的信息。但是,将 A 指定为虚拟基类时,访问哪一个子对象都不成问题。

(0)

相关推荐

  • C++/java 继承类的多态详解及实例代码

    C++/java 继承类的多态详解 学过C++和Java的人都知道,他们二者由于都可以进行面向对象编程,而面向对象编程的三大特性就是封装.继承.多态,所有今天我们就来简单了解一下C++和Java在多态这方面的不同. 首先我们各看一个案例. C++ //测试继承与多态 class Animal { public: char name[128]; char behavior[128]; void outPut() { cout << "Animal" << endl

  • C++ 继承详解及实例代码

    C++继承可以是单一继承或多重继承,每一个继承连接可以是public,protected,private也可以是virtual或non-virtual.然后是各个成员函数选项可以是virtual或non-virtual或pure virtual.本文仅仅作出一些关键点的验证. public继承,例如下: 1 class base 2 {...} 3 class derived:public base 4 {...} 如果这样写,编译器会理解成类型为derived的对象同时也是类型为base的对象

  • C++多继承多态的实例详解

    C++多继承多态的实现 如果一个类中存在虚函数,在声明类的对象时,编译器就会给该对象生成一个虚函数指针,该虚函数指针指向该类对应的虚函数表. 多态的实现是因为使用了一种动态绑定的机制,在编译期间不确定调用函数的地址,在调用虚函数的时候,去查询虚函数指针所指向的虚函数表. 派生类生成的对象中的虚函数指针指向的是派生类的虚函数表,因此无论是基类还是派生来调用,都是查询的是派生类的表,调用的是派生类的函数. 如果发生了多继承,多个基类中都有虚函数,那么该是怎样的呢?虚函数指针如何排列,多个基类的指针为

  • C++继承中的访问控制实例分析

    本文较为深入的探讨了C++继承中的访问控制,对深入掌握C++面向对象程序设计是非常必要的.具体内容如下: 通常来说,我们认为一个类有两种不同的用户:普通用户 和 类的实现者.其中,普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员:实现者则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有部分.如果进一步考虑继承的话就会出现第三种用户,即派生类.派生类可以访问基类的公有(public)成员和受保护(protected)成员,但不能访问基类的私有(p

  • C++多重继承与虚继承分析

    本文以实例形式较为全面的讲述了C++的多重继承与虚继承,是大家深入学习C++面向对象程序设计所必须要掌握的知识点,具体内容如下: 一.多重继承 我们知道,在单继承中,派生类的对象中包含了基类部分 和 派生类自定义部分.同样的,在多重继承(multiple inheritance)关系中,派生类的对象包含了每个基类的子对象和自定义成员的子对象.下面是一个多重继承关系图: class A{ /* */ }; class B{ /* */ }; class C : public A { /* */ }

  • 深入分析C++派生类中的保护成员继承

    protected 与 public 和 private 一样是用来声明成员的访问权限的.由protected声明的成员称为"受保护的成员",或简称"保护成员".从类的用户角度来看,保护成员等价于私有成员.但有一点与私有成员不同,保护成员可以被派生类的成员函数引用. 如果基类声明了私有成员,那么任何派生类都是不能访问它们的,若希望在派生类中能访问它们,应当把它们声明为保护成员.如果在一个类中声明了保护成员,就意味着该类可能要用作基类,在它的派生类中会访问这些成员.

  • 深入解析C++中类的多重继承

    C++类的多继承 在前面的例子中,派生类都只有一个基类,称为单继承.除此之外,C++也支持多继承,即一个派生类可以有两个或多个基类. 多继承容易让代码逻辑复杂.思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java.C#.PHP 等干脆取消了多继承.想快速学习C++的读者可以不必细读. 多继承的语法也很简单,将多个基类用逗号隔开即可.例如已声明了类A.类B和类C,那么可以这样来声明派生类D: class D: public A, private B, protected C{ //类D新

  • C语言模拟实现C++的继承与多态示例

    一.面向过程编程与面向对象编程的区别 众所周知,C语言是一种典型的面向过程编程语言,而C++确实在它的基础上改进的一款面向对象编程语言,那么,面向过程与面向对象到底有什么样的区别呢? [从设计方法角度看] 面向过程程序设计方法采用函数(或过程)来描述对数据的操作,但又将函数与其操作的数据分离开来. 面向对象程序设计方法是将数据和对象的操作封装在一起,作为一个整体来处理. [从维护角度看] 面向过程程序设计以过程为中心,难于维护. 面向对象程序设计以数据为中心,数据相对功能而言,有较强的稳定性,因

  • Python创建类的方法及成员访问的相关知识总结

    一.创建类的方法与成员访问 1.1 创建实例方法并访问 实例方法是指在类中定义的函数.该函数是一种在类的实例上操作的函数. 同__init__()方法一样,实例方法的第一个参数必须是self,并且必须包含一个self参数. 1.2 创建数据成员(属性)并访问 数据成员是指在类中定义的变量,即属性(对象的特征).根据定义位置,属性(成员变量) 有两种:一种是实例属性,另一种是类属性(类变量). 1.2.1 添加与访问方式 类属性的添加与访问方式同对象属性,只是把self关键字换成类名 1.2.1.

  • 深入解析C++编程中基类与基类的继承的相关知识

    基类 继承过程将创建一个新的派生类,它由基类的成员加上派生类添加的任何新成员组成.在多重继承中,可以构建一个继承关系图,其中相同的基类是多个派生类的一部分.下图显示了此类关系图. 单个基类的多个实例 在该图中,显示了 CollectibleString 和 CollectibleSortable 的组件的图形化表示形式.但是,基类 Collectible 位于通过 CollectibleSortableString 路径和 CollectibleString 路径的 CollectibleSor

  • 深入解析C++编程中的纯虚函数和抽象类

    C++纯虚函数详解 有时在基类中将某一成员函数定为虚函数,并不是基类本身的要求,而是考虑到派生类的需要,在基类中预留了一个函数名,具体功能留给派生类根据需要去定义. 纯虚函数是在声明虚函数时被"初始化"为0的函数.声明纯虚函数的一般形式是 virtual 函数类型 函数名 (参数表列) = 0; 关于纯虚函数需要注意的几点: 纯虚函数没有函数体: 最后面的"=0"并不表示函数返回值为0,它只起形式上的作用,告诉编译系统"这是纯虚函数"; 这是一个

  • 解析C++编程中virtual声明的虚函数以及单个继承

    虚函数 虚函数是应在派生类中重新定义的成员函数. 当使用指针或对基类的引用来引用派生的类对象时,可以为该对象调用虚函数并执行该函数的派生类版本. 虚函数确保为该对象调用正确的函数,这与用于进行函数调用的表达式无关. 假定基类包含声明为 virtual 的函数,并且派生类定义了相同的函数. 为派生类的对象调用派生类中的函数,即使它是使用指针或对基类的引用来调用的. 以下示例显示了一个基类,它提供了 PrintBalance 函数和两个派生类的实现 // deriv_VirtualFunctions

  • 深入解析C#编程中struct所定义的结构

    结构是使用 struct 关键字定义的,例如: public struct PostalAddress { // Fields, properties, methods and events go here... } 结构与类共享大多数相同的语法,但结构比类受到的限制更多: 在结构声明中,除非字段被声明为 const 或 static,否则无法初始化. 结构不能声明默认构造函数(没有参数的构造函数)或析构函数. 结构在赋值时进行复制.将结构赋值给新变量时,将复制所有数据,并且对新副本所做的任何修

  • 深入解析Python编程中super关键字的用法

    官方文档中关于super的定义说的不是很多,大致意思是返回一个代理对象让你能够调用一些继承过来的方法,查找的机制遵循mro规则,最常用的情况如下面这个例子所示: class C(B): def method(self, arg): super(C, self).method(arg) 子类C重写了父类B中同名方法method,在重写的实现中通过super实例化的代理对象调用父类的同名方法. super类的初始方法签名如下: def __init__(self, type1, type2=None

  • 深入解析Java编程中final关键字的作用

    final class 当一个类被定义成final class,表示该类的不能被其他类继承,即不能用在extends之后.否则在编译期间就会得到错误. package com.iderzheng.finalkeyword; public final class FinalClass { } // Error: cannot inherit from final class PackageClass extends FinalClass { } Java支持把class定义成final,似乎违背了

  • 深入解析Java编程中final关键字的使用

    在Java中声明属性.方法和类时,可使用关键字final来修饰.final变量即为常量,只能赋值一次:final方法不能被子类重写:final类不能被继承. 1.final成员 声明 final 字段有助于优化器作出更好的优化决定,因为如果编译器知道字段的值不会更改,那么它能安全地在寄存器中高速缓存该值.final 字段还通过让编译器强制该字段为只读来提供额外的安全级别.   1.1关于final成员赋值 1)在java中,普通变量可默认初始化.但是final类型的变量必须显式地初始化.   2

  • 解析C++编程中的继承方面的运用

    C++继承与组合详解 我们知道,在一个类中可以用类对象作为数据成员,即子对象(详情请查看:C++有子对象的派生类的构造函数).实际上,对象成员的类型可以是本派生类的基类,也可以是另外一个已定义的类.在一个类中以另一个类的对象作为数据成员的,称为类的组合(composition). 例如,声明Professor(教授)类是Teacher(教师)类的派生类,另有一个类BirthDate(生日),包含year,month,day等数据成员.可以将教授生日的信息加入到Professor类的声明中.如:

  • 深入解析C++编程中线程池的使用

    为什么需要线程池 目前的大多数网络服务器,包括Web服务器.Email服务器以及数据库服务器等都具有一个共同点,就是单位时间内必须处理数目巨大的连接请求,但处理时间却相对较短. 传 统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务.任务执行完毕后,线程退出,这就是是"即时创建,即 时销毁"的策略.尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于 不停的创建线程,

随机推荐