C++继承(下)

目录

一、继承与友元

二、继承与静态成员

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

1. 继承的方式

2. 菱形继承的问题 

3. 菱形虚拟继承 

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

4.1 普通菱形继承的内存布局

4.2 虚拟继承的内存布局

四、继承的总结和反思

1. 多继承的复杂性

2. 继承与组合

2.1 继承(is-a关系)

2.2 组合(has-a关系)

3. 优先使用组合

4. 小结

五、笔试面试题解析

1. 什么是菱形继承?菱形继承的问题是什么?

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性?

3. 继承和组合的区别?什么时候用继承?什么时候用组合?


一、继承与友元

        友元关系不能被继承。也就是说,基类中声明的友元函数或友元类不能访问派生类的私有或保护成员。因为友元关系是类之间的一种特殊访问权限约定,它不具备继承性,派生类并不会自动继承基类的友元所拥有的特殊访问权。

        在下面代码中,Display 函数是 Person 类的友元,因此它可以访问 Person 类的保护成员 _name。然而,Display 函数并不是 Student 类的友元,因此它不能访问 Student 类的保护成员 _stuNum。这表明友元关系不具备继承性,派生类不会自动继承基类的友元声明。

#include <iostream>
#include <string>
using namespace std;class Person
{
public:friend void Display(const Person& p, const Student& s); // 声明友元函数
protected:string _name; // 姓名
};class Student : public Person
{
protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s)
{cout << p._name << endl; // 可以访问,因为Display是Person的友元cout << s._stuNum << endl; // 编译错误,Display不是Student的友元
}void main()
{Person p;Student s;Display(p, s);
}

        若想让 Display 函数能够访问派生类 Student 的私有或保护成员,需要在 Student 类中显式地声明 Display 函数为友元: 

class Student : public Person
{
public:friend void Display(const Person& p, const Student& s); // 声明Display是Student的友元
protected:int _stuNum;
};

二、继承与静态成员

        基类中定义的静态成员在整个继承体系中是共享的,无论派生出多少个子类,都只有一个该静态成员的实例。静态成员属于类而非某个具体对象,所以在继承体系中,所有类和对象都共用这一个静态成员,对它的访问和修改会影响整个继承体系中的所有相关部分。

        在基类Person中定义了一个静态成员变量_count,用于统计实例化的对象数量:

#include <iostream>
#include <string>
using namespace std;class Person
{
public:Person() { ++_count; } // 构造函数中自增_count
protected:string _name; // 姓名
public:static int _count; // 静态成员变量,统计人的个数
};int Person::_count = 0; // 静态成员变量在类外初始化class Student : public Person
{
protected:int _stuNum; // 学号
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;cout << "人数 : " << Person::_count << endl; // 输出创建的对象数量Student::_count = 0; // 通过Student类修改_count的值cout << "人数 : " << Person::_count << endl; // 输出修改后的值
}int main()
{TestPerson();return 0;
}

        在上述代码中,无论创建多少个StudentGraduate对象,Person::_count始终是一个实例。这是因为静态成员变量属于类本身,而不是类的某个特定对象。我们可以通过打印Person类和Student类中静态成员_count的地址来证明它们是同一个变量: 

cout << &Person::_count << endl; // 输出Person类中_count的地址
cout << &Student::_count << endl; // 输出Student类中_count的地址

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

1. 继承的方式

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

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

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


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;// 这里会报错:二义性成员访问,因为 Assistant 同时从 Student 和 Teacher 继承了 _namea._name = "peter";
}

        通过显式指定父类可以解除二义性,但会导致数据冗余(每个父类都有自己的 _name 成员)。

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

3. 菱形虚拟继承 

        为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承。通过将继承方式改为虚拟继承,可以让派生类共享同一个基类的实例,从而解决数据冗余和二义性问题。在虚拟继承中,编译器会采用特殊的机制,如虚基表虚基表指针,来确保基类在内存中只出现一次,并且正确地解析对基类成员的访问路径。

#include <iostream>
#include <string>
using namespace std;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; //主修课程
};int main()
{Assistant a;a._name = "peter"; //不再二义性cout << a.Student::_name << endl; // 输出 "peter"cout << a.Teacher::_name << endl; // 输出 "peter"cout << &a.Student::_name << endl; // 地址相同cout << &a.Teacher::_name << endl; // 地址相同return 0;
}

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

        为了研究虚拟继承原理,我们写一个简化的菱形继承体系,再借助内存窗口观察对象成员的模型。 

4.1 普通菱形继承的内存布局

#include <iostream>
using namespace std;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.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在普通菱形继承中,类D对象的内存布局如下:

  • 首先是类B继承自类A的部分,包含 _a(来自A)和 _b

  • 然后是类C继承自类A的部分,再次包含 _a(来自A)和 _c

  • 最后是类D自己的成员 _d

这种布局导致了两个_a成员的存在。

4.2 虚拟继承的内存布局

#include <iostream>
using namespace std;class A
{
public:int _a;
};class B : virtual public A
{
public:int _b;
};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;
}

使用虚拟继承后,类D对象的内存布局调整为:

  • B和类C各自包含一个虚基表指针(vbptr),这些指针指向虚基表(vbtbl)。

  • 虚基表中存储了从类B和类C到共享类A实例的偏移量。

  • D对象中只包含一份类A的成员 _a,并且这个_a被放置在内存布局的最后。

        这里可以分析出D对象中将A放到了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。


下面是上面的Person关系菱形虚拟继承的原理解释: 

四、继承的总结和反思

1. 多继承的复杂性

        很多人觉得C++语法复杂,多继承就是一个典型例子。多继承引入后,很容易形成菱形继承结构,而菱形继承又需要通过虚拟继承来解决数据冗余和二义性问题。这些机制在底层实现上非常复杂。因此,一般不建议设计多继承结构,尤其是菱形继承。它不仅会增加代码的复杂度,还可能对性能产生负面影响。

2. 继承与组合

        在面向对象编程中,继承组合是两种重要的复用机制。它们各有特点,适用于不同的场景。

2.1 继承(is-a关系)

        继承体现的是“is-a”的关系。例如,BMW继承自Car,因为BMW是一种汽车。通过继承,BMW类可以复用Car类的功能和属性。然而,继承也被称为“白箱复用”,因为基类的内部实现对子类是可见的。这种高可见性意味着基类的任何修改都可能影响到所有子类,导致子类和基类之间的耦合度很高。

class Car 
{
protected:string _colour = "白色"; // 颜色string _num = "甘EBIT00"; // 车牌号
};class BMW : public Car 
{
public:void Drive() {cout << "好开-操控" << endl;}
};

2.2 组合(has-a关系)

        组合体现的是“has-a”的关系。例如,Car类组合了Tire类,因为汽车有一个或多个轮胎。组合被称为“黑箱复用”,因为被组合对象的内部实现对组合类是不可见的。这种方式耦合度低,组合类之间的依赖关系较弱。

class Tire 
{
protected:string _brand = "Michelin";  // 品牌size_t _size = 17;         // 尺寸
};class Car 
{
protected:string _colour = "白色"; // 颜色string _num = "甘EBIT00"; // 车牌号Tire _t; // 组合的轮胎对象
};

3. 优先使用组合

        尽管继承在某些场景下非常有用,但在实际开发中,我们应优先使用组合。组合的低耦合特性使得代码更易于维护和扩展。继承则更适合用于那些具有明确“is-a”关系的场景,或者当需要实现多态性时。

4. 小结

  • 继承:适合“is-a”关系,复用基类实现,但耦合度高。

  • 组合:适合“has-a”关系,通过组合对象实现功能,耦合度低。

五、笔试面试题解析

1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承:

        菱形继承是一种多继承的特殊情况,其类层次结构形如菱形。具体来说,一个基类派生出两个或多个子类,然后另一个类又同时继承自这两个子类。例如:

class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};

        在这里,类 D 同时继承自类 B 和类 C,而类 B 和类 C 又都继承自类 A,从而形成了菱形的继承结构。

主要问题:

  1. 数据冗余:由于多继承路径,基类 A 的成员在派生类 D 中会存在多个副本(每个继承路径各一份)。这不仅浪费内存空间,还可能导致数据不一致的问题。

  2. 二义性:在访问基类 A 的成员时,编译器无法确定应该通过哪条继承路径(BC)来访问该成员,从而引发编译错误。例如

    D d;
    d.a_member = 1; // 错误:无法确定是通过 B 还是 C 访问 a_member

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性?

菱形虚拟继承:

        菱形虚拟继承是一种通过使用 virtual 关键字来解决菱形继承问题的机制。具体来说,在继承基类时使用 virtual 关键字,可以让所有派生类共享同一个基类实例。例如:

class A {};
class B : virtual public A {}; // 虚拟继承
class C : virtual public A {}; // 虚拟继承
class D : public B, public C {};

解决数据冗余和二义性的原理

  1. 共享基类实例:虚拟继承确保在派生类 D 中,基类 A 的成员只存在一个副本。这样就消除了数据冗余。

  2. 消除二义性:通过共享同一个基类实例,访问基类成员时不再需要指定继承路径,编译器可以明确地找到唯一的成员实例。例如

    D d;
    d.a_member = 1; // 正确:直接访问共享的 a_member

内存布局的变化

        使用虚拟继承后,派生类 BC 中会包含虚基表指针(vbptr),这些指针指向虚基表(vbtbl),而虚基表中存储了从派生类到共享基类的偏移量。这样,派生类 D 对象中只包含一份基类 A 的成员,并且可以通过虚基表指针找到该成员。

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承(is-a关系)

  • 特点:派生类是基类的特化(如“猫是动物”),可复用基类接口并扩展功能。

  • 适用场景

    • 需要实现多态(如虚函数)。

    • 明确逻辑上的层次关系(如GUI控件继承自基类Widget)。

  • 缺点:高耦合性,基类修改可能影响所有派生类。

组合(has-a关系)

  • 特点:类通过包含其他类的对象实现功能复用(如“汽车包含引擎”)。

  • 适用场景

    • 需要复用功能但无需继承接口(如Stack类组合vector实现存储)。

    • 降低耦合,提升代码灵活性(组合类可替换成员对象实现不同功能)。

  • 优点:封装性好,维护成本低。

选择原则

  • 优先组合:除非需要多态或明确的层次关系,否则优先使用组合。

  • 谨慎继承:避免复杂的继承链,尤其是菱形继承,尽量使用虚继承解决冗余问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/79089.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【漫话机器学习系列】233.激活阈(Threshold Activation)

深度学习入门&#xff1a;了解“阈值激活函数”&#xff08;Threshold Activation Function&#xff09; 激活函数是神经网络中至关重要的一环。今天&#xff0c;我们通过一幅简单直观的手绘图&#xff0c;一起理解最早期也最基础的激活函数之一 —— 阈值激活函数&#xff08;…

(4)python中jupyter lab使用python虚拟环境

1. 先了解IPython和IPyKernel简介 IPython 是一个功能强大的交互式 Python 解释器和开发环境。它提供了一种增强的 Python Shell,使得用户能够以交互的方式探索、分析和可视化数据。IPython 的名称来源于 "Interactive Python",体现了其交互式的特性。 IPyKernel 是…

nginx 配置要领

你提出的问题非常好&#xff01;确实在配置 WebSocket 代理时&#xff0c;proxy_pass 的 URL 处理需要特别注意。以下是关键解释和修正建议&#xff1a; 1. 原配置的问题分析 location /ws/ {proxy_pass http://fastapi-container:8000; # 会保留 /ws/ 前缀传递给后端 }这种配…

【AI】DeepSeek 流程图 / 时序图制作,Word 排版错乱问题,文字转直观图形

一&#xff1a;动态流程图 / 时序图制作&#xff08;DeepSeek Draw.IO&#xff09; 工具准备 DeepSeek&#xff08;AI 生成代码&#xff09;&#xff1a;官网&#xff08;免费&#xff09;Draw.IO&#xff08;可视化渲染&#xff09;&#xff1a;官网&#xff08;免费&#…

4. python3基本数据类型

Python3 中有六个标准的数据类型&#xff1a; Number&#xff08;数字&#xff09; String&#xff08;字符串&#xff09; List&#xff08;列表&#xff09; Tuple&#xff08;元组&#xff09; Set&#xff08;集合&#xff09; Dictionary&#xff08;字典&#xff09; Pyt…

WPF之TextBox控件详解

文章目录 1. TextBox概述2. 基本属性与功能3. 输入控制详解3.1 MaxLength3.2 AcceptsReturn3.3 AcceptsTab3.4 CharacterCasing3.5 IsUndoEnabled3.6 自定义输入限制 4. 文本选择与操作4.1 选择属性4.2 选择方法4.3 文本操作4.4 选择事件4.5 实现自定义文本处理功能 5. 滚动支持…

1.4 点云数据获取方式——结构光相机

图1-4-1结构光相机 结构光相机作为获取三维点云数据的关键设备,其工作原理基于主动式测量技术。通过投射已知图案,如条纹、点阵、格雷码等,至物体表面,这些图案会因物体表面的高度变化而发生变形。与此同时,利用相机从特定

【MATLAB第118期】基于MATLAB的双通道CNN多输入单输出分类预测方法

【MATLAB第118期】基于MATLAB的双通道CNN多输入单输出分类预测方法 一、双通道CNN简介 在深度学习领域&#xff0c;卷积神经网络&#xff08;CNN&#xff09;凭借其强大的特征提取能力&#xff0c;已成为图像识别、自然语言处理等任务的核心技术。传统单通道CNN在处理单一模态…

2025上海车展 | 移远通信推出自研NG-eCall QuecOpen方案,助力汽车安全新标准加速落地

4月29日&#xff0c;在2025上海国际汽车工业展览会期间&#xff0c;全球领先的物联网和车联网整体解决方案供应商移远通信宣布&#xff0c;正式发布自主研发的NG-eCall&#xff08;下一代紧急呼叫系统&#xff09;QuecOpen解决方案。 该方案凭借高度集成的软硬件协同设计&…

leetcode76

目录 803ms超时。。。。越改越超时。。。 一些纠缠 代码分析&#xff1a; 代码问题&#xff1a; 改进建议&#xff1a; 示例代码&#xff1a; The error message you’re seeing indicates that there is a reference binding to a null pointer in your code. This typ…

大数据应用开发和项目实战-Seaborn

一、Seaborn概述 Seaborn是基于Python数据可视化库Matplotlib开发的扩展库&#xff0c;专注于统计图形的绘制&#xff0c;旨在通过简洁的代码实现复杂数据的可视化&#xff0c;帮助用户更轻松地呈现和理解数据。其核心设计目标是简化统计可视化流程&#xff0c;提供高级接口和美…

数据科学与计算

Seaborn的介绍 Seaborn 是一个建立在 Matplotlib 基础之上的 Python 数据可视化库&#xff0c;专注于绘制各种统计图形&#xff0c;以便更轻松地呈现和理解数据。 Seaborn 的设计目标是简化统计数据可视化的过程&#xff0c;提供高级接口和美观的默认主题&#xff0c;使得用户…

深入浅出循环神经网络(RNN):原理、应用与实战

1、引言 在深度学习领域&#xff0c;循环神经网络&#xff08;Recurrent Neural Network, RNN&#xff09;是一种专门用于处理**序列数据**的神经网络架构。与传统的前馈神经网络不同&#xff0c;RNN 具有**记忆能力**&#xff0c;能够捕捉数据中的时间依赖性&#xff0c;广泛应…

广州创科——湖北房县汪家河水库除险加固信息化工程

汪家河水库 汪家河水库位于湖北省房县&#xff0c;建于1971年&#xff0c;其地利可谓是天公之作&#xff0c;东西二山蜿蜒起伏&#xff0c;山峰相连&#xff0c;峰峰比高&#xff0c;无有尽头&#xff0c;东边陡峭&#xff0c;西边相对平坦&#xff0c;半山腰有一条乡村道路&am…

C++日更八股--day2

### C sort 的底层原理 这里其实原来问的是你如何优化快速排序&#xff0c;但是我最初只以为是随机选择基准&#xff0c;但是很显然面试官对此并不满意 闲暇之际&#xff0c;看到一篇介绍sort的原理的文章&#xff0c;才知道原来如是也 1.快速排序&#xff1a;作为主要算法&…

UniApp 的现状与 WASM 支持的迫切性

UniApp 的现状与 WASM 支持的迫切性 点击进入免费1 UniApp 的现状与 WASM 支持的迫切性 点击进入免费版2 一、UniApp 的跨平台优势与性能瓶颈 UniApp 凭借“一次开发,多端发布”的核心理念,已成为跨平台开发的主流框架之一。然而,随着移动应用场景的复杂化(如 3D 渲染、音…

如何正确使用日程表

日程安排&#xff0c;是时间管理中非常重要的一项&#xff0c;也是不容易管好的一项。 日程安排&#xff0c;通常指放到日程表里的事情&#xff0c;一般来说&#xff0c;放到日程表的事情要符合以下几个特点&#xff1a; 01.明确具体时间段&#xff0c;比如是下午2点到下午三…

【Token系列】14|Prompt不是文本,是token结构工程

文章目录 14&#xff5c;Prompt不是文本&#xff0c;是token结构工程一、很多人写的是“自然语言”&#xff0c;模型读的是“token序列”二、Prompt写法会直接影响token结构密度三、token分布影响Attention矩阵的聚焦方式四、token数 ≠ 有效信息量五、Prompt结构设计建议&…

研发效率破局之道阅读总结(4)个人效率

研发效率破局之道阅读总结(4)个人效率 Author: Once Day Date: 2025年4月30日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可参考专栏: 程序的艺术_Once-Day…

CNN代码详细注释

import torch from torch import nn#定义张量x&#xff0c;它的尺寸是5x1x28x28 #表示了5个单通道28x28大小的数据 xtorch.zeros([5,1,28,28])#定义一个输入通道是1&#xff0c;输出通道是6&#xff0c;卷积核大小是5x5的卷积层 convnn.Conv2d(in_channels1,out_channels6,ker…