
文章目录
一、封装、继承、多态的含义认知
- 封装:核心思想就是将数据(属性或者成员变量)和操作(方法或成员函数)捆绑在一起,从而形成一个独立的单元结构,即“类”,并且对外部隐藏该对象的内部实现细节。
- 继承:首先它是在封装这个特性上进行的延伸,要想有继承,得先封装出一个类。其核心思想就是允许一个类(称之为派生类\子类)基于另一个类(基类\父类)来创建,让其派生类继承基类的属性和方法,并且可以在派生类中对继承的API进行扩展。
- 多态:首先它的存在也得基于上一个继承特性,多态的产生依赖于继承这个体系结构。字面上看表示一个东西的多种形态,这里的东西主要指的是一个API接口。即同一个接口作用于不同的类对象的时候,可以产生不同的行为。
- 因此从三者含义上看,它们的关系是层层递进的,多态的产生依赖继承,继承的出现基于封装。这三大特性相辅相成,共同构建了C++面向对象编程的基础,目标是使代码更模块化、更易复用、更易维护和扩展。
二、继承体系中的对象转换关系
- 首先继承是一类的存在基于另一个类的创建的,即派生类继承了基类。在这里会涉及到继承时候的访问限定修饰符的作用,当派生类是以public形式继承基类的时候,那么在基类中的成员属性和方法被什么样的限定符修饰,被派生类继承后,依旧保持原来的限定特性。若是以protected继承的基类时候,那么基类中的public属性在派生类中就变为protected特性了类里面属性和方法的限定特性随继承限定特性决定。还有一点就是当基类中的属性是private修饰的时候,派生类依旧是继承下来了,只是在派生类中不可见而已。若想要看到,则可以通过基类中实现Get方法获取
- 上面描述的是派生类以不同的继承方式继承基类的时候,属性和方法的限定特性变化。接下来就得看派生类和基类对象的赋值转换关系了。首先回顾一下类对象的存储方式,当封装一个类之后,创建该类的多个对象时,每个对象都会存储一份类中的成员属性,但是成员方法是存储在一个公共的代码段,因为每个对象都是调用同样的函数,只是参数不同而已,所以没必要每个对象都存储一份成员函数,这样节省了空间。在知道类对象的存储方式之后,那么派生类和基类对象之间的转换就变为类中成员属性的变化了,因为每个类对象只存储对应类中的成员属性。
- 那么下面就看一下具体转换方式:这里类对象之间的转换也就做切片或者切割。为什么这样描述,我们来看,当派生类继承基类时候,那么派生类中必然有基类的成员属性,但是我们不确定派生类是否添加新的成员属性,即使添加也不影响,因为新创建的成员属性属于派生类的。那么转换规则也就可以看出了,就是派生类对象可以赋值给基类对象(这里包括基类对象本身、基类对象指针、基类对象引用),这里就会把不属于基类的成员属性给切割掉,只保留父类的成员属性,也就是继承下来的成员属性,类比于向上兼容思想(去掉不属于自己的东西),因为基类不能拿不属于自己的东西。那么反过来,基类对象是不可以直接赋值给派生类对象的,因为向下转换有多余的成员属性存在,也就是派生类的。但是当基类对象的指针是指向派生类对象的时候,是可以把基类的指针或者引用通过强制类型转换赋值给派生类对象的指针或者引用的。当基类是多态的时候,则可以使用RTTI的dynamic_cast进行安全转换。
- 下面是图解:Student是派生类,Person则是基类。

三、因继承中的作用域诞生出隐藏(重定义)概念?
- 首先明确的是,在继承体系中,每个类都有独立的作用域,因此派生类继承基类之后,两个类也是独立的作用域的。
- 那么就会出现基类和派生类中出现同名的成员属性和成员方法,这时候派生类中的那个成员属性和成员方法就会屏蔽掉基类的那个成员属性和成员方法,这种情况就叫做隐藏(重定义)。若在派生类中要访问同名基类中的成员属性或方法的时候,就需要基类名::成员属性/方法。这里需要注意,成员方法构成隐藏的条件就是只要函数名相同就构成隐藏。
四、继承体系中派生类的默认成员函数变化?
- 我们知道派生类会继承基类的成员函数,因此也会把基类的默认成员函数也继承下来,那么这几个成员函数在派生类中如何形成呢?这里需要记住派生类中的成员属性有一部分是属于基类的,因此是谁的成员属性就得调用谁的默认成员函数。
- 派生类的构造函数:必须先调用基类的构造函数,把属于基类的成员属性初始化之后再调用派生类的构造函数,若基类没有默认的构造函数,则需要在派生类构造函数中的初始化列表中显示实例化基类的成员属性。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 对于析构函数,则是有顺序而言的,先析构派生类的,再调用基类的析构函数。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 而对于构造是先构造基类,再构造派生类。
五、菱形继承的问题所在?
- 单继承:即一个类继承另一个类;
- 多继承:即一个类继承另外两个类;
- 菱形继承:则是先是两个类分别继承同一个类,然后又有一个类继承这两个类,图解如下:

- 由于我们学习了类对象存储的方式,因此从上面的结构分析看,继承同一个类的那两个类,各自存储了一份基类的数据信息,然后新的一个类又去继承这两个类,那么新类中就会存在两份相同的最初基类的数据信息,因此最大问题就是造成了数据二义性和数据冗余的问题。
六、解决菱形继承的方式——虚拟继承(虚继承),为什么可以解决呢?
- 我们从上面的菱形继承中知道,主要问题发生在两个类继承同一个类的时候,这两个类各自存储的都是基类的数据信息,那么要解决这个主要矛盾,因此虚继承就改变了这种存储方式,它把这两个类中存储的相同基类数据信息提取出来,把它放到一个独立的空间区域,也就是单独开辟一个内存空间存储这个基类数据,然后在原来两个类存储基类数据信息的位置存储一个值,这个值就是从该位置地址处到达那个单独开辟的空间地址处的距离——偏移量。然后新的那个类继承这两个类的时候,这两个类里面存储的数据就是偏移量了,此时不管是那个类对象去访问这个基类数据的时候,拿到的都是对应类到达基类数据的偏移量,再进行当前地址加对应的偏移量,最终访问的数据就是唯一的。
- 这里需要补充一下:在编译期间,编译器会自动给类对象创建虚基表,用来存储对应类对象中的数据信息和偏移量的。在访问的时候则是通过指针去指针这张表的,即虚基表指针(虚表指针)。
- 通过图解展示菱形继承中用虚继承为何解决了这个问题:

- 没有虚继承的时候:最总那个类中继承那两个类的数据时候,存在两份相同的数据:如下

- 通过虚继承时候,里面存储的就是偏移量了,相同的部分就被提取出来存在新的位置了:如下

七、多态的形成的必要条件?
- 在继承体系结构中, 基类中的成员函数必须要有虚函数,即被virtual修饰的。并且在派生类中必须重写基类的虚函数。在调用的时候必须通过基类的指针或者引用调用虚函数,这样才构成多态。
- 上面一句提到虚函数的重写(覆盖),也就是派生类在继承基类之后,基类中有虚函数的类成员函数,此时派生类必须重写该虚函数(重写的格式:必须参数类型列表、函数名、返回值类型(协变除外)必须相同),此时就是派生类重写了基类的虚函数。
- 注意:若基类的析构函数定义为虚函数之后,派生类中只要定义了析构函数,都是和基类的析构函数构成重写的,即使不加virtual、函数名也不一样,看起来违背重写规则,但实际上可以理解为是编译器做了特殊处理,编译后把函数名都统一处理为destructor了。
- 上面提到协变,这里做了解什么是协变:即派生类重写了基类的虚函数,在返回值类型上和基类虚函数的返回值类型不一样,基类中虚函数的返回值类型是基类对象的指针或者引用,派生类重写基类虚函数之后,返回值类型可以是派生类对象的指针或者引用,这就构成了协变。
- 还要注意的是:析构函数可以为虚函数,但是构造函数不能为虚函数。
- 为什么构造函数不能为虚函数,而析构函数可以为虚函数?主要是因为类对象创建顺序问题,虚函数机制是依赖于已经完全构造的对象,因为创建过后会有虚函数表指针vptr,而构造函数就是用来创建具体的类对象本身的,若不创建对象本身,就没有vptr,那么虚函数机制也就无法工作。
八、重载、重写(覆盖)、隐藏(重定义)的区别是什么?
- 首先是**重载:**重载是发生在函数之间的,即在同一作用域(比如同一个类域中)在存在相同的两个函数。(函数名必须相同,参数不同(参数的个数、类型顺序、参数类型 )、返回值不固定(于返回值无关))。
- 其次是重写:重写是发生在继承体系结构中的,即派生类重写基类的成员函数(必须是虚函数),条件就是函数名、参数列表、返回值类型(协变除外)都必须相同才构成重写。
- 最后是隐藏:也是在继承体系结构当中的,这里有两种,即成员变量的隐藏和成员函数的隐藏,即当派生类中的成员变量或者成员函数(只要函数名相同)和基类中有相同的,则在派生类对象访问的时候默认就是访问的隐藏之后的数据,若要访问基类的同名成员变量或者函数必须通过 基类名::变量\函数访问。
九、多态的底层原理实现?
- 认识底层原理,就得引出 虚函数表(vtable) 和虚函数表指针(vptr)。
1、虚函数表(静态函数指针数组)
- 什么是 虚函数表? ,即当一个类中存在虚函数的时候,在编译期间,编译器会自动给这个包含虚函数的类(包括它的派生类)分配一个静态的函数指针数组。专门用来存储类中虚函数的实际入口地址的。
- 对于基类中的虚函数而言,会存放它自己虚函数的入口地址,若有纯虚函数的时候,就会存储一个指向纯虚函数的占位符的地址。
- 对于派生类而言,它也有自己的虚函数表,此时分情况,当派生类重写了基类的虚函数,那么存储的就是重写之后的虚函数地址。若没重写,那么存储的就是基类原来虚函数的入口地址。还有就是当派生类有自己的虚函数时候,会在vtable后面追加,把新的虚函数地址放入到这个静态函数指针数组的后面。
- 虚函数表存储在只读数据段的。并且不同类对象有属于自己的vtable,同一个类的不同对象共用一张vtable。
2、虚函数表指针
- 那么什么是虚函数表指针?顾名思义就是指向虚函数表的,它是一个隐藏的指针成员变量,是由编译器在编译的时候自动向包含虚函数类的内存布局中添加的,通常在第一个起始位置。
- 它是用来指向该类的vtable的,是在类对象调用构造函数的时候初始化的。
- 当创建派生类对象的时候,先调用基类的构造,此时基类的vptr,就会指向属于自己的vtable。然后再调用派生类的构造函数,此时派生类会有自己的vptr,并且指向属于自己的vtable。
- 每个类对象都有自己的vptr ,并且指向属于自己的vtable。
- 那么具体流程就是,当一个基类指针或者引用调用一个虚函数的时候,由于是不同类对象赋值给基类指针或引用的,因此在调用虚函数的时候,就会根据基类指针或引用的类对象中的vptr去查找对象中的vtable,从而找到要调用的虚函数。实现指向谁调用谁。
和多态(底层原理)?&spm=1001.2101.3001.5002&articleId=149140654&d=1&t=3&u=6493ca6a04484c8889b538666fd296eb)
7259

被折叠的 条评论
为什么被折叠?



