C++——多态2|virtual与析构函数|C++11override 和 final|重载,重写(覆盖),隐藏(重定义对比| 抽象类|子类和父类虚表|多继承|习题|总结(上)

简介: 笔记

virtual与析构函数


1.png


这里的父类和子类析构完成了重写。


因为析构函数名会被处理成destructor,所以这里析构函数完成虚函数重写。


不加virtual,子类对象没被析构,因为这里是一个普通调用,

2.png



delete b,变成b->destructor(); operator delete(b);


满足多态时,此时子类调用子类析构,父类调用父类析构。

3.png 子类的析构函数重写父类析构函数,才能正确调用,这里对父类析构了俩次是因为,Student里面也继承了一个父类,而我们又创建了一个父类对象,所以对父类析构了俩次。不存在重复析构。由于先delete a所以先析构子类对象。


C++11 override 和 final


.final:修饰虚函数,表示该虚函数不能再被重写

4.png

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

5.png


6.png


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

重载:俩个函数在同一作用域,要求函数名相同,类型不同(包括类型不同,顺序不同,个数不同)


重写(覆盖): 俩函数分别在基类和派生类的作用域,函数名/参数/返回值都必须相同(协变例外),俩个函数必须是虚函数。


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


抽象类


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

7.png

这种情况下,子类也没办法实例化

8.png

因为子类继承了纯虚函数,如果没有重写,则子类也是抽象类


子类重写后正常运行


9.png


普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。

10.png

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。


子类和父类虚表


这里p1和p2共用一个虚表

11.png


student和person创建的对象用的不是同一个虚表,父类虚表存父类虚函数,子类虚表存子类虚函数。

12.png

13.png

通过观察和测试,我们发现了以下几点问题:

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。


2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表

中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数

的覆盖。重写是语法的叫法,覆盖是原理层的叫法。



3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函

数,所以不会放进虚表。


4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。



5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中


b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数


c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。



在vs下,调试的时候,虚表中没有子类自己写的虚函数,其实是fun4进了虚表,只不过没显示

14.png15.png16.png

内存中此时多出来一个地址,这个地址就FUN4这个虚函数的地址


虚函数表是一个函数指针数组,我们可以打印出来


上面这些函数都没有返回值,没有参数


这是一个函数指针类型


17.png18.png

这个函数指针类型名字我们设置为ptr,ptr代表没有返回值,没有参数的函数指针类型


传过来的参数类型是函数指针数组

19.png20.png

*(int*)&s1 是取前四个字节


但传参穿的是函数指针类型,还要再类型转换


21.png22.png

#include<iostream>
using namespace std;
typedef void(*ptr)();
void PrinVFTable(ptr table[])
{
  for (size_t i = 0; table[i] != nullptr; ++i)
  {
  printf("vft[%d]:%p", i, table[i]);//打印地址和函数名
  table[i]();//调用函数,由于是无返回值,无参数,table[i]就是虚表里对应的函数
  }
}
class Base
{
public:
  virtual void Func1()
  {
  cout << "Base::Func1()" << endl;
  }
  virtual void Func2()
  {
  cout << "Base::Func2()" << endl;
  }
  void Func3()
  {
  cout << "Base::Func3()" << endl;
  }
private:
  int _b = 1;
};
class Derive : public Base
{
public:
  virtual void Func1()
  {
  cout << "Derive::Func1()" << endl;
  }
  virtual void Func4()
  {
  cout << "Derive::Func4()" << endl;
  }
private:
  int _d = 2;
};
int main()
{
  Derive s1;
  PrinVFTable((ptr*)*(int*)&s1);//虚表的地址在对象的前四个或前八个字节
  return 0;
}

23.png

Linux下不支持虚表后面给空指针


这样写即可


24.png


多继承


using namespace std;
typedef void(*ptr)();
void PrinVFTable(ptr table[])
{
  for (size_t i = 0; table[i]!=nullptr; ++i)
  {
  printf("vft[%d]:%p", i, table[i]);//打印地址和函数名
  table[i]();//调用函数,由于是无返回值,无参数,table[i]就是虚表里对应的函数
  }
}
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 {
public:
  virtual void func1() { cout << "Derive::func1" << endl; }
  virtual void func3() { cout << "Derive::func3" << endl; }
private:
  int d1;
};

25.png26.png

Derive大小=Base1+Base2+Derive成员d1


所以整体是20


Derive创建的对象里有俩个虚表

27.png



一个是Base1的,一个是Base2的

28.png

Derive继承下来的虚表中,func1是自己重写的func1,所以Base1和Base2里面的func1都是自己重写的。


由于没有重写func2,所以俩个表里面的func2都是父类的。


func3此时没有显示,因为这是子类自己增加的,这里func3是被放进了哪个虚表里?

29.png



这里打印的是第一个虚表(因为第二行打印的是Base1),说明第一个虚表里面有func3


接下来我们打印第二个虚表,第一个虚表在头四个字节,第二个虚表在中间,因为声明的时候谁先继承谁在前面

30.png


由于int *强制转换的级别高,不能直接(int*)&d+sizeof Base1,因为这样没加一次会加4个字节,而我们要取中间四个字节,这样一加会直接将该四个字节跳过


所以要转换为char*,char*一次加1个字节

31.png



我们此时发现没有func3


说明自己写的func3在第一个虚表中


这种方式也可以,利用切片,ptr2是Base2的指针,指向Base2这一块,把子类对象给Base2,完成切片

32.png

注意这俩个func1的地址不一样

33.png

34.png


这里都是Derive的func1但地址不一样(这个问题后面说)


调用的时候去指向对象虚函数表中去找func1地址调用

35.png



切片后ptr1和ptr2指向各自的区域


打印func3地址

36.png

func3地址跟这俩个虚表里的地址都不一样


直接调用d.func1()37.png

观察这俩条语句

Base1* ptr1 = &d;
    ptr1->func1();这是多态调用,先找到虚表

38.png39.png




我们发现直接用子类对象调用func1和用父类指针调用func1,只有一个步骤不同,就是子类对象直接call func1的地址,而父类指针要先找到虚表


执行这俩条语句


Base2* ptr2 = &d;
    ptr2->func1();


这里的eax跟前面的不一样,eax此时是第二个虚表的第0个虚函数地址,但是执行完eax之后,进行了sub ecx,8这里是给ecx-8,ecx是第二个虚表的地址-8,8是第一个虚表的大小,


ptr2调用ecx时,ecx指向ptr2这个位置,然后-8让ptr2指向跟ptr1同一块地方

40.png41.png

最终执行结果调的都是Derive func1.


相关文章
|
存储 算法 C++
【C++数据结构——查找】二分查找(头歌实践教学平台习题)【合集】
二分查找的基本思想是:每次比较中间元素与目标元素的大小,如果中间元素等于目标元素,则查找成功;顺序表是线性表的一种存储方式,它用一组地址连续的存储单元依次存储线性表中的数据元素,使得逻辑上相邻的元素在物理存储位置上也相邻。第1次比较:查找范围R[0...10],比较元素R[5]:25。第1次比较:查找范围R[0...10],比较元素R[5]:25。第2次比较:查找范围R[0..4],比较元素R[2]:10。第3次比较:查找范围R[3...4],比较元素R[3]:15。,其中是顺序表中元素的个数。
619 68
【C++数据结构——查找】二分查找(头歌实践教学平台习题)【合集】
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
955 77
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
573 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
540 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
算法 C++
【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
【数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】 目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果: 任务描述 本关任务:实现二叉排序树的基本算法。 相关知识 为了完成本关任务,你需要掌握:二叉树的创建、查找和删除算法。具体如下: (1)由关键字序列(4,9,0,1,8,6,3,5,2,7)创建一棵二叉排序树bt并以括号表示法输出。 (2)判断bt是否为一棵二叉排序树。 (3)采用递归方法查找关键字为6的结点,并输出其查找路径。 (4)分别删除bt中关键
404 11
【C++数据结构——查找】二叉排序树(头歌实践教学平台习题)【合集】
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
1551 0
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
338 19
|
存储 人工智能 算法
【C++数据结构——图】最短路径(头歌教学实验平台习题) 【合集】
任务描述 本关任务:编写一个程序,利用Dijkstra算法,实现带权有向图的最短路径。 相关知识 为了完成本关任务,你需要掌握:Dijkst本关任务:编写一个程序,利用Dijkstra算法,实现带权有向图的最短路径。为了完成本关任务,你需要掌握:Dijkstra算法。带权有向图:该图对应的二维数组如下所示:Dijkstra算法:Dijkstra算法是指给定一个带权有向图G与源点v,求从v到G中其他顶点的最短路径。Dijkstra算法的具体步骤如下:(1)初始时,S只包含源点,即S={v},v的距离为0。
258 15
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
545 13
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
511 12