继承 -------- 面向对象的三大特性之一
面向对象的三大特性:封装、继承、多态
封装:把数据和方法都封装在一起,想给你访问的变成共有,不想给访问的,写成私有。
继承:继承是类设计层次的复用
多态:
继承方式:
如果不写继承方式,class默认是私有继承,struct默认是公有继承(即继承的访问限定符)
protected/private:在类外面不能访问,在类里面可以访问
公有继承、保护继承、私有继承,总的来说就是取小的那个就对了
私有成员的意义:不想被子类继承的成员可以设计成私有。但是不代表子类没有继承私有
私有和保护的区别:基类中想给子类复用,但是又不想暴露直接访问的成员时,就应该定义成保护
继承中的作用域
C++定义以后会产生域,C语言是局部域,全局域,有些域会影响生命周期,有些域不会,
类域不影响生命周期,只是影响访问。父类和子类是可以定义同名变量的,子类访问时先访问的是子类的,如果就是想访问父类的,则要指定作用域
实际中尽量不要定义同名的成员。(父类和子类是在各自的作用域,不是同一作用域)。
如果同名,例如子类和父类都定义了name,子类会继承父类的name,会有两个name。这时会隐藏父类的。
以下程序,两个func的关系是什么?两个func构成函数重载嘛?
class a
{
public:
void func()
{};
}
class b : public a
{
public:
void func(int i)
{}
}
不构成重载,因为函数重载要求在同一作用域。这两个func构成隐藏关系(如果是成员函数的隐藏,只需要函数名相同就构成隐藏)。
注意:对象中不存储成员函数,只存储成员变量。
对象中不存储成员函数,只存储成员变量:继承后类的大小的改变:多出了父类的成员变量。父类和子类的成员函数还是在各自的代码段中的。
赋值兼容转换
公有继承的情况下:
子类对象可以赋值给父类对象/指针/引用,这个叫做赋值兼容转换,也叫切割或切片(这里虽然是不同类型,但是不是隐式类型转换)
我们之前讲的同类型变量可以赋值,不同类型间赋值要进行强制类型转换或隐式类型转换,
赋值时不用加ocnst就说明了它没有发生隐式类型转换(子类到父类的时候不是隐式类型转换)
这个过程也叫切割或切片
这里虽然是不同类型,但是不是隐式类型转换,也不是强制类型转换,这里算是一个特殊支持,语法天然支持的。因为person& rp = sobj;这句代码能通过就说明了它不是隐式类型转换,因为产生的临时变量具有常性,要实现转换的话,应该加const才可以,但是这里不加都可以
不是说切出来拿走,而是切出来拷贝给父类
但是下面的代码不需要加const就可以编译通过,所以这里不是隐式类型转换
父类对象给子类对象赋值是不行的,即使强制类型转换也不支持,父类是没有办法转回子类的(因为少了一部分东西)。如果是指针或引用的话是可以的,但是是很危险的。
父类指针看到的只是父类的那一部分,就像把它切出来 。
公有继承下,子类和父类是一个is-a的关系,也就是每一个子类对象都是一个特殊的父类对象
父类是转换不成子类的,但是父类的指针、引用可以转换为子类
我们现在写了父类和子类,那子类的六个默认成员函数怎么办呢?
派生类中的六个默认成员函数是怎么处理的,它相比普通类不一样的地方是:它的成员有两个部分,一个部分是自己的,一个部分是父类继承下来的
派生类如何初始化?
子类编译默认生成的构造函数会干什么?
1.对于自己的成员,和以前一样(和普通只定义一个类和对象一样,当成一个普通类就可以,即调用自己的构造函数、析构函数等等)
2.对于继承的父类成员:必须调用父类的构造函数初始化
如果父类没有默认构造呢?这时子类无法生成默认构造,因为它处理不了父类的成员,因为其必须得调用父类的默认构造处理,所以我们需要给子类写一个构造
应该这么写
不允许子类的构造函数初始化父类。要求是这样的:子类的构造函数只能初始化自己的成员,对于父类的成员,子类只能调用父类的构造函数处理
编译生成默认拷贝构造:
1.对于自己的成员:跟类和对象一样(对于内置类型值拷贝,对于自定义类型调用它的拷贝构造)
2.对于继承的父类成员:必须调用父类的拷贝构造初始化。
派生类不写都可以直接调用父类的拷贝构造。(都是自定义类型)
需要我们拿到子类对象中的父类对象。我们把子类对象传给父类对象的引用就构成了切片
也可以强制类型转换等等,不过没什么必要
注意:有深拷贝的时候才需要显式写S
切片很重要
编译器默认生成的operator=、析构都同上。写赋值时记得不能自己给自己赋值
栈溢出基本只有一种情况,就是无限递归了
自己的成员:构造函数、析构函数:内置类型不处理,自定义类型调用他的析构、构造
继承的成员:调用父类析构、构造函数处理
想显式写
析构函数比较特殊
子类的析构函数与父类的析构函数构成隐藏。
这样就可以了
但是我们其实不应该显示的写,因为子类会自动调用父类的析构函数。(为什么多次析构没问题呢,因为没做什么事情,如果析构函数里有对指针的释放,那么就会出问题了,因为对指针释放了两次)
子类和父类的析构函数名不同,为什么它两构成隐藏?
由于之后多态的需要,析构函数的名字会被同一处理成destructor()
先定义的先初始化,后定义的后初始化,先定义的后析构,后定义的先析构,因为要符合后进先出。父类的先构造,子类再构造,子类先析构,父类后析构。
编译器默认生成的赋值同上
如果我们自己显示写调用父类的析构函数的话,顺序就无法保证了,所以子类的析构函数中不需要显式调用父类的析构函数,因为每个子类析构函数后面,会自动调用父类析构函数,这样才能保证先析构子类,再析构父类
静态变量一定是不被包含在对象中的
子类继承父类的相同名字的变量后,我们实际访问的是子类的该变量,其类似于全局变量和局部变量访问时的就近原则。如果想访问父类的,可以指定作用域
为什么栈溢出?
因为这里递归调用了。一直在调用子类的。(子类和父类的operator=构成了隐藏关系)如果想调父类的, 需要我们指定一下
析构也是:编译器自动生成的析构函数,内置类型不处理,自定义类型调用他的析构
构造和析构可以看成一类,拷贝构造和赋值运算符重载可以看成一类
因为子类的析构函数跟父类析构函数构成隐藏。这里其实是找不到该函数(~Person)。
由于多态的需要,析构函数的名字会被统一处理成destructor()
子类对象不用显示调用父类 ,因为它会自动调用
取地址重载不用调用父类的,不用写成合成版本
继承与友元
友元关系不能被继承,也就是说基类的友元不能访问子类私有和保护成员
某个函数是父类的友元,父类被子类继承,不代表它是子类的友元,如果想访问,需要把该函数弄成子类的友元
继承与静态成员
整个继承体系里,只有一个静态成员
顾名思义
静态成员是存在静态区的,且基类与派生类访问的count是同一份
如果没有明确说明,一般都是说公有继承
统计有多少个人,在Person里++一个统计数值
C++11新增了一个final关键字,也叫最终类。
多继承,Derive继承了Base1和Base2,先继承的在前
p2大,因为在栈里
derive其实就是12个字节
单继承和多继承
单继承:一个子类只有一个直接父类时,称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况
Person这个信息在assistant里面就会有两份,两份会导致数据冗余,重复占用空间了,构造了两次,拷贝了两次。
二义性:assistant要访问name时,不知道要访问谁的
如何定义一个不能被继承的类:将父类的构造函数私有,这时子类对象实例化时,就会无法调用构造函数。
这样在继承时就会报错
内存中并没有存Base1、Base2,只是编译器在调试窗口方便我们看
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在assistant的对象中person成员会有两份
研究生助教,既是学生,也是老师。
有两份数据。
也属于菱形继承。为什么左边的virtual放在B而不是C?如右图,B在C里。第二十四节1:04:00
二义性,编译器不知道调谁的_name。
这样就可以了,但这样还是有时会造成空间浪费
如何解决上述问题?
虚继承(菱形虚拟继承)
在腰部位置加上virtual(它和后面讲的多态不同)
就可以这样了,这三种方式访问到的都是同一份
菱形虚拟继承解决了数据冗余和二义性
iostream菱形继承了istream和ostream。
以上就会有数据冗余和二义性的问题。
多继承:
菱形虚拟继承:
我们可以把中间虚继承了
这时就可以
我们可以看到,重复的成员被放到了同一地址,同一成员只有一份。只有这一个地址发生改变。
解决了数据冗余和二义性,但是看上去时变大了,并没有减少空间。真正多的是两个指针,多了8字节。那如果a很大呢,例如a是4000字节,多存一份不就是多40000字节,而如果是虚拟继承,只是多一个指针。
这12和20是距离也是偏移量。
偏移量的作用在于:以后要切片的时候,用指针地址加偏移量,就可以
在虚继承里,B对象有没有a,有,只是怎么存的问题。 但是实际计算B的大小是12。
我们发现除了1和2还有一个指针在这里 .公共a是放在最下面的(高地址处),它是怕遇到这种场景
当我们不知道这个B*的指针是指向B对象还是D对象。对象里只有一个指向表的指针,这个表叫虚基表,通过偏移量找虚基类。
父类有多个成员需要多个指针吗?不用,能找到第一个,就能找到第二个。它是把对象当成整体的
了解:第二十三节3:00:00-第二十四节00:24:00
如何定义一个不能被继承的类?
C++98:
1.父类构造函数私有(即子类不可见)
2.这样就会导致子类对象实例化时,无法调用构造函数(以此来间接实现该类不能被继承)
倒不是继承时会报错,而是实例化对象时会报错
而C++11做出了改进:
新增了关键字final,也叫最终类
这样就会直接报错说A不可以被继承,因为已经被声明成final
多态继承中的指针偏移:
对象的分布上:先继承的在前面
菱形继承二义性的解决:指定作用域
数据冗余的解决:虚继承(virtual)
菱形虚拟继承解决了二义性和数据冗余,还节省了空间(例如当父类的成员变量很大时)
选C第二十三节2:25:30
p2=p3!=p1
继承和组合
公有继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。例如:人和学生的关系(每一个学生都是人),植物和玫瑰花的关系(玫瑰花都是植物)。
什么是组合呢?是一种has-a的关系例如:头和眼睛的关系、轮胎和车
组合也是复用,相比较公有继承,组合的权限较小(能访问组合的公有,但是不能访问保护和私有)。实际中,哪个符合就用哪个。实际运用中,一些类型的关系既可以认为是is-a,也可以认为是has-a(既适合用继承,又适合用组合),这种情况下,优先使用对象组合,而不是类继承
组合:
低耦合,高内聚,所以优先使用组合。
组合是黑箱复用,继承是白箱复用
oj的测试用例就是白盒测试。软件工程提倡联系越少越好,也就是低耦合,互相影响小。
第二十四节00:54:00
白箱复用、黑箱复用。
黑盒测试:不知道你的实现,我以功能的角度去进行测试。(耦合度低)
白盒测试:我知道你的实现,针对你的实现去进行测试。(耦合度高)
继承耦合度高(依赖关系强)。我们提倡耦合度低,越低越好
UML图、类图
多态
多态:多种形态,具体点就是去完成某个行为,当不同对象去完成时会产生出不同的状态。
例如买票,学生、军人买票和普通人买票就不一样。学生、军人、普通人就是三种不同类型的对象。学生买到半价票,普通人买票,军人不用排队买票,他们产生出的状态不同
做同一件事,但是结果不同。
只有成员函数才能加virtual,加了virtual之后这个函数就叫做虚函数。只有成员函数才能是虚函数。
(这里的virtual和虚继承那里的不一样,就像delete也有两种作用一样,就像取地址和引用这种)
虚函数(下图buyticket)间的关系:重写(覆盖)
虚函数的重写/覆盖条件:虚函数函数名、参数、返回值类型相同
注:不符合重写,才是隐藏关系(缺省参数给多少/有没有缺省参数并不影响,当然,参数类型不同就不构成重写了,参数名不写也不行,(参数名不同呢?可以吗?))
多态有两个条件:
1.虚函数的重写
2.使用父类的指针或引用去调用虚函数。
这两个条件有一个不满足就构不成多态
这时和p的类型已经没有关系了,而是看其指向(引用)的对象
不构成多态,看类型,构成多态,看它指向的对象
在构成虚函数时,如果去掉父类,则不符合重写了,不是虚函数了
特例:
特例1:如果去掉子类的virtual,则还是虚函数(但去调父类,则不符合重写,就不是虚函数了),因为它认为是先把父类的虚函数继承下来,继承下来之后重写,重写是它的实现,所以虽然子类中我们没写virtual,但是还是认为是它的实现(子类虚函数不加virtual,依旧构成重写)。实际中最好加上virtual。
特例2:重写的协变。返回值可以不同,但是要求返回值必须是父子关系的指针或者引用。不然会报错。子类返回的是父类的话也不可以,会报错。
随便这样写也可以。第二十四节2:00:00
选B,首先,test不是多态,test的this指针是A类型的,this指针调用func,
虚函数重写是一个接口继承(接口就是把函数的函数名参数返回值等架子拿下来),普通函数继承是实现继承(主要继承的是你的实现)。
接口继承是重写实现。第二十四节2:24:00
1:56:20不存在要用子类演示的场景,因为子类传不过去
去掉父类的virtual就肯定不符合重写了
带虚函数对象的大小的计算
考察的不是内存对齐,考察的是多态,因为该对象里会多一个指针,这个指针叫虚表指针(vftptr虚函数指针),virtual function,(vftable虚函数表),表:能存储多个数据的一个东西。 其实是用了一个数组把虚函数的地址存到虚函数表里。虚函数会把它的地址放到虚函数表。多态的原理的关键:。虚函数会存进虚表,对象里没有虚表,有的是一个虚表的指针。虚函数重写以后呢?
虚函数指针是一个函数指针数组
多态调用:程序运行时,去指向对象的虚表中找到函数的地址,进行调用
普通函数调用:在编译(编译时就有函数的地址)或链接(在符号表里找)时确定(去找)函数的地址,运行时直接调用
多态的原理:虚表(虚函数表)
多态的条件:1.必须完成虚函数的重写2.必须是父类指针或引用去调用虚函数。才能有多态的效果
只要不符合条件,就是直接调用。
看看构成多态和不构成多态调用的区别:
虚函数是被存在哪里的呢?
虚函数不是存在虚表里的,任何函数都是编译好了变好指令,放在公共代码段的。我们写的代码编译好之后的这个文件,是一个可执行程序,程序运行起来之后变成一个进程,进程分段把我们的代码指令加到代码段里去了。
那么虚表里面放的是什么呢?
虚表里实际上放的只是函数的地址,
C++是怎么考量这种行为的呢?
首先,虚函数是存在公共代码段的。
函数的地址是什么?可能是第一句指令的地址。
虚表里放的只是函数的地址。
父类对象和子类对象不是指向同一个虚表,因为这个虚表被重写了(即被覆盖了),是一个子类的虚函数。虚函数表本质是一个函数指针的数组。
构成多态的调用:第二十五节00:13:00
因为是一个引用,如果是父类对象的引用,找到的就是父类的虚表,如果00:14:00
构成多态的调用,运行时到指向对象虚表中找调用虚函数地址,所以p指向谁,调用谁的虚函数
不构成多态调用,属于普通调用,编译时确定调用函数的地址。
虚函数重写的要求:1.要求父类子类都是虚函数(子类可以不写virtual)2.要求函数名、参数、返回值都相同
如果父类有虚函数,子类没有虚函数重写,那么编译器调用时是编译时决议还是运行时决议?
是运行时决议,编译器只是检查父类有没有虚函数,并且是引用,那么久直接去虚表里找。父类的虚表和子类的虚表在这种情况下,都是同一个虚函数都是父类的虚函数。
有些地方会要求析构函数加virtual。
析构函数是否建议加virtual呢?
在继承中,建议把析构函数定义成虚函数。
加了virtual以后,子类和父类能构成虚函数的重写(子类重写了父类)。普通场景析构函数是不是虚函数都没问题,但是当用一个指针接收new出来的子类对象时,再去delete这个指针。同理来一个new出来的父类。不加virtual时,new的是子类对象,但是调用的会是父类的析构函数,符合多态按照多态去调,不符合就按编译决议。00:32:15
析构函数函数名不同,为什么构成重写呢?
因为析构函数会被处理成destructor,所以加了virtual是完成虚函数重写的。
子类析构函数重写父类析构函数,才能正确调用。指向父类调用父类析构函数。指向子类调用子类析构函数
final:去修饰一个类,这个类就不能被继承。或如果一个虚函数不想被重写,就可以在虚函数后面加一个final。virtual void person() final{}
override:C++11新增关键字。只能加在子类,加在子类去检查子类的虚函数是否完成重写,没有完成重写就会报错
重载、重写(覆盖)、隐藏(重定义)的区别是什么?
相同点:它们都是函数之间的关系。
重载:要求1必须在同一作用域。要求2函数名相同,参数不同(类型、顺序、个数,有两种特殊情况)
重写:要求1两个函数分别在基类和派生类的作用域。要求2三同(函数名、参数、返回值,协变例外)要求3两个函数都必须是虚函数。有时子类虽然不写,但是也算是虚函数。
重定义(隐藏):要求1:两个函数分别在基类和派生类的作用域。要求2函数名相同。要求3两个基类和派生类的同名函数不构成重写就是重定义
多态的原理
person p1; person p2;
共用一个虚表还是各自用各自的虚表呢?
共用一个虚表。如果各自用各自的,不就浪费了,p1和p2的虚表指针都是相同的。
同一个类型的对象共用一个虚表。
student s1; student s2;
子类用的是子类的虚表。
重写了,父类是父类的虚表,子类是子类的虚表。
如果没有完成重写,用的是不是同一个虚表呢?没有完成重写,那父类的虚表里是父类的虚函数,子类的虚表里也是父类的虚函数。是不是同一个虚表呢?不是。虽然里面的内容是一样的。
在vs下,不管是否完成重写,子类的虚表跟父类的虚表都不是同一个。(和编译器有关),如果是一个,也是有可能的。
1.派生类只有一个虚表指针,因为派生类是继承的父类,父类里面有一个虚表指针,它就不会再生成虚表指针了。所以单继承的派生类里面也就只有一个虚表指针了。
2.重写为什么也叫作覆盖呢?
覆盖是原理层的叫法。子类的虚表是把父类的虚表拷贝下来,重写的虚函数那个位置会被覆盖成子类的虚函数,重写是语法层的叫法,覆盖是原理层的叫法。
不管我子类是不是重写,如果我子类自己还有虚函数呢?
比如说里面还有一个没有重写的虚函数,这个虚函数放哪呢?
记住,只要是虚函数,虚函数的地址都会放进虚表。
有些编译器(例如vs下)就无法看到子类中自己单独写的虚函数。这并不代表其不进虚表。它是进虚表的。如果非要看,通过内存勉强能看到。也可以通过
虚函数表是一个函数指针数组,我们打印数组里的内容就可以。
注意函数指针取别名时:typedef void(*别名)();
取虚表的对象:
*(int*)&s1
新
虚表指针,可以通过监视窗口看到。其存在虚表,虚表是一个函数指针数组,用来存放虚函数指针
为什么不放在对象里,而是放在虚表里呢?
一个对象可能会有多个虚函数,多个虚表指针
虚函数指针会放在虚表,重写以后,父类里会有一个虚表
子类中会有父类成员,子类与父类的虚表存的内容不完全一样,因为有的部分被重写了
普通函数不用去找地址了,直接调用。
虚函数存在哪里?
不是在虚表里,
任何函数都是编译好了变成指令,放在公共代码段的。
虚表里放得只是函数的地址,虚函数还是放在公共代码段的。
父类对象与子类对象指向的虚表不同
第25节00:24:00
子类没有函数,只有父类有虚函数,是否构成多态?
析构函数建议加virtual,不同析构函数名不同,为什么构成重写?
因为析构函数统一被处理成destructor,所以可以认为她们函数名相同,这样处理的目的就是让他们构成重写
为什么要求构成重写?00:33:00因为可能出现这种错误
建议在继承中,析构函数定义成虚函数
单继承情况下只有一个虚表
override加在子类的虚函数花括号前,检查子类虚函数是否完成重写
抽象类
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化(即不能定义)出对象。派生类继承后规范了派生类必须重写,另外,纯虚函数更体现出了接口继承。
如果不想让一个对象实例化,就把它定义成抽象类
纯虚函数:在虚函数的后面写上=0,则这个函数为纯虚函数。
纯虚函数是不需要实现的,想实现也可以。(可以有函数体,也可以没有函数体)
override是检查重写
抽象类是强制重写
父类无法实例化时,子类也无法实例化,因为子类继承了父类,继承之后子类里也有了纯虚函数,有纯虚函数,如果子类不重写,子类也是抽象类,不合理,可以说是间接强制了子类必须去重写,不然子类会无法实例化出对象。
什么是接口?
两人进行配合,通过一个东西来使得互相写的可以调用。这个东西就叫接口
接口继承和实现继承
虚函数的实现是接口继承。普通继承是实现继承。