C++ 继承

ops/2024/12/22 9:24:19/

C/C++总述:Study C/C++-CSDN博客 

目录

基类&派生类

访问控制和继承

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

改变访问权限

继承中的作用域

派生类的默认成员函数

继承的静态成员与友元

多继承与菱形继承

虚拟继承

继承与组合


面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。

当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类

继承代表了 is a 关系。

// 基类
class Animal {// eat() 函数// sleep() 函数
};//派生类
class Dog : public Animal {// bark() 函数
};

基类&派生类

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

其中,继承方式 是 public、protected 或 private 其中的一个,基类 是之前定义过的某个类的名称。如果未使用继承方式,则默认为 private

访问控制和继承

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。

访问publicprotectedprivate
同一个类yesyesyes
派生类yesyesno
外部的类yesnono

一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

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

我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:

  • 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有保护成员来访问。
  • 保护继承(protected): 当一个类派生自保护基类时,基类的公有保护成员将成为派生类的保护成员。
  • 私有继承(private):当一个类派生自私有基类时,基类的公有保护成员将成为派生类的私有成员。
类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

总结:

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

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

#include <iostream>
using namespace std;//基类People
class People {public:void show();protected:char *m_name;int m_age;
};void People::show() {cout << m_name << "的年龄是" << m_age << endl;
}//派生类Student
class Student : public People {public:void learning();public:using People::m_name;  //将protected改为publicusing People::m_age;  //将protected改为publicfloat m_score;private:using People::show;  //将public改为private
};void Student::learning() {cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}int main() {Student stu;stu.m_name = "gh";stu.m_age = 22;stu.m_score = 100;stu.show();  //compile errorstu.learning();return 0;
}

代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。
不过,我们使用 using 改变了它们的默认访问权限,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。
因为 show() 函数是 private 属性的,所以代码第 36 行会报错。把该行注释掉,程序即可正常运行。
 

继承中的作用域

  1. 在继承体系中基类派生类都有独立的作用域
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
#include <iostream>
using namespace std;//基类People
class People {public:void show();protected:char *m_name;int m_age;
};void People::show() {cout << "嗨,大家好,我叫" << m_name << ",今年" << m_age << "岁" << endl;
}//派生类Student
class Student : public People {public:Student(char *name, int age, float score);public:void show();  //隐藏基类的show()private:float m_score;
};Student::Student(char *name, int age, float score) {m_name = name;m_age = age;m_score = score;
}void Student::show() {cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << endl;
}int main() {Student stu("gh", 22, 100);//使用的是派生类新增的成员函数,而不是从基类继承的stu.show();//使用的是从基类继承来的成员函数stu.People::show();return 0;
}

基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成隐藏。第 37 行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。
但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符,如第 39 行代码所示。

派生类的默认成员函数

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

 

继承的静态成员与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 。

多继承与菱形继承

菱形继承有数据冗余和二义性的问题存在

数据冗余:Assistant类中有Student和Teacher类的,Teacher和Stuent类中又有Person类的,会导致Assistant中重复出现两份一样的Person类的成员的相关代码,并且在Assistant类调用构造函数,拷贝函数等函数时,也会重复调用两次Preson类的相同函数,造成空间浪费等问题,从而导致数据冗余问题

二义性:Assistant类继承了Student和Teacher类,那Student和Teacher类继承的Preson类的成员,在Assistant类中想访问Preson类成员,不能明确知道到底是Student类的还是Teacher类的,从而形成了二义性问题。二义性问题只能在Preson类成员前加Student::或Teacher:: 来明确到底是哪个类的

要解决菱形继承的这两个问题,就需要用菱形虚拟继承来解决

虚拟继承

 解决方法是在“腰部”类增加virtual关键字

这样做可以做到访问Person类中的成员时,访问的都是同一个成员

注意:在VS环境下,使用菱形虚拟继承后,内存中就不是之前那种成员挨个存储了,变为了下图所示的情形,内存中变为了A类,B类,C类,最下面才是D类,并且在加virtual的两个类中,他们内存中的第一个位置存储的是一个地址(虚基表),那个地址是此时的这个存储位置距离父类D中成员位置的字节数,可以通过这个地址,来找到父类的成员

在VS环境中,内存的情况就是上面图片所显示的, 地址中存储的就是该位置距离最下方存储的公共的D类成员的字节数,比如说A类和B类都只有一个成员,并且字节数都是4,那么A类所存储的地址中存的数值就应该是20,因为父类成员的位置与该位置中间隔了20个字节的距离
地址中存储的字节数称为距离或偏移量
所以根据这个知识点,我们在计算A类和B类的大小时,还需要加上那个地址的大小结合上面所说,一般我们不建议使用多继承,尤其是菱形继承,会在代码的复杂度和相关的性能上都有问题

继承与组合

 组合也是一种类复用的手段。

eg:

class Tire
{
protected:string _brand = "Michelin";  // 品牌size_t _size = 18;         // 尺寸
};class Car{
protected:string _colour = "白色"; // 颜色string _num = "xxxxx"; // 车牌号Tire _t; // 轮胎
};  

 区别:

public继承是一种is-a的关系,每个子类对象都是一个父类对象,例如“学生”“人”(子类学生,父类人)

组合是一种has-a的关系,B组合了A,每个B对象中都有一个A,例如“车”包含“轮胎”

如果两个类既可以是is-a,又可以是has-a的关系,那么优先使用组合

  • 继承是一种白盒复用,父类内部的细节对子类可见,破坏了封装。子类将会继承父类的公有和保护成员,一旦父类修改了这些成员的实现,将会影响子类的功能。子类和父类之间的依赖关系强,耦合度高。
  • 组合是一种黑盒复用,父类内部的细节对子类不可见,子类仅可使用父类的公有成员,只要父类的公有成员的实现细节不变,子类影响较小。父子之间没有很强的依赖关系,耦合度较低。

http://www.ppmy.cn/ops/14456.html

相关文章

2、选择什么样的机器人本体

如果说世界是物质的&#xff0c;那么应该先制造出机器人的本体&#xff0c;再让她产生灵魂。如果是精神的呢&#xff0c;世界是无中生有的呢&#xff0c;那就先在仿真中研究算法吧。 而我比较崇尚初中哲学的一句话&#xff0c;世界是物质的&#xff0c;物质是运动的&am…

【随想录】Day31—第八章 贪心算法 part01

目录 题目1: 455. 分发饼干1- 思路2- 题解⭐分发饼干 ——题解思路 题目2: 摆动序列1- 思路2- 题解⭐摆动序列 ——题解思路 题目3: 最大子数组和1- 思路2- 题解⭐ 最大子数组和 ——题解思路 题目1: 455. 分发饼干 题目链接&#xff1a;455. 分发饼干 1- 思路 贪心的思路&am…

Scala详解(6)

Scala 集合 字符串 Scala中字符串同样分为可变字符串和不可变字符串&#xff0c;不可变字符串使用String来定义&#xff0c;可变字符串使用的是StringBuilder来定义 package com.fesco.string ​ object StringDemo { ​def main(args: Array[String]): Unit { ​// 可变字符…

【03-掌握Scikit-learn:深入机器学习的实用技术】

文章目录 前言数据预处理缺失值处理数据缩放特征选择模型训练参数调整模型评估总结前言 经过了对Python和Scikit-learn的基础安装及简单应用,我们现在将更深入地探究Scikit-learn的实用技术,以进一步提升我们的数据科学技能。在本文中,我们将涵盖数据预处理、特征选择、模型…

《深入浅出.NET框架设计与实现》笔记6.4——ASP.NET Core应用程序多种运行模式之四——服务承载

ASP.NET Core应用程序可以在多种运行模式下运行&#xff0c;包括自宿主&#xff08;Self-Hosting&#xff09;、IIS服务承载、桌面应用程序、服务承载。 因此选择和时的模式很重要。 服务承载 在服务承载模式下&#xff0c;ASP.NET Core应用程序将注册为Windows服务&#xf…

【论文阅读】BGE Landmark Embedding: 一种用于大语言模型长上下文检索增强的嵌入方法

大语言模型&#xff08;LLM&#xff09;在面对许多应用时需要能够处理长序列输入&#xff0c;检索增强是处理长上下文语言建模的一种非常有效的方法。然而&#xff0c;现有的检索方法通常与分块的上下文一起工作&#xff0c;这容易导致语义表示质量低下和有用信息检索不完整。今…

力扣HOT100 - 19. 删除链表的倒数第N个节点

解题思路&#xff1a; 链表题目&#xff1a;哑节点、栈、快慢指针&#xff08;双指针&#xff09; 方法一&#xff1a;计算链表长度 class Solution {public ListNode removeNthFromEnd(ListNode head, int n) {ListNode dum new ListNode(0, head);int len getLen(head);…

ORAN每个端点和每个C平面消息的限制

O-RU每个端点的处理限制 当O-RU的处理粒度是基于端点的&#xff0c;即&#xff0c;在O-RU中处理C/U平面消息的处理资源被分配给每个端点时&#xff0c;O-RU可以对每个端点施加特定限制&#xff0c;例如&#xff0c;endpoint-section-capacity、endpoint-beam-capacity、endpoi…