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.hqwc.cn/news/538260.html

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

相关文章

骨传导耳机品牌推荐,超全选购指南附精品推荐

近些年来&#xff0c;骨传导耳机以独特的听音方式受到广泛关注。顾名思义&#xff0c;这类耳机利用骨传导原理&#xff0c;通过振动直接将声音传递至颅骨&#xff0c;让用户在聆听音乐的同时&#xff0c;也能清晰地感知周围环境的声响。面对众多品牌&#xff0c;消费者往往难以…

点云配准论文阅读1-Research on Three-Dimensional Point Cloud Registration Algorithm

Research on Three-Dimensional Point Cloud Registration Algorithm三维点云配准算法研究 Publisher: IEEE发行者 &#xff1a; IEEE Cite This引用此内容 PDF Yuqing Zhang; Shilong Sun; Jingjing Shang; Minghan Yang张玉清;孙世龙; 尚晶晶;杨明翰 Abstract: Accordi…

CentOS本地部署Tale博客并结合内网穿透实现公网访问本地网站

文章目录 前言1. Tale网站搭建1.1 检查本地环境1.2 部署Tale个人博客系统1.3 启动Tale服务1.4 访问博客地址 2. Linux安装Cpolar内网穿透3. 创建Tale博客公网地址4. 使用公网地址访问Tale 前言 今天给大家带来一款基于 Java 语言的轻量级博客开源项目——Tale&#xff0c;Tale…

2.操作系统知识

基础知识部分—Chap2 考点&#xff1a; 进程管理中的pv操作、死锁、银行家算法&#xff08;理解&#xff09;&#xff1b; 文件管理中的路径&#xff08;绝对路径和相对路径&#xff09;&#xff1b; 存储管理&#xff1a;了解存储管理体制的优缺点、管理结构&#xff1b; 1…

sqllab第十二关通关笔记

知识点&#xff1a; 一般字符型注入分类 单引号闭合双引号闭合这是一个双引号闭合 看界面又是一个输入框的注入;通过admin admin进行登录发现页面还是有回显 直接使用万能密码尝试 构造payload:usernameadminor11 没有任何反应&#xff1b;可能是没加注释符的关闭 构造user…

微信小程序之vue按钮切换内容变化

效果图如下&#xff1b; 上代码 <template><view class"content"><view class"searchDiv"><view class"paytab"><view class"buttab" v-for"(t,index) in tabList" :key"index" clic…

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

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

数字化工厂有哪些典型应用?

随着科技的飞速发展&#xff0c;数字化工厂已经成为现代制造业的重要趋势。它将先进的数字化技术应用于制造过程&#xff0c;实现了生产流程的智能化、自动化和高效化&#xff0c;为制造业带来了革命性的变革。本文将深入探讨数字化工厂的典型应用&#xff0c;并揭示其如何推动…

C++内存分布与动态内存管理

文章目录 :dizzy: C/C内存分布:dizzy:C语言中动态内存管理方式  :sparkles:malloc   :sparkles:calloc  :sparkles:reallocfree :dizzy:C语言中动态内存管理方式  :sparkles:new和delete操作内置类型  :sparkles:new和delete操作自定义类型 :dizzy:operator new与ope…

scrcpy远程投屏控制Android

下载 下载后解压压缩包scrcpy-win64-v2.4.zip scrcpy连接手机 1. 有线连接 - 手机开启开发者选项&#xff0c;并开启USB调试&#xff0c;连接电脑&#xff0c;华为手机示例解压scrcpy&#xff0c;在scrcpy目录下打开终端&#xff0c;&#xff08;或添加scrcpy路径为环境变…

Docker进阶:深入了解 Dockerfile

Docker进阶&#xff1a;深入了解 Dockerfile 一、Dockerfile 概述二、Dockerfile 优点三、Dockerfile 编写规则四、Dockerfile 中常用的指令1、FROM2、LABEL3、RUN4、CMD5、ENTRYPOINT6、COPY7、ADD8、WORKDIR9、 ENV10、EXPOSE11、VOLUME12、USER13、注释14、ONBUILD 命令15、…

优化资源利用率:Kubernetes 中装箱的好处和挑战

鉴于 Kubernetes 作为容器编排事实标准的地位&#xff0c;组织不断寻求优化集群资源利用率的方法。其中一种技术是装箱&#xff1a;在集群内有效分配资源&#xff0c;以最大限度地减少运行工作负载所需的节点数量。装箱可以让组织通过减少支持其应用程序所需的节点数量来节省成…