C++ | C++中的继承和组合:代码复用的艺术和应用

news2025/3/1 2:57:54

目录

一、继承:代码复用的艺术

1、继承概念

代码说明1:继承方式和访问控制

代码说明2:作用域与成员访问

代码说明3:构造函数和析构函数

2、基类和派生类对象赋值转换

派生类对象到基类对象的转换(向上转型):

基类对象到派生类对象的转换(了解):

3、多继承、菱形继承和虚拟继承

单继承

多继承

菱形继承

虚拟继承

菱形继承的实现方式(了解):

4、精选面试题

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

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

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

二、多态:基类成员和派生类行为的动态绑定

多态的概念

多态的实现

虚函数表原理

C++11的新特性

动态绑定与静态绑定


07-继承——代码复用的艺术

C++是一种功能强大的编程语言对象的继承与多态是C++面向对象编程的核心概念之一,继承为多态提供了结构基础,而多态则是继承的一种应用,使得程序能够以统一的方式处理不同类型的对象。继承和多态提高了代码的灵活性和可维护性,使得代码重用和扩展性成为可能。

1、继承概念

继承是面向对象编程中的一种机制,它允许我们定义一个新类(派生类)来扩展或修改一个已存在的类(基类)。继承体现了现实世界中的“是一个”(is-a)关系。例如,如果有一个Person基类,我们可以派生出StudentTeacher等类。

示例代码:

#include <iostream>
#include <string>

// 基类:Person
class Person {
public:
    // 构造函数
    Person(std::string name, int age) : _name(name), _age(age) {}

    // 公共成员函数
    void PrintInfo() const {
        std::cout << "Name: " << _name << ", Age: " << _age << std::endl;
    }

protected:
    std::string _name; // 姓名
    int _age;          // 年龄
};

// 派生类:Student
class Student : public Person {
public:
    // 构造函数
    Student(std::string name, int age, int stuid, std::string major)
            : Person(name, age), _stuid(stuid), _major(major) {}

    // 重写基类的成员函数
    void PrintInfo() const  {
        Person::PrintInfo(); // 调用基类的PrintInfo函数
        std::cout << "Student ID: " << _stuid << ", Major: " << _major << std::endl;
    }

private:
    int _stuid;  // 学号
    std::string _major;  // 专业
};

int main() {
    // 创建Person对象
    Person person("Alice", 30);
    person.PrintInfo();

    // 创建Student对象
    Student student("Bob", 20, 12345678, "Computer Science");
    student.PrintInfo();

    return 0;
}
代码说明1:继承方式和访问控制
  • public继承:基类的公有(public)和保护(protected)成员在派生类中成为公有成员,而基类的私有(private)成员在派生类中不可见。
  • protected继承:基类的所有成员(无论是public还是protected)在派生类中都成为受保护的(protected)成员。
  • private继承:基类的所有成员(无论是public还是protected)在派生类中都成为私有的(private)成员。

在示例代码中,Student类使用public继承Person类继承,意味着Person类的公有和保护成员在Student类中都保持原有的访问级别。

代码说明2:作用域与成员访问

作用域是指在代码中定义的命名空间,决定了如何访问类成员。在继承的情况下,在继承体系中,同名的成员会形成隐藏关系。如果派生类中有与基类同名的成员,则派生类的成员会隐藏基类的同名成员。这意味着在派生类的作用域内,同名成员将优先使用派生类中的版本。

在示例代码中,如果Person类有一个_name成员,Student类也有一个_name成员,则在Student类中访问_name将访问派生类中的版本。如果需要访问基类中的_name,可以使用作用域解析运算符::,如Person::_name

代码说明3:构造函数和析构函数

派生类的构造函数必须调用基类的构造函数来初始化基类部分。析构函数则保证了正确的清理顺序。

上述代码没有实现析构函数,编译器自动生成的析构函数会自动调用基类析构函数完成析构。

2、基类和派生类对象赋值转换
派生类对象到基类对象的转换(向上转型)
  • 这种转换称为向上转型(upcasting),是安全的,因为派生类对象包含基类的所有成员。
  • 派生类对象可以自动赋值给基类的对象、指针或引用,这称为切片(slicing),因为只有基类部分被赋值。
  • 例如,如果有一个Student对象,它可以被赋值给一个Person类型的引用或指针。

基类对象到派生类对象的转换(了解)
  • 这种转换称为向下转型(downcasting),通常是不安全的,因为基类对象不包含派生类的额外成员。
  • 基类对象不能直接赋值给派生类对象,因为缺少派生类特有的成员。

提高内容:如果希望将派生类对象被切片而来的基类对象指针向下转型回派生类对象指针,可以使用c++11中引入的dynamic_cast进行转化。C++ | 深入剖析C++中的类型转换-CSDN博客

示例代码:

class Person {
public:
    void Print() {
        std::cout << _name << std::endl;
    }
protected:
    std::string _name; // 姓名
private:
    int _age; // 年龄
};

class Student : public Person {
protected:
    int _stunum; // 学号
};

int main()
{
    Person p; // 创建基类对象
    Student s; // 创建派生类对象
    
    // 向上转型:安全
    p = s; // 将派生类对象赋值给基类对象,发生切片
    
    // 向下转型:不安全,需要显式类型转换
    Person* pPtr = &s; // 基类指针指向派生类对象
    Student* sPtr = static_cast<Student*>(pPtr); 
    // 显式转换,如果pPtr指向的是Student对象,则转换成功
    
    // 使用dynamic_cast进行安全的向下转型
    Student* safeSPtr = dynamic_cast<Student*>(pPtr); 
    // 如果pPtr指向Student对象,则转换成功,否则为nullptr

    return 0;
}

示例代码中,Student类通过public继承方式继承自Person类。这意味着Student对象可以自动转换为Person类型的引用或指针,但Person对象不能转换为Student对象。在实际编程中,应该谨慎使用向下转型,并确保类型转换的安全性,以避免潜在的运行时错误。

3、多继承、菱形继承和虚拟继承
单继承

单继承是最简单的继承形式,其中一个类只继承自一个基类。在单继承中,派生类继承了基类的所有公共和受保护成员(但不包括私有成员),并可以添加新的成员或重写基类的成员。

  • 清晰简单:只有一个基类,因此继承关系非常清晰。
  • 易于理解和维护:由于只有一个直接的基类,派生类的行为和特性容易预测和管理。

多继承

多继承允许一个类从多个基类继承特性。这意味着派生类可以同时继承多个基类的成员。

特点:

  • 灵活性高:可以同时继承多个类的属性和方法,提供更丰富的功能组合。
  • 复杂性增加:多继承可能导致复杂的继承关系,难以理解和维护。
  • 潜在问题:可能产生如菱形继承等问题,需要额外的机制(如虚拟继承)来解决。

菱形继承

菱形继承是一种特殊的多继承场景,其中两个或更多的派生类继承自同一个基类,然后这些派生类又有一个共同的派生类。这种结构在类继承图中形成了一个菱形,因此得名。

菱形继承主要带来两个问题:

  1. 数据冗余:由于基类的成员在每个派生类中都有拷贝,当两个基类提供了同名的成员时,如果没有明确的指示,编译器可能无法确定应该使用哪一个基类的成员。如:Accistant对象中有两份Person的实例。这会导致内存的浪费。
  2. 二义性:当最终的派生类需要访问基类中的成员时,可能会不清楚应该访问哪个派生路径上的基类成员。如:访问Accistant中的name成员,是访问Student中的成员变量,还是Teacher中的name成员变量?

虚拟继承

虚拟继承的出现就是为了解决菱形继承,也就是说,虚拟继承是为了解决菱形继承带来的虚拟继承和二义性的问题。

虚拟继承通过在继承列表中使用virtual关键字来解决菱形继承的问题。当使用虚拟继承时,即使多个基类有一个共同的基类,这个共同的基类也只会被实例化一次。

在上面的例子中,如果我们使用虚拟继承,Child类的定义将如下所示:

class Base {
public:
    int value;
};

class DerivedA : virtual public Base {
};

class DerivedB : virtual public Base {
};

class Child : public DerivedA, public DerivedB {
};

使用虚拟继承后,Child类将只包含一个Base的实例,解决了菱形继承的问题。此外,虚拟继承还有助于解决二义性问题,因为它明确了访问路径,避免了成员的二义性。

菱形继承的实现方式(了解):

虚继承表存储指向虚基类实例的指针。它通常位于对象的内存布局的最前面,通过指针找到偏移量获取基类的数据。

4、精选面试题
1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承是一种多继承的继承结构,其中一个类(最末派生类)继承自两个或多个类(中间派生类),而这些中间派生类又共同继承自同一个基类。这种结构在类图上看起来像一个菱形,因此得名。

问题

  • 数据冗余:在没有虚拟继承的情况下,最末派生类会包含多个基类的副本,导致内存浪费。
  • 二义性:如果基类中有同名成员,最末派生类可能无法确定应该使用哪个基类的成员,导致二义性问题。
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的

通过在中间派生类中使用virtual关键字来指定基类的继承,可以解决菱形继承的问题。

解决数据冗余

  • 使用虚拟继承后,中间派生类会有一个指向基类的指针,而不是复制基类的成员。这样,最末派生类只会有一个基类的实例,避免了数据冗余。

解决二义性

  • 虚拟继承确保了只有一个基类实例,因此消除了二义性问题。如果需要访问基类的成员,编译器可以通过基类指针来确定访问的是哪个实例。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
  • 继承:是一种"是一个"(is-a)的关系,表示一个类(子类)是另一个类(基类)的特殊版本。继承允许子类继承基类的属性和方法,并可以添加或重写它们。
  • 组合:是一种"有一个"(has-a)的关系,表示一个类包含另一个类的实例作为其成员变量。组合提供了更大的灵活性,因为组合的类不受限于单一的继承链。

使用继承的情况

  • 当类之间存在自然的"是一个"关系时,例如"狗是动物"。
  • 当子类需要重用基类的代码,并且希望扩展或修改基类的行为时。

使用组合的情况

  • 当类之间存在"有一个"关系时,例如"汽车有一个引擎"。
  • 当需要更大的灵活性,避免继承带来的紧密耦合和脆弱的基类问题时。
  • 当不希望或不需要子类继承基类的所有属性和方法时。

选择继承还是组合,取决于具体的设计需求和类之间的关系。通常,组合提供了更大的灵活性和更低的耦合度,但在某些情况下,继承可以更清晰地表达类之间的关系并简化代码重用。

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

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

相关文章

Jmeter+Influxdb+Grafana平台监控性能测试过程(三种方式)

一、Jmeter自带插件监控 下载地址&#xff1a;Install :: JMeter-Plugins.org 安装&#xff1a;下载后文件为jmeter-plugins-manager-1.3.jar&#xff0c;将其放入jmeter安装目录下的lib/ext目录&#xff0c;然后重启jmeter&#xff0c;即可。 启动Jmeter&#xff0c;测试计…

python-opencv卷积计算代码

目录 # 尝试不同的卷积核 卷积图片如下&#xff1a; 卷积调用类如下&#xff1a; 当我们在图像上应用卷积时&#xff0c;我们在两个维度上执行卷积——水平和竖直方向。我们混合两桶信息&#xff1a;第一桶是输入的图像&#xff0c;由三个矩阵构成——RGB 三通道&#xff0c…

Cobalt—超简单下载器!!【送源码】

我们每天都在网上冲浪&#xff0c;遇到喜欢的视频、音频总想保存下来慢慢回味。很多平台并不直接提供下载功能&#xff0c;或者下载过程繁琐还伴有各种广告。之前了不起给大家介绍过不少开源的下载工具&#xff0c;如Gopeed、lux、Hitomi-Downloader&#xff0c;各有各的特色。…

机械学习—零基础学习日志(如何理解线性代数2)

零基础为了学人工智能&#xff0c;正在快乐学习&#xff0c;每天都长脑子 引言 在平面中&#xff0c;直线的定义可以理解为&#xff0c;任意缩放同一个平面向量得到所有点的集合。 所以要得到一个三维空间中的直线&#xff0c;只需要将这个向量改成三维向量即可。 什么是线…

Python | Leetcode Python题解之第337题打家劫舍III

题目&#xff1a; 题解&#xff1a; # Definition for a binary tree node. # class TreeNode: # def __init__(self, x): # self.val x # self.left None # self.right Noneclass Solution:def rob(self, root: TreeNode) -> int:def _rob…

数字图像处理(Matlab实践篇)专栏介绍

专栏导读 数字图像处理技术是计算机视觉、医学成像、遥感探测等领域的基石。Matlab&#xff0c;以其强大的数学计算能力和丰富的图像处理工具箱&#xff0c;成为学习和实践数字图像处理的理想选择。本专栏将带领读者从基础概念出发&#xff0c;逐步深入到高级技术&#xff0c;…

Redis:缓存击穿

缓存击穿 在某些 Key 属于极端热点数据&#xff0c;且并发量很大的情况下&#xff0c;如果这个 Key 过期&#xff0c;可能会在某个瞬间出现大量的并发请求同时回源&#xff0c;相当于大量的并发请求直接打到了数据库。这种情况&#xff0c;就是我们常说的缓存击穿或缓存并发问…

小试牛刀-区块链Solana多签账户

目录 1.什么是多签账户 2.多签账户的特点 2.1 多个签名者 2.2 最小签名要求 2.3 常见应用场景 3.多签账户实现 3.1 账户的创建 3.1.1 创建新账户 3.1.2 获取创建和初始账户事务 3.1.3 账户的签名 3.2 代币转移操作 Welcome to Code Blocks blog 本篇文章主要介绍了 …

第八节AWK报告生成器(1)

第八节AWK报告生成器 一,AWK简介 其名称得自于它的创始人阿尔佛雷德•艾侯(Alfred Aho)、彼得•温伯格(Peter Weinberger) 和布莱恩柯林(Brian Kernighan)姓氏的首个字母 AWK是一个文本处理工具&#xff0c;Linux及Unix环境中现有的功能最强大的数据处理引擎之一 现在默认li…

「C++系列」输入/输出

文章目录 一、输入/输出1. 包含iostream库2. 使用std命名空间3. 输出到控制台4. 从控制台读取输入5. 注意事项 二、库头文件三、标准输出流&#xff08;cout&#xff09;1. 基本用法2. 格式化输出3. 缓冲4. 错误处理5. 注意事项 四、标准输入流&#xff08;cin&#xff09;1. 基…

X-Recon:一款针对Web安全的XSS安全扫描检测工具

关于X-Recon X-Recon是一款功能强大的Web安全扫描与检测工具&#xff0c;该工具能够帮助广大研究人员识别网页端输入数据&#xff0c;并执行XSS扫描任务。 功能介绍 1、子域名发现&#xff1a;检索目标网站的相关子域名并将其整合到白名单中。这些子域名可在抓取过程中使用&am…

重启人生计划-积蓄星火

&#x1f973;&#x1f973;&#x1f973; 茫茫人海千千万万&#xff0c;感谢这一刻你看到了我的文章&#xff0c;感谢观赏&#xff0c;大家好呀&#xff0c;我是最爱吃鱼罐头&#xff0c;大家可以叫鱼罐头呦~&#x1f973;&#x1f973;&#x1f973; 如果你觉得这个【重启人生…

MySQL数据分析进阶(十二)设计数据库——PART4

&#xff1b;※食用指南&#xff1a;文章内容为‘CodeWithMosh’SQL进阶教程系列学习笔记&#xff0c;笔记整理比较粗糙&#xff0c;主要目的自存为主&#xff0c;记录完整的学习过程。&#xff08;图片超级多&#xff0c;慎看&#xff01;&#xff09; 【中字】SQL进阶教程 |…

基于SpringBoot+VUE的员工绩效考核管理系统(源码+文档+部署)

主要内容&#xff1a;Java项目、Python项目、前端项目、PHP、ASP.NET、人工智能与大数据、单片机开发、物联网设计与开发设计、简历模板、学习资料、面试题库、技术互助、就业指导等 业务范围&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写…

二级分发域名-子比比

二级分发域名全开源链接 二级分发域名

大数据技术——实战项目:广告数仓(第四部分)

目录 第7章 数据仓库环境准备 7.1 数据仓库运行环境 7.1.1 Hive环境搭建 7.1.2 Yarn环境配置 7.2 数据仓库开发环境 第8章 广告数仓ODS层 8.1 广告信息表 8.2 推广平台表 8.3 产品表 8.4 广告投放表 8.5 日志服务器列表 8.6 广告监测日志表 8.7 数据装载脚本 第7章…

C语言【自定义类型——枚举与联合】详细!!

目录 1、枚举 1.0、什么是枚举 1.1、枚举类型的优点 2、联合体&#xff08;共用体&#xff09; 2.0、什么是联合体 2.1、联合体的特点 2.2、联合体大小的计算 2.2.0、联合体节省空间例子 2.3、联合小练习 1、枚举 1.0、什么是枚举 枚举&#xff0c;顾名思义&#…

打造编程学习的“知识宝库”:高效笔记记录与整理指南

如何高效记录并整理编程学习笔记&#xff1f; 在编程学习的海洋中&#xff0c;高效的笔记记录和整理方法就像一张珍贵的航海图&#xff0c;能够帮助我们在浩瀚的知识中找到方向。如何建立一个既能快速记录又易于回顾的笔记系统&#xff1f;如何在繁忙的学习中保持笔记的条理性…

MySQL的字符集配置

MySQL的字符集配置 创建database创建表插入数据查看字符集配置查看字符集的比较规则关于字符集的配置修改字符集总结 创建database create database dbtest1; show databases;use dbtest1;创建表 create table employees(id int,name varchar(15));插入数据 insert into empl…

二分查找专题——基础二分查找

一、题目解析 二、算法分析 找到数组的中心值与target比较&#xff0c;如果中心值大于target则证明target有可能在数组的左边&#xff08;还有一种情况是不存在&#xff09;&#xff0c;反之在数组的右边。重新设置左右边界&#xff0c;折半数组的长度&#xff0c;如此反复&am…