菱形继承的二义性和数据冗余问题

本文深入探讨面向对象编程中的继承概念,解析公有、保护和私有继承的区别,以及它们在实现接口和代码复用中的角色。文章还讨论了菱形继承的二义性和数据冗余问题,并介绍了解决方案——虚继承。

什么是继承

继承是面向对象复用的重要手段。通过继承定义一个类,继承是类型之间的关系建模,共享公有的东西,实现各自本质不同的东西。
继承的方式分为:
公有继承、保护继承、私有继承
这里写图片描述
总结:
1.基类的私有成员在派生类中是不能被访问的,如果一些基类成员在类外不想被基类对象直接访问,但需要在派生类中能访问,就定义为保护的。可以看出保护成员限定符是因为继承才出现的。
2.public继承是一个接口继承,保持Is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也是一个父类对象。也就是说,我就是你。我包含着你。
3.protected/privated继承是一个实现继承,基类的部分成员并未完全成为子类借口的一部分,
是has-a的关系原则,所以非特殊情况不会使用这两种继承关系,在绝大多数场景下使用的都是公有继承。
4.不管是哪种继承方式,在派生类内部都可以访问积累的共有成员和保护成员,但是基类的私有成员在子类中不可见,就是不能访问,但是也继承下来了。
5.使用关键字class时默认的继承方式是private,使用关键字struct时默认public,不过最好显示的写出继承方式。
6.在实际运用中一般都是public继承,很少有其他两种

继承的格式

class A
{
public:
int _a;
}
class B:public A
{
public:
int _b;
}

这种方式就是B公有继承了A

继承与转换—赋值兼容规则–public继承

1.子类对象可以赋值给父类对象(切片/切割)
2.父类对象不能赋值给子类对象
3.父类的指针/引用可以指向子类对象
4.子类的指针/引用不能指向父类对象(但是可以通过强制类型转换实现,不过有危险性)

继承体系中的作用域

1.在集成体系中基类和派生类都有独立的作用域
2.子类和父类中有同名成员,子类成员将会屏蔽父类对成员的直接访问。可以通过加上类名的方法进行调用(隐藏/重定义)
注意:在集成体系中最好不要定义同名的成员

派生类的默认成员函数

在继承体系中,派生类如果没有显示定义这六个成员函数,编译系统将会默认合成

单继承和多继承

1.单继承就是一个子类只有一个直接的父类
2.多继承就是一个子类有两个或者两个以上的直接的父类

菱形继承

这里写图片描述

class A
{
public:
    int _a;
};
class B:public A
{
public:
    int _b;
};
class C:public A
{
public:
    int _c;
};
class D:public B,public C
{
public:
    int _d;
};

int main()
{
    D d;
    d._a = 10;//这里调用成员的时候就会出现错误,对_a的访问不明确

    d._b = 2;
    d._c = 3;
    d._d = 4;
    }

这里写图片描述
要想解决这个问题也很简单,只需要在调用的时候加上类的域名就好

d.B::_a = 10;
d.C::_a = 20;

运行结果如下图
这里写图片描述

菱形继承的对象模型

这里写图片描述

由此可以看出菱形继承存在二义性问题,和数据冗余问题,存在了两份A类的成员

解决方案:
  1. 在调用对象的时候,指明类域就可以解决二义性问题
  2. 用虚继承既可以解决数据冗余的问题。
    虚继承的格式,B类和C类虚拟继承A
    格式:

class B:virtual public A
class C:virtual public A

进行改变后,上面的代码再次运行就不会出现错误,因为_a变为公共的资源,具体可以打开监视窗口,发现只要改变_a的值,B和C类中的_a会同时改变
这里写图片描述

虚继承的对象模型如下图

这里写图片描述
在内存中具体的图示(代码就是上面的那个)
这里写图片描述
总结:
1.虚继承解决了在菱形继承中子类对象包含多分父类对象的数据冗余和浪费空间的问题
2.虚继承体系看起来很复杂,在实际应用中我们通常不会定义如此复杂的集成体系。一般不到万不得已不要使用菱形虚拟继承,因为它解决数据冗余问题的同时也带来了性能上的损耗

这个是我之前写的继承,更加详细一点
[继承] (http://blog.csdn.net/chan0311/article/details/69791225)

转载于:https://www.cnblogs.com/chan0311/p/9427331.html

<think>我们正在讨论面向对象编程中的多重继承,特别是菱形继承问题。用户的问题集中在“什么是菱形继承问题”以及它在多重继承中的表现。根据用户提供的引用,我们可以总结如下: 引用[2]引用[3]都描述了菱形继承问题的核心:当一个类D同时继承自两个类BC,而BC又都继承自同一个基类A时,如果A中有一个方法被BC分别重写,那么D调用该方法时就会产生二义性:到底应该调用B的方法还是C的方法? 此外,引用[4]引用[5]提到了C++中解决菱形继承问题的方法:虚继承(virtual inheritance)。通过虚继承,可以确保在菱形继承结构中,最终派生类只包含一份基类子对象,从而避免二义性数据冗余。 因此,我们的回答应该包括: 1. 菱形继承问题的定义结构(用类图表示)。 2. 菱形继承导致的两个主要问题二义性数据冗余。 3. 解决方法(以C++的虚继承为例)及其原理(虚基类指针虚基类表)。 4. 其他语言(如Java)如何避免菱形继承问题(使用接口)。 同时,我们还需要注意Python中的多重继承情况(因为用户之前的问题是关于Python的),但本次问题更偏向于一般性的面向对象概念。 回答结构: 1. 定义:什么是菱形继承? 2. 问题二义性数据冗余。 3. 解决方案:虚继承(C++)接口(Java)。 4. Python中的情况(因为用户之前的问题涉及Python,但本次问题没有特别指明语言,所以简要提一下Python的MRO机制)。 最后,按照要求,生成相关问题。</think>### 面向对象编程中的菱形继承问题 #### 1. 什么是菱形继承问题菱形继承(Diamond Inheritance)是多重继承中的经典问题,发生在以下类结构中: ```mermaid classDiagram A <|-- B A <|-- C B <|-- D C <|-- D class A { +data } class B { } class C { } class D { } ``` - **结构特征**:类D同时继承自BC,而BC又都继承自同一个基类A - **核心问题**:当D访问A的成员时,存在两条继承路径(D→B→A D→C→A),导致二义性内存冗余[^2][^5] #### 2. 菱形继承引发的两大问题 1. **二义性问题(Ambiguity)** - 当A的方法在BC中被不同方式重写时,D无法确定调用哪个版本 ```cpp class A { public: void show() { cout << "A"; } }; class B : public A { public: void show() { cout << "B"; } }; class C : public A { public: void show() { cout << "C"; } }; class D : public B, public C {}; D d; d.show(); // 错误:对'show'的调用不明确 ``` 2. **内存冗余问题(Redundancy)** - D会包含**两份A的实例**(通过B继承一份,通过C继承一份) ```cpp class A { int data[100]; }; // 占用400字节 class B : public A {}; class C : public A {}; class D : public B, public C {}; sizeof(D); // 结果约800字节(包含两份A的数据) ``` #### 3. 解决方案:虚继承(C++示例) C++通过**虚继承(virtual inheritance)** 解决菱形问题: ```cpp class A { public: int data; }; // 基类 // 虚继承声明 class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C { public: using B::data; // 显式指定使用B路径的data }; int main() { D d; d.data = 10; // ✅ 无二义性 cout << sizeof(d); // 大小≈sizeof(A)+指针开销[^4][^5] } ``` **虚继承原理**: 1. 虚基类指针:BC各增加一个虚基类指针(通常4字节) 2. 共享实例:D只保留**一份A的实例**,BC通过虚指针访问 3. 内存优化:牺牲8字节指针空间,避免数百字节冗余[^5] #### 4. 其他语言的解决方案 1. **Java/Go**:完全禁用多重继承,通过接口实现类似功能 ```java interface Pet { void play(); } // 接口无数据成员 class Dog extends Animal implements Pet { // 单继承+多接口 public void play() { ... } // 强制实现方法 } ``` - 优点:杜绝二义性冗余[^3] - 缺点:无法复用基类实现 2. **Python**:通过方法解析顺序(MRO)解决 ```python class A: pass class B(A): pass class C(A): pass class D(B, C): pass print(D.mro()) # 输出: [D, B, C, A, object] ``` - 按MRO顺序搜索方法(广度优先) - 使用`super()`调用父类方法 #### 5. 最佳实践建议 1. **优先使用组合**:用"has-a"替代"is-a"关系 2. **接口分离**:Java风格接口优于多重继承 3. **谨慎使用虚继承**:C++中仅当必须共享基类状态时使用 4. **明确调用路径**:如C++的`using B::data`指定路径[^4] > 菱形继承的本质是**继承路径收敛**导致的状态冲突,合理使用虚继承、接口或组合方案可平衡灵活性与复杂性[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值