Cpp7 — 继承和多态

继承

1.基本概念

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

2.继承方式

如果不写继承方式,class默认是私有继承,struct默认是公有继承

protected/private:在类外面不能访问,在类里面可以访问

公有继承、保护继承、私有继承,总的来说就是取小的那个就对了

私有成员的意义:不想被子类继承的成员可以设计成私有。但是不代表子类没有继承私有

私有和保护的区别:基类中想给子类复用,但是又不想暴露直接访问的成员时,就应该定义成保护

基类私有成员无论以什么方式继承都是不可见的。这里的不可见指的是基类的私有成员还是被继承到了派生类对象,但是派生类不管在类内还是类外都访问不了它了。

继承中的作用域

C++定义以后会产生域,C语言是局部域,全局域,有些域会影响生命周期,有些域不会,

类域不影响生命周期,只是影响访问。父类和子类是可以定义同名变量的,子类访问时先访问的是子类的,如果就是想访问父类的,则要指定作用域

实际中尽量不要定义同名的成员。(父类和子类是在各自的作用域,不是同一作用域)。

如果同名,例如子类和父类都定义了name,子类会继承父类的name,会有两个name。这时会隐藏父类的。如果想使用父类的,需要指定作用域。

1.在继承体系中,基类和派生类都有独立的作用域。

2.子类和父类中有同名成员,成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义(如果想在子类成员函数中使用基类成员,可以基类::基类成员以此显示访问

3.对于成员函数,只要函数名相同,就构成隐藏。

以下程序,两个func的关系是什么?两个func构成函数重载嘛?

class A
{
public:
    void func()
    {
        ;
    }
};

class B : public A
{
public:
    void func(int n)
    {
        ;
    }
};

不构成重载,因为函数重载要求在同一作用域。

对象中不存储成员函数,只存储成员变量。

继承后类的大小的改变:多出了父类的成员变量。父类和子类的成员函数还是在各自的代码段中的。

2.赋值兼容转换

公有继承情况下:

子类对象可以赋值给父类的对象/指针/引用,这个叫做赋值兼容转换,也叫切割或切片(虽然是不同类型,但不是隐式类型转换)

我们之前讲的同类型变量可以赋值,不同类型间赋值要进行强制类型转换或隐式类型转换,

赋值时不用加const就说明了没有发生隐式类型转换。

person& rp = sobj;这句代码能通过就说明了它不是隐式类型转换,因为产生的临时变量具有常性,要实现转换的话,应该加const才可以,但是这里不加却可以

不是说切出来拿走,而是切出来拷贝给父类

但是下面的代码不需要加const就可以编译通过,所以这里不是隐式类型转换

父类对象给子类对象赋值是不行的,即使强制类型转换也不支持,因为少了一部分东西。如果是指针或引用的话是可以的,但是是很危险的,存在越界访问的问题。

派生类的默认成员函数

我们现在写了父类和子类,那子类的六个默认成员函数怎么办呢?

派生类如何初始化?

子类编译默认生成的构造函数会干什么?

1.对于自己的成员,和以前一样(和普通只定义一个类和对象一样,当成一个普通类就可以,即调用自己的构造函数、析构函数等等)

2.对于继承的父类成员:必须调用父类的构造函数初始化

如果父类没有默认构造呢?这时子类无法生成默认构造,因为它处理不了父类的成员,因为其必须得调用父类的默认构造处理,所以我们需要给子类写一个构造

应该这么写

 

不允许子类的构造函数初始化父类。要求是这样的:子类的构造函数只能初始化自己的成员,对于父类的成员,子类只能调用父类的构造函数处理

编译生成默认拷贝构造:

1.对于自己的成员:跟类和对象一样(对于内置类型值拷贝,对于自定义类型调用它的拷贝构造)

2.对于继承的父类成员:必须调用父类的拷贝构造初始化。

 

派生类不写都可以直接调用父类的拷贝构造。(都是自定义类型) 

 需要我们拿到子类对象中的父类对象。我们把子类对象传给父类对象的引用就构成了切片

也可以强制类型转换等等,不过没什么必要 

注意:有深拷贝的时候才需要显式写S

切片很重要

编译器默认生成的operator=、析构都同上。写赋值时记得不能自己给自己赋值(子类和父类的operator=会构成隐藏的)

栈溢出基本只有一种情况,就是无限递归了

自己的成员:构造函数、析构函数:内置类型不处理,自定义类型调用他的析构、构造

继承的成员:调用父类析构、构造函数处理

想显式写

析构函数比较特殊

子类的析构函数与父类的析构函数构成隐藏。

 这样就可以了

 但是我们其实不应该显示的写,因为子类会自动调用父类的析构函数。(为什么多次析构没问题呢,因为没做什么事情,如果析构函数里有对指针的释放,那么就会出问题了,因为对指针释放了两次)

 子类和父类的析构函数名不同,为什么它两构成隐藏?

由于之后多态的需要,析构函数的名字会被同一处理成destructor()

先定义的先初始化,后定义的后初始化,先定义的后析构,后定义的先析构,因为要符合后进先出。父类的先构造,子类再构造,子类先析构,父类后析构。

编译器默认生成的赋值同上

如果我们自己显示写调用父类的析构函数的话,顺序就无法保证了,所以子类的析构函数中不需要显式调用父类的析构函数,因为每个子类析构函数后面,会自动调用父类析构函数,这样才能保证先析构子类,再析构父类

静态变量一定是不被包含在对象中的

子类继承父类的相同名字的变量后,我们实际访问的是子类的该变量,其类似于全局变量和局部变量访问时的就近原则。如果想访问父类的,可以指定作用域

 

 为什么栈溢出?

因为这里递归调用了。一直在调用子类的。(子类和父类的operator=构成了隐藏关系)如果想调父类的, 需要我们指定一下

析构也是:编译器自动生成的析构函数,内置类型不处理,自定义类型调用他的析构

 构造和析构可以看成一类,拷贝构造和赋值运算符重载可以看成一类

 

 因为子类的析构函数跟父类析构函数构成隐藏。这里其实是找不到该函数(~Person)。

由于多态的需要,析构函数的名字会被统一处理成destructor()

子类对象不用显示调用父类 ,因为它会自动调用

取地址重载不用调用父类的,不用写成合成版本

继承与友元

友元关系不能被继承,也就是说基类的友元不能访问子类私有和保护成员

某个函数是父类的友元,父类被子类继承,不代表它是子类的友元,如果想访问,需要把该函数弄成子类的友元

继承与静态成员

整个继承体系里,只有一个静态成员

顾名思义

 静态成员是存在静态区的,且基类与派生类访问的count是同一份

如果没有明确说明,一般都是说公有继承

 统计有多少个人,在Person里++一个统计数值

多继承,Derive继承了Base1和Base2,先继承的在前

 

 

p2大,因为在栈里

derive其实就是12个字节 

单继承和多继承

单继承:一个子类只有一个直接父类时,称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况

Person这个信息在assistant里面就会有两份,两份会导致数据冗余,重复占用空间了,构造了两次,拷贝了两次。

二义性:assistant要访问name时,不知道要访问谁的。

如何定义一个不能被继承的类:C++98:将父类的构造函数私有,这时子类对象实例化时,就会无法调用构造函数。

这样在继承时就会报错 

C++11新增了一个final关键字,也叫最终类。

内存中并没有存Base1、Base2,只是编译器在调试窗口方便我们看 

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在assistant的对象中person成员会有两份

 研究生助教,既是学生,也是老师。

有两份数据。

二义性,编译器不知道调谁的_name。

指定作用域就可以了,但这样还是有时会造成空间浪费。

也属于菱形继承。为什么左边的virtual放在B而不是C?

如何解决上述问题?

虚继承(菱形虚拟继承)

在腰部位置加上virtual(它和后面讲的多态不同)

 

 就可以这样了,这三种方式访问到的都是同一份

 菱形虚拟继承解决了数据冗余和二义性

iostream菱形继承了istream和ostream。

多继承的例子:

以上就会有数据冗余和二义性的问题。

多继承:

菱形虚拟继承:

我们可以把中间虚继承了

这时就可以 

我们可以看到,重复的成员被放到了同一地址,同一成员只有一份。只有这一个地址发生改变。 重复的成员放到了公共的地方。

解决了数据冗余和二义性,但是看上去时变大了,并没有减少空间。真正多的是两个指针,多了8字节。那如果a很大呢,例如a是4000字节,多存一份不就是多40000字节,而如果是虚拟继承,只是多一个指针。

 

这多出来的数据实际代表的是偏移量十六进制(这12和20是距离也是偏移量)。该数据+偏移量得到的就是_a。

偏移量的作用在于:以后要切片的时候,用指针地址加偏移量,就可以找到继承的被重复的数据

有数据冗余和二义性

 在虚继承里,B对象有_a,只是怎么存的问题。 但是实际计算B的大小是12。

我们发现除了1和2还有一个指针(这个指针指向一个表,表里存的是偏移量。为什么不直接把偏移量存在这里呢?怕遇到下面这样的场景)在这里 .公共a是放在最下面的(高地址处),它是怕遇到这种场景

但我们不知道这个B*的指针是指向B对象还是D对象。对象里只有一个指向表的指针,这个表叫虚基表,通过偏移量找虚基类。

父类有多个成员需要多个指针吗?不用,能找到第一个,就能找到第二个。它是把对象当成整体的

如何定义一个不能被继承的类?

C++98:

1.父类构造函数私有(即子类不可见)

2.这样就会导致子类对象实例化时,无法调用构造函数(以此来间接实现该类不能被继承)

倒不是继承时会报错,而是实例化对象时会报错 

 而C++11做出了改进:

新增了关键字final,也叫最终类

这样就会直接报错说A不可以被继承,因为已经被声明成final

多态继承中的指针偏移:

对象的分布上:先继承的在前面

菱形继承二义性的解决:指定作用域

数据冗余的解决:虚继承(virtual)

菱形虚拟继承解决了二义性和数据冗余,还节省了空间(例如当父类的成员变量很大时)

选C

p2=p3!=p1 

 

继承和组合

公有继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。例如:人和学生的关系(每一个学生都是人),植物和玫瑰花的关系(玫瑰花都是植物)。

什么是组合呢?是一种has-a的关系例如:头和眼睛的关系、轮胎和车

组合也是复用,相比较公有继承,组合的权限较小(能访问组合的公有,但是不能访问保护和私有)。实际中,哪个符合就用哪个。实际运用中,一些类型的关系既可以认为是is-a,也可以认为是has-a(既适合用继承,又适合用组合),这种情况下,优先使用对象组合,而不是类继承

组合:

低耦合,高内聚,所以优先使用组合。

组合是黑箱复用,继承是白箱复用

oj的测试用例就是白盒测试。软件工程提倡联系越少越好,也就是低耦合,互相影响小。

白箱复用、黑箱复用。

黑盒测试:不知道你的实现,我以功能的角度去进行测试。(耦合度低)

白盒测试:我知道你的实现,针对你的实现去进行测试。(耦合度高)

继承耦合度高(依赖关系强)。我们提倡耦合度低,越低越好

UML图、类图

多态

多态:多种形态,具体点就是去完成某个行为,当不同对象去完成时会产生出不同的状态。

例如买票,学生、军人买票和普通人买票就不一样。学生、军人、普通人就是三种不同类型的对象。学生买到半价票,普通人买票,军人不用排队买票,他们产生出的状态不同

做同一件事,但是结果不同。

只有成员函数才能加virtual,加了virtual之后这个函数就叫做虚函数。只有成员函数才能是虚函数。

(这里的virtual和虚继承那里的不一样,就像delete也有两种作用一样,就像取地址和引用这种)

虚函数(下图buyticket)间的关系:重写(覆盖)

 虚函数的重写/覆盖条件:虚函数函数名、参数、返回值类型相同

注:不符合重写,才是隐藏关系(缺省参数给多少/有没有缺省参数并不影响,当然,参数类型不同就不构成重写了,参数名不写也不行,(参数名不同呢?可以吗?))

多态有两个条件:

1.虚函数的重写

2.使用父类的指针或引用去调用虚函数。

这两个条件有一个不满足就构不成多态

这时和p的类型已经没有关系了,而是看其指向(引用)的对象

 

1.不是父类的指针或引用去调用:

这样g更不行

2.不符合重写virtual

 但是父类写,子类不写virtual可以

3.参数不同

不构成多态,看类型,构成多态,看它指向的对象

在构成虚函数时,如果去掉父类,则不符合重写了,不是虚函数了

多态特例:

特例1:如果去掉子类的virtual,则还是虚函数(但去调父类,则不符合重写,就不是虚函数了),因为它认为是先把父类的虚函数继承下来,继承下来之后重写,重写的是它的实现,所以虽然子类中我们没写virtual,但是还是认为是它的实现(子类虚函数不加virtual,依旧构成重写)。实际中最好加上virtual。

特例2:重写的协变。即返回值可以不同,但是要求返回值必须是父子关系的指针或者引用。不然会报错。子类返回的是父类的话也不可以,会报错。

 这样会报错,这样才对。(即使是父子对象也不行,必须是父子指针或引用,且父是父,子是子)

参数不同:

 

题:

选B,首先,test不是多态,test的this指针是B类型的,this指针调用func,调用的就是B的func。func构成重写,但是val实际是1,因为虚函数重写是一个接口继承(接口就是把函数的函数名参数返回值等架子拿下来),普通函数继承是实现继承(主要继承的是你的实现)。

接口继承是重写实现。

编译器识别是不是重写时,都不怎么看子类的相关字段的,所以子类成员函数前面加不加virtual无所谓

这样也可以

这样呢

带虚函数对象的大小的计算

考察的不是内存对齐,考察的是多态,因为该对象里会多一个指针,这个指针叫虚表指针(vftptr虚函数指针),(vftable虚函数表),表:能存储多个数据的一个东西。其实是用了一个数组把虚函数的地址存到虚函数表里。父类和子类不是共用一个虚表,当函数被重写时,虚函数表内的该函数地址就会发生变化。虚函数会把它的地址放到虚函数表。多态的原理的关键:虚函数会存进虚表,对象里没有虚表,有的是一个虚表的指针。虚函数重写以后呢?

虚函数准确调用的原理:如果传的是父类对象,那就去父类对象的虚函数表,如果是子类对象,那就去子类对象的虚函数表

多态的本质原理:符合多态的两个条件,调用时,会到指向对象的虚表中找到对应的虚函数地址进行调用

虚函数表是一个函数指针数组,里面存的是虚函数的指针

多态调用:程序运行时,去指向对象的虚表中找到函数的地址,进行调用

普通函数调用:在编译(编译时就有函数的地址)或链接(在符号表里找)时确定(去找)函数的地址,运行时直接调用

多态的原理:虚表(虚函数表)

多态的条件:1.必须完成虚函数的重写2.必须是父类指针或引用去调用虚函数。才能有多态的效果

只要不符合条件,就是直接调用。

看看构成多态和不构成多态调用的区别:

虚函数是被存在哪里的呢?

虚函数不是存在虚表里的,任何函数都是编译好了变好指令,放在公共代码段的。我们写的代码编译好之后的这个文件,是一个可执行程序,程序运行起来之后变成一个进程,进程分段把我们的代码指令加到代码段里去了。

那么虚表里面放的是什么呢?

虚表里实际上放的只是函数的地址,

实际调用时,如何得知该调用的是子类还是父类的虚函数?

虽说会发生切割切片,也就是都看到的是父类对象,但是因为指针指向虚函数表里的不同,所以可以得知。(父类对象和子类对象指向的并不是同一个虚表,因为子类的虚表被重写了也就是覆盖了)

C++是怎么考量这种行为的呢?

首先,虚函数是存在公共代码段的。

函数的地址是什么?可能是第一句指令的地址。

虚表里放的只是函数的地址。

父类对象和子类对象不是指向同一个虚表,因为这个虚表被重写了(即被覆盖了),是一个子类的虚函数。虚函数表本质是一个函数指针的数组。

因为是一个引用,如果是父类对象的引用,找到的就是父类的虚表

构成多态的调用,运行时到指向对象虚表中找调用虚函数地址,所以p指向谁,调用谁的虚函数

不构成多态调用,属于普通调用,编译时确定调用函数的地址。

虚函数重写的要求:1.要求父类子类都是虚函数(子类可以不写virtual)2.要求函数名、参数、返回值都相同

如果父类有虚函数,子类没有虚函数重写,那么编译器调用时是编译时决议还是运行时决议?

是运行时决议,编译器只是检查父类有没有虚函数,并且是引用,那么久直接去虚表里找。父类的虚表和子类的虚表在这种情况下,都是同一个虚函数都是父类的虚函数。

有些地方会要求析构函数加virtual。

析构函数是否建议加virtual呢?

在继承中,建议把析构函数定义成虚函数。

加了virtual以后,子类和父类能构成虚函数的重写(子类重写了父类)。普通场景析构函数是不是虚函数都没问题,但是当用一个指针接收new出来的子类对象时,再去delete这个指针。同理来一个new出来的父类。不加virtual时,new的是子类对象,但是调用的会是父类的析构函数,符合多态按照多态去调,不符合就按编译决议。

析构函数函数名不同,为什么构成重写呢?

因为析构函数会被处理成destructor,所以加了virtual是完成虚函数重写的。

子类析构函数重写父类析构函数,才能正确调用。指向父类调用父类析构函数。指向子类调用子类析构函数

虚函数重写的两个例外:

1.协变(基类与派生类虚函数返回值类型不同,但是返回值必须是父子关系的指针或引用,也可以是其他父子,必须是先父后子) 

2.析构函数的重写

虚表个数:单继承情况下一般只有一个虚表

C++11增加两个关键字

final:去修饰一个类,这个类就不能被继承。或如果一个虚函数不想被重写,就可以在虚函数后面加一个final。virtual void person() final{}

override:C++11新增关键字。只能加在子类,加在子类去检查子类的虚函数是否完成重写,没有完成重写就会报错。virtual void person() override{}

重载、重写(覆盖)、隐藏(重定义)的区别是什么?

相同点:它们都是函数之间的关系。

重载:要求1必须在同一作用域。要求2函数名相同,参数不同(类型、顺序、个数,有两种特殊情况)

重写(覆盖):要求1两个函数分别在基类和派生类的作用域。要求2三同(函数名、参数、返回值,协变例外)要求3两个函数都必须是虚函数。有时子类虽然不写,但是也算是虚函数。

重定义(隐藏):要求1:两个函数分别在基类和派生类的作用域。要求2函数名相同。要求3两个基类和派生类的同名函数不构成重写就是重定义

多态的原理

虚表不是在对象里,对象里只有虚表指针。虚函数会放进虚表,虚函数重写以后,父类里会有虚表。

共用一个虚表还是各自用各自的虚表呢?

共用一个虚表。如果各自用各自的,不就浪费了,因为p1和p2的虚表指针都是相同的。同一个类型的对象共用一个虚表。

多继承的虚表个数

这个func3在哪里放着呢?

在第一个虚表里,谁先继承就在谁里。

但是父类和子类就不一样了,父类用父类的,子类用子类的

多态的条件:

1.函数的重写

2.父类指针或引用去调用

2破坏就会不构成多态

虚函数重写的条件:

1.虚函数

2.函数名、参数、返回值

重写了,父类是父类的虚表,子类是子类的虚表。

如果没有完成重写,用的是不是同一个虚表呢?没有完成重写,那父类的虚表里是父类的虚函数,子类的虚表里也是父类的虚函数。是不是同一个虚表呢?不是。虽然里面的内容是一样的。

在vs下,不管是否完成重写,子类的虚表跟父类的虚表都不是同一个。(和编译器有关),如果是一个,也是有可能的。

1.派生类只有一个虚表指针,因为派生类是继承的父类,父类里面有一个虚表指针,它就不会再生成虚表指针了。所以单继承的派生类里面也就只有一个虚表指针了。

2.重写为什么也叫作覆盖呢?

覆盖是原理层的叫法。子类的虚表是把父类的虚表拷贝下来,重写的虚函数那个位置会被覆盖成子类的虚函数,重写是语法层的叫法,覆盖是原理层的叫法。

不管我子类是不是重写,如果我子类自己还有虚函数呢?

比如说里面还有一个没有重写的虚函数,这个虚函数放哪呢?

只要是虚函数,虚函数的地址都会放进虚表。

有些编译器(例如vs下)无法看到子类中自己单独写的虚函数。这并不代表其不进虚表。它是进虚表的。

虚函数表是一个函数指针数组,我们打印数组里的内容就可以。

注意函数指针取别名时:typedef void(*别名)();

取虚表的对象:*(int*)&s1

为什么不放在对象里,而是放在虚表里呢?

一个对象可能会有多个虚函数,多个虚表指针

虚函数指针会放在虚表,重写以后,父类里会有一个虚表

子类中会有父类成员,子类与父类的虚表存的内容不完全一样,因为有的部分被重写了

虚函数存在哪里?

不是在虚表里,而是在代码段,任何函数都是在代码段的。虚表里放的是函数的地址,因为虚表本质是函数指针数组

任何函数都是编译好了变成指令,放在公共代码段的。

虚表里放得只是函数的地址,虚函数还是放在公共代码段的。

父类对象与子类对象指向的虚表不同

子类没有函数,只有父类有虚函数,是否构成多态?

构成,因为编译器不看子类完没完成重写,只是粗略检查父类有没有虚函数。

但是我们会发现,子类的虚函数表里存的是父类的虚函数,因为这时没有覆盖。也就是如果不完成重写,我们把搞成虚函数就没什么意义,反而会使效率变慢。

析构函数建议加virtual,这样能和父类的析构函数构成重写,不同析构函数名不同,为什么构成重写?为什么函数名不相同还是构成重写?

因为析构函数统一被处理成destructor,所以可以认为它们函数名相同,这样处理的目的就是让他们构成重写。

为什么要求构成重写?00:33:00因为可能出现这种错误

不加virtual,析构函数的调用错误,因为这是一个普通调用,delete先调用destructor,再调用operator delete(ptr2),现在不符合多态,编译时决议,

子类析构函数重写父类析构函数,才能正确调用,指向父类调用父类析构函数,指向子类调用子类析构函数

建议在继承中,析构函数定义成虚函数

25节00:48:00-00:55:00-26节2:30:00

抽象类

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化(即不能定义)出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。派生类继承后规范了派生类必须重写,另外,纯虚函数更体现出了接口继承。

如果不想让一个对象实例化,就把它定义成抽象类

纯虚函数是不需要实现的,想实现也可以。(可以有函数体,也可以没有函数体)

override是检查重写

抽象类是强制重写

父类无法实例化时,子类也无法实例化,因为子类继承了父类,继承之后子类里也有了纯虚函数,有纯虚函数,如果子类不重写,子类也是抽象类,不合理,可以说是间接强制了子类必须去重写,不然子类会无法实例化出对象。

接口继承和实现继承

虚函数的实现是接口继承。普通继承是实现继承。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

列宁格勒的街头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值