C++——多态

作者:几冬雪来

时间:2023年10月31日

内容:C++部分多态内容讲解

目录

前言: 

什么是多态: 

虚函数: 

协变: 

c++11中override和final:

final:

override:

重载,重写(覆盖) ,隐藏(重定义)的对比:

虚表:

多态存在的问题:

为什么不能是父类的对象:

虚表存放的位置:

派生类中自己的对象存放的位置:

静态多态与动态多态:

静态多态: 

动态多态:

多继承和单继承的关系的虚表: 

派生类函数的大小: 

地址不相同问题:

抽象类:

代码:

总结:


前言: 

在上一篇博客当中我们讲解了面向对象的第二大特性——继承,而今天我们将再学习C++里面一个十分重要的知识点——多态。多态对比起之前学习的所有的内容难度有一个质的提高。

 

什么是多态: 

通俗来说在C++中多态就是多种形态,也就是在编程中或者现实中要完成某件事情,但是不同对象的结果是不一样的

举个最简单的例子——那就是高铁买票,成年人需要全价买票,学生买票有一定的打折,老人和儿童买票只需要半价,军人能优先购买

虚函数: 

在学习多态之前我们要了解一下多态中出现的一个新名词——虚函数

虚函数这里我们又使用了虚继承中使用到的关键字——virtual。虽然它们都是同一个关键字,但是在不同地方二者表达的也是也不同

在继承中它是虚继承,在多态中它是虚函数

那么我们的虚函数是怎么样书写的呢?

在这里虚函数也是十分容易书写的,只要在普通的成员函数之前加上virtual,这样就能将普通的成员函数变为虚函数了

这里只有成员函数才能加virtual,普通函数不能加virtual

而虚函数在这里就会完成一个操作,才编译器中我们将其称为——重写

虚函数的重写是指,在派生类中存在一个根基类完全一样的虚函数

这里的完全一样指的是函数名,参数和返回值都相同的函数

我们就将派生类的函数叫做,重写了父类的虚函数,也能叫做重写和覆盖

而在这里我们的虚函数的重写是多态的条件之一,而另一个条件则是多态必须是基类的指针或者是引用去调用

这里我们就成功实现多态。

在这里多态指的是将不同的对象传递过去,然后调用不同的函数。多态调用看指向的对象,普通对象看当前的类型

这里按照我们以往的知识去了解的话,BuyTicket应该指向的是Person,二者打印的会是全价票。但是就因为在这里的参数里面是基类的引用

因此这个p我们既可以去调用到基类也可以调用到派生类。 

如果调用的并不是基类的指针或者是引用的话,在这个地方二者就是去访问基类,并不会去访问派生类中的成员函数。 

这就是多态不是基类的指针或者是引用去调用所导致的结果

同样的,如果我们基类没有书写virtual也会导致我们无法打印出来派生类中的“半价票”。(派生类不写virtual并没有影响

简单来说就是要完成多态的实现的话,必须满足两个条件一个是虚函数,另一个是需要基类的指针或者引用,两个条件缺一不可

如果两个条件中其中一个没有完成,那么这里就是普通调用,那就调用指向的内容

这里还要注意一个问题,虽然在条件的时候就有提到过必须是基类的引用或者指针,那么如果这个地方将基类换成派生类的引用或者指针的话会怎么样

看上图我们就可以分析出来,如果这个地方是派生类的指针或者引用的话,我们依旧可以去调用派生类对象,但是无法去调用基类

协变: 

在上边我们讲过重写的条件是虚函数和基类的指针或者引用,而且基类的指针和引用就引出出来了三同问题

三同问题就是函数名,参数和返回值都相同

但是这里我们要讲到的协变就牵扯到了三同问,这里的协变就是一个多态的大坑。

协变在这里讲的就是在书写多态的时候,三同中的返回值可以不同,但是要求返回值必须是父子关系的指针和引用

像这里我们就对返回值进行了修改,从报错中可以看出来编译器在警告这里的重写虚函数的返回类型有差异

那么如果要返回值不同且必须是父子关系的指针或者引用,这里的代码又应该如何去书写呢

这样子就是返回值不同,但是返回值是父子类的指针或者引用的写法

虽然这里有提到了协变和这一种写法,但是在现实中基本不可能遇到协变的场景,返回类型直接定为void就没有协变这种麻烦事情了

c++11中override和final:

接下来我们就来讲解C++11中提供的两个关键字,一个叫做override一个叫做final

而这两个关键字通常被用来解决C++中多态里面的一些问题

final:

首先在这里提到的就是final关键字。

多态里面如果我们不想被继承要怎么做呢?

如果不想被继承重写的话,在这个地方我们就在父类的函数的后面加上一个final,这样子我们的父类函数就不让重写了

派送类中如果调用父类的构造函数的话,这个地方我们的代码就会报错

因为Benz无法从Car继承

C++中我们的final可以去修饰类,类不能被继承,也可以用来修饰函数,使函数不能被重写

override:

接下来讲解的是C++11中的另一个关键字——overside。  

在多态中我们明白,虚函数这个代码出现的意义就是要进行重写,如果没有重写那么虚函数就没有意义

而这里的override就起到了一个作用——检查重写

override和final不一样的是,final是放在父类的虚构函数后面,而override是放在派生类的虚构函数后面。 

这里它的作用就是帮助派生类检查是否完成重写,如果没有重写的话它就会报错

重载,重写(覆盖) ,隐藏(重定义)的对比:

学习到现在我们了解到了,在C++中不仅重载操作,更是在继承和多态这里多出来了重写和隐藏的操作

而今天就来简单讲一讲这三个概念有什么不同

首先就是我们最先学习到的知识——重载。 

重载是要求两个函数在同一作用域且函数名和参数都不同,它和隐藏与重写并没有什么关系

接下来就是在继承时期学习的——隐藏,我们也可以称其为重定义

这里隐藏要求的是两个函数分别在基类和派生类的作用域中,且函数名相同,并且在这里隐藏和重写之间有一定的关系

最后是重写,也是要在基类和派生类中,但是重写条件更加严格,它需要实现三同,并且两个函数必须是虚函数

在上面我们说重写和隐藏之间有一定的关系

就如上图一样,在隐藏的操作下如果我们再满足虚函数外加三同的条件,那么这个时候我们的操作就不单单是隐藏,这里的操作就从隐藏变成重写了

虚表:

再然后我们就来讲解C++多态中的一个重要的知识点——虚表

这里我们先写一行代码。

这里就是一道会常考的笔试题,这里问sizeof(Base)的大小是多少

这里最后计算从出来的Base的大小为8,那么怎么算出来的这个结果,接下来我们就来对其分析一下。

在这个地方我们通过调试和监视可以看出,Base的大小是8是因为在这里我们进行了对齐的操作,但是Base的大小并不是我们真正的目的

在监视中看见除了_b以外,在监视中出现了一个指针。 

这个指针就是我们的虚函数指针又或者叫虚函数表指针

这里虚函数就会被放到虚表里面去,但是其本质还是放在代码段中,虚表存放的是虚函数中的地址

在这里我们可以看出来Func1和Func2是虚函数,因此二者都存放着虚函数表中,而Func3不是虚函数,因此它不在虚函数表中。 

那么在这个地方又引出了一个问题,那就是它重写了以后会发生什么呢?

那么在上面的这段代码中,Person和Student中的虚函数表中是怎么样的

同样在这里用调试中的监视窗口去查看。

在监视窗口的观察下我们可以看出来,虚函数表指针中存放有一个虚函数,如果在代码中有多个虚函数的话,虚函数表中就有多个虚函数

看完了Mike之后接下来我们就来观察一下John对象

从图中我们可以看出在John中存放着自己的_b。同时在John里面还我们包含了一个父类,父类里面有一个虚表,而我们就可以用一个指针指向虚表

如果被重写在这个虚表里面存放的是重写的虚函数

那么这样我们就可以分析出为什么多态可以做到指向父类调用父类,指向子类调用子类的操作

这里代码中是父类的调用或者指针,如果我们指向父类,那么我们看到的就是父类

如果我们指向子类,那么这个地方就先进行切片,将父类部分切出来给给代码,实际看到的依旧是一个父类

无论在这里传的是父类还是子类,对于编译器而言它看到的都是父类

那么我们是怎么实现多态的呢?

这就是多态实现的原理,如果不符合多态,那么在编译的时候就确定地址。如果符合多态,那么它到运行的时候到指向对象的虚函数表中找调用函数的地址

在调用的时候,如果传的是父类,那么我们就去父类中找到它的虚表;如果传的是子类,那么先切片,我们看到的依旧是父类对象,但是这个父类是子类里面的父类,它的虚表已经被覆盖,找到的是子类的地址

多态存在的问题:

在了解多态的过程中,因为多态实现条件的严苛,我们可以延伸出来很多问题

为什么不能是父类的对象:

那么就从第一个条件开始讲起

我们说过多态的实现必须要是父类的指针或者引用,这里就延伸出了一个问题,如果是父类的对象还能实现多态吗? 

答案是不行:这是因为对象的切片和指针或者引用的切片有些许的不同

这里先将代码写出来,接下来就要去调试里面找到窗口

找到父类的对象和父类的指针或者引用有哪些不一样的地方

在派生类对象中,它是由3个部分构成,派生类虚表和父类虚表并不相同,它的构成一部分是父类一部分是派生类自己

这里派生类有自己的虚表是因为它完成了虚函数的重写。 

像左边3种写法都是切片的方法。 

首先如果是指针和引用,这里的指针可以指向父类对象,它在父类对象中看到的是父类的虚表。它指向子类的话,看到的是子类中父类的那一部分

那么对于这个指针而已,我们指向父类对象看到的是父类对象,指向子类对象看到的也是父类对象

子类父类对象和子类当中的父类对象的虚表是不一样的

而这里不一样的虚表是第一个虚表

借助这张图,我们这里就可以来详细讲解的重写或者覆盖的操作了。 

这里我们的做法是先把父类的虚表给拷贝下来,拷贝完了之后重写后用重写的地址对其进行覆盖操作。 

因此指针或者引用并不会发生什么拷贝问题

但是对象的切片需要拷贝

对象切片的拷贝中,我们的_a(上图见)会拷贝过去,但是虚表并不会拷贝过去。 这里不拷贝虚表的原因是,如果拷贝虚表,我们再用父类的指针去指向父类对象反而会去调用子类

因此我们得出一个结论子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中是父类虚函数函数子类虚函数我们无法确定

虚表存放的位置:

接下来我们要问的问题就是虚表存放的位置

对于存放数据的位置,不外乎就是栈,堆,数据段(静态区) ,代码段(常量区)。那么这个地方虚表究竟存放在哪里呢?

这里我们虚表存放的位置是常量区,因为虚表是不能被修改的

而这里我们是通过取地址的方法,取出栈,静态区,常量区和堆处的地址,接下来让它们的地址与虚表的地址相比较

在这里可以看见栈和堆距离两个虚表相差盛远,静态区和常量区的地址和虚表的地址比较接近,最后再比较静态区和常量区

比较的结果是常量区相比较静态区,到两个虚表的byte较少,因此这个地方我们就推测虚表存放的位置为常量区

派生类中自己的对象存放的位置:

在多态中我们还有一个问题,那就是派生类中自己的对象的位置在哪里

简单口头讲解可能不太清楚,我们来看一下图。

这里要简单的补充一点知识,那就是在这里虚表在内存空间结束的位置通常会加一个0,但是并不是每个编译器都会这样做,有些编译器就没有这样做。 

然后就是这里派生类Func3处的问题,在监视窗口里面我们并没有看到派生类的Func3。但是在内存窗口处有一个地址,它可能是Func3

那么我们要怎么验证Func3的存在

这里的一个方法就是将其强制打印。

在这里我们给每一个Func后面都增加一句打印。如果在调用的时候打印了Func3的数据,那就可以证明那个地址就是Func3的地址

那么这个地方我们打印虚表要怎么打?

这里要先明白虚表,它的全名叫做虚函数表,简称虚表,它的本质可以认为是函数指针数组。此处就需要我们定义一个函数指针变量。 

接下来我们就要将函数指针数组的代码书写出来

书写出来后我们依次去访问里面的内容。

下一个步骤就是要打印虚表,打印虚表的话就需要我们将虚表的地址,而虚表的地址就在头四个字节上

这里我们通过代码的实现可以明显的看出来在虚表中不止有两个地址,在这个地方还存在着第三个地址

而这个代码在监视窗口下只显示两个地址,我们可以将其看成监视窗口的一个小bug

但是即使到了这一步我们也仅仅是确定了监视窗口出现了一个bug,而不能说明这个地址放的就是Func3

这里为了证明这个地址存放的是Func3,我们还需要做最后一个步骤。

在这个地方我们增加了一个步骤,那就是在打印虚表的同时去进行调用函数的操作。

而在上边我们又做了一个伏笔,每个函数都有对应的打印标记。

借用这个函数标记我们就可以访问对应的函数

那么如果在这个地方借助第三个不确定的地址打印出来了Func3函数里面对应的数据,这个地方我们才能正式的确定,Func3是存放在虚表里面的

而事实确实是如此,在这里我们出现了Func3对应的函数标记,因为Func3里面的数据被打印出来了,所以这里就能确定这个地址就是Func3的地址

同时这里也要注意一个点,那就是监视窗口有时候是不可靠的,在使用监视窗口的时候要留个心眼,相对比内存的可信度要更加的高

静态多态与动态多态:

编译器中我们还将多态分为静态的多态和动态的多态

那么这里的静态多态和动态多态有什么区别吗?

静态多态: 

首先这里我们要谈的是静态的多态

静态(编译时)的多态,这里指的就是函数重载

例如上图,就是一个简单的函数重载。

这个地方我们传不同的参数,编译器在编译的时候通过函数名修饰规则,它会去匹配不同的函数。

这个就是静态的多态,也被叫做静态绑定,它们在编译的时候就确定好了地址

动态多态:

接下来就是动态的多态

这个地方动态的多态指的是继承,虚函数重写,实现的多态

这里和静态的多态不一样的地方是,静态的多态是在编译时确认的地址,而动态的多态则是在运行时去虚表中找到它的地址,然后再去调用

多继承和单继承的关系的虚表: 

再下来就是讲一下多继承关系的虚表。这个地方不讲解单继承的虚表是因为在上面虚表的一系列实现中我们都是使用的单继承的方式。

这里我们就不再去讲解一遍单继承。

这个地方我们就要先写一个多继承的代码出来。

同时上边打印虚表的代码也要使用到。

接下来就对我们的多继承先进行一下分析,在Base1中有一个func1和一个func2,同样的在Base2中也有一个func1和一个func2

接下来在派生类中我们重写了func1,并且还增添了一个func3,与此同时同时它们各种都有一个整形

这里的父类就是一个普通的继承,并没有什么问题,这里主要会出现的问题还是在派生类那里。 

派生类函数的大小: 

那么派生类中Dervide的大小是多少

这里我们就用Derive去定义一个d

接下来通过d去测量Derive的大小

最后测得它的大小是20个字节。 

在这里的Base1中的大小是8个字节,有一个虚表的指针,外加一个整形。同样的Base2里面也是一个虚表指针和一个整形,二者相加的大小为16个字节,最后再加上自己的一个成员,大小正好为20个字节,它有两个虚表

但是这个地方就出现了一个问题那就是Derive里面的func3在哪里

按照我们上边的表来看,20字节的空间,一个是Derive自己整形,另外就是两个虚表指针,这么看来func3没有存储的空间

那么,这里就出现三种情况,一种是放在Base1中,另一种是放在Base2中,最后一种是两边都放。而为了验证func3属于以上的哪种情况,这里就需要打印它的虚表

这里通过打印出来的虚表我们可以观察到,func3是放置在Base1里面的。但是这里的结果中出现了一个,那就是Derive的func1地址不一样了

但是这里地址虽然不一样,但是最后它们都去调用到了func1

那么这就成为了我们下一个问题。

地址不相同问题:

作为上边问题的总结。

在这个地方我们func1和func2都重写了func1,但是之后Base1和Base2的虚表中func1的地址不一样

这个地方要解决或者了解这个问题,就需要汇编/反汇编的帮助

像这里,在调试的情况下我们去使用反汇编,同时也用监视窗口去查看反汇编里面的eax与两个虚表

那么虚表在这里要解决的问题代码也将其写出来

这里重写的func1在Base1和Base2里面的地址不同

这就是我们要问题所在,地址不同但是却又调用同一个函数,这里就要借助汇编来探查

那么在汇编中我们会看到什么呢?

在这里我们可以看到,在编译器的汇编中,当箭头走到call eax处。这个地方去使用监视窗口,通过监视窗口可以看见eax的地址和Base1中func1的地址是一样的

然后在下来我们就要F11进行call里面查看

这个地方的jmp指令才是真正存放数据的地址

也就是说,在汇编中我们call的地址一般都是这个jmp指令的地址

然后在这里最后结尾处还有一个地址0C32840

这里的这段汇编代码再F11一次后,这个地方它会跳到此处来,这个地方也就是派生类中的重写的func1里面

这里的Base1就讲解好了,接下来就是Base2里面数据的讲解

这个地方我们的第一步都是一样的,在call中eax和Base2中的func1地址是一致的

但是当我们进入这个代码观察jmp的时候,这个地方会有不同之处

这个地方的jmp的地址和我们一开始Base1中jmp的地址不同

与此同时,在jmp后面附带的地址也和Base1的地址不同

这个地方完了更深一步的探索,我们要再次F11进入jmp里面,查看它会跳转到哪个地方。

这个地方jmp以后会回到我们上图的第一行里面,再下来继续执行jmp指令,这个地方的jmp指令的后边有一个地址00C328C3

这个地方又刚刚好是我们Base1中真正存放数据的指针。 

最后实现了和Base1中相同的操作

也就是说在这里最后都调用到了func1,只不过Base2做执行了几行代码

而这里多执行几行代码的核心就是这个。

这里的ecx就类似我们的this指针,而汇编中这段的意思是这样的:这里执行时,this指针进行-8的操作

那么为什么这里要this指针-8呢?

我们要想知道为什么this指针-8就需要这张图,此前有了解过Base1和Base2之间相差了8个字节的大小

这里的ptr1指向Base1开始的位置,ptr2指向Base2开始的位置。在上边的解析中,ptr2减去8则来到了ptr1的位置,那么为什么ptr2要减回到ptr1这个位置呢

这里我们首先要明白,这个地方我们要调用的是派生类中的func1,这个地方的this指针应该指向的是Derive对象

这里Base1的开始位置恰恰好于Derive的开始位置重合,所以ptr1不需要去移动。这里ptr2需要移动的原因是,它要去调用Derive函数。this指针要保证是正确的,这个地方ptr1因为和Derive位置重合,所以我们可以对其进行调用

这个地方我们不能调用ptr2的原因是因为它的指针指向是不正确的,这里的this指针传的是ptr2,因此在实际调用之前进行一定程度的封装与修正。

所以汇编中这段代码的作用实际上就是修正this指针。 

也可以理解为Base1这里的地址是真地址,Base2处的地址是“假”地址,它被接下来封装在这个过程中进行了修正

抽象类:

接下来我们来介绍一下抽象类,而在抽象类中有一个概念,它在这里叫做纯虚函数。

那么这里纯虚函数的代码该怎么写?

像这里在虚函数后面加上赋值0之后的函数被我们称为纯虚函数。

而这里的抽象类就是包含着纯虚函数的类,它想表达的意思就是,这个类在现实世界中没有对应的实体。

因此抽象类这里就有一个特点,那就是它不能实例化对象。

而不能实例化对象也就意味着正常情况下我们并不能去使用它。

而且解决抽象类使它可以被正常使用的方法也是很简单。

在这里我们只要对其进行重写就可以去使用它了,这里要注意的是必须要经历重写操作才能使用,如果只是继承没有重写的话,代码函数会报错。

这里还要注意一个点,因为父类的纯虚函数没有对象,所有它里面没有虚表的寻找。但是派生类对其进行了重写操作,这样会是派生类中有对象,派生类对比父类是存在虚表的。

而纯虚函数在多态中有一个作用,那就是它间接强制派生类重写虚函数。(override是进行检查二者不同)

代码:

#include <iostream>

using namespace std;

//class Person
//{
//public:
//	virtual Person* BuyTicket() const
//	{
//		cout << "全价票" << endl;
//		return 0;
//	}
//};
//
//class Student :public Person 
//{
//public:
//	virtual Student* BuyTicket() const
//	{
//		cout << "半价票" << endl;
//		return 0;
//	}
//};
//
//void func(const Person& p)
//{
//	p.BuyTicket();
//}
//
//int main()
//{
//	func(Person());
//	func(Student());
//
//	return 0;
//}

//class Person
//{
//public:
//	virtual ~Person()
//	{
//		cout << "~Person()" << endl;
//	}
//};
//
//class Student :public Person
//{
//public:
//	virtual ~Student()
//	{
//		cout << "~Student()" << endl;
//	}
//};
//
//int main()
//{
//
//	return;
//}

//class Car
//{
//public:
//	virtual void Drive()
//	{
//
//	}
//};
//
//class Benz :public Car
//{
//public:
//	virtual void Drive() override 
//	{
//		cout << "Benz-舒适" << endl;
//	}
//};
//
//int main()
//{
//
//	return 0;
//}

//class Car
//{
//public:
//	virtual void Drive() = 0;
//};
//
//class Benz :public Car
//{
//public:
//	virtual void Drive()
//	{
//		cout << "Benz-舒适" << endl; 
//	}
//};

//class Base
//{
//public:
//	virtual void Func1()
//	{
//		cout << "Func1()" << endl;
//	}
//
//	virtual void Func2()
//	{
//		cout << "Func2()" << endl;
//	}
//
//	void Func3()
//	{
//		cout << "Func3()" << endl;
//	}
//
//private:
//	char _b = 1; 
//};
//
//int main()
//{
//	cout << sizeof(Base) << endl;
//
//	Base b1;
//
//	return 0; 
//}

// class Person
//{
//public:
//	virtual void BuyTicket() 
//	{
//		cout << "全价票" << endl;
//	}
//
//	virtual void Func1()
//	{
//		cout << "Person::Func1()" << endl; 
//	}
//
//	virtual void Func2()
//	{
//		cout << "Person::Func2()" << endl;
//	}
//
//	int _a = 0;
//};
//
//class Student :public Person 
//{
//public:
//	virtual void BuyTicket() 
//	{
//		cout << "半价票" << endl;
//	}
//
//	virtual void Func3()
//	{
//		cout << "Person::Func3()" << endl;
//	}
//
//	int _b = 1;
//};
//
//void Func(Person& p)
//{
//	p.BuyTicket();
//}

//typedef void(*FUNC_PTR)();
//
打印函数指针数组
//void PrintVFT(FUNC_PTR* table)
//{
//	for (size_t i = 0; table[i] != nullptr; i++)
//	{
//		printf("[%d]:%p->", i, table[i]);
//
//		FUNC_PTR f = table[i];
//		f(); 
//	}
//	printf("\n");
//}

int main()
{
	Person ps;
	Student st;

	int vft1 = *((int*)&ps);
	PrintVFT((FUNC_PTR*)vft1);

	int vft2 = *((int*)&st);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}
//
//int main()
//{
//	int i = 1;
//	int d = 1.1;
//	cout << i << endl;
//	cout << d << endl;
//
//	Person ps;
//	Person* ptr = &ps;
//
//	ps.BuyTicket();
//	ptr->BuyTicket();
//
//	return 0;
//}

//class Base1
//{
//public:
//	virtual void func1()
//	{
//		cout << "Base1::func1" << endl;
//	}
//	virtual void func2()
//	{
//		cout << "Base1::func2" << endl;
//	}
//private:
//	int b1;
//};
//
//class Base2
//{
//public:
//	virtual void func1()
//	{
//		cout << "Base2::func1" << endl;
//	}
//	virtual void func2()
//	{
//		cout << "Base2::func2" << endl;
//	}
//private:
//	int b2;
//};
//
//class Derive :public Base1, public Base2
//{
//	virtual void func1()
//	{
//		cout << "Derive::func1" << endl;
//	}
//	virtual void func3()
//	{
//		cout << "Derive::func3" << endl;
//	}
//private:
//	int d1;
//};
//
int main()
{
	Derive d;
	cout << sizeof(d) << endl;

	int vft1 = *((int*)&d);
	int vft2 = *((int*)((char*)&d+sizeof(Base1)));

	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}
//
//int main()
//{
//	Derive d;
//	Base1* ptr1 = &d;
//	ptr1->func1();
//
//	Base2* ptr2 = &d;
//	ptr2->func1();
//
//	return 0;
//}

class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz" << endl;
	 }
};

class Bmw:public Car
{
public:
	virtual void Drive()
	{
		cout << "Bmw" << endl;
	}
};

void Func(Car& p)
{
	p.Drive();
}

int main()
{
	Benz b;
	Func(b);

	Bmw m;
	Func(m);

	return 0;
}

总结:

到这里我们C++多态部分的知识就落下帷幕了,多态在C++部分是一个十分重要的板块。在进行考试,面试又或者考研,多态都是其中不可或缺的一部分,而多态所延伸出来的问题我们也要去好好的掌握,最后希望这篇博客能带来帮助。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/110827.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深度学习_2 数据操作

数据操作 机器学习包括的核心组件有&#xff1a; 可以用来学习的数据&#xff08;data&#xff09;&#xff1b;如何转换数据的模型&#xff08;model&#xff09;&#xff1b;一个目标函数&#xff08;objective function&#xff09;&#xff0c;用来量化模型的有效性&…

HTML5+CSS3+JS小实例:交互式图片鼠标悬停景深对焦效果

实例:交互式图片鼠标悬停景深对焦效果 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"><meta name="viewport"…

elasticsearch一些重要的配置参数

先看一下官网给我们提供的全部的参数配置项 官网地址 官方文档链接&#xff1a;注意版本是8.1Configuring Elasticsearch | Elasticsearch Guide [8.1] | Elastic​编辑https://www.elastic.co/guide/en/elasticsearch/reference/current/settings.html 重要&#xff08;基本…

SpringBoot+MINIO

Linux安装MINIO https://blog.csdn.net/tongxin_tongmeng/article/details/133934115 MINIO创建桶MINIO创建秘钥MINIO的API路径 http://your-server-ip:9000 注意&#xff1a;API路径在日志文件中/opt/minio/minio.log pom.xml <!-- https://mvnrepository.com/artifact/com…

最新Microsoft Edge浏览器如何使用圆角

引入 最近我看了edge官方的文档&#xff0c;里面宣传了edge的最新UI设计&#xff0c;也就是圆角&#xff0c;但是我发现我的浏览器在升级至最新版本之后&#xff0c;却没有圆角 网上有很多人说靠实验性功能即可解锁&#xff0c;但是指令我都试过了&#xff0c;每次都是搜索无结…

云原生-AWS EC2使用、安全性及国内厂商对比

目录 什么是EC2启动一个EC2实例连接一个实例控制台ssh Security groups规则默认安全组与自定义安全组 安全性操作系统安全密钥泄漏部署应用安全元数据造成SSRF漏洞出现时敏感信息泄漏网络设置错误 厂商对比参考 本文通过实操&#xff0c;介绍了EC2的基本使用&#xff0c;并在功…

用起来顺手的在线表结构设计软件工具Itbuilder,与你共享

在线表结构设计软件工具需功能简洁&#xff0c;去除晦涩难懂的设置&#xff0c;化繁为简&#xff0c;实用为上&#xff0c;上手非常容易&#xff0c;这些itbuilder统统可以做到。 itbuilder是一款基于浏览器开发的在线表结构设计软件工具&#xff0c;借助人工智能提高效率&…

KnowledgeGPT:利用检索和存储访问知识库上增强大型语言模型10.30

利用检索和存储访问知识库上增强大型语言模型 摘要引言2 相关研究3方法3.1 任务定义3.2 知识检索3.2.1 代码实现3.2.2 实体链接3.2.3 获取实体信息3.2.4 查找实体或值3.2.5 查找关系 3.3 知识存储 4 实验 摘要 大型语言模型&#xff08;LLM&#xff09;在自然语言处理领域展现…

Flask_Login使用与源码解读

一、前言 用户登录后&#xff0c;验证状态需要记录在会话中&#xff0c;这样浏览不同页面时才能记住这个状态&#xff0c;Flask_Login是Flask的扩展&#xff0c;专门用于管理用户身份验证系统中的验证状态。 注&#xff1a;Flask是一个微框架&#xff0c;仅提供包含基本服务的…

__attribute__中的constructor和destructor--如何让程序退出时调用指定函数

背景 假设你在开发一个基础组件x&#xff0c;然后你设计了一个x_init接口用来初始化这个组件&#xff0c;相应地你设计了一个x_deinit来去初始化。这样其它模块要用到这个组件时&#xff0c;先调一下x_init, 用完了再调一下x_deinit。init和deinit这是一对很常见的接口&#x…

前端的简单介绍

前端核心的分析 CSS语法不够强大&#xff0c;比如无法嵌套书写&#xff0c;倒是模块化开发中需要书写很多重复的选择器 没有变量和合理的样式复用机制&#xff0c;使逻辑上相关的属性值必须字面量的心事重复的输出&#xff0c;导致难以维护 CSS预处理器,减少代码的笨重&#…

网课 - 网页视频-倍速播放-快进-拖动进度条-增大音量 - 火狐Firefox浏览器

本文使用的浏览器为火狐Firefox浏览器。 用浏览器播放视频&#xff0c;比如看网课、看在线电影电视剧时&#xff0c;经常能遇到的情况与解决方案&#xff1a; 音量太小&#xff0c;即使调整到100%还是不够响亮 这时可以安装插件“600% Sound Volume”, 安装之后可在原来音量的…

测试计划驱动开发模式 TPDD:一种比 TDD 更友好的开发模式

相信大部分开发团队都在使用TDD&#xff0c;并且还有很多开发团队都 对外声明 在使用 TDD 开发模式。 之所以说是“对外声明”&#xff0c;是因为很多开发团队虽然号称使用的是 TDD 开发模式&#xff0c;实际开发过程中却无法满足 TDD 的要求。 实际上&#xff0c;测试驱动的…

qt 系列(一)---qt designer设计常用操作

最近转战qt, 主要用qt designer 进行GUI开发&#xff0c;记录下实战经验~ 1.前言 qt 是跨平台C图形用户界面应用程序开发框架&#xff0c;可以使用的IDE工具有 qt creator 和 vs, 这里我主要使用 Visual Studio 2017 工具进行程序开发与编写。 2. 环境配置 只写关键步骤~~ …

笔记本电脑的键盘鼠标如何共享控制另外一台电脑

环境&#xff1a; 联想E14 x2 Win10 across 2.0 问题描述&#xff1a; 笔记本电脑的键盘鼠标如何共享控制另外一台电脑 解决方案&#xff1a; 1.下载across软件&#xff0c;2台电脑都按装&#xff0c;一台设为服务端&#xff0c;一台客户端 2.把配对好设备拖到右边左侧…

uniapp保存网络图片

先执行下载uni.downloadFile接口&#xff0c;再执行保存图片uni.saveImageToPhotosAlbum接口。 // 保存二维码 saveQrcode() {var _this this;uni.downloadFile({url: _this.qrcodeUrl, //二维码网络图片的地址success(res) {console.log(res);uni.saveImageToPhotosAlbum({fi…

21.12 Python 实现网站服务器

Web服务器本质上是一个提供Web服务的应用程序&#xff0c;运行在服务器上&#xff0c;用于处理HTTP请求和响应。它接收来自客户端&#xff08;通常是浏览器&#xff09;的HTTP请求&#xff0c;根据请求的URL、参数等信息生成HTTP响应&#xff0c;并将响应返回给客户端&#xff…

Pytorch 猫狗识别案例

猫狗识别数据集https://download.csdn.net/download/Victor_Li_/88483483?spm1001.2014.3001.5501 训练集图片路径 测试集图片路径 训练代码如下 import torch import torchvision import matplotlib.pyplot as plt import torchvision.models as models import torch.nn as…

注意!注意!注意!新规|Temu平台强制欧代英代,警惕产品被拒!

注意&#xff01;注意&#xff01;注意&#xff01;新规&#xff5c;Temu平台强制欧代英代&#xff0c;警惕产品被拒&#xff01; 欧代&#xff0c;英代信息怎么办理呢 TEMU平台上有售卖产品必需要求产品打上英代,欧代信息! 10月15日&#xff0c;Temu正式实施欧代&英代新规…

《利息理论》指导 TCP 拥塞控制

欧文费雪《利息原理》第 10 章&#xff0c;第 11 章对利息的几何说明是普适的&#xff0c;任何一个负反馈系统都能引申出新结论。给出原书图示&#xff0c;本文依据于此&#xff0c;详情参考原书&#xff1a; 将 burst 看作借贷是合理的&#xff0c;它包含成本(报文)&#xf…