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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 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++语言旅途中有所帮助!

相关文章
|
1月前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
44 7
C# 9.0 新特性解析
|
1月前
|
自然语言处理 编译器 Linux
|
22天前
|
编译器 PHP 开发者
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
27 1
|
23天前
|
数据安全/隐私保护 iOS开发 开发者
iOS 14隐私保护新特性深度解析####
随着数字时代的到来,隐私保护已成为全球用户最为关注的问题之一。苹果在最新的iOS 14系统中引入了一系列创新功能,旨在增强用户的隐私和数据安全。本文将深入探讨iOS 14中的几大隐私保护新特性,包括App跟踪透明度、剪贴板访问通知和智能防追踪功能,分析这些功能如何提升用户隐私保护,并评估它们对开发者和用户体验的影响。 ####
|
26天前
|
设计模式 安全 数据库连接
【C++11】包装器:深入解析与实现技巧
本文深入探讨了C++中包装器的定义、实现方式及其应用。包装器通过封装底层细节,提供更简洁、易用的接口,常用于资源管理、接口封装和类型安全。文章详细介绍了使用RAII、智能指针、模板等技术实现包装器的方法,并通过多个案例分析展示了其在实际开发中的应用。最后,讨论了性能优化策略,帮助开发者编写高效、可靠的C++代码。
35 2
|
26天前
|
安全 编译器 C++
【C++11】新特性
`C++11`是2011年发布的`C++`重要版本,引入了约140个新特性和600个缺陷修复。其中,列表初始化(List Initialization)提供了一种更统一、更灵活和更安全的初始化方式,支持内置类型和满足特定条件的自定义类型。此外,`C++11`还引入了`auto`关键字用于自动类型推导,简化了复杂类型的声明,提高了代码的可读性和可维护性。`decltype`则用于根据表达式推导类型,增强了编译时类型检查的能力,特别适用于模板和泛型编程。
22 2
|
4天前
|
安全 编译器 C++
C++ `noexcept` 关键字的深入解析
`noexcept` 关键字在 C++ 中用于指示函数不会抛出异常,有助于编译器优化和提高程序的可靠性。它可以减少代码大小、提高执行效率,并增强程序的稳定性和可预测性。`noexcept` 还可以影响函数重载和模板特化的决策。使用时需谨慎,确保函数确实不会抛出异常,否则可能导致程序崩溃。通过合理使用 `noexcept`,开发者可以编写出更高效、更可靠的 C++ 代码。
11 0
|
4天前
|
存储 程序员 C++
深入解析C++中的函数指针与`typedef`的妙用
本文深入解析了C++中的函数指针及其与`typedef`的结合使用。通过图示和代码示例,详细介绍了函数指针的基本概念、声明和使用方法,并展示了如何利用`typedef`简化复杂的函数指针声明,提升代码的可读性和可维护性。
21 0
|
1月前
|
PHP 开发者
PHP 7新特性深度解析
【10月更文挑战第40天】随着PHP 7的发布,这个广泛使用的语言带来了许多令人兴奋的新特性和性能改进。本文将深入探讨PHP 7的主要变化,包括类型声明、错误处理机制、性能优化等方面,帮助开发者更好地理解和应用这些新特性。
33 4
|
1月前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
36 2

推荐镜像

更多
下一篇
DataWorks