c++虚继承,多继承 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
yiouejv
V2EX    C++

c++虚继承,多继承

  •  
  •   yiouejv 2021-02-28 19:50:29 +08:00 2059 次点击
    这是一个创建于 1684 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    虚指针,虚函数剖析

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

    #include <iostream> using namespace std; class A { protected: nt 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(); }; 

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

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


    最后补充两个知识点:

    函数的隐藏和覆盖

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

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

    在这里插入图片描述

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     862 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 32ms UTC 21:36 PVG 05:36 LAX 14:36 JFK 17:36
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86