目录
2.3、无名对象和具名对象在继承关系中的差别:(主要在保护属性上的差别)
3.2、构造函数、析构函数、拷贝构造、赋值函数都不具有继承性
第一课:继承和派生
1、面向对象的三大特征:
封装、继承和派生、多态性
封装:
封装:(Encapsulation)是面向对象程序设计最基本的特性,把数据(属性)和函数(方法,操作)合成一个整体,这在计算机世界中是用类与对象实现的。
继承和派生:(继承和派生一体两面)
继承(inheritance)机制:是类型层次结构设计中实现代码的复用重要手段。
派生:保持原有类特性的基础上进行扩展,增加新属性和新方法,从而产生新的类型。
在面向对象程序设计中,继承和派生是构造出新类型的过程。呈现类型设计的层次结构,体现了程序设计人员对现实世界由简单到复杂的认识过程。
2、继承的概念与定义
层次概念是计算机的重要概念:
C++通过类派生( class derivation)的机制来支持继承。被继承的类称为基类(base class)或超类(superclass),新产生的类为派生类(derived class)或子类(subclass)。基类和派生类的集合称作类继承层次结构(hierarchy)。
Base对象的时候,会先设计基类对象Objiect;
在析构的时候,会先析构掉派生类Base对象,再析构基类Object对象
2.1、思考:sruct和class的继承时候的区别?
不添加访问限定符,默认继承方式是怎样呢?
派生类是class,默认都是私有继承
2.2、两层继承关系:
谈到继承关系,首先要想到对象,根据对象进行分析
先说以下三种方式继承方式的总结:
无论是哪种继承方式,派生类的成员函数都是可以访问基类中无名对象的公有和保护属性,而基类的私有属性是不可被访问的
前提:派生类是公有继承
在公有继承关系中,基类的(无名)对象的【私有属性】不能被派生类对象的成员函数 b.fun()访问。基类对象的公有和保护属性是可以被派生类对象访问的。派生类对象可以访问自己的公有、私有、保护
在继承关系中【保护属性】是可以看作是【公有属性】
前提:派生类是私有继承
在私有继承关系中,基类的(无名)对象的保护属性和公有属性也是可以被派生类的成员函数进行访问的,但是基类的私有属性不可被访问
也肯定可访问自己的私有、公有和保护
前提:派生类是保护继承
在保护继承关系中,基类的(无名)对象的保护属性和公有属性也是可以被派生类的成员函数进行访问的,但是基类的私有属性不可被访问
2.3、无名对象和具名对象在继承关系中的差别:(主要在保护属性上的差别)
无名对象的公有、保护可被访问,私有属性不可被访问;
具名对象的公有属性可被访问,私有和保护不可被访问。
此处aa到底是在公有块中、私有块中,还是保护块中,这是无关的,具名对象都只能访问公有属性。
例题:
注意外部函数和成员函数的区别:
上方代码是公有继承;
如果是私有或保护继承的话,外部函数中b对象就无法访问保护和私有成员。那无名对象A就成了私有属性对象,b.az也是不能访问的,aa对象是b对象的公有成员对象,可访问其公有属性b.aa.az
2.4、三层继承关系:
前提:B私有继承A、C公有继承B
C的成员函数fun可以访问自己的私有、公有和保护;也可以访问B的公有和保护;由于无名对象A是B中的私有成员,所以C的fun不能访问B里面的私有对象A的各类属性
2.5、同名隐藏
就近原则进行访问,隐藏父对象中的同名数据。在继承关系中,基类和派生类中存在同名数据的时候,只访问派生类中的数据,隐藏基类中的同名数据
可通过 “ 访问限定符 ” 来访问隐藏的数据
同名隐藏属性:
同名隐藏函数:
注意:此处不存在函数重载
3、讨论公有继承“是一个”
3.1、赋值兼容规则:
C++面向对象编程中一条重要的规则是:公有继承意味着 "是一个"。一定要牢牢记住这条规则。在任何需要基类对象的地方都可以用公有派生类的对象来代替,这条规则称赋值兼容规则。它包括以下情况:
1、派生类的对象可以赋值给基类的对象,这时是把派生类对象中从对应基类中继承来的隐藏对象赋值给基类对象。反过来不行,因为派生类的新成员无值可赋。
2、可以将一个派生类的对象的地址赋给其基类的指针变量,但只能通过这个指针访问派生类中由基类继承来的隐藏对象,不能访问派生类中的新成员。同样也不能反过来做。
3、派生类对象可以初始化基类的引用。引用是别名,但这个别名只能包含派生类对象中的由基类继承来的隐藏对象。
理解:学生是一个人,所以学生对象可以赋值给人
obja = base;这个过程会将base对象中,包含的Object部分的无名对象,进行切片赋值给obja这个对象;
Object & objb = base;
Object * objc = &base;
以上这三种赋值,都只是取出派生类对象中的无名对象部分,赋值给了基类
3.2、构造函数、析构函数、拷贝构造、赋值函数都不具有继承性
构造和析构
多留意派生类对象在构建的时候,基类对象里面有没有缺省的构造函数
构建顺序是:先构建基类对象,再按照成员声明顺序进行构建,最后构建派生类对象;
析构顺序是:先析构派生类对象,再析构成员对象,最后析构基类对象
拷贝构造
注意:调动父对象的拷贝构造,要写在初始化列表处,不能写在函数体内部
3.3、面试问题
1、如何实现一个不可以被继承的类?
由于派生类对象的构造,要先构造基类部分,而且派生类可以继承基类的 private 成员,
但是却不能访问,因此可以把基类的构造函数实现成 private 私有的构造函数,那么任何一个派生类在继承这个基类的时候,都会由于无法调用基类的私有构造函数,而导致这个基类不能被任何派生类继承。
2、实现一个能被继承的类型,但是不能在部函数中创建对象的类型
在基类的构造函数中,把构造函数实现成protect属性
在继承关系中,保护属性倾向于公有;在外部函数中,保护属性倾向于私有
3、实现一个不能被继承的类型。但是能在部函数中创建对象的类型
在C98、C99标准中不能实现,在C11标准中可以,C11标准中引入【final】关键字,放在类名之后,就不能被继承
4、如何实现一个类定义的对象不能进行拷贝构造和赋值呢?
你可能给出的第一个答案就是,把这个类的拷贝构造 和 operator=赋值函数私有化,这样做完全可以,但是对于每一个类都得进行更改,代码修改量比较大,那么请问有没有更简单的办法呢?当然有,可以实现一个基类,把这个基类的拷贝构造和 operator=赋值函数私有化,那么任何从这个基类继承的派生类,都不能够拷贝构造和 operator=了
第三课 多重继承与派生类成员标识
1、多重继承或多继承
两个不同基类指针指向同一个派生类对象地址时候,两个基类指针所指向的地址实际不一样,各自指向自己对应的内容
引申:
在多重继承时候,只能把派生类对象给到基类对象,或者把派生类对象地址给基类对象指针,反之是不行的
static_cast和dynamic_cast详解_ShyHerry的博客-CSDN博客_dynamic_cast
2、数据冗余带来的函数二义性问题
多重继承、多继承带来的二义性问题,要给出限定符、或者使用基类对象的指针来调动fun函数,可以避免二义性
3、菱形继承
也会存在数据冗余,会出现二义性
最终产生的es对象中存在两个不同的 _idperson,顺着两条路径,会有两个不同的身份证号。数据冗余的解决办法就是通过虚基类(虚基类:继承方式是虚继承)
两个身份证号显然是不合理的。可以把class Person这个共同基类设置为虚基类,这样从不同
路径继承来的同名数据成员在内存中就只有一个拷贝,同名函数也只有一种映射。
还有菱形继承中,派生类对象地址给到基类指针的时候,要说明路径
4、虚基类(面试)
虚继承的目的:是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类;
“指针”——>偏移量,消除冗余
理解的时候,说是用指针指向Person的内容;
实际上,在不同编译器中采取不同方式,这里通用情况下是偏移量的形式,在原本存放Person对象内容的位置存放偏移值,偏移到Person的_id位置的值
因此,最终Person的_id就只有一份
虚基类的菱形继承的方式下:
注意构建es的时候,最先构建Person基类对象,然后再按照顺序分别构建Employee和Student对象
而且在构建好Person的时候,构建Employee和Student对象前,偏移量的值就已经给好了
第4课 多态和虚函数
1、多态
多态性是面向对象程序设计的关键技术之一。若程序设计语言不支持多态性,不能称为面向对象的语言。
多态性(polymorphism)是考虑在不同层次的类中,以及在同一类中,同名的成员函数之间的关系问题。
函数的重载,运算符的重载,属于编译时的多态性。
以类的虚成员函数为基础的运行时的多态性,是面向对象程序设计的标志性特征。体现了类推和比喻的思想方法。
2、编译时的多态和运行时的多态
运行时的多态必须满足:公有继承、虚函数、通过指针或者引用调用虚函数三个条件。有一个条件不满足就退化成编译时的多态。用对象调动虚函数不具有多态性
编译时多态我们称为早期绑定;运行时多态称为晚绑定
3、运行时多态的设计思想
对于相关的类型,确定它们之间的一些共同特征(属性和方法),将共同特征被转移到基类中,然后在基类中,把这些共同的函数或方法声明为公有的虚函数接口。然后使用派生类继承基类,并且在派生类中重写这些虚函数(重写的时候要:同返回类型、同函数名、同参数列表),以完成具体的功能。这种设计使得共性很清楚,避免了代码重复,将来容易增强功能,并易于长期维护。
客户端的代码(操作函数)通过基类的引用或指针来指向这些派生类型对象,对虚函数的调用会自动绑定到派生类对象上重写的虚函数。
4、定义虚函数的规则
类的成员函数定义为虚函数,但必须注意以下几条:
1、三同:派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名隐藏,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
同名隐藏这里注意:得是同返回类型、同函数名、不同参数列表。返回值不同则不是同名隐藏。
同名覆盖存在于多态中。
如果基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
注意调动虚函数时候,需要用指针或者引用来调动虚函数
2、只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
3、静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。(第2、3点总结:没有this指针的函数就不能作为虚函数)
4、内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。(inline只是一种建议,是否采取内联方式由编译器决定。实际上也是可以的,virtual是强制性的)
5、构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是在初始化成员之前设置虚表指针
6、析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
7、实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。(这句话应该修改为:必须使用引用或指针指向虚函数才能实现运行时多态,但并不一定是基类指针或引用指向派生类对象,也可以是基类指针指向基类对象,派生类指针指向派生类对象)
8、在运行时的多态,函数执行速度要稍慢一些:为了实现多态性。每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
9、如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。
正确的定义必须不包括virtual。
6、多态的原理(面试)
虚函数指针表简称虚表,虚表就是虚函数指针的集合,虚函数指针表本质是一个存储虚函数指针的指针数组,这个数组的首元素之上存储RTTI(运行时类型识别信息的指针),从数组下标0开始依次存储虚函数地址,最后面放了一个nullptr。
在定义Object obj对象的时候,先进入构造函数,设置好this指针,然后再设置好虚表
虚表是在.data区设置的,基类Object中的字节大小包含虚表指针4个字节、属性值value4个字节,一共8个字节;Base类中包含基对象Object8个字节,以及自身的属性值num4个字节,一共12个字节;Test类中包含Base对象12个字节,以及属性值count4个字节,一共16个字节
此处op不能指向show函数,因为Object中没有写过show函数,这个op受制于定义它的类型,它的类型中存在的函数,才能用op指向并显示出来。其他op指向的函数调动具体看派生类中是否有重写过,如果重写过,那op调动的就是派生类中重写了的虚函数,否则就调动原本基类中的虚函数
7、在运行时多态中,虚表是如何调用的?
汇编代码:
edx指向对应对象的虚表地址,然后通过edx+偏移量来调动edx所指之物,即所指的方法函数地址
8、静态联编和动态联编
联编是指计算机程序彼此关联的过程,是把一个标识符名和一个存储地址联系在一起的过程,也就是把一条消息(函数的调用)和函数的入口地址相结合的过程,和一个对象的操作(方法)相结合的过程。
自我理解:函数在调用时,调动已经定义好的函数对应的地址,而编译的时候函数名就没有了
静态联编(static binding)早绑定:
静态联编是指在编译和链接阶段,就将函数实现和函数调用关联起来。
C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。C++语言中,函数重载和函数模板也是静态联编。
如果使用对象名加点" . "成员选择运算符,去调用对象虚函数,则被调用的虚函数是在编译时确定的(称为静态联编)
动态联编(dynamic binding)亦称滞后联编(late binding)或晚绑定:
动态联编是指在程序执行的时候才将函数实现和函数调用关联起来。
C++语言中,使用类类型指针或引用调用虚函数(成员选择符用箭头号" -> "),则程序动态地(运行时)选择虚函数,称为动态联编。
示例1:
汇编代码:
示例2:
示例3:在构造和析构中调动虚函数是静态联编,而不是动态联编(优化)
构造和拷贝构造是设置虚表指针;析构是重置虚表指针
示例4:在含有虚函数的类中使用menset一定要谨慎
9、new、delete
10、虚析构函数
11、纯虚函数
纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:定义一个基类时,会遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式为:
virtual 返回类型 函数名(参数表) = 0;
“ =0 "表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明;函数的声明是为了在虚函数表中保留一个位置。“ =0 "本质上是将指向函数体的指针定义为nullptr(vs2019编译器会让虚表指针指向一个虚函数,当需要实例化一个对象的时候,这个虚函数就会返回异常)。
抽象类:
含有纯虚函数的基类是不能用来定义对象的。
纯虚函数没有实现部分,不能产生对象,所以含有纯虚函数的类是抽象类。
带有纯虚函数的类称为抽象类。抽象类是一种特殊的类,它是为了抽象和设计的目的而建立的,它处于继承层次结构的较上层(基类)。抽象类是不能定义对象的,在实际中为了强调一个类是抽象类,可将该类的构造函数说明为保护的访问控制权限。
抽象类的主要作用:
是将有关的组织在一个继承层次结构中,由它来为它们提供一个公共的根,相关的子类是从这个根派生出来的。
抽象类刻画了一组子类的操作接口的通用语义,这些语义也传给子类。一般而言,抽象类只描述这组子类共同的操作接口,而完整的实现留给子类。
抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类了。
抽象类的规定:
(1)抽象类只能用作其他类的基类,不能建立抽象类对象。
(2)抽象类不能用作参数类型、函数返回类型或显式转换的类型。
(3)可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类,进而实现多态性。
类的类型概念:
接口继承和实现继承
公有继承的概念看起来很简单,进一步分析,会发现它由两个可分的部分组成:函数接口的继承和函数实现的继承。
为类的设计者:
有时希望派生类只继承成员函数的接口(声明),纯虚函数;
有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现,虚函数。
有时则希望同时继承接口和实现,并且不允许派生类改写任何东西,非虚函数。
工厂模式
简单工厂模式:输入类型名,创建对应类型的对象
C11的final和override关键字