【C++取经之路】继承

目录

继承的概念及定义

单继承的格式

继承方式和访问限定符 

继承后子类访问基类成员的权限

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

切片

继承中的作用域

引申:重载和隐藏的区别

派生类的默认成员函数

继承与友元

继承与静态成员

如何实现一个不能被继承的类

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

虚拟继承

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

多继承中指针偏移问题

继承的总结和反思


继承的概念及定义

在C++中,继承是一种面向对象编程的重要特性,它允许一个类(称为派生类或子类)继承另一个类(称为基类或父类)的成员变量(通常称为属性)和成员函数(通常称为方法)。通过这种方式,派生类可以重用基类的代码,并且可以添加或覆盖基类中的方法。

根据继承的基类的数量,C++中的继承可以分为单继承和多继承。

单继承:指一个派生类只从一个基类派生的情况。

多继承:指一个派生类从多个基类派生的情况。

单继承:

多继承:

单继承的格式

class 子类名 :继承方式 父类 { };

这里借助一段简单的代码来帮助理解。

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "Peter";int _age = 18;
};class Student : public Person
{int _stuid;//学号
};int main()
{Student s;s.Print();return 0;
}

先解释一些基本的概念,后面再通过调试感受继承。 

继承方式和访问限定符 

继承后子类访问基类成员的权限

1)如果子类通过public继承(公有继承)自父类,那么基类(父类)的public成员在子类中为public成员,在子类的外部可以直接访问。

2)如果子类通过public继承(公有继承)自父类,那么基类(父类)的protected成员在子类中为protected成员,在子类的外部不可以直接访问。

这里列举了这两个例子,剩下的可以从上表中看出。

总结:

1)基类的private成员无论以什么方式继承再派生类中都是不可见的。这里的不可见指的是:基类的私有成员还是被继承到派生类中,但是语法       上限制派生类对象不管是在类里面还是在类外面都不能去访问它。

2)基类的private成员在派生类中是不能被直接访问的。如果基类成员不想在派生类外直接被访问,但是需要在派生类中能直接被访问,那么可        以在基类中定义为protected。可见,保护成员限定符就是为继承而生的。

3)通过上述表格,可以发现,基类的私有成员在子类中都是不可见的。基类的其它成员在子类的访问方式 = min(该成员在基类的访问限定符,        继承方式),其中,public > protected > private。

4)如果不显式写继承方式,那么使用class时,默认为私有继承,使用struct时,默认为公有继承。但最好显式的写出继承方式。

5)实际应用中一般使用的都是public继承。因为protected/private继承下来的成员都只能在派生类里使用,实际中扩展维护性不强。

好了,基本的概念已经解释完毕,下面通过调试看看继承。

可以看到,通过派生类创建的对象s中,不仅有派生类自己的成员_stuid,还有继承自基类的成员_name和_age。至于如何去修改 _name和_age的值,后面再说吧~

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

只是文字描述很抽象,还是上代码吧~

class Person
{
protected:string _name; //姓名string _sex;  //性别int _age;     //年龄
};class Student : public Person
{
public:int _No;	 //专业课排名
};void test()
{Student sobj;//1.子类对象可以赋值给父类的对象/指针/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.父类对象不能赋值给子类对象sobj = pobj;//3.父类的指针可以通过强制类型转换赋给派生类指针Student* ps1 = (Student*)pp;pp = &pobj;Student* ps2 = (Student*)pp;//这种情况也可以,但是存在越界访问的问题
}int main()
{test();return 0;
}

将父类对象赋值给子类对象,编译器报错如下:

 总结:

 ● 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。这里有个形象的说法,叫切片,下面会解释~

 ● 基类对象不能赋值给派生类对象。

 ● 基类的指针可以通过强制类型转换赋值给派生类的指针,但不一定安全。

切片

先来看一张图,就知道为什么叫做切片了~

把子类赋值给父类,就相当于把红色部分切过去赋给父类。所以说切片这个说法很形象~

上面说到,基类对象不可以赋值给派生类对象,通过上图,我们可以这么理解:基类有的派生类都有,因而派生类可以赋值给基类,但是,派生类有的基类未必有,所以基类不可以赋值给派生类。 

继承中的作用域

1)在继承体系中,基类和派生类都有独立的作用域。

2)如果基类和派生类中有同名成员,那么派生类成员将屏蔽父类的同名成员,这种情况叫隐藏,也叫重定义。

3)基类和派生类中,对于成员函数,只要函数名相同,就构成隐藏。

4)在继承体系里,最好不要定义同名成员。

关于隐藏,这里还想再更详细的说一遍。

隐藏:通常指在派生类中定义了与基类中某个成员同名的成员,从而导致基类中的那个成员在派生类的作用域内被隐藏,这并不意味着基类中的成员被删除或不可访问,而是说,在派生类的作用域内,直接访问将访问到的是派生类中的成员,除非使用 基类 :: 基类成员 显式访问。

下面运行一段代码验证结论:

class Person
{
protected:string _name = "小李子";int _num = 111;
};class Student : public Person
{
public:void Print(){cout << "_name:" << _name << endl;cout << "直接访问_num:" << _num << endl;cout << "指定访问_num:" << Person::_num << endl;}
protected:int _num = 999;
};int main()
{Student s;s.Print();return 0;
}

引申:重载和隐藏的区别

派生类的默认成员函数

● 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数(不传参也能调),则必须在派生类构     造函数的初始化列表显式调用。

文字描述太抽象了,还是上代码吧~

基类中没有默认构造函数

class Person
{
public:Person(const int& num) :_num(num) {}
protected:int _num;
};class Student : public Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;cout << "num" << _num << endl;}
protected:string _name = "李华";int _age = 18;
};int main()
{Student s;s.Print();return 0;
}

  

正确操作: 

class Person
{
public:Person(const int& num) :_num(num) {}
protected:int _num;
};class Student : public Person
{
public://基类没有默认构造函数,需要在派生类的初始化列表显式调用基类的构造函数Student(): Person(1){}void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;cout << "num:" << _num << endl;}
protected:string _name = "李华";int _age = 18;
};int main()
{Student s;s.Print();return 0;
}

 

上面没讲如何修改继承来的属性,其实这种做法就可以修改继承来的属性了~

● 派生类的拷贝构造函数必须调用基类的拷贝构造函数来完成基类部分的拷贝。

● 派生类的operator=(赋值重载)必须调用基类的赋值重载来完成基类部分的赋值。

● 派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类的资源。

● 派生类对象初始化先调用基类构造再调派生类构造。

● 派生类对象析构清理先调用派生类的析构再调用基类的析构。

以下是我在学习继承过程中做的一些笔记,大概是关于要不要写派生类默认构造的问题,希望对你有用~

1)如果基类有默认构造函数,那么派生类可以选择不提供构造函数,此时,在派生类中,编译器会自动生成一个默认构造,该构造函数会调用        基类的默认构造函数

2)如果需要初始化派生类的特有成员变量,那么应该在派生类中提供构造函数

3)派生类自动生成的拷贝构造函数调用基类的拷贝构造函数来完成基类部分的拷贝,对派生类特有的成员变量执行浅拷贝。

下面这张图是派生类对象和基类对象的构造函数、析构函数执行顺序:

上面已经演示了派生类的构造函数调用基类构造函数初始化基类部分的代码,下面将演示如何在派生类中调用基类的拷贝构造函数处理基类部分的拷贝。

class Person
{
public:Person():_name(""), _age(0) {}Person(const Person& p) :_name(p._name), _age(p._age){}
protected:string _name;int _age;
};class Student : public Person
{
public:Student():Person(),_id("") {}Student(const Student& s) : Person(s), _id(s._id) {}
protected:string _id;// 学号
};int main()
{Student s;return 0;
}

直接将s传过去给Person的拷贝构造函数,因为父类的拷贝构造函数会通过切片来拿到父类的那部分。 赋值重载函数等也是同理。

继承与友元

友元关系不能继承,也就是说基类友元不能访问派生类的私有和保护成员。请看代码~

class Student;//声明class Person
{
public:friend void DisPlay(const Person& p, const Student& s);
protected:string _name;
};class Student : public Person
{
protected:int _No;
};void DisPlay(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._No << endl;   //尝试访问派生类的保护成员
}int main()
{Person p;Student s;DisPlay(p, s);return 0;
}

可以看到,Person类对象中的保护成员_name在友元声明后,是可以在类外访问的,但是继承自Person类的Student类对象中的保护成员_No并不能访问到。说明:基类友元不能访问派生类的私有和保护成员。

继承与静态成员

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

请看代码:

class Person
{
public:Person() { ++_count; }
protected:string _name;
public:static int _count; //统计人数
};int Person::_count = 0; //初始化class Student : public Person
{
protected:int _No;
};class Graduate : public Student
{
protected:string _seminarCourse; // 研究科目
};void Test()
{Student s1;Student s2;Student s3;Graduate s4;cout << "人数:" << Person::_count << endl;Student::_count = 0;cout << "人数:" << Person::_count << endl;
}int main()
{Test();return 0;
}

这说明了整个继承体系里只有一个_count,Student中的_count一改,Person中的_count也跟着改,因为它们就是同一个。

也可以通过监视窗口看看_count的地址。

 

_count从0变到3的过程中,_count的地址始终不变,说明只有一个_count。

如何实现一个不能被继承的类

可以通过将构造函数和析构函数声明为protected和private来阻止其他类继承该类,但是其他类仍然可以通过友元关系来继承它。用final关键字修饰类,可以彻底防止被继承,格式如下。

class A final
{
public:
    int _a;
};

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

上面讲的全是单继承,从这部分开始,将讲到多继承中的一种特殊情况——菱形继承。

这张图描述的就是菱形继承。 直接上代码~

class Person
{
public:string _name;
};class Student : public Person
{
protected:int _No;
};class Teacher : public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; //主修课程
};void Test()
{Assistant a;
}int main()
{Test();return 0;
}

监视窗口:

可以看到,一个a对象中,有两份_name,这就是菱形继承带来的问题——数据冗余和二义性

请看下面代码的运行结果:

class Person
{
public:string _name;
};class Student : public Person
{
protected:int _No;
};class Teacher : public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; //主修课程
};void Test()
{Assistant a;a._name = "Peter";
}int main()
{Test();return 0;
}

 二义性,就是不知道要访问哪一个,例如此处的_name,到底是访问继承自Student中的_name还是访问继承自Teacher中的_name不明确。

解决二义性的一个方法是:指定访问哪个父类的成员。

a.Student::_name = "Peter"; / /指定访问

虽然这种方式可以解决二义性问题,但是数据冗余问题并没有得到解决。 

那么有没有可以根治菱形继承数据冗余和二义性的方法呢?有的,虚拟继承就是为了解决这一问题而生的。

虚拟继承

虚拟继承的格式:

class 子类名 :virtual 继承方式 父类

 上代码验证一下虚拟继承是否可以解决数据冗余和二义性的问题~

class Person
{
public:string _name = "李华";
};class Student : virtual public Person
{
protected:int _No;
};class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; //主修课程
};void Test()
{Assistant a;a._name = "张三";
}int main()
{Test();return 0;
}

监视窗口:

 

执行第76行后:

可以看到,执行完第36行后,_name全被改为了“张三”,说明虽然监视窗口上显示3个_name,但是它们的地址是一样的,也就是说,对象a中只有一个_name。这样就解决了菱形继承带来的数据冗余和二义性问题。这里我没说明白, 如果还有疑问,请看原理部分~

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

为了了解虚拟继承的原理,这里给出一个简化的菱形继承体系,再借助内存窗口观察对象成员模型。

这里通过内存窗口再次看看数据冗余和二义性的问题~

class A
{
public:int _a;
};class B : public A
{
public:int _b;
};class C : public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

内存窗口:

这一个整体为一个D的对象,该对象继承自B和C。通过这张图,可以看到该对象中既有一份来自B的_a,又有一份来自C的_a(观察数值就可以发现),这很好的展现了菱形继承的问题。

下面,将上述代码改为虚拟继承,再通过内存窗口看看是否还存在数据冗余和二义性的问题。

可以看到,_a只有一份了,并且放在了最后,所以数据冗余和二义性的问题不存在了。C中的_a一改B中的也跟着改了,说明B和C共用同一个_a,问题是,B和C是如何找到公共的_a呢?还有个疑问,上图未框起来的部分究竟是何物?这两部其实是都是地址,下面会通过内存窗口看看它们里面的存的内容是什么。

注意这里的计算:

0x0137F970 + 20(十进制) = 0x0137F984(十六进制的计算)

0x0137F978 + 12(十进制) = 0x0137F984

对比加粗的结果和上图中用粉红色框起来的地址,发现计算结果和粉红色框起来的地址是一样的。这并不偶然,其实这就是虚拟继承解决数据冗余和二义性的原理了。上面只是一个引子,接下来总结原理~

接上面的问题——B和C是如何找到公共的_a的,其实是通过虚基表指针(vptr),也就是上图红色框起来的两个地址,虚基表指针指向同一张表,叫虚基表(vtable),虚基表里存的是偏移量,通过偏移量就可以找到_a。这里只是针对上面的测试代码进行说明,下面换种说法,尽量不针对某种情形,而是适用于广泛的场景~

什么是虚基表?

在讲适应性更广的说法之前,先了解一下虚基表,因为会用到它。

为了实现虚拟继承,C++引入了虚基表(vtable)的概念。虚基表用于记录虚基类(即被虚拟继承的基类)在派生类中的偏移量。

当一个类被声明为虚拟继承时(例如测试代码中的B和C),编译器会为该类生成一个虚基表指针(测试代码中B和C各有一个),当访问虚基类的成员(例如测试代码中的A)时,编译器会先通过vptr找到虚基表,然后根据虚基表中的偏移量定位虚基类在派生类中的实际位置,从而正确的访问虚基类成员。

原理部分的最后,根据测试代码画出一张图,来帮助理解虚拟继承的原理。

多继承中指针偏移问题

当一个基类指针指向一个派生类对象时,这个指针本身只包含该基类部分的地址。这句话很抽象,请看代码~

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };class Derive : public Base1, public Base2
{
public:int _d;
};int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

下面来分析p1、p2、p3的指向。

 

p1和p3指向Base1的起始位置,但是范围却不相同,p1只包函基类Base1(红色部分),p3包函整个部分。p2指向Base2的起始位置,只包含基类Base2(橙色部分)。 

继承的总结和反思

继承,它是一种“is-a”的关系,也就是说派生类是一个特殊的基类。优先使用组合而不是继承,还有,尽量不要写多继承,尤其是菱形继承,会坑自己~


完~

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

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

相关文章

【Java】解决Java报错:IllegalStateException during HTTP Request

文章目录 引言一、IllegalStateException的定义与概述1. 什么是IllegalStateException&#xff1f;2. IllegalStateException在HTTP请求中的常见触发场景3. 示例代码 二、解决方案1. 确保响应只被提交一次2. 正确管理Servlet的生命周期3. 避免重复访问输入流和输出流4. 使用框架…

HTML静态网页成品作业(HTML+CSS)—— 名人霍金介绍网页(6个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有6个页面。 二、作品演示 三、代…

vscode切换Python解释器

在vscode上切换解析器解决方案&#xff1a; 1、确认自己已经安装了python环境 2、command shift p ,在这里切换即可&#xff0c;见下图&#xff1a; 3、如果状态栏也就是右下角不现实切换操作的话&#xff0c;打开设置&#xff1a;

【六】Linux安装部署Nginx web服务器--及编写服务器启动脚本

一、部署安装nginx 1、查看nginx是否安装依赖包 [rootlocalhost ~]# rpm -q zlib-devel pcre-devel package zlib-devel is not installed package pcre-devel is not installed 2、若没有则安装nginx 依赖包 [rootlocalhost ~]# yum -y install zlib-devel* pcre-dev…

01 Pytorch 基础

paddle不需要放数据到gpu&#xff01; 区别&#xff1a;1.batch_norlization 不同 2. 1.数据处理 1.取一个数据&#xff0c;以及计算大小 &#xff08;剩下的工作&#xff0c;取batch&#xff0c;pytorch会自动做好了&#xff09; 2.模型相关 如何得到结果 3.模型训练/模型…

6月13日 Qtday1

#include "mywidget.h" //腾讯会议的登录界面 MyWidget::MyWidget(QWidget *parent): QMainWindow(parent) {this->setFixedSize(468,830);//主窗口大小this->setStyleSheet("background-color:rgb(255,255,255)");//主窗口背景this->setWindowTi…

Oxlint 会取代 Eslint 吗?

最近&#xff0c;一个基于 Rust 的代码检查工具 Oxlint 在国外前端社区引起了热议&#xff0c;许多专家对其给予了高度评价。那么&#xff0c;相比于它的大哥 Eslint&#xff0c;Oxlint 有哪些优势&#xff1f;它会在未来取代 Eslint 吗&#xff1f;本文将讨论这个话题。 Oxc 和…

现货黄金投资价格怎么分析 低买高卖是核心!

我们做现货黄金投资&#xff0c;总是离不开对黄金价格的分析&#xff0c;分析其实就是一种理性的思考&#xff0c;我们对现货黄金当前走势进行一番思考&#xff0c;进而判断它未来的走向&#xff0c;以此作为自己投资入场的基础。那黄金投资价格怎么分析呢&#xff1f;下面我们…

uniapp开发微信小程序预加载分包

微信小程序分包是一种优化小程序项目结构和性能的方式。它允许开发者将小程序代码包拆分成多个子包&#xff0c;在用户需要时动态加载这些子包&#xff0c;从而减少小程序的首次加载时间和主包的体积。&#xff08;总体积不得大于20M&#xff0c;主包&#xff08;共同文件静态资…

Matlab|基于主从博弈的智能小区代理商定价策略及电动汽车充电管理

目录 一、主要内容 二、部分代码 三、程序结果 四、下载链接 一、主要内容 主要做的是一个电动汽车充电管理和智能小区代理商动态定价的问题&#xff0c;将代理商和车主各自追求利益最大化建模为主从博弈&#xff0c;上层以代理商的充电电价作为优化变量&#xff0c;下层以…

Java I/O模型

引言 根据冯.诺依曼结构&#xff0c;计算机结构分为5个部分&#xff1a;运算器、控制器、存储器、输入设备、输出设备。 输入设备和输出设备都属于外部设备。网卡、硬盘这种既可以属于输入设备&#xff0c;也可以属于输出设备。 从计算机结构的视角来看&#xff0c;I/O描述了…

在 Kubernetes 上拉取 Harbor 私有仓库镜像并部署服务

上一篇讲解了IntelliJ IDEA和Jib Maven插件配合&#xff0c;镜像一键推送到Harbor私服仓库&#xff0c;今天来讲解下怎么让k8s直接拉取Harbor 私有仓库上面的镜像 创建 Kubernetes Secret 用于拉取镜像 因为 Harbor 仓库是私有的&#xff0c;我们需要创建一个 Kubernetes Sec…

JavaScript面向对象

一、编程思想 面向过程介绍 面向过程就是分析出解决问题所需要的步骤&#xff0c;然后用函数把这些一步一步实现&#xff0c;使用的时候再一个一个依次调用就可以了。 面向过程&#xff0c;就是按照我们分析好了的步骤&#xff0c;按照步骤解决问题。 面向对象编程&#xf…

阻抗控制(Impedance Control)和导纳控制(Admittance Control)例子

阻抗控制(Impedance Control)和导纳控制(Admittance Control) 是两种用于机械臂或机器人交互控制的策略。阻抗控制定义的是机器人端部的力和位置之间的关系,而导纳控制则定义的是外力和运动之间的关系。导纳控制常用于处理机器人与环境交互中的力控制问题。 适用场景对比…

185.二叉树:二叉搜索树的最近公共祖先(力扣)

代码解决 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/class Solution { public:// 函数用于寻找二叉搜索树中节点 p 和 q 的最低…

Honor of Kings 2024.06.13 (2)

【第一局】准确的说&#xff0c;其实对面优势更加明显&#xff0c;可惜黄忠和墨子喜欢杀人&#xff0c;而我又是不喜欢杀人的&#xff0c;打了好几次失误 【第二局】阵容本来很有优势&#xff0c;这个二呆射手跟第一局黄忠一样爱杀人&#xff0c;应该说三路的输出都爱杀人&…

小主机折腾记26

双独立显卡调用问题 前两天将tesla p4从x99大板上拆了下来&#xff0c;将880G5twr上的rx480 4g安装到了x99大板上&#xff0c;预计是dg1输出&#xff0c;rx480做3d运算。安装完驱动后&#xff0c;还想着按照之前tesla p4的设置方法去设置rx480&#xff0c;结果果然&#xff0c…

Serverless 使用OOS将http文件转存到对象存储

目录 背景介绍 系统运维管理OOS 文件转存场景 前提条件 实践步骤 附录 示例模板 背景介绍 系统运维管理OOS 系统运维管理OOS&#xff08;CloudOps Orchestration Service&#xff09;提供了一个高度灵活和强大的解决方案&#xff0c;通过精巧地编排阿里云提供的OpenAPI…

AcWing 477:神经网络 ← 拓扑排序+链式前向星

【题目来源】https://www.acwing.com/problem/content/479/【题目描述】 人工神经网络&#xff08;Artificial Neural Network&#xff09;是一种新兴的具有自我学习能力的计算系统&#xff0c;在模式识别、函数逼近及贷款风险评估等诸多领域有广泛的应用。 对神经网络的研究…

Rust : windows下protobuf和压缩传输方案

此前dbpystream库是用python开发 web api。今天在rust中试用一下protobuf。 本文关键词&#xff1a;编译器、protobuf、proto文件、序列化、zstd压缩&#xff0c;build。 一、 protobuf编译器下载 具体见相关文章。没有编译器&#xff0c;protobuf无法运行。 windows参见&am…