C++·继承

        面向对象编程有三大特性:封装、继承、多态。

        封装我们前几节已经讲过了,第一层封装是将一个数据和方法都封装到一个类中,想让用户访问的定义成公有,不想让用户访问的定义成私有,第二层封装就类似于迭代器、适配器的思想,将各种容器的访问方法都封装成迭代器,让用户使用容器的时候不用关心底层的细节或访问的方式,只需要迭代器就可以使用各种容器了。

1. 继承的概念和定义

1.1 继承的定义

        继承(inheritance)机制是面向对象程序设计使代码可以复用的重要手段,它允许程序员在保持原有类特性的基础上进行拓展,增加功能,这样产生新的类,称为派生类或子类,原有类称为基类或父类

        比如我们想维护学生和老师两个类,但这两个类有姓名和年龄两个相同的成员变量,但分别又有学号和工号两个不同的成员变量。此时我们就可以考虑让这两个类继承另一个Person类的内容。

                        

        继承的语法就是在派生类的类名后面加上 :+继承方式+基类 此时Person类中的内容就被继承给了Student和Teacher类。

        我们可以验证一下:

        ​​​​​​​        ​​​​​​​        

        可以看到派生类实例化后可以使用基类的各种内容了,说明父类已经将自己的所有内容继承给了子类。

1.2 继承后的访问方式变化

        继承的操作就像是把基类的所有内容拷贝进了派生类,而不用让我们自己 C+V 过去,但是根据基类成员的访问限定类型继承方式的不同,拷贝进派生类后的成员也会获得新的访问限定类型。

        可以看到Person类的成员变量的访问限定方式我选择的是 proteced 而不是我们之前经常写的 private ,这是因为我认为Student和Teacher类中需要修改继承过来的这两个成员变量,而如果在父类中将这两个成员变量定义成 private 那么在子类中这两个成员变量就变成了不可见状态,不可见状态就是说在子类中无法访问或修改这两个成员。

        可以看到当父类中的private内容被继承给子类后,子类就无法对其访问或修改。

        但是不可见状态并不是不存在,这个要东西理解一下,就是当我们不通过子类访问不可见内容是可以做到的。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        可以看到在这段代码中,两个子类的实例化还是可以通过父类的成员函数来访问到对于他俩来说的不可见成员。

        好了解释完不可见状态之后我们来看一下继承后的访问方式变化到底是怎么变的

        总结:

        1. 基类的private成员在派生类中无论以何种方式继承都是不可见的。

        2. 基类其他成员在派生类中的访问方式取 min(基类成员访问限定符,继承方式),其中public>protected>private

        3. 在实际运用中一般都是用 public继承。

        最后在提一下父类的private成员问题,当父类的某个成员是private成员,其实当时设定的时候就是不想让子类能访问到这个成员,因此才设置成private的,仅父类可访问。

2. 基类和派生类对象赋值兼容转换

                                

        我们看上面这段代码,很怪,将派生类直接赋值给了父类。

        派生对象 可以赋值給 基类对象、基类指针、基类引用。这种操作有种形象的叫法,叫做切割。就是说把派生类中的父类那部分切割下来给过去。或者用更专业的叫法,赋值兼容转换

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

        我们之前有学过一种和这个很像的操作也是在两个不同类型的变量之间赋值,我们管那种操作叫 截断---整型提升。但是这个切割是特殊的,截断整型提升在操作的中间会产生临时变量,但是切割不会。

                        

        如果是把子类中属于父类那块的地址切割下来给到一个父类的实例化,或者把子类中属于父类那块的引用切割下来给到一个父类的实例化,通过更改父类的实例化都可以起到修改到子类的效果。

        

3. 继承中的作用域

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

        2. 基类和派生类中的同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫做隐藏。如果想在子类中访问父类的同名成员,就要 父类::成员名 显示访问。

        3. 如果是成员函数的隐藏,只需要同名就可以构成隐藏,不需要像函数重载那样考虑参数类型。

        4. 在实际应用继承体系时尽量不要定义同名的成员,完全在给自己挖坑。

4. 派生类默认成员函数

4.1 默认构造

        下面我们看看子类构造的时候其中的父类成员的构造动作是什么。

                        

        可以看到当子类使用编译器自动生成的默认构造时,子类实例化的时候会直接调用父类的默认构造。

        如果想在子类构造的同时更改一下属于父类的那些成员,就是说显示写子类的默认构造该如何做。

        首先要保证父类的构造中可以支持修改这一变量,然后在子类的构造函数初始化列表中调用父类的这个构造函数并附上参数。

        这种写法是规定的,之所以有这种规定就是因为当父类已经有构造的时候为了防止子类中瞎搞,所以要求如果像更改父类的成员初始化内容就要通过父类的自己构造,而不是在子类中瞎写。这就像是类中的自定义变量初始化会调用它的默认构造一个道理。父类也可以当子类的一个自定义变量来看待。

4.2 拷贝构造

        在不显示写子类的拷贝构造的情况下,编译器自动生成的拷贝构造是会调用父类的拷贝构造的。

        ​​​​​​​        

        那如果我们需要显示写出子类的拷贝构造的时候也要在初始化列表显示调用父类的拷贝构造,否则编译器会调用父类的默认构造来匹配子类的拷贝构造,结果就是程序正常运行,但是父类的内容没用拷贝过来,其中父类的内容明显是用的默认构造生成的。

        ​​​​​​​        

        这里在使用显示的拷贝构造时,我们就用到了上面的赋值兼容转换概念,将子类中的父类区域切割给父类的拷贝构造当参数使用。

        

4.3 赋值操作符重载

        赋值操作符重载写起来比较简单,唯一需要注意的就是 operator= 在父类和子类之间构成了隐藏关系,因此在子类中要使用父类的operator=需要声明类域。否则会去掉用子类的operator=从而形成无限递归。

        当然,这些拷贝构造或者赋值重载都是在需要深拷贝的时候才需要使用的,我们这里写只是介绍用法。

4.4 析构函数

        子类析构函数在显示写的时候规定不要去调用父类的析构函数,编译器自己会去调用父类的析构函数,因此子类的析构函数只需解决那些会开空间的变量。

        在构造子类对象的时候要遵循先构造父类在构造子类的顺序,因为子类中的某些成员变量在构造的时候可能会用到父类中的一些成员,如果此时先构造子类对象的时候就会出现乱码,因为父类的对象还没有构造生成。但是对于这个问题无需担心,因为构造对象的顺序是根据成员变量的声明顺序走的,而不是参数列表中的顺序,而因为子类是继承了父类而产生的,因此父类永远都比子类先构造。

        再说回析构的问题,父类需要比子类后析构,因为子类在析构过程中可能会用到父类中的一些成员,而反过来父类永远不可能用得到子类的成员。但是在实际编写析构的时候, 不会像构造那样因为有语法的规则存在,正好固定的了构造顺序。相反,析构时如果把父类的析构写在子类的析构函数中时,很明显就先析构了父类。为了避免这种情况的出现,于是规定了不用在子类中写父类的析构,编译器会自己在子类析构之后去析构父类。

5. 继承与友元

        父类的友元关系不能继承给子类,也就是说父类的友元不能访问子类的私有或保护成员。

6. 继承与静态成员

        基类中有一个static静态成员,则整个继承体系中也只有一个这样的成员。无论派生出多少个子类,都只有一个static静态成员。这就不得不回忆起在讲类和对象的时候,对于类的静态成员有一个规则:类中的静态成员变量属于所有类的对象。子类对象中隐含了一个父类对象,自然子类的对象也与父类的对象共用一个静态成员变量。

        ​​​​​​​        

        可以看到,父类对象与子类对象的静态变量是同一块空间,还是印证了前面那个规则。

        如果忘记了类中静态成员变量的诸多规则,传送:

C++语言·类和对象(下)-CSDN博客文章浏览阅读801次,点赞15次,收藏22次。本文详细阐述了C++中构造函数、初始化列表、隐式类型转换、explicit关键字、静态成员、友元、内部类和匿名对象的概念,以及编译器在某些场景下的优化策略,帮助读者掌握这些关键概念和实践技巧。https://blog.csdn.net/atlanteep/article/details/137886869?spm=1001.2014.3001.5501

7. 多继承同棱形继承等复杂继承逻辑

        ​​​​​​​        ​​​​​​​                 

        单继承:一个子类只继承一个直接父类的继承关系。

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        多继承:一个子类有两个或以上直接父类的继承关系

        ​​​​​​​        ​​​​​​​        ​​​​​​​        

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

        菱形继承逻辑存在数据冗余二义性的问题。

        二义性是指,在西红柿类中访问植物类的成员时,如果直接访问就会有蔬菜中的植物类成员变量,还是水果类中的植物类成员变量的歧义,因此想解决这一问题需要指定植物类所在的类域。

        数据冗余是指西红柿中存了两份植物的信息,但其实对于西红柿来说只需要一份植物信息就够了,如果当植物类很大时,两份植物类会造成大量的数据冗余问题。C++为了解决这种情况打了一个补丁,使用虚拟继承。

        ​​​​​​​                

        因为现在植物类只在西红柿类中出现了一份,因此现在二义性的问题也随之解决了

        虚拟继承要在产生数据冗余的第一时间加上

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        

        比如在上面这个逻辑中,要在B、C类的地方就要加上虚拟继承。

        其实为了避免这种复杂的情况出现,在设计类的时候我们可以接受多继承,但是尽量不要设计菱形继承。

8. 继承的总结和反思

        多继承就是C++语法复杂的一个体现,有了多继承就会出现令行继承,有了菱形继承的问题就要设计出虚拟继承的概念,底层实现就越来越复杂。因此多继承可以认为是C++的缺陷之一,后来的很多语言就没有多继承了,比如JAVA

        继承和组合

        组合就是说把另一个类通过成员变量手动塞到这个类中,形成组合的关系。

        继承是一种 is-a 的关系,就是说每个派生类对象都是基类对象的特殊体。

        组合是一种 has-a 的关系,假设B组合了A,那每个B中都有一个A。

        根据软工中 高内聚,低耦合 的理念,尽量使用组合结构而不是继承结构,因为组合结构访问不了另一个类的保护对象,因此在结构上相比于继承拥有更低耦合度。

        但具体用哪一种还是看具体情况,比如 车轮-车 的关系中就用组合, 蔬菜-西红柿 的关系中就用继承,如果是 链表-队列 这种既可以用组合也可以用继承的关系中当然首选组合。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值