C++继承

时间:2022-10-28 01:18:48

继承的概念

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

继承的语法

C++继承
我们看到Person是父类,也称为基类;
Student是子类,也称为派生类;

继承方式

继承方式就是子类想以什么样的方式来继承父类;
总共有3种继承方式:public、protected、private;
子类以不同的方式继承父类,表现出来的现象也是不一样的;
下面我们来具体讨论一下子类分别以3种不同的方式继承父类表现出来的现象:
C++继承
总结:
1、子类无论使用何种方式来继承父类,父类的成员都会被继承下来;
2、父类的私有成员,也会被子类继承下来,但是子类不能对其进行访问!只有在父类内部才能访问父类的私有成员;
3、对于父类来说,如果我们想让我们的成员不能在父类类外进行访问,但是能够在子类中进行访问,我们可以把父类的成员的访问权限用protected修饰;
4、如果是用class定义的子类,那么class定义的子类继承父类的默认继承方式是private;如果是用struct关键字定义的子类,那么struct定义的子类继承父类默认继承方式是public;具体效果如下图:
C++继承
C++继承
5、实际上上面那张表的关系我们不必去记住,在实际中我们多数情况都是用public方式继承的!在Java中更是只有public继承方式!如果我们硬是要记这张表的话,我们有一个公式:父类成员在子类中的访问方式=Min{继承方式,父类成员在父类中的访问权限};(对于父类中的私有成员该公式不适用,这个特例我们只需要记住就行,无论以那种方式继承父类的私有成员,我们子类都无法访问父类的私有成员)

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

1、我们可以看一下,下面的赋值语句:
C++继承
通过前面的学习,我们知道这样的赋值语句是正确的,b是个引用,但是b并不是直接引用的a,而是引用的一个临时变量!主要是因为编译器帮我们做了隐式类型转换,由于a是double类型的数据,作为int类型的引用b是不能直接引用a的,但是为了完成我们的需求,编译器会自己创建一个int类型的临时变量,然后用a类型的数据来构造这个临时变量,最后在让b引用这个临时变量;
通过上面的例子,我们可以发现,当我们用A类型去引用一个B类型的数据时,由于类型不匹配,编译器会创建一个类型匹配的临时变量,来帮助我们完成引用;
但是对于父类来说,它不需要借助临时变量,父类的引用可以直接引用子类! 同是子类对象可以直接赋值给父类,&子类的指针也可以直接赋值给父类指针;我们把这种行为叫做切片,寓意把子类中父类的那部分切来赋值给父类,这种行为是天然支持的!
C++继承
当然,以上是Person类与Student无任何关系的情况下,编译器的做法,但是现在Person类与Student有关系啊,还是父子关系!那么编译器就允许Person直接引用Student实例化对象的内部中的Person部分!
比如:
C++继承
C++继承
接着我们再来介绍一下,子类直接赋值给父类:
C++继承
C++继承
我们可以看到p2内部的数据与s对象内部Person内部的数据是一样的:
C++继承
对于&Student对象在赋值给Person*就更简单了!
Person类型的指针直接指向Student内部的Person那一部分!;
C++继承
2、通过上面的了解,我们知道了父类的指针、引用、赋值可以直接使用子类来完成,也就是子类对象可以赋值给父类对象,那么父类对象可不可以直接赋值给子类对象?或者说为什么子类对象可以赋值给父类对象?
主要是因为子类对象中含有父类对象中的全部数据,子类对象自然可以赋值给父类对象!但是反过来,父类对象含有子类中的全部数据吗?显示是没有的!难道我们给子类对象赋值只赋值一半吗?这显然是不行的!因此父类对象不能赋值给子类对象!
3、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换;

继承中的作用域

1、在继承体系中子类和父类都有自己的作用域;
2、如果子类和父类中没有同名成员时,子类是可以直接访问父类成员的!当然如果子类和父类中出现了同名成员,那么编译器就会隐藏父类中的同名成员,我们就不能直接使用父类中与子类中同名的成员了,比如父类中有一个int _age;成员,在子类中也有一个int _age对象,那么子类会隐藏对于父类_age的直接访问,我们之后再通过子类对象._age访问到的就是子类的_age;
如果我们硬是要访问父类中的_age成员呢?
不是说了嘛,父类和子类都有自己独立的作用域,我们可以通过子类对象.父类作用域::_age的方式来访问父类的_age;
当然其他未与发生同名的成员我们也还是可以直接进行访问的!
3、 对于成员函数的隐藏只需要函数名相同就会构成隐藏,编译器不会考虑参数哪些!
下面我们可以来看个面试题:
C++继承
根据刚才的理论,我们可以知道,当子类中的成员与父类中的成员同名时,编译器会隐藏父类的直接访问!子类的fun成员函数与父类的fun成员函数同名,编译器会隐藏父类中的fun函数!因此父类中的fun函数与子类的fun函数构成隐藏关系!这里我们需要特别注意这两个函数不构成函数重载,函数重载的首要条件就是:函数名相同的两个函数要处于同一作用域下; 这里父类的fun处于父类作用域,子类fun处于子类作用域,虽然两个都叫fun但是由于作用域的不同,无法构成函数重载!
注意只有当子类以public的方式继承父类时才有切片这一说法,子类对象才可以赋值给父类对象!以proteced、private方式继承父类的子类是无法完成切片的!

子类的默认成员函数

由于我们子类是继承的父类,那么父类的所用成员变量都会被我们给继承下来!也就是说在子类的存储模型中:既有子类自己的成员变量,也有父类的成员变量;
但是现在,我们该怎么给子类进行初始化呢?给子类的成员初始化还好说,我们直接找到那个成员,然后直接进行赋值就可以了,但是对于父类成员来说,我们应该怎么赋值呢?我们也直接使用直接访问的方式给父类赋值?
比如:
C++继承
如果是这样的赋值,显然是有弊端的!这样的赋值对于父类中public、protected成员时奏效的(默认子类都是以public的方式继承父类),因为在子类中我们可以直接访问这两种类型的父类成员变量!但是如果父类有私有成员变量呢?父类的私有成员变量也会被子类继承下来,但是父类的私有成员变量在子类中是不可访问的!那么我们就无法完成对于父类的初始化!
我们要想完成对于父类私有成员的初始化,要么成为父类的友元,要么在父类的类型中!因此像这种在子类中直接访问父类成员,然后给父类成员变量初始化的操作是行不通的!
那么我们应该如何完成对于父类的初始化?
对于父类的初始化,我们就调用父类的构造函数来初始化呗!可是在子类中没有父类的名字啊,我们该如何调用父类的构造函数?没有名字?不就是匿名对象?匿名对象是怎么初始化的,我们就用匿名对象那种调用构造函数的方式呗!
因此,对于子类中父类的初始化我们可以改成:
C++继承

在B类的初始化列表,显示的调用父类的构造函数,然后在初始化子类自身的成员变量;因此,我们在利用B类实例化对时,会调用B的默认构造函数,在初始化列表,先初始化父类,于是就去调用父类的默认构造函数,最后在来初始化B类本身的成员变量,因此对于上述代码B b;我们会先看到A类的构造函数被调用,随后在调用B类构造函数;先继承的先构造!
对于子类中父类的所有操作,我们秉持着一个原则就行了,对于子类中父类的操作,调用父类中的相关接口来处理就行了,避免我们自己处理时,对于父类的私有成员无法处理!
C++继承
同理对于子类拷贝构造函数的写法如下:
C++继承
子类的赋值运算符
C++继承
子类的析构函数:
C++继承
对于析构函数我们这里需要再多说一句,编译器会对类的析构函数进行特俗处理,我们看到的析构函数是~类名()但是在编译器看类每个类的析构函数名字都是统一的,都是:destrutor();无论我们类的析构函数名字多么的花里胡哨,在编译器看来,他们都是统一的名字:destrutor(),编译器只认这个名字!

继承与友元

友元关系不能被继承!

C++继承

继承静态成员

基类定义了一个static静态成员,那么这个静态成员也会被子类继承下去,由子类与父类共享!如果静态成员在父类中不是私有成员的话,那么我们也是可以通过子类的类域和子类的对象去访问的;父类的静态成员被继承下来过后,也是子类的静态成员!
C++继承

多继承

单继承:一个子类只有一个父类!这样的继承关系被称为单继承!
C++继承
多继承:一个子类有多个父类!这种继承关系被称为多继承!
C++继承

菱形继承

既然有了多继承,那么菱形继承的关系就必定会出现:
C++继承

菱形继承的问题

对于菱形继承的话,势必会造成数据的冗余,和数据的二义性!
就比如:
菱形继承代码:
C++继承
菱形继承图解:
C++继承
我们再来画一画Saber类的存储模型:
C++继承
通过画图我们可以发现,在Saber父类Student和Teacher中Person都被继承了一次,然后Saber继承Student和Teacher类,就相当与Saber继承了两次Person类,这对于Saber来说是没必要的!Saber只需要一份Person类就可以了!这样的菱形继承就造成了数据的冗余;同是也会造成访问的不确定性,比如我用Saber类对象去访问_age时,会出现访问不明确,编译器不知道我们需要访问那个父类的_age,Student和Teacher的_age不会构成隐藏,隐藏是指父类和子类的成员同名,但是Student与Teacher是平级,故不构成隐藏;
C++继承
我们在访问_age时必须明确指定我们要访问那个类域下的_age:
C++继承

菱形继承问题的解决

既然菱形继承存在数据冗余和二义性的问题,那么有没有什么办法可以解决菱形继承带来的问题?
虚拟继承可以解决菱形继承数据冗余和二义性的问题;
为了方便演示,我们利用一个比较简单的继承关系来演示:
C++继承
C++继承
假设我们没有采用虚拟继承,那么D类对象的数据模型如下:
C++继承
现在,我们使用虚拟继承:
C++继承
我们再来查看一下,d对象的数据分布:
C++继承
我们可以明显发现,B类中的A数据与C类中的A数据被编译器合并成了一份!这不就解决了数据冗余的问题嘛,可是编译器是如何做到的呢?通过d对象的内存分布图,我们可以看到B类域和C类域没有在存储_a变量了,而是只存了一个指针,这个指针是干嘛的呢?B、C又是如何去找到他们的成员_a的呢?
我们可以利用内存窗口,分别查看一下B、C类中存的指针是指向的什么的:
C++继承
我们再来看一下C类中的指针指向的空间是不是也是存的相对_a的偏移量:
C++继承
C++用这样的做法就巧妙的解决了菱形继承带来的数据冗余和二义性的问题!

既然是这样的话那么对于这种虚拟继承的切片问题,C++是如何处理的,比如:
D d;
B&b=d;
b是引用的d对象的那一部分呢?
还是一样的,b对象引用的d对象中属于B类的那一部分:
C++继承
同理对于:父类指针是如何指向d对象的呢,比如:
D d;
Bptrb=&d;
C
ptrc=&d;

C++继承
现在我们理解了父类指针指向子类对象时是如何访问父类成员的,现在我们再来看一个父类指针先指向子类对象,在指向父类对象:

D d;
B*ptrb=&d;//①
ptrb->_a++;
B b;
ptrb=&b;//②
ptrb->_a++;

现在我们来理解一下①号ptrb指针与②号ptrb指针意义是否一样?
肯定是不一样的!①号ptrb指针实际上是指向的D类中B类部分的数据的,往大了说ptrb就是指向D类对象的!指向B类有点不纯正!但是反过来我们再来看②号ptrb指针,指向的就是一个纯正的B类对象!没有半点掺假的意思!可是编译器是如何区分我们的一个父类指针到底是指向子类对象中的父类部分还是直接就是指向父类对象本身的呢?要知道如果是指向子类中父类部分,那么ptrb在访问_a数据时可不是直接访问的,而是通过计算偏移量来计算的!如果ptrb指向的是一个纯正的B类对象的话,那么访问_a成员的方式就是直接访问,用上面那一套方式来访问_a成员是行不通的!难道我们每次在利用ptrb指针访问_a成员的时候还要先判断一下ptrb指向的对象?然后根据具体对象来确定具体的访问_a的方式?这显然是代价巨大的!为什么不同一一下访问_a的方式呢?事实上C++也是这么做的!在使用虚拟继承的时候C++也是对于父类的存储模型做了改变,比如:B类不是采用虚拟继承的方式继承了A类嘛,C++在处理的时候,就改变了原来普通继承那样存储A类对象的方式,而是采用与D类一样的存储模型,通过计算偏移量的方式来访问A类对象:
C++继承
因此对于虚拟继承来说,虚拟继承不仅会改变子类的存储模型,也会改变父类的存储模型!
这样一来的话,不管我们ptrb是指向父类对象还是子类对象的指针,我们统一的处理方式都是先计算偏移量,然后在访问_a成员!这样就避免了区分ptrb指向的类型的时间消耗!
在B类或C类、D类中存储的那个指针实际上是一张表的地址,这个表被叫做虚假表,这个表中不止存储着偏移量的信息,也还存储着一些其他信息!

注意:虚拟继承不要滥用,虚拟继承是专门用来解决菱形继承的方法,不要在其他地方去使用!