C++:继承与派生

    

        为什么会有继承这样的语法呢??试想这样一个场景:假设我们这个App需要去获取不同类型用户的数据,并进行分类,那么就需要我们去写对应不同的类,比如说学生、老师、军人、公司职工…………每个类都需要有名字、联系方式、家庭住址、年龄……,我们会发现这样每个类都要写一份,非常冗余,于是我们的祖师爷为了解决这个问题,设计出了继承的语法,比如说用户的共同点是都是用户,我们就可以写一个关于人的类,作为基类,而不同类型用户就作为基类的派生类,去继承基类的成员,从而达到我们的目的。

一、继承的概念和定义

1.1 继承的概念

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

从下面这个代码可以很好地体现继承的特点

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};
class Student : public Person
{
protected:int _stuid; // 学号
};
class Teacher : public Person
{
protected:int _jobid; // 工号
};
int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}


        继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和Teacher复用了Person的成员。如果我们使用监视窗口查看Student和Teacher对象,可
以看到变量的复用。调用Print可以看到成员函数的复用。
       监视窗口看上去给人的感觉就是,子类把父类当成了自己的一个成员一样

1.2 继承的定义

1、格式:Person是父类,也称作基类。Student是子类,也称作派生类

2、继承关系和访问限定符

我们会发现继承方式和访问限定符都存在公有、保护、私有这三种继承模式。

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

 总结:

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

 2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。(所以我们不想让子类继承就写成私有,想让子类继承就写成保护。可以理解为private防外人也防儿子,但是protected防外人但是不防儿子。

3、 上面表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。

4、使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,我们常用的一般都是class,所以最好显示地写出继承方式

5、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承
,因为protetced/private继承下来的成员都只能在派生类的类里
面使用,实际中扩展维护性不强

 

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

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

2、基类对象不能赋值给派生类对象

 我们开看下面这段代码

class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};
int main()
{//以前我们所知道的强制类型转化double d = 1.1;int i = d;//子类对象可以赋值给父类对象/指针/引用Student sobj;Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//反之  基类对象不能赋值给派生类对象sobj = pobj;
}

3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。

来看看下面这段代码:

class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};
int main()
{//基类的指针可以通过强制类型转换赋值给派生类的指针Student sobj;Person* pp = &sobj;Student * ps1 = (Student*)pp;   // 这种情况转换时可以的。ps1->_No = 10;Person pobj = sobj;pp = &pobj;Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题ps2->_No = 10;return 0;
}

三、继承中的作用域

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

2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义
。(在子类成员函数中,可以使用 基类::基类成员 显示访问

3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

4. 注意在实际中在继承体系里面最好不要定义同名的成员。(但是有些情况下不可避免,后面会说)

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

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

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

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

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

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

5. 派生类对象初始化先调用基类构造再调派生类构造,派生类对象析构清理先调用派生类析构再调基类的析构
 

     我们来观察下面这个Person类,再来看看如何去写Student的构造、析构、赋值重载以及拷贝构造。

class Person{public://构造函数Person(const char* name = "peter"): _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){if (this != &s){Person::operator=(s);_num = s._num;}cout << "Student& operator=(const Student& s)" << endl;return *this;}~Student(){cout << "~Student()" << endl;}// 子类析构函数完成时,会自定调用父类析构函数,保证先析构子再析构父protected:int _num; //学号};

思考:

1、既然我们知道了怎么显式写,但是真的有必要吗??我们将子类的成员函数全部注释掉看看结果:

总结:

(1)构造函数最好是显式写一下。

(2) 如果不涉及资源管理,没必要写拷贝构造和赋值构造,涉及到资源管理需要进行深拷贝,就需要我们去显式写。

(3)析构函数一般情况想不用写,但在后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(涉及到虚函数)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
 

 2、子类和父类的构造和析构顺序是怎么样的?

五、继承与友元

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

 

 解决方案就是在学生类里也搞个友元。

六、继承与静态成员

        基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员。静态成员无法继承,但是理论上来说是可以访问的。

思考:

1、如何计算该程序中一共创建了多少个对象??

设置一个静态成员变量,然后在基类的构造函数里++

如上图,我们可以发现如果把静态成员设为公有,那么就会出现可以修改的情况,所以我们最好把静态成员变量设成私有,然后用写一个静态成员函数getcount去获取结果

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

      将基类的构造函数藏在私有作用域里,这样继承的时候子类不可见基类构造函数,就无法创建出子类对象了!(将析构函数设成私有也可以)

 但是你可能会想,把构造函数放在私域,那A不是也创建不了对象了吗???确实是这样的!

但是也有一个解决的方法,就是利用静态成员函数去返回这个构造函数。 

3、为什么2用静态成员函数呢??

            因为这涉及到先有鸡还是先有蛋的问题,我们不创建对象就调用不了这个函数,但是要调用这个函数又需要一个对象,所以为了解决这个问题,只能将该函数变成静态成员函数,这样我们可以通过类限定符去访问他 

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

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

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

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


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

 

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

     为了能够更好地观察虚继承的过程,我们选择一个较为简单的模型。

class A
{
public:int _a;
};
// class B : public A
class B :  public A
{
public:int _b;
};
// class C : public A
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;
}

我们先看看没有进行虚继承时,内存中是怎样的

我们看看虚继承后是怎样的,为了能更好地观察,我们用32位环境

 思考:

1、不是为了解决冗余问题吗??为什么这边的空间反而变大了??

           解决数据冗余要付出存指针的代价,但是如果冗余的对象超过指针的大小的话,那么就赚了。哪怕真的损耗了一点空间。。至少二义性解决了

2、存的地址指向的空间难道不算空间消耗么??

       理论上来说是有一定的损耗,但是如果我们创建了多个d对象,其实就是多个d类型对象在共用一个 空间,那么就可以忽略不计了,因为理论上来说他们的偏移量都是不会变的!!比如我们再写个d2.

所以如果我们创建了多个d对象,那么存地址的优势就更突出了!!因为偏移量都是一样的

3、 A有一个_a对象,所以有一个指针,如果A有多个对象是存多个指针吗?

       我们看看下面的图:

所以哪怕A对象成员变量很多,也只会存一个地址。综合2和3我们可以得到一个结论,无论是创建特别多的d对象,还是说这个a的成员变量非常多,都只会存一个地址。所以虚继承显然能够解决数据冗余的问题。

4、既然原来b中的a空间存的是一个地址,那如果用b类型或者c类型的指针存d的地址,然后再分别访问_a   或者是用d赋值给b和c类型的对象得到的会是地址吗??

      

从该图我们可以看到并不会, d赋值给b和c的时候,他们会先通过这个地址找到存放偏移量的空间,然后再回来找到_a,最后也是按照存地址的方式去展现。而B C类型的指针存放d地址的时候,也会通过这个地址找到偏移量,然后再回过头来找_a。所以在这个过程中他们自己会进行处理。从这里我们也可以看到虚继承的访问会存在一定的时间消耗,因为已经不是单纯地解引用了,而是经过了多一层的解引用和计算。

5、下面这道题的运行结果是什么

class A{
public:A(const char *s) { cout << s << endl; }~A(){}
};
class B :virtual public A
{
public:B(const char *s1, const char*s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:C(const char *s1, const  char*s2) :A(s1) { cout << s2 << endl; }
};class D : public C, public B
{
public:D(const char *s1, const char *s2, const char *s3, const char *s4):B(s1, s2),C(s1, s3),A(s1){cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

6、为什么虚基表的前四个字节位置是空出来的?? 

       这是为了后面的虚函数表做准备的,用来存从虚基表找到虚函数表的偏移量(菱形继承多态)。在多态那一环节会去验证。

 八、继承的总结和反思

1. 很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱
形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设
计出菱形继承。否则在复杂度及性能上都有问题。

2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

3. 继承和组合

优先使用组合,而不是类继承

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

        类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见(可以直接访问被继承对象的成员变量)。

  对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口(只能通过接口去访问被组合对象的成员变量)。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

继承的缺点有以下几点:

①:父类的内部细节对子类是可见的。

②:子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。

③:如果对父类的方法做了修改的话(比如增加了一个参数),则子类的方法必须做出相应的修改。所以说子类与父类是一种高耦合,违背了面向对象思想。

一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

组合的优点:

①:当前对象只能通过所包含的那个对象去调用其方法(只能用接口不能用成员变量),所以所包含的对象的内部细节对当前对象时不可见的。

②:当前对象与包含的对象是一个低耦合关系,如果修改包含对象的类中代码不需要修改当前对象类的代码。 

③:对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象。

组合的缺点:①:容易产生过多的对象。②:为了能组合多个对象,必须仔细对接口进行定义。

结论:优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

  这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承

 

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

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

相关文章

品牌方如何借热点做话题?媒介盒子分享

每时每刻都有热点&#xff0c;品牌方总想着借热点来做话题营销&#xff0c;然而有些热点能让品牌有效曝光&#xff0c;有些热点稍不注意就会让品牌产生负面舆论&#xff0c;今天媒介盒子就来和大家好好聊聊&#xff1a;品牌方如何借热点做话题。 一、 热点选择 品牌方可以通过…

域内令牌窃取

前言 有这样一种场景&#xff0c;拿到了一台主机权限&#xff0c;是本地管理员&#xff0c;同时在这台主机上登录的是域管成员&#xff0c;这时我们可以通过dump lsass或通过 Kerberos TGT &#xff0c;但是这是非常容易被edr命中的。 本文就通过令牌窃取进行研究&#xff0c…

pgsql常用索引简写

文章来源&#xff1a;互联网博客文章&#xff0c;后续有时间再来细化整理。 在数据库查询中&#xff0c;合理的使用索引&#xff0c;可以极大提升数据库查询效率&#xff0c;充分利用系统资源。这个随着数据量的增加得到提升&#xff0c;越大越明显&#xff0c;也和业务线有关…

《鸟哥的Linux私房菜》第6章——总结与习题参考答案

目录 一、 简介 二、一些新了解的指令 1.touch- 修改文件时间或创建新文件 2.umask-新建文件/目录的默认权限 3.文件隐藏属性 4.文件特殊权限 5.file-观察文件类型 三、简答题部分 一、 简介 本章介绍了一些常用的文件与目录指令&#xff0c;包括新建/删除/复制/移动/查…

2024HVV行动-进军蓝中研判(log4j2、fastjson、Struts2、Shiro)

1、log4j2 特征&#xff1a; 恶意请求中包含 JNDI 协议地址&#xff0c;如"ldap://"、"rmi://"等&#xff0c;被 log4j2 解析为 JNDI 查找。 原理&#xff1a; 在日志输出中&#xff0c;未对字符进行严格的过滤&#xff0c;执行了 JNDI 协议加载的远程恶…

教师如何搭建学生查询考试分数的平台?

随着信息技术的快速发展&#xff0c;搭建一个学生查询考试分数的平台已经成为现代教育管理的重要组成部分。这样的平台不仅可以提高成绩管理的效率&#xff0c;还能为学生提供便捷、及时的成绩查询服务。那么&#xff0c;作为教师&#xff0c;我们应该如何搭建这样一个平台呢&a…

【海贼王的数据航海】栈和队列

目录 1 -> 栈 1.1 -> 栈的概念及结构 1.2 -> 栈的实现 1.2.1 -> Stack.h 1.2.2 -> Stack.c 1.2.3 -> Test.c 2 -> 队列 2.1 -> 队列的概念及结构 2.2 -> 队列的实现 2.2.1 -> Queue.h 2.2.2 -> Queue.c 1 -> 栈 1.1 -> 栈的…

如何缩小你和大厂UI设计师之间的差距?重点关注你的作品集!

据说金三银四&#xff0c;在新一轮招聘旺季到来之前&#xff0c;你做过UI作品集吗&#xff1f;一个好的作品集可以为你的面试增加很多分数&#xff01;在开始之前&#xff0c;分享五个适合交互设计作品集的软件&#xff1a; 一、即时设计 这是一款专门为UI设计而设计的在线矢…

多线程(volatile)

volatile的功能 保证内存可见性禁止指令重排序 内存可见性 简单的理解 两(多)个线程同时针对一个变量进行操作, 一个线程读, 一个线程修改, 此时读到的值不一定是修改过后的值 即读线程没有感知到变量的变化 (其实是 编译器/JVM 对于代码在多线程情况下的优化进行了误判) 从 J…

Linux:设置别名命令alias

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 在Linux中alias命令用于为一串字符&#xff08;常代表命令&#xff09;设置一个别名&#xff0c;该别名在Bash读取并解析一行命令时会被展开。 下面是该命令的语法。 用…

BOM

文章目录 1. BOM概述1.1 什么是BOM1.2 BOM构成 2. window 的常见对象2.1 窗口加载事件2.2 调整窗口大小事件 3. 定时器3.1 setTimeout()3.2 停止 setTimeout() 定时器3.3 setInterval()*案例--倒计时效果 3.4 清除setInterval() 定时器*案例--发送短信 3.5 this 指向问题 4. JS…

二叉搜索树题目:恢复二叉搜索树

文章目录 题目标题和出处难度题目描述要求示例数据范围进阶 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 解法三思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;恢复二叉搜索树 出处&#xff1a;99. 恢复二叉搜索树 难度 5 级 题目描述 …

西门子PLC常用底层逻辑块分享_单/双输出电机

文章目录 前言一、功能概述二、单输出电机程序编写1.创建自定义数据类型2.创建FB功能块“单输出电机”3.编写程序 三、双输出电机程序编写1.创建自定义数据类型2.创建FB功能块“双输出电机”3.编写程序 前言 本文分享一个自己编写的电机控制逻辑块。 一、功能概述 手动状态、…

2024年1月粮油调味行业分析(TOP品牌/店铺/商品销售数据分析)

鲸参谋监测的某东1月份粮油调味市场销售数据已出炉&#xff01; 根据鲸参谋电商数据分析平台显示&#xff0c;今年1月份&#xff0c;某东平台上粮油调味品的销量约6200万件&#xff0c;环比上个月增长45%&#xff0c;同比去年下滑15%&#xff1b;销售额约25亿元&#xff0c;环…

Android 监听卫星导航系统状态及卫星测量数据变化

源码 package com.android.circlescalebar;import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.conte…

后渗透--利用ebpf隐藏后门用户

原理 首先我们要搞清楚ssh登陆的流程 先获取到ssh的pid 然后利用strace strace -f -p 830可以看到他打开了/etc/passwd去读取内容 那么我们的思路就很简单&#xff0c;hook ssh进程的read syscall exit,然后篡改返回内容 代码 ebpf // build ignore#include "my…

LeetCode2115. 从给定原材料中找到所有可以做出的菜

拓扑排序 题面 题目链接&#xff1a;2115. 从给定原材料中找到所有可以做出的菜 - 力扣&#xff08;LeetCode&#xff09; 你有 n 道不同菜的信息。给你一个字符串数组 recipes 和一个二维字符串数组 ingredients 。第 i 道菜的名字为 recipes[i] &#xff0c;如果你有它 所有…

html5cssjs代码 018颜色表

html5&css&js代码 018颜色表 一、代码二、效果三、解释 这段代码展示了一个基本的颜色表&#xff0c;方便参考使用&#xff0c;同时也应用了各种样式应用方式。 一、代码 <!DOCTYPE html> <html lang"zh-cn"> <head><title>编程笔记…

速卖通安全测评补单技术提升运营安全性

对于一个新品来说&#xff0c;最大的问题就是评论。没有评论&#xff0c;你的广告就不能打的很靠前&#xff0c;那样你的转化率就会非常低&#xff0c;数据也很差。新品运气不好的来两个一星差评&#xff0c;链接可能就此废掉&#xff0c;做不上去了。所以虽然平台管的非常的严…

智能工厂核心功能系统-MES生产管理系统

MES在未来智能制造中扮演着至关重要的角色&#xff0c;通过其在生产管理中的应用&#xff0c;将帮助企业实现智能化转型&#xff0c;提升生产效率和产品质量&#xff0c;推动整个制造业向着更加智能、高效、可持续的方向发展。 通过对MES在未来智能制造发展趋势中的地位进行深…