【C++高阶】C++继承学习手册:全面解析继承的各个方面

📝个人主页🌹:Eternity._
⏩收录专栏⏪:C++ “ 登神长阶 ”
🤡往期回顾🤡:模板进阶
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

继承

  • 📖1. 继承的概念及定义
    • ⛰️继承的概念
    • 🌄继承定义
  • 📙2. 基类和派生类对象赋值转换
  • 📕3. 继承中的作用域
    • 🎩成员变量隐藏
    • 🎈成员函数隐藏
  • 📚4. 派生类的默认成员函数
    • 🧩默认成员函数
    • 🧩派生类默认函数特征
  • 📒5. 友元与静态成员变量
    • 🍂友元
    • 🍁静态成员
  • 📜6. 多继承
    • 🌞菱形继承
    • 🌙虚拟继承
    • ⭐虚拟继承解决数据冗余和二义性的原理
  • 🔥7. 总结


前言: 当我们踏上C++编程的旅程时,继承无疑是一个无法回避且至关重要的概念。作为面向对象编程的三大特性之一,继承不仅让我们能够创建出层次清晰、结构合理的代码,还极大地提高了代码的可重用性和可维护性。在本文中,我们将一起深入探讨C++继承的奥秘,从基础概念到高级应用,逐步揭开它的神秘面纱

C++继承允许我们定义一个基类(或称为父类),并从这个基类中派生出新的类(称为派生类、子类)。派生类会继承基类的成员和成员函数,同时还可以添加自己的成员和成员函数。这种能力使得我们能够构建出复杂的类层次结构,实现代码的模块化和复用

在本文的学习中我们不仅仅要了解继承的基本概念。在实际编程中,我们还需要掌握如何正确使用继承、如何避免常见的继承陷阱、以及如何利用继承来优化我们的代码结构。因此,本文将带领大家从多个角度全面学习C++继承,包括继承的语法规则、访问控制、构造函数与析构函数的调用、多重继承与菱形继承等问题

让我们一起踏上学习C++继承的旅程吧!


📖1. 继承的概念及定义

⛰️继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

继承代码示例

class A 
{
public:void func(){cout << "A::func()" << endl;}
protected:int _a = 10;
};// 继承后父类A的成员_a(成员函数+成员变量)都会变成子类的一部分
class B : public A
{
public:// ......
protected:int _b = 100;
};int main()
{A a ;B b;a.func();b.func(); // b可以调用A中的成员函数return 0;
}

🌄继承定义

我们从刚刚的代码示例可以看到A是基类(父类),B是派生类(子类)

定义格式
在这里插入图片描述
注意:在定义继承的时候继承方式可以省略不写,如果不写则是根据基类的定义来决定默认继承方式,但是建议定义时带上继承方式

class定义的类默认private继承,struct定义的类默认public继承

继承关系和访问限定符
在这里插入图片描述
在这里插入图片描述


继承基类成员访问方式的变化
继承方式和访问限定符都有三种,虽然它们组合一共有9中能使用的方法,但是我们最常用的只有红色框里面的两种用法
在这里插入图片描述

这里我们有以下几点需要注意:

  • 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  • 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

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

关于赋值规则这里我们先提两点:

  • 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去
  • 基类对象不能赋值给派生类对象

在这里插入图片描述


我们在讲C++入门知识的时候讲过,引用类型不同的变量时,会产生一个临时变量,临时变量具有常性,需要const修饰,但是在继承中就不需要const修饰

代码示例

int main()
{int c = 1;double d = 1.1;const int& r = d; // 中间产生了一个临时变量,临时变量具有常性,需要const修饰B b;A a = b; // 子类可以赋值给基类// b = a; // false, 基类不可以赋值给子类A& ra = b; // is-a 的关系中间不会产生临时对象,父子类的赋值兼容规则(切割/切片)return 0;
}

继承中的对象是is-a 的关系,它们中间并不会产生临时对象,这就是父子类的赋值兼容规则(切割/切片)


📕3. 继承中的作用域

关于作用域的注意事项:

  • 在继承体系中基类和派生类都有独立的作用域。
  • 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  • 注意在实际中在继承体系里面最好不要定义同名的成员

🎩成员变量隐藏

当继承的基类与子类有同名的成员变量时,不指定的话,会调用子类的成员变量

代码示例

class A 
{
protected:int _a = 10;
};class B : public A
{
public:void Print(){cout << "_a:" << _a << endl;// cout << "A: _a:" << A::_a << endl; // 要想成功打印A类的元素必须要指定cout << "_b:" << _b << endl;}
protected:int _a = 99;int _b = 100;
};int main()
{B b;// 成员变量同名// A 和 B中的 _a 构成隐藏b.Print(); // // _a = 99 , _b = 100; 就近原则return 0;
}

🎈成员函数隐藏

在继承中,同名函数并不会构成函数重载,因为他们在不同的作用域,每个类都是独立的,成员函数满足函数名相同就构成隐藏

代码示例

class A 
{
public:void func(){cout << "func()" << endl;}
protected:int _a = 10;
};class B : public A
{
public:// void func(int b){cout << "func(int b)" << endl;}
protected:int _b = 100;
};int main()
{B b;// 成员函数同名// A 和 B中的 func() 构成隐藏b.func(); // 打印“func(int b)”
}

📚4. 派生类的默认成员函数

🧩默认成员函数

默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个
在这里插入图片描述

相关文章:默认成员函数


🧩派生类默认函数特征

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的operator=必须要调用基类的operator=完成基类的复制
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  • 派生类对象初始化先调用基类构造再调派生类构造
  • 派生类对象析构清理先调用派生类析构再调基类的析构
  • 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系

在这里插入图片描述
综上所述:关于基类和子类的调用顺序,一般情况都是先父后子,但是析构必须先子后父,来避免析构完父类之后,子类出错

继承默认函数的实现代码示例

class A 
{
public:A(){}A(int a):_a(a){cout << "A()" << endl;}A(const A& a):_a(a._a){cout << "A(const A& a)" << endl;}A& operator=(const A& a){cout << "A& operator=(const A& a)" << endl;if (&a != this){_a = a._a;}return *this;}~A(){cout << "~A()" << endl;}
protected:int _a = 10;
};class B : public A
{
public:B(){}B(int a, int b)// :_a(a) // _a不是基类成员不能这样初始化:A(a),_b(b){cout << "B()" << endl;}B(const B& b)// :_a(a) // _a不是基类成员不能这样初始化:A(b), _b(b._b){cout << "B(const A& a, const B& b)" << endl;}B& operator=(const B& b){cout << "B& operator=(const B& b)" << endl;if (&b != this){// 需要调用A类的 operator=A::operator=(b);_b = b._b;}return *this;}// 析构函数会先析构父类,而有时候先析构父类,子类会出事// 不需要显式调用父类析构~B(){cout << "~B()" << endl;}
protected:int _b = 100;
};
int main()
{B b1(1, 100);B b2(b1);B b3(2, 200);b1 = b3;return 0;
}

📒5. 友元与静态成员变量

🍂友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员,因为朋友的朋友不一定也是自己的朋友,如果基类,子类都想使用必须都在各自的域里面声明

代码示例

class A 
{
public:friend void Print(const A& a, const B& b);
protected:int _a = 10;
};class B : public A
{
public:// 
protected:int _b = 100;
};void Print(const A& a, const B& b)
{cout << a._a << endl;cout << b._b << endl;
}int main()
{A a;B b;Print(a, b);
}

🍁静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例

代码示例

class A 
{
public:A(){++_count;}static int _count;
protected:int _a = 10;
};
int A::_count = 0;
class B : public A
{
public:// 
protected:int _b = 100;
};int main()
{A a;B b;cout << A::_count << endl;
}

📜6. 多继承

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

class B : public A

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

class D : public B , public C

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

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

🌞菱形继承

在这里插入图片描述

class A
{
protected:int _a = 1;
};
class B :public A
{
protected:int _b = 2;
};
class C :public A
{
protected:int _c = 3;
};
class D :public B, public A
{
protected:int _d = 4;
};

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在D的对象中_a成员会有两份,我们在访问的时候无法明确知道访问的是哪一个,必须要显示指定访问哪个父类的成员,但是数据冗余任然无法解决!
在这里插入图片描述


🌙虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在A和B的继承A时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用

代码示例

class A
{
protected:int _a = 1;
};
class B : virtual public A
{
protected:int _b = 2;
};
class C : virtual public A
{
protected:int _c = 3;
};
class D : public B, public A
{
protected:int _d = 4;
};

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

  • 虚拟继承通过将共同的祖先类(即虚基类)的拷贝在派生类对象中只保留一份,来解决这个问题。具体来说,虚拟继承会在内存中创建一个虚基表,并在派生类对象中存储一个指向这个虚基表的指针(即虚基表指针)。虚基表中存的偏移量。通过偏移量可以找到下面的A,而无需在派生类对象中多次存储这些数据成员。因此,虚拟继承通过减少重复存储的数据成员来消除数据冗余
  • 虚拟继承通过改变派生类访问虚基类成员的方式来解决这个问题。在虚拟继承中,派生类对象通过虚基表指针来访问虚基类(即共同祖先类)的成员。由于虚基表中存储了虚基类成员的地址,因此派生类对象可以明确地知道应该访问哪个虚基类成员,从而消除了二义性。

在这里插入图片描述

加上表中偏移量可以找到最底下的A


🔥7. 总结

回顾学习过程,我们学会了如何定义基类与派生类,掌握了访问控制规则,理解了构造函数与析构函数在继承中的作用,还探讨了多重继承及其带来的挑战。这些知识不仅丰富了我们的编程技能,更为我们解决实际问题提供了有力的工具

在结束对C++继承的学习之旅后,我们不禁感叹其强大的功能和灵活性。通过深入探究继承的基本概念、语法规则以及高级应用,我们逐渐揭开了其背后的奥秘,并体验到了它在面向对象编程中的独特价值

学习C++继承并非一蹴而就的过程。它需要我们不断地实践、思考、总结和创新。在未来的编程之路上,我们将继续深化对继承的理解,探索其更多的应用场景和高级特性,如虚继承、接口继承等,我们也要认识到继承并非万能的。在使用继承时,我们需要权衡其带来的好处和潜在的风险,避免过度使用导致代码结构复杂、难以维护。我们应该根据具体的需求和场景,选择最合适的编程范式和工具!!!
在这里插入图片描述

希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述

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

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

相关文章

使用 Django 创建 App

文章目录 步骤 1&#xff1a;创建 Django 项目步骤 2&#xff1a;创建 App步骤 3&#xff1a;配置 App步骤 4&#xff1a;编写代码步骤 5&#xff1a;运行服务器 在 Django 中&#xff0c;App 是组织代码的基本单元&#xff0c;它可以包含模型、视图、模板等组件&#xff0c;帮…

LabVIEW RT在非NI硬件上的应用与分析

LabVIEW RT&#xff08;实时操作系统&#xff09;可运行在非NI&#xff08;National Instruments&#xff09;硬件上&#xff0c;如研华工控机&#xff0c;但需要满足特定硬件要求。本文从硬件要求、开发和运行差异、可靠性、稳定性、优势和成本等多角度详细分析在非NI硬件上运…

可视化大屏:颜值低了,就要多看多学习,切忌讳疾忌医。

Hi&#xff0c;这是第15期了&#xff0c;再发一波three.js的效果。 可视化大屏与Three.js结合可以创建出令人印象深刻的交互式3D可视化效果&#xff0c;为用户呈现更加生动和交互式的数据展示。

Wells Fargo 借助 MongoDB 推出下一代银行卡支付

MongoDB客户案例导读 MongoDB以其灵活的数据模型、高性能的实时分析能力和可扩展的分布式架构&#xff0c;帮助Wells Fargo富国银行显著提升了数据处理效率和业务响应速度&#xff0c;为其大型机现代化和数字化转型提供了强有力的技术支撑。 金融服务市场正在经历一场变革&am…

Django里的Form组件

Form组件提供 自动生成HTML标签和半自动读取关联数据 (“半自动”是指还得需要自己手写输入数据进来)表单验证和错误提示 要想创建并使用该组件&#xff0c;操作步骤如下&#xff1a; 在 views.py 里创建类 # 在 views.py 文件里from django import formsclass AssetForm(fo…

R语言数据探索和分析23-公共物品问卷分析

第一次实验使用最基本的公共物品游戏&#xff0c;不外加其他的treatment。班里的学生4人一组&#xff0c;一共44/411组。一共玩20个回合的公共物品游戏。每回合给15秒做决定的时间。第十回合后&#xff0c;给大家放一个几分钟的“爱心”视频&#xff08;链接如下&#xff09;&a…

跨界合作机会:通过淘宝数据挖掘潜在的合作伙伴与市场拓展方向

淘宝平台汇聚了众多商家和消费者&#xff0c;生成了大量的交易数据&#xff0c;这些数据为商家提供了挖掘跨界合作机会和市场拓展方向的丰富线索。以下是如何利用淘宝数据来寻找潜在的合作伙伴和探索新的市场机会的一些策略&#xff1a; 消费者行为分析&#xff1a;通过跟踪消费…

leetcode:不同的二叉树

class Solution { public:int numTrees(int n) {vector<int> dp(n1);dp[0] 1;dp[1] 1;for(int i 2;i < n;i){for(int j 1;j < i;j) // 当根节点为j时{dp[i] dp[j-1] * dp[i-j];}}return dp[n];} }; /* dp[i] i个不同的数组成的二叉搜索数的个数假设 i 5当根…

VUE之重定向redirect

VUE之路由和重定向redirect 这个小知识点是在学习做项目的时候遇到的一个问题&#xff0c;借鉴了一个他人的项目&#xff0c;是一个酒店管理系统&#xff0c;拿到源码之后导到我的vscode里。 参考链接 导的过程比较顺利&#xff0c;正常安装&#xff0c;加依赖&#xff0c;没有…

Springboot的小型超市商品展销系统-计算机毕业设计源码01635

摘 要 科技进步的飞速发展引起人们日常生活的巨大变化&#xff0c;电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用。信息时代的到来已成为不可阻挡的时尚潮流&#xff0c;人类发展的历史正进入一个新时代。在现实运用中&#xff0c;应用软件的工作…

git clone 项目报“鉴权失败”的解决办法

#问题展示# git clone https://gitee.com/soaringsoft/.....git 正克隆到...... Username for https://gitee.com:...... Password for https://.....gitee.com:...... remote: [session-1440f183] Unauthorized fatal: git clone https://gitee.com/soaringsoft/.....gi…

第 2 章:Spring Framework 中的 IoC 容器

控制反转&#xff08;Inversion of Control&#xff0c;IoC&#xff09;与 面向切面编程&#xff08;Aspect Oriented Programming&#xff0c;AOP&#xff09;是 Spring Framework 中最重要的两个概念&#xff0c;本章会着重介绍前者&#xff0c;内容包括 IoC 容器以及容器中 …

cloudflare worker访问自己的网站显示521问题解决

写在前面&#xff1a;如果你的网站不是在80端口上运行的&#xff0c;开一下80端口可能就行了… 1.在cloudlare上添加域名 前文搭建了自己的DNS服务器&#xff08;DNS服务器搭建&#xff09;&#xff0c;现在想通过自己的DNS服务器解析域名&#xff0c;需要四步&#xff1a; 添…

第17章通信系统架构设计理论与实践

常见的5种常用的网络架构和构建网络的相关技术&#xff0c;以及网络构建的分析和设计方法。 17.1通信系统概述 通信技术和网络技术的发展&#xff0c;通信网络发生很大变化&#xff0c;入网的形式变化&#xff0c;传输的速率的提高、接入网络的方式多样化、网络结构的更为复杂…

ATA-3040C功率放大器的正确使用方法

功率放大器是一种用于增强电信号功率的重要设备。正确使用功率放大器可以确保信号的稳定放大&#xff0c;并避免设备损坏。下面将介绍功率放大器的正确使用方法。 确定输入信号的功率范围&#xff1a;在使用功率放大器之前&#xff0c;需要确定输入信号的功率范围。过大的输入功…

端午顶流!国台酒再度携手叠滘龙船赛,持续深耕广东市场

执笔 | 洪大大 编辑 | 扬 灵 在刚刚过去的端午期间&#xff0c;备受瞩目的2024中国&#xff08;佛山&#xff09;叠滘龙船漂移大赛在佛山市叠滘水乡精彩上演。作为“全国唯一”的水上弯道竞速民间体育项目&#xff0c;叠滘龙船漂移大赛凭借着极高的操作难度和观赏性&#xf…

SQL Server中的FOR XML PATH以及Split

前提 数据库是在2016年以下的版本&#xff0c;以上的版本&#xff0c;现在有最新的函数可以支持者两个了 Split 在c#中Split可以将以指定字符分割的字符串转换为字符串数组&#xff0c;这里主要是用于&#xff0c;主表存储了多个从表的id&#xff0c;存储的时候用的字符串&a…

2024.6.12 作业 xyt

今日课堂练习&#xff1a;vector构造函数 #include <iostream> #include <vector> using namespace std;void printVector(vector<int> &v) {vector<int>::iterator iter;for(iterv.begin(); iter ! v.end(); iter){cout << *iter <<…

Nginx与Gateway

Nginx与Gateway Nginx 基本介绍 Nginx 是一款轻量级的高性能 Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。它由俄罗斯的 Igor Sysoev 所开发&#xff0c;最初供俄罗斯大型的门户网站及搜索引擎 Rambler 使用。 Nginx 的特点在于其占用…

【数据结构】【版本1.1】【线性时代】——单链表

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《算法神殿》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、顺序表的问题二、链表的概念三、单链表的模拟实现3.1 定义3.2 打印3.3 创建新节点3.4 头插3.5 尾插3…