c++ 虚继承,多继承相关总结

看这一篇文章之前强烈建议先看以下我之前发布的

虚指针,虚函数剖析

例1: 以下代码输出什么?

#include <iostream>
using namespace std;

class A
{
protected:
 int m_data;
public:
 A(int data = 0) {m_data=data;}
 int GetData() { return doGetData(); }
 virtual int doGetData() { return m_data; }
};

class B : public A
{
protected:
 int m_data;
public:
 B(int data = 1) { m_data = data; }
 int doGetData() { return m_data; }
};

class C: public B
{
protected:
 int m_data;
public:
 C(int data=2) { m_data = data; }
};

int main(int argc, char const *argv[])
{
 C c(10);

 cout << c.GetData() << endl;
 cout << c.A::GetData() << endl;
 cout << c.B::GetData() << endl;
 cout << c.C::GetData() << endl;
 cout << c.doGetData() << endl;
 cout << c.A::doGetData() << endl;
 cout << c.B::doGetData() << endl;
 cout << c.C::doGetData() << endl;
 return 0;
}

构造函数从最初始的基类开始构造,各个类的同名变量没有形成覆盖,都是单独的变量。

理解这两个重要的C++特性后解决这个问题就比较轻松了。 下面我们详解这几条输出语句。

cout << c.GetData() << endl; 本来是要调用C类的 GetData(), C中未定义, 故调用 B 中的, 但是 B 中也未定义, 故调用 A 中的 GetData(), 因为 A 中的 doGetData()是虚函数,所以调用 B 类中的 doGetData(),而 B 类的 doGetData() 返回 B::m_data, 故输出1。

cout << c.A::GetData() << endl; 因为 A 中的 doGetData() 是虚函数,又因为 C 类中未重定义该接口,所以调用 B 类中的 doGetData(), 而 B 类的 doGetData() 返回 B::m_data, 故输出 l 。

cout << c.B::GetData() << endl; C调用哪一个GetData() 本质上都是调用的A::GetData(), 调用到 doGetData() 虚函数,再调用父类B覆盖后的虚函数,返回B::m_data, 所以前5个都是1

cout << c.A::doGetData() << endl; 显示调用A::doGetData(), 返回 A::m_data, 是0

cout << c.B::doGetData() << endl;, cout << c.C::doGetData() << endl; 都将调用B::doGetData(), 返回B::m_data, 是1

所以结果为: 1 1 1 1 1 0 1 1

方便排版,请忽略掉换行。

最后附上内存结构图:

例2: 为什么虚函数效率低?

因为虚函数需要一次间接的寻址,而普通的函数可以在编译时定位到函数的地址,虚函数是要根据虚指针定位到函数的地址。多增加了一个过程,效率肯定低一些,但带来了运行时的多态。

C++支持多重继承,从而大大增强了面向对象程序设计的能力。多重继承是一个类从多个基类派生而来的能力,派生类实际上获取了所有基类的特性。当一个类是两个或多个基类的派生类时,必须在派生类名和冒号之后,列出所有基类的类名,基类间用逗号隔开。 派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们。派生类可以是另一个类的基类,这样,相当于形成了一个继承链。当派生类的构造函数被激活时,它的所有基类的构造函数也都会被激活。

在面向对象的程序设计中,继承和多重继承一般指公共继承。 在无继承的类中,protected 和 private 控制符是没有差别的,在继承中,基类的 private 对所有的外界都屏蔽(包括自己的派生类), 基类的 protected 控制符对应用程序是屏蔽的, 但对其派生类是可访问的。

虚继承

什么是虚继承?它与一般的继承有什么不同?它有什么用?

虚拟继承是多重继承中特有的概念。 虚拟基类是为解决多重继承而出现的。 请看下图:

类D继承自类B和类C, 而类B和类C都继承自类A.

在类D中会两次出现A。为了节省内存空间,可以将B、C对A 的继承定义为虚拟继承,而A就成了虚拟基类。 最后形成如下图所示的情况:

代码如下:
class A;
class B : public virtual A;
class C : public virtual A;
class D : public B, public C;

注意: 虚函数继承和虚继承是完全不同的两个概念.

例3: 请评价多重继承的优点和缺陷。

多重继承在语言上并没有什么很严重的问题,但是标准本身只对语义做了规定,而对编译器的细节没有做规定。所以在使用时(即使是继承),最好不要对内存布局等有什么假设。为了避免由此带来的复杂性,通常推荐使用复合。

  1. 多重继承本身并没有问题,不过大多数系统的类层次往往有一个公共的基类,而这样的结构如果使用多重继承,稍有不慎,将会出现一个严重现象————菱形继承,这样的继承方式会使得类的访问结构非常复杂。 但并非不可处理,可以用virtual继承(并非唯一的方法)
  2. 从哲学上来说,C++多重继承必须要存在,这个世界本来就不是单根的。从实际用途上来说,多重继承不是必需的。
  3. 多重继承在面向对象理论中并非是必要的————因为它不提供新的语义,可以通过单继承与复合结构来取代。 而Java则放弃了多重继承,使用简单的interface取代。 因为C++中没有interface这个关键字,所以不存在所谓的“接口”技术。但是C++可以很轻松地做到这样的模拟,因为C++中的不定义属性的抽象类就是接口。
  4. 多重继承本身并不复杂,对象布局也不混乱,语言中都有明确的定义。真正复杂的是使用了运行时多态(virtual)的多重继承(因为语言对于多态的实现没有明确的定义)。
  5. 要了解C++,就要明白有很多概念是C++ 试图考虑但是最终放弃的设计。你会发现很多Java、C#中的东西都是C++考虑后放弃的。

不是说这些东西不好,而是在C++中它将破坏C++作为一个整体的和谐性,或者C++ 并不需要这样的东西。

举个例子来说明,C#中有一个关键字base用来表示该类的父类,C++却没有对应的关键字。为什么没有?其实C++中曾经有人提议用一个类似的关键字 inherited, 来表示被继承的类,即父类。 这样一个好的建议为什么没有被采纳呢?因为这样的关键字既不必须又不充分。 不必须是因为 C++有一个 typedef* inherited,不充分是因为有多个基类,你不可能知道 inherited 指的是哪个基类。

例4: 在多继承的时候,如果一个类继承同时继承自 class A 和 class B, 而 class A 和 B 中都有一个函数叫 foo(), 如何明确地在子类中指出调用是哪个父类的 foo()?

class A
{
public:
 void foo() { cout << "A foo" << endl; }
};

class B
{
public:
 void foo() { cout << "B foo" << endl; }
};

class C : public A, public B
{

};

int main(int argc, char const* argv[])
{
 C c;
 c.A::foo();
 return 0;
}

C 继承自 A 和 B, 如果出现了相同的函数foo(), 那么C.A::foo(), C.B::foo() 就分别代表从 A 类中继承的 foo 函数和从 B 类中继承的 foo 函数。

例5: 以下代码输出什么?

class A
{
 int m_nA;
};

class B
{
 int m_nB;
};

class C : public A, public B
{
 int m_nC;
};

int main(int argc, char const* argv[])
{
 C* pC = new C;
 B* pB = dynamic_cast<B*>(pC);
 A* pA = dynamic_cast<A*>(pC);
 cout << (pC == pB) << endl;
 cout << (pC == pA) << endl;
 cout << ((int)pC == (int)pB) << endl;
 cout << ((int)pC == (int)pA) << endl;
 return 0;
}

当进行pC=pB比较时,实际上是比较pC指向的对象和隐式转换pB后pB 指向的对象 (pC指向的对象)的部分,这个是同一部分,是相等的。

但是,pB实际上指向的地址是对象C中的父类B部分,从地址上跟pC不一样,所以直接比较地址数值的时候是不相等的。

内存结构图如下:

例6: 如果鸟是可以飞的,那么驼鸟是鸟么?驼鸟如何继承鸟类?

鸟是可以飞的。 也就是说,当鸟飞行时,它的高度是大于0的。 驼鸟是鸟类(生物学上)的一种, 但它的飞行高度为0(驼鸟不能飞)。

不要把可替代性和子集相混淆。 即使驼鸟集是鸟集的一个子集(每个驼鸟集都在鸟集内),但并不意味着鸵鸟的行为能够代替鸟的行为。 可替代性与行为有关,与子集没有关系。 当评价一个潜在的继承关系时,重要的因素是可替代的行为,而不是子集。

如果一定要让驼鸟来继承鸟类, 可以采取组合的办法, 把鸟类中的可以被驼鸟继承的函数挑选出来,这样驼鸟就不是"a kind of"鸟了,而是"has some kind of"鸟的属性而已。

class bird
{
public:
 void eat();
 void sleep();
 void fly();
};

class ostrich
{
public:
 void eat();
 void sleep();
};

例7: C++中如何阻止一个类被实例化?

使用抽象类,或者构造函数被声明成private。

最后补充两个知识点:

函数的隐藏和覆盖

  • 函数的隐藏: 没有定义多态的情况下,即没有加virtual的前提下,如果定义了父类和子类,父类和子类出现了同名的函数,就称子类的函数把同名的父类的函数给隐藏了。
  • 函数的覆盖:是针对多态来说的。 如果定义了父类和子类,父类中定义了公共的虚函数,如果此时子类中没有定义同名的虚函数,那么在子类的虚函数表中将会写上父类的该虚函数的函数入口地址,如果在子类中定义了同名虚函数的话,那么在子类的虚函数表中将会把原来的父类的虚函数地址覆盖掉,覆盖成子类的虚函数的函数地址。

总结: 本文的重点还是承接之前“虚指针,虚表剖析”的内容,对于多重继承,没有探究其内存结构,并且也不是很好弄清楚,其功能大多数可以被组合(composition)的方式实现,C++标准没有给出编译器具体的多继承的实现细节,不同的编译器有不同的做法。

以上就是c++虚继承,多继承相关总结的详细内容,更多关于c++ 继承的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++多重继承二义性原理实例解析

    在派生类中对基类成员访问应该是唯一的,但是在多继承时,可能会导致对基类某成员访问出现不一致的情况,这就是C++多继承中的二义性. 有两种继承的情况会产生多义性 一.如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的成员变量进行访问时,可能产生二义性,继承关系如下图所示: #include <iostream> using namespace std; class A{ public: int a; }; class B1 : public A{ public: i

  • C++多重继承及多态性原理实例详解

    一.多重继承的二义性问题 举例: #include <iostream> using namespace std; class BaseA { public: void fun() { cout << "A.fun" << endl; } }; class BaseB { public: void fun() { cout << "B.fun" << endl; } void tun() { cout &l

  • 实例代码讲解c++ 继承特性

    --派生类需要自己的构造函数. 派生类中可以根据需要添加额外的数据成员和成员函数,甚至可以给予继承的原成员函数新的定义. 基类指针或引用可指向派生对象,反过来则只能使用强制类型转换. 派生类对象可使用基类的非私有成员. 可使用派生对象初始化基类对象或赋值. 一般不允许将基类对象赋给派生类对象(上面第三条),特殊情况下可以. 已有派生类对象初始化创建的派生类对象. 已有派生类对象给另一个派生类对象赋值. 派生类对象的析构函数被调用后会自动调用基类的析构函数. C++11增加了允许继承构造函数的机制

  • 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

  • C++类继承 继承后函数的值实现详解

    类的继承会首先寻找基类,若基类未实现,则会寻找派生类的函数 1. class继承,函数不继承 #include <stdio.h> class Base { public: Base(){} ~Base(){} int a; void setA() { a = 1; } }; class A:public Base { public: A(){} ~A(){} void setA() { a = 2; } }; class B:public Base { public: B(){} ~B(){

  • C++中的封装、继承、多态理解

    封装(encapsulation):就是将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体,也就是将数据与操作数据的源代码进行有机的结合,形成"类",其中数据和函数都是类的成员.封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,特定的访问权限来使用类的成员.封装可以隐藏实现细节,使得代码模块化. 继承(inheritance):C++通过类派生机制来支持继承.被继承的类型称为基类或超类,新产生的类为派生类或子类.保持已有类的特性而构造新类的过

  • 详解C++基础——类继承中方法重载

    一.前言 在上一篇C++基础博文中讨论了C++最基本的代码重用特性--类继承,派生类可以在继承基类元素的同时,添加新的成员和方法.但是没有考虑一种情况:派生类继承下来的方法的实现细节并不一定适合派生类的需求,此时派生类需要重载集成方法. 二.重载方法及虚函数 我们讨论<C++ Primer Plus>中的如下场景:银行记录客户信息,包括客户姓名.当前余额.客户这一类别当然能够创建客户对象.存款.取款以及显示信息.银行需要特殊记录具有透支权限的客户,因此这一类别的客户要额外记录透支上限.透支贷款

  • 详解c++ 继承

    面向对象程序设计中最重要的一个概念是继承.继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易.这样做,也达到了重用代码功能和提高执行效率的效果. 当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可.这个已有的类称为基类,新建的类称为派生类. 继承代表了 is a 关系.例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等. 基类 & 派生类 一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数.定义一个派

  • 详解C++基础——类继承

    一.前言 好吧,本系列博客已经变成了<C++ Primer Plus>的读书笔记,尴尬.在使用C语言时,多通过添加库函数的方式实现代码重用,但有一个弊端就是原来写好的代码并不完全适用于现在的情况.OOP设计思想中类的继承相比来说更为灵活,可以添加新的数据成员和方法,也能修改继承下来方法的实现细节,同时还保留了原有的代码.开始进入正题. 二.类继承示例 场景如下:现需要记录乒乓球运动成员的信息,包括姓名和有无空余桌台.其中有一部分成员参加过比赛,需要将这一部分单独提出并记录他们在比赛中的比分.因

  • C++中的多态与多重继承实现与Java的区别

    多态问题 笔者校招面试时被问到了著名问题「C++ 与 Java 如何实现多态」,然后不幸翻车.过于著名反而没有去准备,只知道跟虚函数表有关.面试之后比较了 C++ 和 Java 多态的实现的异同,一并记录在这里. C++ 多态的虚指针实现 首先讨论 C++. 多态也即子类对父类成员函数进行了重写 (Override) 后,将一个子类指针赋值给父类,再对这个父类指针调用成员函数,会调用子类重写版本的成员函数.简单的例子: class Parent1 { public: virtual void s

  • C++多重继承引发的重复调用问题与解决方法

    本文实例讲述了C++多重继承引发的重复调用问题与解决方法.分享给大家供大家参考,具体如下: 前面简单介绍了一个C++多重继承功能示例,这里再来分析一个多重继承引发的重复调用问题,先来看看问题代码: #include "stdafx.h" #include<stdlib.h> #include<iostream> using namespace std; class R//祖先类 { private: int r; public: R(int x = 0):r(x

  • C++ 中继承与动态内存分配的详解

    C++ 中继承与动态内存分配的详解 继承是怎样与动态内存分配进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性.如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧.下面来看看这两种情况: 一.派生类不使用new 派生类是否需要为显示定义析构函数,复制构造函数和赋值操作符呢? 不需要! 首先,来看是否需要析构函数,如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数.实际上,派生类的默认构造

随机推荐