http://bigasp.com/archives/486

如何正确使用C++多重继承

2011年06月17日 — Asp J

原创文章,转载请注明:转载自Soul Apogee
本文链接地址:如何正确使用C++多重继承

C++多重继承一直是一个让人搞不太清楚的一个问题,但是有时候为了实现多个接口,多重继承是基本不可避免,当然在Windows下我们有强大的COM来帮我们搞定这个事情,不过如果你想自己实现一套类似于COM的东西出来的时候,麻烦事情就来了。

在COM里面,有两个很基础的,而且我们都会用到的特性:
1. 纯虚接口:一般使用一个只有纯虚函数的类来作为接口
2. 引用计数:在C++中一般都使用引用计数来管理对象的生命周期

这两个功能在一般设计C++接口的时候也经常用到。其实说到底,上面这两个特性牵扯到的是多重继承的二个表现:
1. 多重继承中的数据存储
2. 多重继承中的虚函数

在COM中,纯虚接口是使用的interface来定义的,引用计数是通过IUnknown接口来实现的,所有的接口都是从IUnknown这种接口中派生出来的。当我们需要某一个类实现多个接口的时候,就经常会遇见这样的多重继承:
multi-inheritance-com:

哦?!是不是很眼熟,ios,istream,ostream,iostream。。各种C++书籍最喜欢用的一个示例。好吧,现在我们先自己实现一个吧,看看到底要怎么使用多重继承。

多重继承中对象的的数据存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <stdio.h>
 
class IBase
{
public:
    IBase() : n(0) {}
    virtual ~IBase() {}
    void show() { printf("%dn", n); }
    int inc() { return ++n; }
    int dec() { return --n; }
 
protected:
    int n;
};
 
class IA : public IBase
{
public:
    virtual ~IA() {}
};
 
class IB : public IBase
{
public:
    virtual ~IB() {}
};
 
class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};
 
int main(int argc, char* argv[])
{
    CImpl o;
    IA *pA = &o;
    IB *pB = &o;
 
    pA->inc();
    pA->show();
 
    pB->dec();
    pB->show();
 
    return 0;
}

编译,OK,成功了!好,运行试一试。
run-result-1:

为什么是1和-1呢?明明n只在继承的一个类IBase里面有,一次加1,一次减一,结果不是应该是1和0么?是不是很奇怪?

这便是使用多重继承的时候经常产生的第一个问题:多副本的数据存储。
当然这个问题很好解决,只需要使用虚继承即可解决。只需要在IA和IB的定义中,在public IBase前加入virtual关键字即可。

1
2
class IA : virtual public IBase
class IB : virtual public IBase

现在再让我们来看一看运行结果:
run-result-2:

结果已经正确了,为什么会发生这种情况呢?虚继承到底干了些什么呢?

我们先来看看在没有使用虚继承的情况下,CImpl的在内存中是怎么样的:
cimpl-memory-normal:

对于普通的public继承(非虚继承),C++会将基类和派生类的内容放在一块,合起来组成一个完整的派生类,在内存上看,它们是连在一起的。按
照这样的规则,在这里,IA和IB中就会各包含一个IBase,而IA,IB和CImpl中的部分内容又共同组成了CImpl。在将CImpl对象o的指
针转型成IA的指针pA过程中,指针将被移动到IA所在的部分区域,同样在转型成IB的过程中,指针将被移动到IB所在的部分区域(也就是说,转型之后,
指针的值都不一样)。在之后的操作中,pA操作的便是IA这个部分中的IBase,而pB操作的便是IB这个部分中的IBase,最后IA部分中的
IBase变成了1,而IB部分中的IBase变成了-1。所以输出的结果也就变成了1和-1了。

之后我们修改成了虚继承,看看到底发生了什么?
cimpl-memory-virtual:

原来的IA和IB中的IBase部分变成了一个指向基类的指针,而基类也变成了一个单独的部分。这样一旦对基类做任何的修改,都会通过这个指针重定向到这
个独立的基类上去,于是,就不存在多副本的数据存储了,这个诡异的问题也就解决了。但是当然从这个图上我们也可以看到,使用虚继承后,访问数据多了一次跳
转,这多出的一次跳转将导致效率的下降一倍甚至更多,所以如果一个类使用的非常频繁,很明显应该尽量避免使用虚继承。

二义性

当然数据的存储只是使用多重继承中遇到的一个问题,现在我们来看另外一个问题,函数的二义性。
首先我们先把数据的存储抛开,单纯的来看一个只有函数的继承关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class IBase
{
public:
    virtual ~IBase() {}
    void foo() { }
};
 
class IA : public IBase
{
public:
    virtual ~IA() {}
};
 
class IB : public IBase
{
public:
    virtual ~IB() {}
};
 
class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
};
 
int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();    // 直接调用CImpl的foo函数
 
    return 0;
}

编译一下,试试。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

出错了!杯具。。为什么?错误还这么奇怪,神马叫做可以是IBase中的foo又可以是IBase中的foo呢?

这就是使用多重继承的时候经常产生的第二个问题:二义性。
在使用多重继承时,如果有两个被继承的类拥有共同的基类,那么就很容易出现这种情况。那什么是二义性呢?
我们先来看一个更简单的继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A
{
public:
    void foo();
};
 
class B : public A
{
public:
    void foo();
};
 
class C : public B
{
public:
    void foo();
};

我们可以把继承关系中,两个类之间沿着基类方向的相隔的继承级数看成一个距离,那么C到A的距离是2,B到A的距离就是1。当然距离不能为负。
当我们对ABC中某个对象调用foo函数的时候,编译器会优先选择离当前指针类型的距离最短的一个函数实现去调用,也就是说,foo函数的查找路径是C->B->A,找到一个最近的去调用。
而对于我们当前这个继承关系来说,IA和IB还是各包含一份IBase的实例,虽然在内存里这里仅仅是包含一份数据,但是在编译的过程中,IA和IB中还
包含了一份从IBase中继承下来的函数列表。所以有两个包含有foo函数类与CImpl类的距离是一样的,所以在对CImpl调用foo函数,就产生了
所谓的二义性,除非我们指定使用IA::foo或者IB::foo,否则编译器将无法决定使用哪一个基类的foo函数。

1
o.IA::foo();    // 指定调用CImpl从IA部分继承过来的foo函数,这样就可以编译通过了。

当然如果我们这样写代码也是不行的:

1
2
IBase *pBase = &o;    // 指针转义时的二义性,不知道是使用IA中的IBase部分,还是IB中的IBase部分
o.inc();                    // 数据访问时的二义性,不知道是访问IA中IBase部分的n,还是IB中IBase部分的n

多重继承中的虚函数

既然直接使用多重继承会有如此多的问题,那么我们能不能通过虚函数来解决这个问题呢?

这里小小的提一下,刚刚二义性里面说到两个类的距离,对于编译器来说,一般是找离当前的类距离最近的函数实现来调用(或者数据来访问),而虚函数则是让编译器做相反的事情:找一个离当前类反向距离最远的函数实现来调用。

好,我们先把上面的程序做一点点小改变,把foo()函数变成一个虚函数,看看有什么变化。

1
2
3
4
5
6
class IBase
{
public:
    virtual ~IBase() {}
    virtual void foo() {}    // 变成虚函数了
};

编译,结果还是失败。
error C2385: ambiguous access of ‘foo’
could be the ‘foo’ in base ‘IBase’
or could be the ‘foo’ in base ‘IBase’
error C3861: ‘foo’: identifier not found

产生问题的原因依然是二义性。即便换成virtual函数,也不能改变二义性这个问题。为什么呢?
因为我们是用的.运算符来访问的,而不是用指针,所以这里虚函数和普通函数没有任何区别。=.=。。。
好,我们再来小小的修改一下,把他变成指针,让他通过虚表去访问,看看行不行。

1
2
CImpl *p = &o;
p->foo();

编译,结果。。。还是一样失败。。。
好吧,我们可以把调用foo()的几句话都去掉,来看看CImpl中生成的虚表到底是个什么样子。
debug-result-vptr-1:

在这个实例中,IA和CImpl部分公用一个虚表,而IB则使用另外的一个虚表(两个虚表这个特性主要是在指针类型转换的时候有用,这里就不说了)。
在这IA的虚表中存在一个指向IBase::foo()的指针,在IB的虚表中也存在一个指向IBase::foo()的指针,所以在CImpl中,可以
找到两个IBase::foo()函数的指针,这样,编译器就无法确定到底应该使用哪一个IBase::foo()函数作为他自己的foo()函数了。二
义性也就产生了。

既然如此,那解决起来就没有什么别的办法了,只能把foo函数的最终在CImpl中实现一次了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    virtual void foo() { }
};
 
int main(int argc, char* argv[])
{
    CImpl o;
    o.foo();
 
    CImpl *p = &o;
    p->foo();
 
    return 0;
}

编译一下,通过了!对于o.foo()来说,这当然是意料之中,离CImpl距离最近的foo函数实现,就是CImpl自己嘛,当然没有问题。
对于后面这个p->foo()的调用,编译器现在也已经可以决定对于CImpl这个类来说,离他最远的foo函数调用是谁了——也是他自己。
所以这里就不会产生二义性的问题了。

在多重继承中编译器对this指针的修正
这里再让我们来看看这次编译出来的虚表,看看还有什么发现。
debug-result-vptr-2:

0x004112a3 [thunk]:CImpl::foo`adjustor{4}’ (void) *
这个看上去很怪的函数是什么呢?我们反汇编一下他看看。
virtual-function-wrapper:

这里可以看到有一句汇编指令:sub ecx, 4。这条指令的左右其实是在修正this指针。
因为从IB的虚表来的请求,this指针都是指向CImpl中IB的部分,而当调用CImpl中的foo函数时,如果还使用IB的this指针,那么程序就会出错,所以这里需要先将this指针修正为CImpl所在的地址,才能调用CImpl的foo函数。

在程序运行的时候,this指针一般被存储在ecx寄存器中,或者当前函数的第一个参数传递进去,不过不同的语言或者不同的编译器编译出来的代码可能会不一样。

我们这里的析构函数都是虚函数,所以我们还可以在截图中看到,编译器会对析构函数做同样的处理。

如何同时解决数据访问和二义性问题呢

貌似到现在都只提到最简单的一种多重继承的情况,但是实际上我们已经遇到了很多的问题了,既然多重继承中会有这么多问题,那我们有没有什么比较通用的方法能把他们一起解决了呢?
方法肯定是有的:
1. 使用虚继承
这算是一种确实可行的方法,只是说会带来额外的时间和空间的开销,访问任何一个数据,都需要通过虚继承表进行跳转,不过一般来说够用了。

2. 虚函数当接口,继承多个接口,统一实现
这个思想就类似于COM了,只是说COM用的是纯虚函数,对于那些会产生二义性的类,我们在最后都实现一边,这样就不会有问题了。这样带来的时间开销也仅
仅是调用时查询一次虚表。但是麻烦的地方就是,有时候继承一下,你可能就要实现一下了,比如引用计数神马的,当然你也可以通过模版来简化你的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class IBase
{
public:
    virtual ~IBase() {}
    virtual void show() = 0;
};
 
class IA : public IBase
{
public:
    virtual ~IA() {}
    virtual int inc() = 0;
};
 
class IB : public IBase
{
public:
    virtual ~IB() {}
    virtual int dec() = 0;
};
 
class CImpl : public IA, public IB
{
public:
    CImpl() : n(0) {}
    virtual ~CImpl() {}
    int inc() { return ++n; }
    int dec() { return --n; }
    void show() { printf("%dn", n); }
 
private:
    int n;
};

3. 通过纯虚函数实现模版方法,将函数转移
这种实现比较复杂,wtl中用的比较多,一般是用在引用计数上,好处很明显,就是可以继承,不用每个类都实现一个引用计数,而只用将新的基类的引用计数转移至原本存在的类上就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class IBase
{
public:
    virtual ~IBase() {}
    void foo() {}
};
 
class IA : public IBase
{
public:
    virtual ~IA() {}
};
 
class IShifter
{
public:
    virtual ~IShifter() {}
    void foo() { do_foo(); }
 
protected:
    virtual void do_foo() = 0;
};
 
class IB : public IShifter
{
public:
    virtual ~IB() {}
};
 
class CImpl : public IA, public IB
{
public:
    virtual ~CImpl() {}
    void foo() { IA::foo(); }
 
protected:
    virtual void do_foo() { IA::foo(); }
};

(转)如何正确使用C++多重继承的更多相关文章

  1. C++解析(24):抽象类和接口、多重继承

    0.目录 1.抽象类和接口 1.1 抽象类 1.2 纯虚函数 1.3 接口 2.被遗弃的多重继承 2.1 C++中的多重继承 2.2 多重继承的问题一 2.3 多重继承的问题二 2.4 多重继承的问题 ...

  2. C++中的多重继承(二)

    1,本文分析另一个多重继承问题及其工程中的解决方案,单继承加多接口实现的开发方式: 2,多重继承的问题三: 1,多重继承可能产生多个虚函数表: 1,实际工程中可能造成不可思议的问题,并且这些问题很难以 ...

  3. C++ 中的多重继承的问题

    如何正确使用C++多重继承 BY R12F · PUBLISHED 2011年06月17日 · UPDATED 2012年03月11日   原创文章,转载请注明:转载自Soul Apogee本文链接地 ...

  4. Django Model 数据表

    Django Model 定义语法 版本:1.7主要来源:https://docs.djangoproject.com/en/1.7/topics/db/models/ 简单用法 from djang ...

  5. Django Model 定义语法

    简单用法 from django.db import models class Person(models.Model): first_name = models.CharField(max_leng ...

  6. python基础——多重继承

    python基础——多重继承 继承是面向对象编程的一个重要的方式,因为通过继承,子类就可以扩展父类的功能. 回忆一下Animal类层次的设计,假设我们要实现以下4种动物: Dog - 狗狗: Bat ...

  7. C++多重继承子类和父类指针转换过程中的一个易错点

    这两天有个C++新手问了我一个问题,他的工程当中有一段代码执行不正确,不知道是什么原因.我调了一下,代码如果精简下来,大概是下面这个样子: class IBaseA { public: ; int m ...

  8. C++中的多重继承与虚继承的问题

    1.C++支持多重继承,但是一般情况下,建议使用单一继承. 类D继承自B类和C类,而B类和C类都继承自类A,因此出现下图所示情况: A          A \          / B     C ...

  9. java中接口与多重继承的关系

    在Java语言中, abstract class 和interface 是支持抽象类定义的两种机制.正是由于这两种机制的存在,才赋予了Java强大的 面向对象能力.abstract class和int ...

随机推荐

  1. 【转】Windows10下80端口被PID为4的System占用导致Apache无法启动的分析与解决方案

    昨天刚更新了Windows10,总体上来说效果还是蛮不错的,然而今天在开启Apache服务器的时候却发现,Apache莫名其妙的打不开了,起初以为是权限的问题,于是使用管理员身份的控制台去调用命令ne ...

  2. cocos2dx 初探 - VS2012 HELLOWORLD

    最近决定用cocos2dx 来做些试验性的东西,先装了个vs2012 再从网上下了cocos2dx-2.1.4就开工了. 仅是Windows 桌面调试还是很简单的. 上面三个项目源: Hellocpp ...

  3. MySQL字符串类型转换时间类型

    如果MySQL数据库里面的某个时间用的是varchar(或者是char)类型的,这样可以方便系统使用而不用随便转换时间类型来适应数据库版本的不同,当要把取出的字段转换成时间类型的时候,可以按如下方法操 ...

  4. 代C语言上机实践

    这已经是开学第十二周了,个人感觉严老师教的这批学生效果不是很好,有的竟然毫不知道main函数前边的 int是做什么的.只知按照书本上给的样例程序一个字一个字的敲到编译器中,然后点击运行.有错误也不知道 ...

  5. 判断浏览器是否支持某个css3属性的javascript方法

    判断浏览器是否支持css3某个属性的方法: /** * 判断浏览器是否支持某一个CSS3属性 * @param {String} 属性名称 * @return {Boolean} true/false ...

  6. The preview is empty because of the setting.Check the generation option.

    前些日子在pd中添加存储过程, 参考:深蓝居的博文 http://www.cnblogs.com/studyzy/archive/2009/12/18/1627334.html 创建视图的时候,会在属 ...

  7. Linux procfs详解

    1.0 proc文件系统总览在类Unix系统中体现了一种良好的抽象哲学,就是几乎所有的数据实体都被抽象成一个统一的接口--文件来看待,这样我们就可以用一些简单的基本工具完成大量复杂的操作.在Linux ...

  8. javascript debut trick, using the throw to make a interrupt(breakpoint) in your program

    console.log('initialize'); try { throw "breakPoint"; } catch(err) {} when I debug the extj ...

  9. <七> jQuery 设置内容和属性

    设置内容 text() - 设置或返回所选元素的文本内容 html() - 设置或返回所选元素的内容(包括 HTML 标记) val() - 设置或返回表单字段的值 设置属性 jQuery attr( ...

  10. ExtJS4.2学习(17)表单基本输入控件Ext.form.Field(转)

    鸣谢:http://www.shuyangyang.com.cn/jishuliangongfang/qianduanjishu/2013-12-11/189.html --------------- ...