【C++】面向对象编程的三大特性:深入解析继承机制(三)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 【C++】面向对象编程的三大特性:深入解析继承机制

【C++】面向对象编程的三大特性:深入解析继承机制(二)https://developer.aliyun.com/article/1617389


三、菱形继承及菱形虚拟继承

3.1 继承分类

单继承:当一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:属于多继承的一种特殊情况

3.2 菱形继承问题

从上面可以看出来,菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。

class Person
{
    public :
    string _name ; // 姓名
};
class Student : public Person
{
    protected :
    int _num ; //学号
};
class Teacher : public Person
{
    protected :
    int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
    protected :
    string _majorCourse ; // 主修课程
};
void Test ()
{
    // 这样会有二义性无法明确知道访问的是哪一个
    Assistant a ;
    a._name = "peter";
}

Assistant的对象中Person成员会有两份,那么如果单纯a._name = "peter"会产生二义性,编译器无法明确知道访问的是哪一个直接父类的成员。

解决办法:使用指定类域限定符访问

a.Student::_name = "xxx";
a.Teacher::_name = "yyy";

不足之处:需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决

3.3 虚继承(菱形虚拟继承)

3.3.1 虚继承概念

对此引入虚继承这个概念,可以用于解决菱形继承的二义性和数据冗余的问题。

class Person
{
    public :
    string _name ; // 姓名
};
class Student : virtual public Person
{
    protected :
    int _num ; //学号
};
class Teacher : virtual public Person
{
    protected :
    int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
    protected :
    string _majorCourse ; // 主修课程
};
void Test ()
{
    Assistant a ;
    a._name = "peter";
}

我们需要在腰部位置上将继承改为虚继承(使用virtual)

3.3.2 虚拟继承解决数据冗余和二义性的原理

这里需要借助内存窗口观察对象成员的模型,调试窗口有时为了方便观察进行了调整,导致结果不准确。

class A
{
    public:
    int _a;
};
// class B : public A
class B : virtual public A
{
    public:
    int _b;
};
// class C : public A
class C : virtual public A
{
    public:
    int _c;
};
class D : public B, public C
{
    public:
    int _d;
};
int main()
{
    D d;
    d.B::_a = 1;
    d.C::_a = 2;
    d._b = 3;
    d._c = 4;
    d._d = 5;
    return 0;
}  

从下列两个模拟来看,一个是没有虚继承导致数据冗余和二义性,一个是完成虚继承解决了数据冗余和二义性

具体说明:

  • 第一种,这里B和C类都存储了一个变量_a,导致数据冗余和二义性。
  • 第二种,这里可以看出D对象中将A放到了对象组成的最下面,这个_a同时属于B和C,修改这个 _a同样会影响到其他类的 _a。
  • 这里我们需要知道B和C如何去找到公共的A,这里是通过B和C的两个指针(就是属于x类内存中第一个存储的地址),去指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量(十六进制)可以找到下面的_a。

注意:

  • 内存存储的顺序是按照声明顺序或者继承顺序存储的。

3.3.3 多继承指针偏移特性

继承的指针偏移特性,为了支持动态绑定和多态性。多继承的情况下,由于不同的父类可能有不用的虚函数表,因此需要通过指针偏移来正确访问不同父类的虚函数表。这种指针偏移通常由编译器自动生成的,确保访问父类的成员或者调用父类的虚函数时,能够正确地定位到父类对象内存位置。

四、继承总结和反思

4.1 设计继承建议

一般不建议设计出多继承,一定不要设计出菱形继承,不然在复杂度及性能上都有问题。实践中可以设计多继承,但是切记不要设计菱形继承,因为太复杂,容易出现各种问题

4.2 继承和组合

  • public继承:是一种is-a的关系,也就是说每个派生类对象都是一个基类对象
  • 组合:是一种has-a的关系,假设B组合了A,每个B对象中都有一个A对象

继承允许你根据基类的实现派生类的实现。这种通过生成派生类的复用通常被称为为白箱复用(white-box reuse),术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对子类可见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合性度高

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组合或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先实现对象组合有助于你保持每个类被封装

实践尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合

组合比如:

class A
{
private:
    int _ a;
}
//组合
class B
{
  private:
    A _aa;
    int _b;
}

4.3 低耦和与高内聚

  • 低耦和:类和类之间、模块与模块之间关系不那么紧密,关联不高
  • 高耦合:类和类之间、模块与模块之间关系很紧密,关联很高

五、笔试面试题

问题:

  1. 什么是菱形继承?菱形继承的问题是什么?
  2. 什么是菱形虚拟继承、解决数据冗余和二义性的?
  3. 继承和组合的区别?什么时候用继承?什么时候用组合?

回答:

  1. 菱形继承是指在面向对象编程中的一种继承关系,其中一个列同时继承两个不同的类,而这两个类又都继承自同一个父类,形成了一个菱形的继承结构。
  2. 数据冗余和二义性问题,并且还有代码维护困难、不稳定性
  3. 菱形虚拟继承是C++中用来解决菱形继承问题的一种机制,派生类通过虚拟继承来自共同基类,被虚继承的基类只会在继承层次结构中存在一份实例,而不会出现多次复制
  4. 继承和组合是面向对象编程中两种不同的关系模式,继承是一种"is-a"关系,表示的是类之间的一种分类关系,即子类是父类的一种特殊形式,而组合是一种"has-a"关系,表示一个类包含另一个类作为基一部分,但是它们之间不具有层次关系。
  5. 当存在明确的"是一个is-a"关系,且子类需要继承父类的行为和属性时,使用继承,当需要在不同的类之间建立层次关系时,使用继承。
  6. 当对象之间存在包含关系,一个对象包含另一个对象作为其一部分时,使用组合。当对象之间的关系更加动态,或者不需要建立明确的层次关系时,使用组合。

六、继承是子类拷贝一份父类数据吗?(重点)

在继承中,子类和父类之间是一种逻辑关系子类没有真正地拥有一份父类代码的副本,而是通过继承机制在需要时访问父类的功能。继承的这种特性使得子类不仅可以直接使用父类已有的功能,还可以通过重写(override)或扩展(extend)来修改或增加功能,而这一切都是通过共享父类的代码实现的,而不是通过复制代码实现的。

继承的实现是在运行时,通过类之间的关系来实现的。子类通过引用父类的成员,而不是复制父类的代码。因此,子类并没有物理上拥有父类的代码,而是通过继承关系“共享”父类的代码。

因此,继承不是拷贝,它是一种更高级的代码复用机制,通过类的层次结构来共享行为,而不是简单地复制粘贴代码。这样不仅可以减少代码重复,还可以通过多态和重写机制来增强代码的灵活性和可扩展性。


以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!

相关文章
|
25天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
35 3
|
27天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
41 3
|
29天前
|
缓存 JavaScript 前端开发
Vue3与Vue2生命周期对比:新特性解析与差异探讨
Vue3与Vue2生命周期对比:新特性解析与差异探讨
82 2
|
8天前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
23 7
C# 9.0 新特性解析
|
1月前
|
编译器 程序员 定位技术
C++ 20新特性之Concepts
在C++ 20之前,我们在编写泛型代码时,模板参数的约束往往通过复杂的SFINAE(Substitution Failure Is Not An Error)策略或繁琐的Traits类来实现。这不仅难以阅读,也非常容易出错,导致很多程序员在提及泛型编程时,总是心有余悸、脊背发凉。 在没有引入Concepts之前,我们只能依靠经验和技巧来解读编译器给出的错误信息,很容易陷入“类型迷路”。这就好比在没有GPS导航的年代,我们依靠复杂的地图和模糊的方向指示去一个陌生的地点,很容易迷路。而Concepts的引入,就像是给C++的模板系统安装了一个GPS导航仪
103 59
|
14天前
|
自然语言处理 编译器 Linux
|
7天前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
17 2
|
10天前
|
PHP 开发者
PHP 7新特性深度解析及其最佳实践
【10月更文挑战第31天】本文将深入探讨PHP 7带来的革新,从性能提升到语法改进,再到错误处理机制的变革。我们将通过实际代码示例,展示如何高效利用这些新特性来编写更加健壮和高效的PHP应用。无论你是PHP新手还是资深开发者,这篇文章都将为你打开一扇窗,让你看到PHP 7的强大之处。
|
10天前
|
安全 编译器 PHP
PHP 8新特性解析与实践应用####
————探索PHP 8的创新功能及其在现代Web开发中的实际应用
|
13天前
|
Kubernetes Cloud Native 调度
云原生批量任务编排引擎Argo Workflows发布3.6,一文解析关键新特性
Argo Workflows是CNCF毕业项目,最受欢迎的云原生工作流引擎,专为Kubernetes上编排批量任务而设计,本文主要对最新发布的Argo Workflows 3.6版本的关键新特性做一个深入的解析。

推荐镜像

更多