详解c++继承与多继承

news2024/11/16 21:50:29

在这里插入图片描述

目录

  • 🚄什么是继承
    • 🚉继承的概念
    • 🚃继承的定义
  • 🚇继承基类成员访问方式的变化
  • 🚆基类和派生类对象赋值转换
  • 🚐继承时的作用域
  • 🚗派生类的默认成员函数
  • 🚓继承、友元、静态成员
  • 🚚复杂的菱形继承及菱形虚拟继承
  • 🏍️继承总结


🚄什么是继承

🚉继承的概念

继承是面向对象程序设计中的一种重要机制,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法,并在此基础上进行扩展和修改。通过继承,子类可以获得父类的所有公共成员,包括字段、属性、方法等,并且可以添加自己的额外成员。

继承的概念体现了面向对象程序设计的层次结构。父类通常表示一般化的概念,而子类则表示更具体的概念。例如,一个“动物”类可以作为父类,而“狗”和“猫”类可以作为子类,它们继承了“动物”的共同特征,如呼吸、移动等,同时也可以定义自己特有的行为,如“狗”可以叫和咬东西,“猫”可以爬树。

继承的好处之一是代码复用。通过继承,我们可以在不改变原有类的情况下扩展功能,减少了重复编写代码的工作量。如果多个类具有共同的属性和方法,我们可以将它们抽象成一个父类,然后让这些类继承该父类,从而实现代码的复用。

另外,继承还可以提高代码的可维护性和扩展性。当需要修改或添加功能时,我们只需要在父类中进行修改或添加,而不需要改动所有的子类。这样可以减少潜在的错误和重复的劳动。

总之,继承是面向对象程序设计中非常重要的概念,它通过建立类之间的层次结构,实现了代码的复用、可维护性和扩展性。合理地使用继承可以使代码更加清晰、简洁和易于理解。

🚃继承的定义

C++中类的继承使用关键字 publicprotectedprivate 来指定继承的访问级别。继承的定义格式如下:

class 派生类名 : 访问级别 基类名
{
    // 派生类的成员
};

其中,派生类名表示派生类的名称,基类名表示基类的名称。访问级别可以是 public、protected 或 private,用来指定继承的访问级别。如果没有指定访问级别,则默认为 private 继承。

派生类可以继承多个基类,每个基类之间用逗号,分隔。继承多个基类时,访问级别可以分别指定,也可以一起指定。

下面是一个简单的 C++ 类继承的示例:

#include <iostream>

using namespace std;

// 定义一个基类
class Shape
{
public:
    void setWidth(int w)
    {
        width = w;
    }
    void setHeight(int h)
    {
        height = h;
    }
protected:
    int width;
    int height;
};

// 定义一个派生类
class Rectangle : public Shape
{
public:
    int getArea()
    {
        return (width * height);
    }
};

// 主函数
int main()
{
    Rectangle rect;

    rect.setWidth(5);
    rect.setHeight(7);

    cout << "Area of the rectangle: " << rect.getArea() << endl;

    return 0;
}

在这个示例中,Shape 是一个基类,Rectangle 是一个派生类。Rectangle 继承了 Shape 的 setWidth() 和 setHeight() 方法,并添加了自己的 getArea() 方法。在主函数中,创建了一个 Rectangle 对象,调用了它的方法来计算矩形的面积。

运行结果:

在这里插入图片描述

🚇继承基类成员访问方式的变化

继承方式的访问控制规则是:public继承保持基类的访问级别不变,protected继承将基类的public和protected成员变为派生类的protected成员,private继承将基类的public和protected成员变为派生类的private成员。而基类的private成员在任何情况下都不可见。

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

总结:

  1. 基类的 private 成员在派生类中无论以什么方式继承都是不可见的。这意味着,虽然基类的 private 成员被派生类继承了,但是派生类中无法直接访问它们,也不能在派生类的对象中使用这些成员。因此,如果需要在派生类中访问基类的成员,应该将它们定义为 protected 成员,而不是 private 成员。

  2. 在实际使用中,public 继承是最常用的继承方式,因为它能够保持基类的接口不变,同时也能够访问基类的 protected 成员。而 protected 和 private 继承则很少使用,因为它们会导致代码的可读性和可维护性变差。因此,建议在设计类的时候,根据实际需求选择合适的继承方式和访问控制规则,以提高代码的可读性和可维护性。

🚆基类和派生类对象赋值转换

派生类对象可以赋值给基类的对象、基类的指针和基类的引用,这种赋值操作被称为切片或切割。这意味着派生类对象中的基类部分可以被赋值给基类对象。

基类对象不能直接赋值给派生类对象,因为派生类对象包含了基类对象的成员,而基类对象没有派生类对象的成员。

基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用。但是,只有当基类的指针指向派生类对象时,这样的转换才是安全的。

如果基类是多态类型(即含有虚函数),可以使用运行时类型信息(RTTI)的dynamic_cast来进行识别并进行安全转换。dynamic_cast可以在运行时检查指针或引用的类型,并在类型不匹配时返回空指针或引发异常。

需要注意的是,dynamic_cast只能用于具有多态性的类层次结构中,即必须至少有一个虚函数。

在这里插入图片描述

示例代码:

#include <iostream>

using namespace std;


#include <iostream>
using namespace std;

class Animal 
{
protected:
    string _name; // 名称
    int _age; // 年龄
public:
    Animal(string name, int age) : _name(name), _age(age) 
    {}
    void sound() 
    {
        cout << "Animal sound" << endl;
    }
};

class Cat : public Animal 
{
public:
    Cat(string name, int age) : Animal(name, age) 
    {}
    void sound()
    {
        cout << "Meow" << endl;
    }
};

class Dog : public Animal 
{
public:
    Dog(string name, int age) : Animal(name, age) 
    {}
    void sound()
    {
        cout << "Woof" << endl;
    }
};

void Test() 
{
    Cat cat("Tom", 3);
    Dog dog("Max", 5);

    // 子类对象可以赋值给父类对象/指针/引用
    Animal animal = cat;
    Animal* animalPtr = &dog;
    Animal& animalRef = cat;

    // 基类对象不能赋值给派生类对象
    //cat = animal;

    // 基类的指针可以通过强制类型转换赋值给派生类的指针
    animalPtr = &cat;
    Cat* catPtr1 = (Cat*)animalPtr;
    catPtr1->sound();

    animalPtr = &dog;
    Cat* catPtr2 = (Cat*)animalPtr;
    catPtr2->sound(); // 这种情况转换后会存在越界访问的问题,因为实际对象是Dog而不是Cat
}

int main() 
{
    Test();
    return 0;
}

运行结果:

在这里插入图片描述

🚐继承时的作用域

在继承体系中,基类和派生类都有独立的作用域。这意味着它们可以拥有相同名称的成员,但在使用时会有一些规则需要注意:

  • 子类和父类中有同名成员时,子类成员会屏蔽父类对同名成员的直接访问。这种情况被称为隐藏或重定义。在子类成员函数中,如果想要访问父类的同名成员,可以使用基类名加上作用域解析运算符(::)来进行显示访问。

  • 需要注意的是,如果是成员函数的隐藏,只需要函数名相同就会构成隐藏。即使参数列表不同,也会隐藏父类中的同名函数。

  • 在实际编程中,最好避免在继承体系中定义同名的成员,以避免产生混淆和错误。

总之,继承体系中的同名成员会发生隐藏,子类可以通过基类名加作用域解析运算符(::)来访问父类的同名成员。在设计继承体系时,应该避免同名成员的存在,以减少潜在的问题。

🚗派生类的默认成员函数

在派生类中,如果我们没有显式地定义以下6个成员函数:构造函数、拷贝构造函数、赋值重载运算符、析构函数和取地址重载,编译器会自动生成它们。这些自动生成的成员函数会按照以下规则进行生成和调用:

  1. 派生类的构造函数必须调用基类的构造函数来初始化基类的成员。如果基类没有默认的构造函数,派生类的构造函数必须在初始化列表中显式调用基类的构造函数来完成基类成员的初始化。

  2. 派生类的拷贝构造函数必须调用基类的拷贝构造函数来完成基类成员的拷贝初始化。

  3. 派生类的赋值运算符必须调用基类的赋值运算符来完成基类成员的赋值。

  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数来清理基类成员。这样才能保证派生类对象先清理派生类成员,再清理基类成员的顺序。

  5. 派生类对象的初始化顺序是先调用基类的构造函数,然后调用派生类的构造函数。

  6. 派生类对象的析构顺序是先调用派生类的析构函数,然后调用基类的析构函数。

需要注意的是,在一些场景中,析构函数需要进行重写(即基类的析构函数需要声明为虚函数)。重写的条件之一是函数名相同。由于析构函数名会被编译器特殊处理为destructor(),所以如果父类的析构函数没有被声明为虚函数,子类的析构函数和父类的析构函数会构成隐藏关系,无法实现多态的析构。因此,在继承体系中,如果需要使用多态的析构,应该将基类的析构函数声明为虚函数。

在这里插入图片描述

🚓继承、友元、静态成员

继承与友元:

友元关系不能被继承。在C++中,基类的友元不能访问子类的私有和保护成员。这是因为继承是一种"is-a"关系,子类应该继承基类的接口,而不是继承基类的友元关系。

如果基类的友元需要访问子类的私有和保护成员,可以考虑将子类的私有和保护成员提升为公有成员,或者将子类作为友元类。但是,这样会破坏封装性,应该谨慎使用。

继承与静态成员:

当基类定义了静态成员时,整个继承体系中只有一个这样的成员实例。无论有多少个派生类,它们都共享同一个静态成员。

静态成员是属于类的,而不是属于对象的。每个类只有一个静态成员,不管创建多少个对象,都只有一个静态成员实例。在继承体系中,派生类继承了基类的静态成员,并且它们共享同一个静态成员实例。这意味着无论创建多少个派生类的对象,它们都可以访问和修改同一个静态成员。

这种特性可以用于在整个继承体系中共享一些数据或方法,而不需要为每个派生类都创建一个独立的静态成员。这可以提高代码的效率和可维护性。

🚚复杂的菱形继承及菱形虚拟继承

复杂的菱形继承和菱形虚拟继承是继承关系中的两个特殊情况。

菱形继承指的是一个派生类同时继承了两个直接或间接基类,而这两个基类又共同继承自同一个基类。这种继承关系形成了一个菱形的结构,因此得名。例如,假设有一个基类Animal,两个派生类Cat和Dog分别继承自Animal,然后再有一个类Pet同时继承自Cat和Dog,这样就形成了一个菱形继承关系。

在这里插入图片描述

菱形继承会导致一些问题,例如,Pet对象中会包含两个Animal对象的副本,造成资源的浪费和冗余。为了解决这个问题,C++引入了菱形虚拟继承。

菱形虚拟继承是通过在虚拟继承时使用关键字virtual来解决菱形继承问题的。在菱形继承中,Pet类可以使用虚拟继承来继承自Cat和Dog,这样就只会有一个Animal对象的副本被共享,避免了资源的浪费和冗余。

在这里插入图片描述

使用菱形虚拟继承时,需要注意解决虚基类的初始化问题,通常在派生类的构造函数中显式调用虚基类的构造函数来完成初始化。

总之,复杂的菱形继承和菱形虚拟继承是在多重继承中出现的特殊情况,需要特别注意继承关系和初始化问题。

示例代码:

#include <iostream>
#include <string>
using namespace std;

// 基类 Animal
class Animal {
public:
    Animal(string name, int age)
    {
        _name = name;
        _age = age;
        cout << "Animal()" << endl;
    }
    ~Animal()
    {
        cout << "~Animal()" << endl;
    }
    string _name;
    int _age;
};

// 派生类 Cat
class Cat : virtual public Animal {
public:
    Cat(string name, int age)
        :Animal(name, age)
    {
        cout << "Cat()" << endl;

    }
    ~Cat()
    {
        cout << "~Cat()" << endl;

    }
    void speak()
    {
        cout << "cat speak" << endl;
    }
};

// 派生类 Dog
class Dog : virtual public Animal {
public:
    Dog(string name, int age)
        :Animal(name, age)
    {
        cout << "Dog()" << endl;

    }
    ~Dog()
    {
        cout << "~Dog()" << endl;

    }
    void speak()
    {
        cout << "dog speak" << endl;
    }
};

// 派生类 Pet
class Pet : public Cat, public Dog {
public:
    Pet(string name, int age)
        :Cat(name, age), Dog(name, age), Animal(name, age)
    {
        cout << "Pet()" << endl;

    }
    ~Pet()
    {
        cout << "~Pet()" << endl;

    }
    void speak()
    {
        cout << "Pet speak" << endl;
    }
};

int main() 
{
    Pet pet("Tom", 18);
    pet.speak();
    return 0;
}

运行结果:

在这里插入图片描述

🏍️继承总结

多继承确实是C++语法中一个复杂的特性,特别是在存在菱形继承和菱形虚拟继承的情况下,底层实现会更加复杂。因此,一般情况下不建议设计多继承和菱形继承,以避免复杂度和性能上的问题。许多后来的面向对象语言,如Java,都不支持多继承,这也可以视为C++中的一个缺陷。

继承和组合是两种不同的关系:

  • 公有继承表示一种is-a(是一个)的关系,即派生类对象也是基类对象。这种继承方式通过生成派生类的复用,通常被称为白箱复用。在继承中,基类的内部细节对子类是可见的,这可能破坏基类的封装性,基类的改变会对派生类产生很大影响,且派生类和基类间的依赖关系很强,耦合度较高。
  • 对象组合是一种has-a(有一个)的关系。假设B组合了A,每个B对象中都有一个A对象。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用,因为对象的内部细节对外部是不可见的,对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度较低。

所以,在实际开发中,建议优先使用对象组合而不是类继承。对象组合具有较低的耦合度和良好的封装性,有助于提高代码的维护性。不过,继承仍然有其适用的场景,比如实现多态性时是必需的。在选择继承或组合时,应根据具体的关系和需求来判断,优先选择组合,只有在适合继承和需要实现多态性时才使用继承。

在这里插入图片描述

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

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

相关文章

运维级影像归档与通信系统(PACS)源码

运维级医院PACS系统源码&#xff0c;带演示&#xff0c;带使用手册和操作说明书 &#xff0c;带三维重建与还原功能&#xff0c;开发环境&#xff1a;VC MSSQL。 一、影像归档与通信系统&#xff08;PACS&#xff09;概述 PACS影像归档与通信系统”( Picture Archiving and C…

小型双轮差速底盘灭火功能的实现

1. 功能说明 灭火机器人是一种特殊的机器人&#xff0c;专门用于进行火灾扑救和灭火任务。它们通常具备以下功能和特点&#xff1a; ① 火灾侦测&#xff1a;灭火机器人配备了各种传感器和探测设备&#xff0c;可以检测烟雾、温度升高等火灾迹象。 ② 火灾扑救&#xff1a;灭火…

cadence virtuoso layout MOS串联线在layout中合并(merge)掉

如图&#xff0c;net10合并掉 解决办法&#xff1a; shiftE&#xff0c;取消勾选Abut server&#xff0c;save

第六章应用层

1.应用层概述 应用层是计算机网络体系结构的最顶层&#xff0c;是设计和建立计算机网络的最终目的&#xff0c;也是计算机网络中发展最快的部分。 早期基于文本的应用(电子邮件、远程登录、文件传输、新闻组) 20世纪90年代将因特网带入千家万户的万维网www 当今流行的即时通信…

彻底解决IDEA输出中文乱码问题

本文一共有3种方法&#xff0c;针对的情况是输出中文乱码问题 问题描述 无法正确输出中文字符&#xff1a;&#xff08;请正确分辨自己是哪一种乱码问题&#xff01;&#xff09; 解决方法 1、最容易想到 File -> Settings -> File Encodings下设置编码格式为UTF-8…

安卓:JzvdStd——网络视频播放器

目录 一、JzvdStd介绍 JzvdStd的特点和功能&#xff1a; JzvdStd常用方法&#xff1a; 二、JzvdStd使用 1、补充知识&#xff1a; 例子&#xff1a; MainActivity &#xff1a; VideoPageAdapter &#xff1a; activity_main&#xff1a; video_page&#xff1a; …

如何在C#中处理空值

在任何编程语言中开发应用程序时&#xff0c;经常会遇到空异常或空引用异常。空指针或空引用是指不引用有效的内存位置或对象的指针。这是一个困扰程序员已经很久的问题&#xff0c;自从程序员开始编写程序以来。空值是一个特殊的值&#xff0c;表示没有有效值可用。当将空值赋…

hbuilderx主题色分享-github风格

效果 步骤 hbuilderx总共有三种主题&#xff0c;绿柔主题Default,酷黑主题Monokai,雅黑主题Atom One Dark,修改主题色是基于三种主题之一的&#xff0c;不能直接创建一个新主题&#xff0c;比如下方配置是基于Atom One Dark(对象名为[Atom One Dark])&#xff0c;则当前hbuild…

【Sortable】前端拖拽库 | 简洁 | 实用 | 强大

前言 官网 - http://www.sortablejs.com/index.html中文文档 - https://www.itxst.com/sortablejs/neuinffi.htmlnpm - https://www.npmjs.com/package/sortablejs npm下载 npm i sortablejsumd <script src"https://www.itxst.com/package/sortable/sortable.min.js…

达梦数据库食用说明

环境准备 达梦数据库支持Windows、Linux和Unix操作系统&#xff0c;达梦正式版需要授权&#xff0c;我们学习的话选择试用即可&#xff0c;在本机使用VM安装一个Centos&#xff0c;然后去达梦官网下载适用自己平台的安装包。 本教程使用的是VM安装的centos7.9。所以选择X86架…

伯俊ERP与金蝶云星空对接集成表头表体组合查询连通应付单新增(应付单(伯俊)(KD 应付单)ok)

伯俊ERP与金蝶云星空对接集成表头表体组合查询连通应付单新增(应付单(伯俊)&#xff08;KD 应付单&#xff09;ok) 数据源系统:伯俊ERP 伯俊科技&#xff0c;依托在企业信息化建设方面的领先技术与实践积累&#xff0c;致力于帮助企业实现全渠道一盘货。伯俊提供数字经营的咨询…

经典文献阅读之--NoPe-NeRF(优化无位姿先验的神经辐射场)

0. 简介 在没有预先计算相机姿态的情况下训练神经辐射场&#xff08;NeRF&#xff09;是具有挑战性的。最近在这个方向上的进展表明&#xff0c;在前向场景中可以联合优化NeRF和相机姿态。然而&#xff0c;这些方法在剧烈相机运动时仍然面临困难。我们通过引入无畸变单目深度先…

SZMMSZ5246BT1G 稳压二极管(齐纳Zener二极管)的特性和应用详解

关于齐纳二极管&#xff1a;是一种特殊的二极管&#xff0c;也被称为肖特基二极管&#xff08;Schottky Diode&#xff09;。它是由金属与半导体材料的结合构成的。与普通的PN结二极管不同&#xff0c;齐纳二极管的结由金属与半导体材料组成&#xff0c;而不是两个不同的半导体…

菜单 vue3 h函数创建组件

目录 index/.vue <template><div class"menu_table"><divclass"table_row"v-for"(item, index) in menuList"click"item.disabled ! true && itemClick(item)"><!-- :style"{border-top:item.line…

一文了解DMX512透明屏的工作原理

DMX512透明屏是一种新型的显示屏技术&#xff0c;它采用了DMX512控制协议&#xff0c;可以实现透明显示效果。 DMX512是一种数字控制协议&#xff0c;常用于舞台灯光和音响设备的控制&#xff0c;通过DMX512控制器可以实现对透明屏的亮度、颜色、动画等参数的调节。 DMX512透明…

泛微最新漏洞汇总

泛微 e-cology 前台SQL注入漏洞 app.name"泛微 e-cology 9.0 OA" 验证poc: POST /weaver/weaver.file.FileDownloadForOutDoc HTTP/1.1 Host: {{Hostname}} Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q0.9 Connection: close …

探索前端图片如何携带token进行验证

前言 图片在前端开发中扮演了重要的角色&#xff0c;它们不仅仅是美观的元素&#xff0c;还可以传递信息和激发用户的兴趣。随着应用场景的增多&#xff0c;前端开发人员就需要在图片加载过程中携带验证的信息。如 token&#xff0c;用于身份验证、权限控制等方面。通过在图片的…

揭示C语言中CPU对register变量分配的决策过程

揭示C语言中CPU对register变量分配的决策过程 博主简介一、引言1.1、register变量的定义和用途1.2、CPU对register变量分配的重要性 二、CPU寄存器分配的概述2.1、CPU寄存器的作用和程序执行过程中的角色2.2、不同类型的CPU寄存器&#xff08;通用寄存器、特殊寄存器等&#xf…

redis基础总结(数据类型)

Redis十大数据类型 String String 是redis最基本数据类型,一个key对应一个value. String类型是二进制安全的,意思是Redis的string类型可以包含任何数据,比如jpg图片或者序列化的对象; String类型是最基本的数据类型,一个redis中字符串value最多是512M; String类型在redis底层…

【已解决】span的宽度与高度如何设置

本博文源于笔者基础不扎实的情况下遇到的一个问题&#xff0c;问题是我有三个span&#xff0c;想让它们宽度与高度再大点&#xff0c;结果发现怎样设置都设置不了。最后不经意间解决问题 文章目录 1、问题再现2、解决方案3、解决效果 1、问题再现 <span>1</span>…