C++ 第三弹继承和多态

您所在的位置:网站首页 c加加类体系中不能被派生类继承的有 C++ 第三弹继承和多态

C++ 第三弹继承和多态

2023-07-03 04:57| 来源: 网络整理| 查看: 265

目录

1.继承

1.1什么是继承?

1.2语法格式

1.3继承权限

1.4继承概念语法格式

1.5赋值兼容规则

1.6继承体系中的作用域

1.7在继承体系中的构造和析构

1.8静态成员继承

1.9友元的继承

1.10不同继承方式下子类的对象模型

1.11继承和组合

2.多态

2.1什么是多态

2.2多态的分类

2.3实现条件

2.4重写

2.5抽象类

2.6C++实现多态的原理

2.7多态面试题

1.继承 1.1什么是继承? 继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 保 持原有类特性的基础上进行扩展 ,增加功能,这样产生新的类,称派生类。继承 呈现了面向对象 程序设计的层次结构 ,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用, 继 承是类设计层次的复用 1.2语法格式 class Student :public Person{ public: int _stuid; int _major; }; 1.3继承权限

1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。 2. 基类 private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected 。 可以看出保护成员限定符是因继承才出现的。 3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min( 成员在基类的访问限定符,继承方式 ) ,public > protected > private。 4. 使用关键字 class 时默认的继承方式是 private ,使用 struct 时默认的继承方式是 public ,不过 最好显示的写出继承方式。 5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced/private 继承,也不提倡 使用protetced/private 继承,因为 protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强 6. 基类的private成员变量和成员函数也会被继承,但是子类无权访问访问 1.4继承概念语法格式

好像没有这个东西 

1.5赋值兼容规则

前提:必须是public的继承方式。因为:public继承下,子类和基类是is-a的关系,即:可以将子类对象看成是一个基类对象

1.5.1基类和派生类的赋值转换 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用 。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。 基类对象不能赋值给派生类对象。 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。 1.5.2站在模型的角度分析 创建A类的对象时,会将成员函数和成员变量分开存储:a)普通的成员变量存储在变量中;b)静态的成员变量存储在全局区;c)成员函数存储在代码片段区。 class A { protected: int a; } class B : public A { protected: int b; }

1.6继承体系中的作用域

1.6.1基类和子类隶属于不同的作用域

1.6.2同名隐藏||重定义

基类和子类具有相同名称的成员变量,与成员变量的类型是否相同无关

基类和子类具有相同名称的成员函数,与成员函数的类型是否相同无关

通过子类对象调用同名成员时,只能访问到子类自己的同名成员,基类的无法直接访问到,如果想要通过子类对象访问基类中同名的成员,必须加:基类名称::同名成员

  1. 在继承体系中 基类 和 派生类 都有 独立的作用域。 2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类 :: 基类成员 显示访问) 3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。 4. 注意在实际中在 继承体系里 面最好 不要定义同名的成员。   1.7在继承体系中的构造和析构

1.7.1子类的构造方法怎么写,子类对象构造的过程

1.父类没有声明构造函数:

子类也没有声明自己的构造函数,则父类和子类均由编译器生成默认的构造函数。

子类中声明了构造函数(无参或者带参),则子类的构造函数可以写成任何形式,不用顾忌父类的构造函数。在创建子类对象时,先调用父类默认的构造函数(编译器自动生成),再调用子类的构造函数。

2.父类只声明了无参构造函数

如果子类的构造函数没有显式地调用父类的构造,则将会调用父类的无参构造函数。也就是说,父类的无参构造函数将会被隐式地调用。

3.父类只声明了带参构造函数

因为父类只有带参的构造函数,所以如果子类中的构造函数没有显示地调用父类的带参构造函数,则会报错,所以必需显示地调用。

4.父类同时声明了无参和带参构造函数

子类只需要实现父类的一个构造函数即可,不管是无参的还是带参的构造函数。如果子类的构造函数没有显示地调用父类的构造函数(无参或带参),则默认调用父类的无参构造函数。

总结一下:

1.当父类有显式地声明了构造函数时,子类最低限度的实现父类中的一个

2.当父类没有声明构造函数时,子类可以不声明构造函数或者任意地书写构造函数

1.7.2 子类对象析构的过程

编译器在编译器阶段,会在子类析构函数最后一条语句之后添加调用基类析构函数的指令

1.7.3 派生类六大默认成员函数

1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。 2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。 3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。 4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。 5. 派生类对象初始化先调用基类构造再调派生类构造。 6. 派生类对象析构清理先调用派生类析构再调基类的析构。 7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同 。那么编译器会对析构函数名进行特殊处理,处理成 destrutor() ,所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。 1.8静态成员继承

在整个继承体系中,静态成员只有一份

可以看做子类共享父类的静态成员的内存,子类可以修改该内存中的值,因此父类调用时也被修改了。 所以并不能看成继承关系,可以认为是共享关系。

在子类重新定义一个相同的静态成员变量,子类和父类的静态成员变量其实是两个不同的东西了(开辟了两块静态变量空间),修改子类并不会修改父类中的值。

子类可以继承、覆盖父类的静态成员函数

1.9友元的继承

友元关系不能继承的,因为友元不是类的成员

1.10不同继承方式下子类的对象模型

1.10.1对象模型是什么?

对象模型是指在面向对象编程中,为了描述和处理现实世界中的实体、事物或概念所构建的抽象模型。它通常由类、对象、属性、方法等元素组成,用于描述实体之间的关系、结构和行为。

1.10.2不同继承方式

1.单继承

一个子类只有一个直接父类时称这个继承关系为单继承,基类部分在上,子类部分在下

2.多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承,基类部分在上,(多个基类与其在继承列表中出现的先后次序保持一致)子类部分在下,注意:每个基类前都应该显式给出继承权限,否则就是默认的继承权限

3.菱形继承:单继承加多继承

3.1语法格式

class Person { public : string _name ; // 姓名 }; class Student : public Person { protected : int _num ; //学号 }; class Teacher : public Person { protected : int _id ; // 职工编号 }; class Assistant : public Student, public Teacher { protected : string _majorCourse ; // 主修课程 };

3.2存在的问题

二义性:基类中的一个属性被两个派生类继承,最终子类继承的时候会从这两个不同的派生类得到两个属性,造成歧义。

3.3解决方案

1.让访问明确化:在成员之前添加类名::    2.菱形虚拟继承,利用虚继承,关键字virtual。

4.虚拟继承

4.1怎么实现虚拟继承?

在继承权限前virtual

4.2与普通继承方式的区别?

1.虚继承的原理

虚继承用于解决多继承条件下的菱形继承问题(数据冗余、存在二义性)。 底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(也被称作虚基表,不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。 实际上,vbptr 指的是虚基类表指针(virtual base table pointer,也叫虚基表指针),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。说人话就是:BC的指针和A在同一块内存中,BC的偏移量不同,通过偏移量找到对应的A

图解;

2.虚基类的声明和语法形式

class 派生类名:virtual 继承方式 基类名

3.虚基类的注意事项

一个类可以在一个类族中用作虚基类,也可以用作非虚基类。 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的对象。 虚基类子对象是由最派生类(最后派生出来的类)的构造函数通过调用虚基类的构造函数进行初始化 (最派生类会先去调用虚基类的构造函数)。 最派生类是指在继承类结构中建立对象时所指定的类。 在派生类的构造函数的成员初始化列表中,必须列出对虚基类构造函数的调用,如果没有列出,则表示使用该虚基类的缺省构造函数。 在虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中,都要列出对虚基类构造函数的调用。但只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。 在一个成员初始化列表中,同时出现对虚基类和非虚基类构造函数的调用时,基类的构造函数先于非虚基类的构造函数执行。 虚基类并不是在声明基类时声明的,而是在声明派生类是,指定继承方式时声明的。因为一个基类可以在生成一个派生类作为虚基类,而在生成另一个派生类时不作为虚基类。

4.虚基表指针的存放位置

  虚基表指针是存放在数据段的   虚基表指针是放在对象的开头的5.补充说明

编译器会生成默认的成员函数----给对象前4个字节中填充虚基表的地址

基类成员是通过从虚基表中获取偏移量来访问的

5.菱形虚拟继承

上面子类对象模型,包括虚基表中的内容

1.11继承和组合 public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。 组合是一种has-a的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A对象。 优先使用对象组合而不是对象继承。 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语 “ 白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以 “ 黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。 2.多态 2.1什么是多态? 通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。 2.2多态的分类

1.静态多态||早绑定||静态绑定||静态联编

程序在编译时可以确定函数的具体行为,即:具体调用那个函数(函数重载,模板)

2.动态多态||晚绑定||动态绑定||动态联编

在程序运行时,根据基类的指针或者引用引用的具体的类对象,选择对应的虚函数进行调用,因为编译阶段,编译器无法知道基类的指针或者引用到底指向那个类的对象

2.3实现条件

1.必须在继承的体系下

2.子类必须对基类中的虚函数进行重写

3.虚函数调用:必须通过该基类的指针或者引用调用虚函数

4.多态体现:当程序运行时,根据基类的指针或者引用指向不同的类的对象,选择合适的虚函数进行调用

2.4重写

2.4.1什么是重写?

 虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的

返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。  

1.基类的成员函数必须是虚函数

2.子类虚函数原型必须要和基类的虚函数原型一直,实现不同

在重写基类虚函数时,派生类的虚函数在不加virtual 关键字时,虽然也可以构成重写 ( 因 为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.4.2特殊情况

1.协变

基类与派生类虚函数返回值类型不同, 派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

2.析构函数

基类与派生类析构函数的名字不同 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成 destructor 。

2.4.3 final 和 override

fifinal :修饰虚函数,表示该虚函数不能再被重写 override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

2.4.4函数重载 || 重定义 || 重写区别

2.5抽象类

2.5.1什么是抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

 

2.5.2特性

不能实例化对象

子类必须对抽象类中所有的纯虚函数进行重写,否则子类也是抽象类

2.5.3接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成 多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数 2.6C++实现多态的原理

2.6.1对象模型

类中包含有虚函数,编译器会给该类的对象多增加4个字节(32位系统),目的:存储虚函数表(虚表)的地址

2.6.2虚表中虚函数的存放方式

1.基类:按照虚函数在类中声明的先后次序依次添加到虚表中,本质:实际就是函数指针数组

2.子类虚表构造:

I:将基类虚表中内容拷贝一份放到子类的虚表中

II: 如果子类重写了某个基类虚函数,则使用子类虚函数的地址替换虚表中相同偏移量位置的基类虚函数地址

III: 子类新增加的虚函数按照其在虚表中声明的先后次序依次添加到虚表的最后

3.注意事项

同一个类的多个对象共享同一份虚表,即不同对象前4个字节中存放的虚表的地址是一样的

基类和子类不会共享虚表,虚表各自是各自的,即使虚表中内容完全相同

虚表是在编译时候生成好的

虚表和虚函数都是存放在代码段的 虚表存的是虚函数指针 对象中存的也不是虚表而是虚表指针

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

虚表实际上是在构造函数初始化列表阶段进行初始化的

拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

2.6.3虚函数调用原理

必须满足多态的条件,则多态按照一下方式处理:

1. 从基类的指针或者引用:指向的实际对象的前4个字节中获取虚表的地址

2. 传递该函数的参数 以及 this指针

3. 从虚表中找到对应的虚函数进行调用

构成多态,指向谁就调用谁的虚函数,跟对象有关。

不构成多态,对象类型是什么就调用谁的虚函数,跟类型有关。

如果出现菱形继承

 

 

2.7多态面试题



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3