C++中的继承/虚继承原理

C++中的继承

文章目录

        • C++中的继承
        • 1.继承的概念和定义
        • 1.1 继承定义
        • 1.12 继承关系和访问限定符
        • 2.基类和派生类对象的复制转换
        • 3.继承中的作用域
        • 4.派生类的默认成员函数
          • 继承与友元
        • 6.**继承与静态成员**
        • **复杂的菱形继承及菱形虚拟继承**
        • 7.虚继承解决数据冗余和二义性的原理

1.继承的概念和定义

继承是一种提高代码复用率的重要方式,它允许程序员在保持原有类的特性的基础上去增加其他特性、功能,这样的类叫做派生类,继承是类设计层次的复用

class Person
{
public:void Print(){cout << "name: " << _name << endl;cout << "age: " << _age << endl;}protected:string _name = "牡丹";int _age = 18;
};class Student :public Person
{
protected:int _stuid;
};class Teacher :public Person
{
protected:int _jobid;
};int main(void)
{Student s;Teacher t;s.Print();t.Print();return 0;
}

运行结果为:

明明Student和Teacher当中都没有Print()函数啊,为什么还是正常运行呢?因为它们都继承了Person类。

1.1 继承定义

下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。

1.12 继承关系和访问限定符

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

总结

1.基类的private成员在派生类当中无论是什么继承方式,都是不可见的,是隐藏的,在类外,在派生类当中都是无法调用到private的成员,都是隐藏的。

2.private成员在派生类中是不能被访问的,但是如果想要在类外不能被访问,但是在派生类当中被访问到就可以使用protected

3.public > protected > private,基类的其他成员在子类的访问方式 == 权限小的那个

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的

写出继承方式

5.在实际运用中一般使用都是publicb继承,几乎很少使用protetced/private继承,也不提倡使用

protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中

扩展维护性不强。

2.基类和派生类对象的复制转换

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。有一种很形象的说法叫做切片或者切割。就是把派生类当中父类的那部分切来赋值过去。

  • 基类的对象不能赋值给派生类

  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才

    是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来

    进行识别后进行安全转换。

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 = StudentPerson* pp = &sobj;//Person = StudentPerson& rp = sobj;//Person = Student//2.基类对象不能赋值给派生类对象sobj = pobj;//Student = Person// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;Student * ps1 = (Student*)pp; // 这种情况转换时可以的。Student* = (Student*)Person*ps1->_No = 10;pp = &pobj;//Person =PersonStudent* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_No = 10;
}

3.继承中的作用域

1.在继承体系中基类派生类都有独立的作用域

2.子类和父类中有同名的成员,子类成员和父类成员的关系叫做隐藏,如果非要访问父类的成员,可以添加访问限定符显示访问

3.函数只需要函数名相同就能构成隐藏,参数什么的无关。

4.最好不要定义同名的成员

class Person
{
public:protected:string _name="牡丹";int _num=18;
};class Student :public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "学号:" << _num << endl;cout << "学号:" << Person::_num << endl;}
protected:int _num = 520;
};int main(void)
{Student s1;s1.Print();return 0;
}

运行结果:

class Person
{
public:void Print(int a){cout << "姓名:" << _name << endl;cout << "学号:" << _num << endl;}
protected:string _name="牡丹";int _num=18;
};class Student :public Person
{
public:void Print(){Person::Print(1);cout << "姓名:" << _name << endl;cout << "学号:" << _num << endl;cout << "学号:" << Person::_num << endl;}
protected:int _num = 520;
};int main(void)
{Student s1;s1.Print();return 0;
}

这里构成的是函数的隐藏而不是重载,因为不在同一个作用域

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

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个

成员函数是如何生成的呢?

1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分,如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

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

3.派生类的operator=必须要调用基类的operator=完成基类的复制。

4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类

对象先清理派生类成员再清理基类成员的顺序。

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

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

class Person
{
public:Person(const char* name = "牡丹"):_name(name){cout << "Person()" << endl;}Person(const Person& p):_name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person& operator=(const Person& p)" << endl;if (this != &p){_name = p._name;}return *this;}~Person(){cout << "~Person()" << endl;}protected:string _name;
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){Person::operator =(s);_num = s._num;}return *this;}~Student(){cout << "~Student()" << endl;}
protected:int _num; //学号
};
void Test()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;
}int main(void)
{Test();return 0;
}

这段代码:

实例化对象s1的时候会先去调用Student的构造函数,然后再Student的构造函数当中显示的调用了父类的构造函数。
实例化对象s2的时候先调用Student的拷贝构造,在拷贝构造的初始化列表当中显示的调用父类的拷贝构造
实例化s3的时候和s1一样
s1赋值给s3的时候先去调用子类的operator=,但是在子类当中会调用父类的operator=

继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

Display是基类Person的友元,假如Student把Person当中的友元关系也给继承了,那么这里的s._stuNum是可以访问的,但是结果的不可访问,所以友元关系是不能被继承的

6.继承与静态成员

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

个static成员实例

class Person
{
public :Person () {++ _count ;}
protected :string _name ; // 姓名
public :static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :string _seminarCourse ; // 研究科目
};
void TestPerson()
{Student s1 ;Student s2 ;Student s3 ;Graduate s4 ;cout <<" 人数 :"<< Person ::_count << endl;Student ::_count = 0;cout <<" 人数 :"<< Person ::_count << endl;
}

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

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

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

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

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。

class Person
{
public:string _name; // 姓名
};class Student : public Person
{
protected:int _num; //学号
};class Teacher : public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};void Test()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name = "peter";// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}

虚继承可以解决菱形继承的二义性和数据冗余问题。


class Person
{
public:string _name; // 姓名
};class Student :virtual public Person
{
protected:int _num; //学号
};class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};void Test()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name = "peter";// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}int main(void)
{Test();return 0;
}

这样,指向的_name就都是同一个_name了。

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

可以看到,虚继承的成员都放在公共区了,但是这里又多了两个地址,看起来像是两个指针

可以看到,这里有一个140c,14的16进制就是20,c就是12,刚好记录了当前类到公共区域的距离。

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

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

相关文章

亚马逊云科技Zero ETL数据库,助力企业走向数据驱动的业务增长之路

据Forrester研究&#xff0c;相对于数据应用不够成熟的公司&#xff0c;那些有效获取业务洞察的公司&#xff0c;有高达8.5倍的可能性实现至少20%的收入增长。然而&#xff0c;要实现这一增长&#xff0c;需要简化一项流程——在数据分析前管理和准备好数据。这就是为什么亚马逊…

Java动态规划LeetCode1137. 第 N 个泰波那契数

方法1&#xff1a;通过动态规划解题&#xff0c;这道题也是动态规划的一道很好的入门题&#xff0c;因为比较简单和容易理解。 代码如下&#xff1a; public int tribonacci(int n) {//处理特殊情况if(n0){return 0;}if(n1||n2){return 1;}//定义数组int[]dpnew int[n1];//初…

随机数检测(三)

随机数检测&#xff08;三&#xff09;- 块内最大游程检测、二元推导检测、自相关检测、矩阵秩检测 3.8 块内最大游程检测方法3.9 二元推导检测方法3.10 自相关检测3.11 矩阵秩检测 如果商用密码产品认证中遇到问题&#xff0c;欢迎加微信symmrz或13720098215沟通。 3.8 块内最…

uniapp - [全端兼容] 多选弹框选择器,弹框形式的列表多选选择器组件插件(底部弹框式列表多选功能,支持数据回显、动态数据、主题色等配置)

前言 网上的教程都太乱了,各种不兼容且 BUG 太多,注释也没有很难进行改造。 本文 实现了 uniapp 全端兼容的弹框多选选择器,从底部弹出列表项进行多选(可回显已选中和各种主题色、样式配置), 您可以直接复制代码,稍微改改样式就能用了。 如下图所示,数据列表(支持接口…

23 | MySQL是怎么保证数据不丢的?

以下内容出自《MySQL 实战 45 讲》 23 | MySQL是怎么保证数据不丢的&#xff1f; binlog 的写入机制 1、事务执行过程中&#xff0c;先把日志写到 binlog cache&#xff0c;事务提交的时候&#xff0c;再把 binlog cache 写到 binlog 文件中。 2、一个事务的 binlog 是不能被…

C++的范围for语句详解 附易错实例

&#x1f4af; 博客内容&#xff1a;C读取一行内个数不定的整数的方式 &#x1f600; 作  者&#xff1a;陈大大陈 &#x1f680; 个人简介&#xff1a;一个正在努力学技术的准前端&#xff0c;专注基础和实战分享 &#xff0c;欢迎私信&#xff01; &#x1f496; 欢迎大家&…

Linux开发工具【gcc/g++】

Linux开发工具之【gcc/g】 上文我们已经学习了vim编辑器的相关操作和使用&#xff0c;已经可以在Linux下编写代码了&#xff0c;有了代码就需要编译运行&#xff0c;此时就需要用到Linux中的编译工具gcc/g了&#xff0c;其中gcc是C语言的编译器&#xff0c;g是C的编译器&#…

分销架构总结

概述 对于过往分销系统的经验总结。视角上会不区分实物及虚拟服务的分销。 分销定义&#xff1a;将产品从生产者转移到消费者。 分销职责&#xff1a;获客&#xff0c;服务(售前&#xff0c;售中&#xff0c;售后)。核心是通过不同分销渠道将产品能卖出去。 在整体分销网络…

go-zero的rpc服务案例解析

go-zero的远程调用服务是基于gRpc的gRPC教程与应用。 zero使用使用gRpc需要安装protoc插件&#xff0c;因为gRpc基于protoc插件使用protocol buffers文件生成rpc服务器和api的代码的。 gRPC 的代码生成还依赖 protoc-gen-go&#xff0c;protoc-gen-go-grpc 插件来配合生成 Go…

小程序本地生活

2023年7月1号 感觉就是视频要快点看不完 不然哪天接口又失效了 Page({/*** 页面的初始数据*/data: {// 存放轮播图的数据swiperList:[],// 存放九宫格的数据gridList:[]},/*** 生命周期函数--监听页面加载*/onLoad(options) {this.getSwiperList()this.getGridList()},// 获…

YApi-高效、易用、功能强大的可视化接口管理平台——(二)YApi 分组权限

YApi 分组权限 认识 YApi角色划分项目权限分组权限分组操作创建分组项目列表添加成员分组删除 认识 YApi YApi 是一个开源的接口管理平台&#xff0c;可以方便地管理和测试 API 接口&#xff0c;支持接口文档自动生成、Mock 数据生成、接口测试和接口监控等功能。YApi 支持多人…

cmake列表

目录 cmake多行注释 求list长度 demo 获取list的值 demo 插入list demo 追加list demo 查找列表中的字符 demo 删除list list向前删除和向后删除数据 demo 字符串反转 demo 特殊操作 Readinglist(LENGTH <list> <out-var>)list(GET <list> &…