继承详解——C++深度学习解析

news/2024/7/22 13:50:36/文章来源:https://blog.csdn.net/strive_mianyang/article/details/139021188

        前言 : 本节内容是讲解面向对象中三大特性之一的继承。 继承是面向对象程序设计中代码复用的一种重要手段。 通过继承, 我们就可以在原本的代码上进行扩展, 而不是重新写一段代码, 让代码变得冗余。 同时, 继承也体现了面向对象程序设计的层次结构,不同于我们以前经常见到的函数的复用, 继承是层次上的复用, 也可以说是类层次的复用

        注:本节讲述的是c++里面的继承, 适合正在学习c++的友友们进行观看。

        

目录

一.什么是继承

1.1继承体系

1.2定义格式

1.3继承方式

二、继承的作用域问题

三、继承的切片问题

四、派生类的默认成员函数

4.1派生类的构造函数:

4.2派生类的析构函数

4.3派生类的拷贝构造函数

 4.4派生类的赋值重载

五、继承与友元

六、继承与静态成员

七、菱形继承和菱形虚拟继承


一.什么是继承

1.1继承体系

        在继承体系中, 被继承的那个类叫做基类或者父类, 继承来的那个类叫做派生类或者子类

        我们通过一个具体的实例来看一下继承是怎么实现类的复用的:

定义一个继承体系:

#include<iostream>
using namespace std;
#include<vector>
#include<string>//基类
class Person 
{
public://构造函数Person(const string name, const string sex) :_name(name),_sex(sex){}//打印接口void Print(){cout << "姓名" << _name << "    性别:" << _sex << endl;}private:string _name;        //姓名string _sex;         //性别
};//学生类, 子类
class Student : public Person //继承的写法就是 :+ 继承方式 + 继承的基类
{
public:Student(string name, string sex, int num) :Person(name, sex) //初始化列表调用基类的构造函数方式,_num(num){}private:int _num;             //学号
};//教师类, 子类
class Teacher : public Person  //继承的写法就是 :+ 继承方式 + 继承的基类 
{
public:Teacher(string name, string sex, int wages) :Person(name, sex) //初始化列表调用基类的构造函数方式,_wages(wages){}private:int _wages;           //工资
};

        在这个继承体系里面, 虽然Student和Teacher表面上只定义了_num 或者_wages。但是,Student和Teacher都继承了Person类。

        而Person类里面定义了_name, _sex,以及外部接口Print(),  那么Student和Teacher里面就会隐式的多出来一份Person类的_name, _sex, 以及它的Print()。 

        当我们进行Studen类或者Teacher类的实例化的时候, 实例化对象里面就会有继承来的成员变量或者外部接口。 如图实例化一个Student对象:

        

 所以, 继承就是: 对于子类或者派生类来说。 父类也就是基类有的, 我也有。 

1.2定义格式

 定义一个继承的格式:

1.3继承方式

        派生类使用不同的继承方式从基类继承来的成员,权限是不同的。下面是根据不同方式派生类继承来的成员的权限图:

        上面的图其实总结下来就是可以分为两类:一类是基类里面的private成员、一类是基类里面的public, protected成员。 

  •         对于基类里面的private成员。 不管派生类使用什么方式继承, 继承后这些成员都在派生类中不可见。
  •         对于基类里面的非private成员, 看继承方式和这个成员在基类之中的权限哪个小。 这个权限就是派生类中的权限。 比如 student私有继承基类的公有成员。 那么继承后的这个成员就是私有类型。 

        实际上, 我们在实际运用中一般只会用到public继承, 因为private和protected继承来的成员只能在类域中使用, 实际的可维护性不强

二、继承的作用域问题

        在继承体系中。 虽然派生类继承了基类。 但是对于派生类的成员变量和基类的成员变量来说, 都有它们自己的作用域。 而且, 假如一个派生类继承了多个基类, 那么每个基类都有一个属于自己的区域。 这就叫类域。

        类域可以帮助我们解决继承体系中的重名问题。并且如果基类的私有成员虽然是不可见的, 但它在自己的类域中同样可以被访问。      

        如果子类和父类中有同名的成员, 那么父类的成员将在自己的类域中隐藏起来。我们直接访问是访问不到的:

class Person 
{
public://打印接口void Print(){cout << "姓名" << _name << "    性别:" << _sex << endl;}private:string _name = "张三";        //姓名string _sex = "男";         //性别
};class Student : public Person 
{
public:void Print() {cout << "学号: " << _num << endl;}private:int _num = 111;             //学号
};int main() 
{Student stu;stu.Print();}

        这里要注意的是:成员函数只要函数名相同, 就会构成隐藏

想要访问基类中隐藏在类域中的成员, 那么就要利用域操作符显示的调用:

成员变量的隐藏:

        如果是继承多个父类, 并且父类之间有函数名或者成员变量重名, 那么编译器就会显示目标不明确, 无法编译:

//基类1
class Person1
{
public://打印接口void Print(){cout << "1:" << "姓名" << _name << "    性别:" << _sex << endl;}string _name = "张三";        //姓名string _sex = "男";         //性别int _num = 11;
};
//基类2
class Person2
{
public://打印接口void Print(){cout << "2:" << "姓名" << _name << "    性别:" << _sex << endl;}string _name = "李四";        //姓名string _sex = "男";         //性别int _num = 22;
};
//基类3
class Person3
{
public://打印接口void Print(){cout << "3:" << "姓名" << _name << "    性别:" << _sex << endl;}string _name = "王五";        //姓名string _sex = "男";         //性别int _num = 33;
};class Student : public Person1, public Person2, public Person3
{
public:};int main() 
{Student stu;stu.Print();}

        想要成功编译就要指定是调用哪个父类接口:

三、继承的切片问题

        派生类和父类的关系就是:父类有的, 派生类也有。 

        并且, 派生类可以给父类进行赋值。 这个赋值操作,有一个形象的叫法叫做 切片

        

        使用派生类给父类赋值、 父类的指针指向派生类, 父类的引用引用派生类。 这些都是切片操作。 (注意, 不能派生类的指针或者引用指向父类, 否则不安全, 容易发生野指针。

        

四、派生类的默认成员函数

对于一个类来说, 有六个默认成员函数:

  • 默认构造函数 : 对于内置类型不做处理, 对自定义类型去调用它的默认构造
  • 析构函数 : 对于内置类型不做处理, 对于自定义类型去调用它的析构函数
  • 默认拷贝构造 : 对于内置类型进行浅拷贝, 对于自定义类型去调用它的拷贝构造
  • 默认赋值重载 : 对于内置类型进行浅拷贝, 对于自定义类型去调用它的赋值重载
  • 移动构造函数 :移动构造是c++11新增, 涉及右值引用知识点。本篇不会讲到
  • 移动赋值重载 :移动赋值重载是c++11新增, 涉及右值引用知识点, 本篇不会讲到

4.1派生类的构造函数:

  •         对于一个普通的类, 我们只有内置类型和自定义类型。如果有自定义类型,这个自定义类型是在初始化列表进行初始化,并且我们可以显示的调用该自定义类型的构造函数, 也可以隐式的调用该自定义类型的构造函数(当隐式调用时, 自定义类型必须有默认构造
  •         如果是在继承体系中的话, 基类部分的初始化同样是在初始化列表部分。 并且我们同样可以选择显示的调用该自定义类型的构造函数, 或者隐式的调用该自定义类型的构造函数(当隐式调用时, 基类中必须有默认构造)。
class Person 
{
public://构造函数Person(string name, string sex) :_name(name),_sex(sex){cout << "Person()" << endl;}string _name = "张三";        //姓名string _sex = "男";         //性别int _num = 11;
};class Student : public Person 
{
public:Student(string name, string sex, int num) :_num(num),Person(name, sex){cout << "Student()" << endl;}int _num = 111;             //学号
};int main() 
{Student stu("李四", "男", 111);}

 

这里需要注意的是 :初始化列表的初始化顺序是:先初始化基类, 再初始化自定义类型, 最后初始化内置类型。 而对于基类、 自定义类型、 内置类型, 会按照他们的声明顺序进行初始化。

4.2派生类的析构函数

        派生类的析构函数是先析构派生类对象, 再析构基类的对象:

4.3派生类的拷贝构造函数

class Person 
{
public://构造函数Person(string name, string sex) :_name(name),_sex(sex){cout << "Person()" << endl;}//拷贝构造Person(const Person& per) {cout << "const Person&()" << endl;}string _name = "张三";        //姓名string _sex = "男";         //性别int _num = 11;
};class Student : public Person 
{
public://构造函数Student(string name, string sex, int num) :_num(num),Person(name, sex){cout << "Student()" << endl;}//拷贝构造Student(const Student& stu) :Person(stu),_num(stu._num){cout << "const Student&()" << endl;}int _num = 111;             //学号
};int main() 
{Student stu("李四", "男", 111);Student st(stu);
}

 

 4.4派生类的赋值重载

        派生类的赋值重载, 必须调用基类的赋值重载完成复制。


class Person 
{
public://构造函数Person(string name, string sex) :_name(name),_sex(sex){cout << "Person()" << endl;}//赋值重载void operator=(const Person& per) {cout << "operator=()-Person" << endl;}string _name = "张三";        //姓名string _sex = "男";         //性别int _num = 11;
};class Student : public Person 
{
public://构造函数Student(string name = "张三", string sex = "男", int num = 111):_num(num),Person(name, sex){cout << "Student()" << endl;}//赋值重载void operator=(const Student& stu) {Person::operator=(stu);//调用stu的cout << "operator=()-Student" << endl;}int _num = 111;             //学号
};int main() 
{Student stu("张三", "男", 44);Student st;st = stu;}

五、继承与友元

        友元的关系不能继承。基类的友元不能访问子类私有和保护成员。

        如下为示例:

class Person 
{
public:friend void Display(const Person& per);//构造函数Person(string name, string sex) :_name(name),_sex(sex){cout << "Person()" << endl;}private:string _name = "张三";        //姓名string _sex = "男";         //性别int _num = 11;
};class Student : public Person 
{
public://构造函数Student(string name = "张三", string sex = "男", int num = 111):_num(num),Person(name, sex){cout << "Student()" << endl;}private:int _num = 111;             //学号
};void Display(const Student& stu)
{cout << "姓名: " << stu._name << "  " << "性别: " << stu._sex << "学号: " << endl;
}int main() 
{Student stu("张三", "男", 44);Person st = stu;
}

如图, 虽然我们已经在基类中设置了友元。但友元不能继承, 我们在DIsplay函数并不能访问Student中的成员变量。 

六、继承与静态成员

基类如果定义了static静态成员, 则整个继承体系里面只有一个这样的成员。


class Person 
{
public://构造函数Person(string name, string sex) :_name(name),_sex(sex){cout << "Person()" << endl;}string _name = "张三";        //姓名string _sex = "男";         //性别static int sum;             //定义的静态变量
};
int Person::sum = 100;      //静态变量初始化class Student : public Person 
{
public://构造函数Student(string name = "张三", string sex = "男", int num = 111):_num(num),Person(name, sex){cout << "Student()" << endl;}int _num = 111;             //学号
};int main() 
{Student stu("张三", "男", 44);Person st = stu;cout << Person::sum << endl;
}

七、菱形继承和菱形虚拟继承

        我们先了解一下两个概念之后再了解菱形继承的概念, 这两个概念一个叫单继承, 另一个叫多继承。

        一个派生类只继承了一个基类, 那么这就是一个单继承:

class A 
{
public:A() {cout << "A()" << endl;}
};//B类继承A
class B : public A
{
public:B() {cout << "B()" << endl;}
};//C类继承B
class C : public B 
{
public:C() {cout << "C()" << endl;}
};

 

如果一个派生类继承了多个基类, 那么这就是一个多继承: 

class A 
{
public:A() {cout << "A()" << endl;}
};//B类继承A
class B
{
public:B() {cout << "B()" << endl;}
};//C类继承B
class C
{
public:C() {cout << "C()" << endl;}
};class D : public A, public B, public C
{
public:D() {cout << "D()" << endl;}
};

菱形继承时上面多继承的一种情况, 下面为一个菱形继承的代码:

//基类A
class A 
{
public:A() {cout << "A()" << endl;}
};//B类继承A
class B : public A
{
public:B() {cout << "B()" << endl;}
};//C类继承B
class C : public A
{
public:C() {cout << "C()" << endl;}
};class D : public B, public C
{
public:D() {cout << "D()" << endl;}
};

 

如果我们给A类一个成员变量_a:

那么D类就继承来了两份_a, 如图:

在这里利用代码演示一下:

class A 
{
public:A() {cout << "A()" << endl;}int _a;   //定义一个变量_a
};//B类继承A
class B : public A   //B继承A后, 有一份_a
{
public:B() {cout << "B()" << endl;}
};//C类继承B
class C : public A   //C继承A后, 有一份_a
{
public:C() {cout << "C()" << endl;}
};class D : public B, public C      //D继承B, C类, 这样D中就有了两份_a。
{
public:D() {cout << "D()" << endl;}
};

vs中变量d的监视窗口:

        由上面可以看出菱形继承具有数据冗余和二义性的问题。 

        为了解决这个问题, 祖师爷发明了虚拟继承。虚拟继承可以解决二义性和数据冗余的问题。但是使用虚拟继承的地方必须用对, 虚拟继承一般用在”腰间“比如将B类和C类继承A类的方式改成虚拟继承, 那么就可以解决问题。

---------------------------

接下来就是比较偏向底层的知识。很重要!很重要!很重要!

---------------------------

下面是我定义的一个菱形继承体系:


class A 
{
public:A() {cout << "A()" << endl;}int _a;   //定义一个变量_a
};//B类继承A
class B : public A   //B继承A后, 有一份_a
{
public:B() {cout << "B()" << endl;}int _b;
};//C类继承B
class C : public A   //C继承A后, 有一份_a
{
public:C() {cout << "C()" << endl;}int _c;
};class D : public B, public C      //D继承B, C类, 这样D中就有了两份_a。
{
public:D() {cout << "D()" << endl;}int _d;
};

 我现在定义一个D类型的实例化对象:

从上面的内存图我们就能比较真实的看到d1的底层存储情况。并且能够看到_a的重复存储。(0x11就是_a)

那么看一下菱形虚拟继承在底层是如何解决这个问题的:


class A 
{
public:A() {cout << "A()" << endl;}int _a = 0x11;   //定义一个变量_a
};//B类继承A
class B : virtual public A   //B继承A后, 有一份_a
{
public:B() {cout << "B()" << endl;}int _b = 0x22;
};//C类继承B
class C : virtual public A   //C继承A后, 有一份_a
{
public:C() {cout << "C()" << endl;}int _c = 0x33;
};//菱形虚拟继承
class D : public B, public C      //D继承B, C类, 这样D中就有了两份_a。
{
public:D() {cout << "D()" << endl;}int _d = 0x44;
};

现在我定义d2, 并观察内存图:

         图中我们可以观察到虽然菱形继承的问题被解决了, 但是在B类和C类中出现了一些 ”多余“

的东西。 

        但是, 这些”多余的东西“其实并不多余, 它们是一个叫做虚基表的指针。 通过这两个框框中存的地址, 就可以找到B类或者C类的虚基表

        那么什么是虚基表? 虚基表就是用来寻找虚继承来的数据域的。 对于B类和C类来说, 就是来寻找A类的。 虚基表里面存放的是一个偏移量, 通过这个偏移量, 我们就可以找到A的数据域。 如下图:

        我们先看B类的虚基表, 也就是绿框框的虚基表。 这个虚基表中存的是0x14, 那么转化为10进制就是20。我们可以看到, 从图中画黄色四角星的位置到A的数据域正好是20个字节偏移量。 

        接下来我们再看C类的虚基表, 也就是蓝框框的虚基表, 这个虚基表中村的是0x0c, 也就是12, 我们可以看到, 从图中画绿色四角形的位置到A的类域正好是12个字节的偏移量。 

        通过以上分析我们知道了菱形虚拟继承如何解决的数据冗余和二义性问题。 但是, 为什么要引入虚基表呢? D类中的B, C类数据域为什么要找到A类数据域呢?
        当我们使用切片的时候,B, C类就要去寻找A类的数据域。


int main() 
{D d2;//切片,b中有_b, _a。 所以要找到d2中的B类数据域和A类数据域。 B b = d2;//同上。C c = d2;}

 

----------------------

以上, 就是本节的全部内容。

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

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

相关文章

摸鱼大数据——Hive表操作——复杂类型

1、hvie的SerDe机制 其中ROW FORMAT是语法关键字&#xff0c;DELIMITED和SERDE二选其一。本次我们主要学习DELIMITED关键字相关知识点 如果使用delimited: 表示底层默认使用的Serde类:LazySimpleSerDe类来处理数据。 如果使用serde:表示指定其他的Serde类来处理数据,支持用户自…

破解面试难题:面试经典150题 之双指针法详解与代码实现

面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 125. 验证回文串 125. 验证回文串 STL容器 思路&#xff1a; 遍历输入的字符串&#xff0c;将其中的字母字符转换为小写&am…

排序进阶----插入排序,希尔排序

各位看官们好&#xff0c;接下来鄙人想与大家分享的实现被称为六大排序之一的插入排序。其实关于这六大排序在我们最开始就已经接触过了。我们在最开始学习c语言的时候&#xff0c;我们要学习到其中之一的冒泡排序。虽然现在看起来冒泡排序确实是没有太大的实际效果&#xff0c…

【物联网实战项目】STM32C8T6+esp8266/mqtt+dht11+onenet+uniapp

一、实物图 前端uniapp效果图&#xff08;实现与onenet同步更新数据&#xff09; 首先要确定接线图和接线顺序&#xff1a; 1、stm32c8t6开发板连接stlinkv2下载线 ST-LINK V2STM323.3V3.3VSWDIOSWIOSWCLKSWCLKGNDGND 2、ch340串口连接底座&#xff08;注意RXD和TXD的连接方式…

kali下载zsteg和stegpy

1.kali下载zsteg 从 GitHub 上克隆zsteg到kali git clone https://github.com/zed-0xff/zsteg 切换目录 cd zsteg 用于安装名为 zsteg 的 Ruby Gem 包 gem install zsteg 2.kali下载stegpy 下载网站内的stegpy-master压缩包GitCode - 开发者的代码家园 并拉到kali中 切换到s…

【豆伴匠】L1-L12更新完,一站式解决文史积累、阅读、写作难题,弯道超车,寒假必备

合抱之木&#xff0c;生于毫末&#xff1b; 九层之台&#xff0c;起于垒土&#xff1b; 千里之行&#xff0c;始于足下。 豆伴匠是什么&#xff1f; 豆伴匠内容包括&#xff1a;人、文、史、作四个模块&#xff0c;全面覆盖文史知识及读写技巧。 目前&#xff0c;豆伴匠有L…

【MySQL】SQL 基础

文章目录 【 1. SQL 的书写规则 】1.1 大小写规则1.2 常量的表示1.3 注释1.4 HELP 系统帮助 【 2. 常用数据库函数 】2.1 SHOW DATABASES 显示数据库2.2 CREATE DATABASE 创建数据库2.3 ALTER DATABASE 修改数据库2.4 DROP DATABASE 删除数据库2.5 USE 选择数据库 【 3. RDBMS …

PyTorch张量索引用法速查

作为数据科学家或软件工程师&#xff0c;你可能经常处理大型数据集和复杂的数学运算&#xff0c;这些运算需要高效且可扩展的计算。PyTorch 是一个流行的开源机器学习库&#xff0c;它通过 GPU 加速提供快速灵活的张量计算。在本文中&#xff0c;我们将深入研究 PyTorch 张量索…

【架构-19】架构风格比较

独立构件风格(Independent Components): 适用场景:需要灵活扩展和组合的复杂大数据应用 特点: 高度解耦:各组件之间高度独立,可单独开发和部署 灵活性和可扩展性:易于根据需求添加或替换组件 复杂度高:需要管理多个独立的组件及其交互 通信开销:组件间需要通过网络通信,可能会…

深入探索C++继承机制:从概念到实践的全面指南

目录 继承的概念及定义 继承的概念 继承的定义 定义格式 继承方式和访问限定符 继承基类成员访问方式的变化 默认继承方式 基类和派生类对象赋值转换 继承中的作用域 派生类的默认成员函数 继承与友元 继承与静态成员 继承的方式 菱形虚拟继承 菱形虚拟继承原理 继承…

超详细的前后端实战项目(Spring系列加上vue3)前端篇+后端篇(三)(一步步实现+源码)

好了&#xff0c;兄弟们&#xff0c;继昨天的项目之后&#xff0c;开始继续敲前端代码&#xff0c;完成前端部分&#xff08;今天应该能把前端大概完成开启后端部分了&#xff09; 昨天补充了一下登录界面加上了文章管理界面和用户个人中心界面 完善用户个人中心界面 修改一…

华为设备WLAN配置之AP上线

WLAN基础配置之AP上线 配置WLAN无线网络的第一阶段&#xff0c;AP上线技术&#xff1a; 实验目标&#xff1a;使得AP能够获得来自AC的DHCP地址服务的地址&#xff0c;且是该网段地址池中的IP。 实验步骤&#xff1a; 1.把AC当作三层交换机配置虚拟网关 sys Enter system view,…

halcon SVM 缺陷检测分类

一、概述 训练数据 二、算子解释 compactness Halcon 算子 compactness_halcon compactness-CSDN博客 *计算输入区域的紧凑度 compactness (Region, Compactness) 原理解释 convexity 每个输入区域的凸度 Halcon 算子 convexity_halcon convexity-CSDN博客 *计算每个输…

Unity LayerMask避坑笔记

今天使用Physics2D.OverlapAreaNonAlloc进行物理检测时候&#xff0c;通过LayerMask.NameToLayer传入了int值的LayerMask&#xff0c;结果一直识别不到&#xff0c;经过Debug才找到问题&#xff0c;竟是LayerMask的“值”传输有问题&#xff0c;记录一下。 直接贴代码输出结果&…

《SpringBoot》系列文章目录

SpringBoot是由Pivotal团队提供的全新框架&#xff0c;旨在简化新Spring应用的初始搭建以及开发过程。以下是一些关于SpringBoot的详细介绍&#xff1a; 设计目的&#xff1a;SpringBoot通过特定的方式来进行配置&#xff0c;使得开发人员不再需要定义样板化的配置&#xff0c…

HNU-计算机体系结构-实验1-RISC-V流水线

计算机体系结构 实验1 计科210X 甘晴void 202108010XXX 1 实验目的 参考提供为了更好的理解RISC-V&#xff0c;通过学习RV32I Core的设计图&#xff0c;理解每条指令的数据流和控制信号&#xff0c;为之后指令流水线及乱序发射实验打下基础。 参考资料&#xff1a; RISC-…

【引领光子学革命:机器学习与深度学习重塑设计与应用新纪元】

光子器件的逆向设计&#xff1a;利用深度学习技术&#xff0c;可以优化多参数光子器件的设计。通过大量的数据分析和模式识别&#xff0c;深度学习算法能够预测和优化光子器件的性能&#xff0c;从而缩短设计周期并降低设计成本。 超构表面与超材料设计&#xff1a;在新型光学材…

浅谈金融行业数据安全分类分级

数据安全管理是一项从上而下的、多方配合开展的工作。在进行数据安全管理组织架构建设时&#xff0c;需要从上而下建设&#xff1b;从而全面推动数据安全管理工作的执行和落地&#xff1b;以保证数据安全的合法合规、并长效推动业务的发展和稳定运行。 金融行业机构应设立数据…

Hexo博客部署到云服务器

1、本地搭建hexo 本地搭建hexo过程详见hexo官网&#xff0c;步骤比较详细&#xff0c;按照步骤搭建即可 2、hexo主题 我使用的Butterfly主题&#xff0c;主题配置请查看Butterfly安装文档 3、部署到云服务器 3.1、服务器环境 nginx 搭建 使用云服务商提供的远程登陆登录进…

vscode远程登录阿里云服务器【使用密钥方式--后期无需再进行密码登录】【外包需要密码】

1&#xff1a;windows主机上生成【私钥】【公钥】 1.1生成公钥时不设置额外密码 1.2生成公钥时设置额外密码【给外包人员使用的方法】 2&#xff1a;在linux服务器中添加【公钥】 3&#xff1a;本地vscode连接linux服务器的配置 操作流程如下 1.1本地终端中【生成免密登录…