深入解析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 指定为虚拟基类时,访问哪一个子对象都不成问题。

时间: 2016-01-21

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

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

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

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

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++/java 继承类的多态详解及实例代码

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

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

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

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

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

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

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

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

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

C++ 类的继承与派生实例详解

 C++ 类的继承与派生实例详解 继承性是面向对象程序设计最重要的特性之一,使软件有了可重用性,C++提供的类的继承机制. 继承与派生的概念 一个新类从已有的类那里获得已有的特性,这种现象称为类的继承.同样也可以说成已有的类派生出来了新的类.类A继承自类B也就是类B派生了类A.所以继承和派生的关系就像小学时把字句和被字句的造句一样.有了继承与派生后,就有了父类/基类与子类/派生类,C++中将类B称为父类/基类,将类A称为子类/派生类. 派生类的声明: #include <iostream> u

Python编程之多态用法实例详解

本文实例讲述了Python编程之多态用法.分享给大家供大家参考.具体分析如下: 什么是多态?顾名思义,多态就是多种表现形态的意思.它是一种机制.一种能力,而非某个关键字.它在类的继承中得以实现,在类的方法调用中得以体现.多态意味着变量并不知道引用的对象是什么,根据引用对象的不同表现不同的行为方式. 我们先看一个简单的例子,运算符多态: a=34 b=57 print(a+b) a="世界" b="你好" print(a+b) 我们不知道+法运算符左右两个变量是什么类

Python中类的定义、继承及使用对象实例详解

本文实例讲述了Python中类的定义.继承及使用对象的方法.分享给大家供大家参考.具体分析如下: Python编程中类的概念可以比作是某种类型集合的描述,如"人类"可以被看作一个类,然后用人类这个类定义出每个具体的人--你.我.他等作为其对象.类还拥有属性和功能,属性即类本身的一些特性,如人类有名字.身高和体重等属性,而具体值则会根据每个人的不同:功能则是类所能实现的行为,如人类拥有吃饭.走路和睡觉等功能.具体的形式如下: 例:类的概念: class 人类:             

JavaScript继承与聚合实例详解

本文实例讲述了JavaScript继承与聚合.分享给大家供大家参考,具体如下: 一.继承 第一种方式:类与被继承类直接耦合度高 1. 首先,准备一个可以被继承的类(父类),例如 //创建一个人员类 function Person(name) {//现在Person里面的域是由Person里面的 来this来控制的 this.name=name; } 2. 然后,有个需要继承父类的子类 function Teacher(name,books) { Person.call(this,name);//

Java类的继承实例详解(动力节点Java学院整理)

一.你了解类吗? 在Java中,类文件是以.java为后缀的代码文件,在每个类文件中最多只允许出现一个public类,当有public类的时候,类文件的名称必须和public类的名称相同,若不存在public,则类文件的名称可以为任意的名称(当然以数字开头的名称是不允许的). 在类内部,对于成员变量,如果在定义的时候没有进行显示的赋值初始化,则Java会保证类的每个成员变量都得到恰当的初始化: 1)对于  char.short.byte.int.long.float.double等基本数据类型的

C++类中的继承实例详解

C++类中的继承实例详解 实例效果: 实现代码: #include<iostream> #include<string> using namespace std; class Person { public: Person(const char* name = "abc") :_name(name) { cout << "Person()" << endl; } Person(const Person& pp)

C++类继承之子类调用父类的构造函数的实例详解

C++类继承之子类调用父类的构造函数的实例详解 父类HttpUtil: #pragma once #include <windows.h> #include <string> using namespace std; class HttpUtil { private: LPVOID hInternet; LPVOID hConnect; LPVOID hRequest; protected: wchar_t * mHostName; short mPort; string send

javascript 中的继承实例详解

javascript 中的继承实例详解 阅读目录 原型链继承 借用构造函数 组合继承 寄生组合式继承 后记 继承有两种方式:接口继承和实现继承.接口继承只继承方法签名,而实现继承则继承实际的方法. 由于函数没有签名,在ECMAScript中无法实现接口继承.ECMAScript只支持实现继承,而且实现继承主要依靠原型链来实现. 下面介绍几种js的继承: 原型链继承 原型链继承实现的本质是重写原型对象,代之以一个新类型的实例.代码如下: function SuperType() { this.pr

C++ 通过指针实现多态实例详解

 C++ 通过指针实现多态实例详解 1.父类(DBConnector) 1)DBConnector.h #include <string> using namespace std; class DBConnector { private: string name; public: DBConnector(); DBConnector(string _name); ~DBConnector(); void show(); }; 2)DBConnector.cpp #include "D