C++:继承

前言:

        本篇仅作为C++的继承知识的梳理以及学习分享,并不完全保证看完本片就能学会继承,但可以对继承有一个概念。

关于继承的概念:

        什么是继承:

                那么对于继承的理解可以从现实生活中去理解。继承嘛,继承遗产是继承,继承家业也是继承。那么继承就可以理解为,家里父母亲的东西就是我的东西,可能现在不是但早晚都是,而我的东西还是我的东西。

        继承的好处:

                假设我们现在需要写一个学校管理系统,而学校里既有学生还有老师等等,他们都有个共同的属性是人,而学生有学生独有的属性如:学号,老师也有老师独有的属性如:职称。

                所以如下图因为老师和学生同时都拥有人的属性,所以通过继承,让老师和学生都继承Person类,就相当于他们都拥有了Person的成员变量,而他们自己的成员变量又是独有的。

继承的定义:

        继承格式:              

        继承关系与访问限定符:

                

                如上图,子类继承父类的方式有三种,那么这三种继承都有什么区别呢?

                那么在这里小编就用一个简单的例子进行举例:假设你是一个富有的继承人。当你接手家业时,你的父亲说:“我担心如果我把全部资产都交给你,你可能会因此不再努力。与此同时,我也希望为社会做一些贡献。因此,我决定将三分之一的资产用于成立一个基金会,专门资助那些通过我们家族学校的孩子们的大学学费。这个基金会的资金你是无法动用的。另一部分三分之一的资产我会存起来,等你结婚成家后,每个月都会按固定金额给予你,而不是一次性给你。最后的三分之一将直接传给你。

                那么在上图例子中,最开始三分之一的钱成立基金会的钱属于私有成员你继承不了,另外三分之一的钱属于保护,你不能直接继承,最后三分之一可以直接继承。

        那么就能很清楚理解在继承关系中,访问限定符影响子类对父类成员的访问权限。不同的访问限定符会导致子类对继承自父类的成员的不同访问能力:

  public:子类可以访问父类的公共成员,不论访问限定符如何,公共成员都可以被继承并访问。

 protected:子类可以访问父类的保护成员。虽然这些成员在父类中是受保护的,但子类可以继承并使用这些成员。

 private:子类不能直接访问父类的私有成员。私有成员只对定义它的类本身可见,因此这些成员不会被子类继承。

                当继承方式为protected时,父类除了private里的所有成员都会被继承为保护属性,那么在主函数里就调用不到p的Print函数。那么私有继承也是同理。

        当继承方式是公有继承时,public属性的Print函数碰public继承,最后继承还是public的成员函能直接调用。而protected属性的成员函数_name通过public继承,会变为protected属性的成员变量,并不能直接显示调用。而_age成员变量是私有的不能继承。

        小结:

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

                2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的

                3.虽然看似继承方式有很多种,但在常用的基本就是public继承,毕竟你想,你写一个父类,肯定是希望子类去继承它,你都写成保护了的话那还继承干嘛。

                4.对于继承方式和访问限定符,最后继承的属性为取小的那一个。如:公用碰保护->保护,保护碰保护->保护,保护碰公有->保护。

继承中基类与派生类对象的赋值转换:

        在讲派生类对象与基类对象的转换赋值之前我们先来重新回顾一下关于普通类型的转换过程

        

        如上图,当两个不同类型的值进行赋值操作时,程序并不会直接将a值赋给b,而是会隐式类型转换,将a的值赋值给临时变量,然后临时变量类型转换成int类型再赋值给b。                                                           

        那么如何能证明有临时变量呢?首先我们要知道临时变量是具有常性的就像被const修饰一样,不可被修改,所有我们可以进行以下操作来证明。

        

        那么从上图可以看到,创建了一个int类型的b引用去引用a,这里并不是直接引用a而是引用中间的临时变量,而临时变量是具有常性的,所有普通的引用不行,必须有经过const修饰的引用才行。

      那么对普通类型的转换有所了解后,我们再来看基类与派生类的对象直接的赋值。

                1.首先C++有硬性规定,父类是不能赋值给派生类,而派生类的是可以赋值给父类,所以他们是一个单向的关系。

        

        

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

                2.1将派生类的对象赋值给基类的对象

        那么可以看到之前上图的例子,当person类型的对象p被student类型的对象s赋值时,其p自身的内容就改为s中父类p类型的值。

                2.2将派生类的对象赋值给基类的引用。

                       是的,基类的引用是可以直接引用派生类的对象。

                

        

        正如上如所示,基类的引用直接引用派生类的对象,并通过p1引用修改派生类对象里基类的成员对象。并且可以看出在引用的时候基类并没有被const修饰,那么就可以证明在基类引用派生类时,并没有临时对象的产生。

        在基类引用派生类对象时候,其实也发生了切割/切片,基类的引用仅仅只影响派生类中的父类的成员。

        

                2.3 将派生类的对象赋值给基类的指针。

                        那么既然能使用基类引用引用派生类,那么同理基类的指针同样能指向派生类。

继承中的作用域

        在继承中基类和子类都是拥有一个独立的作用域,那么也就是说因为是两个不同的作用域,所以基类和子类会允许同时存在同名成员函数与成员变量,

         正如上图代码,person类与student类(继承person)中,都拥有同名的成员函数func与成员变量num。但编译检查并不会报错,因为他们都处在不同的类域,而在派生类student中想要访问父类的num成员变量则需要通过域作用限定符":"去指定域中查找。

        那么子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问),需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

        所以在实际中在继承体系里面最好不要定义同名的成员

继承中派生类的默认成员函数:

        派生类的默认构造函数:

                对于派生类的构造函数,我们可以将派生类看作是一个特殊的父类。C++规定当创建一个派生类对象,编译器会先调用父类的默认构造再调用子类的默认构造,这与栈的对象创建过程保持一致。因为子类必定包含一个父类,这是母庸置疑的。如果没有显示调用父类的默认构造,编译器也会去自动调用,如果父类没有默认构造则会进行报错。

        父类有默认构造,且不显示调用:

        父类有默认构造,显示调用:

        

        父类没有默认构造,且不传参:

       

         派生类的拷贝构造函数:

                当调用派生类的拷贝构造时候,如果拷贝构造里没有显示调用父类的拷贝构造函数,那么编译器则会自动调用父类的默认构造函数,因为拷贝构造也是构造。如果希望调用派生类的拷贝构造时也会调用父类的拷贝构造则需要进行显示调用。

                当不显示调用父类的拷贝构造:

                当显示调用父类的拷贝构造时:

        派生类的operator=:

                在派生类的operator=中,必须同过父类的访问限定符显式调用父类的operator=,才能派生类中的基类赋值。

        这里必须显示调用父类的operator=,如果不显示那么编译器会默认调用的是子类的operator=就形成了死递归。

        派生类的析构函数:

                关于派生类的析构函数C++也是有规定的,会先调用派生类的析构函数再调用父类的析构函数。

                一方面是为了保持与栈销毁顺序一致(先创建的后销毁,后创建的先销毁),另一方面是为了防止特殊情况的发生。

        从上图可以看到,在student类型对象s的销毁中,是先调用自身的析构函数进行销毁,最后在调用父类的析构函数进行S对象中的父类成员进行析构。

        那么我们如果我们可不可以先调用父类的析构函数再调用子类的析构函数呢?答案是可以的,但我们来看这样一种情况。

        可以看到基类里有一个int类型的指针指向new来的十块空间,并且调用父类的析构函数时会释放这些空间。如果我们先析构父类再析构子类,那么编译器同样会再次调用父类的析构。

        对同一块空间释放两次那必然会产生报错,就算不调用两次析构的话那我们来看下图的情况,如果我先析构父类又再次去调用父类里的对象,绝对是坑。

        所以编译器为了防止程序员坑所以让父类的析构自己来。因为先析构子类再析构父类怎么样都不会产生问题。

继承与友元

        C++规定友元不能被继承,也就是说基类友元不能访问子类私有和保护成员。其实这也符合生活,有一个很通俗易懂的例子:爸爸的朋友并不是你的朋友。

        那么在上图例子中可以看到,在父类person中声明了一个友元函数Display,而在派生类中没有去声明,所以在访问派生类的成员变量时编译器进行报错。因为派生类的成员变量是protected不能显示调用。如果想要在Display中访问要么就算同样也在派生类去声明友元,要么就算在派生类创建一个public的get函数访问。

关于继承与静态成员

       如果在基类中定义一个static的静态成员变量,那么在整个继承体系里面有且只有一个这样的成员,无论派生出多少个子类,都只有一个共同的static成员对象。

        ​​​​​

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

        在从C++中有单继承,那么也有多继承。并且这在现实生活中也很合理,就比如一个人可以同时拥有多种身份,比如白天你在学校上课那么你的身份则是学生,下课回家玩游戏的时候,你的身份就是游戏中的那个角色的身份。

        单继承:

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

        

        多继承:

                一个子类至少有两个或以上的父类

        菱形继承:

                C++在支持多继承的时候就产生了一个巨大的坑——菱形继承。就像上图的例子,Student类与Game类是不是都同时拥有人的属性,那么如果在定义一个Person类让他们继承时,就产生了菱形继承。     

                产生菱形继承就会引发数据冗余以及二义性的问题。

    

        我们来举一个简单的例子:

        上图例子,我们有一个基类A,派生类B,C同时继承了A,此时在定义一个类D继承了B与C。

        可以看到在main函数里定义了一个D类对象d1,通过调试发现d对象里的成员变量里有两个a,这是因为派生类B继承了A,所以B类里有个成员对象a,那么C也是同理。所以在D类里会同时有两个A类的成员变量。在给_a赋值的时候如果不显示指定访问的是哪个_a编译器就会报错因为对_a的不确定,只能通过显示访问才能解决。

        那么这样是不是就很不符合常理。假设在学校里,一个人同时拥有老师和学生的属性,并都是基于人的属性,假设人的属性里有年龄,性别等等。但你总不可能是老师的时候,你的年龄,性别是一套信息,而当你是学生的时候你的年龄性别又是另一套信息,这不符合常理。

       

        C++标准在1990年代就引入了虚拟继承(virtual inheritance)用来解决多继承引发的菱形继承问题。

        在B类与C类的继承前添加关键字virtual(虚拟的意思),主要这里是在中间进行虚拟继承而不是在最后D的位置添加virtual。

        

        virtual是如何解决这个问题的呢?

                普通菱形继承的底层:

        通过调试,取到d1的地址中可以看到在d1的内存里开了24个字节(有内存对齐),跟我们之前所想的一样,d1里有两个实例的a变量地址。

        

                虚拟菱形继承的底层:

       

        观察上图可以看到,经过虚拟继承后,原本d中存放两个a的位置变成了一串地址,而在存储d变量地址的下一个地址存储了a变量。此时我们查找存放两个a的位置的地址会发现,他们指向了一块地址空间,而这块地址空间存放的是一串数值。0x00DF9B44地址存放的是16进制的14(也就是十进制的20),而0x00DF9B4C存放的是16进制的0c(也就是10进制的12)。    

        20刚好就是0x005FF9E0到0x005FF9F4的偏移量,那么12也是0x005FF9E8到0x005FF9F4的偏移量。那么通过虚基表里的偏移量d1.B::_a与d1.C::_a都能找到同一个_a。完美解决了二义性的问题。

总结:

        许多人都说C++是最难学的语言之一,那么多继承就算一个体现。支持多继承就会引发菱形继承,有了菱形继承就要去解决菱形继承的问题,底层实现就更复杂了。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值