【C++】多态语法

2023-05-16

文章目录

    • 多态的概念及构成
    • 虚函数重写
    • override 和 final
    • 重载,重写,重定义区别
    • 抽象类(接口类)
    • 多态的原理
    • 虚函数表和函数地址
    • 打印虚函数表
    • 多继承中的虚函数表

多态的概念及构成

概念:
通俗的来讲,就是多种形态,同一个函数,不同对象去调用产生不同的状态

构成条件:
1.必须通过基类的指针或者引用调用虚函数
2.调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数:被virtual修饰的函数

#include <iostream>
using namespace std;


class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "成人 全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生 半价" << endl;
	}
};

void Function(Person& per)
{
	per.BuyTicket();
}

int main()
{
	Person p;
	Student s;
	Function(p);
	Function(s);
	return 0;

}

在这里插入图片描述
相同的函数,传入不同的对象,产生不同结果这就是多态

虚函数重写

又称覆盖, 派生类中有一个函数与基类完全相同的虚函数(函数名,返回值,参数列表完全相同),称子类的虚函数重写了基类的虚函数

若派生类中一函数和基类中一虚函数完全相同,那么其可以不加virtual关键字依然构成重写,但是这样不是很规范,不推荐使用

虚函数重写的两个例外
1.协变(基类和派生类返回值不同):派生类称谢基类虚函数时,返回值可以不同。但是基类虚函数返回的必须是基类对象的指针或引用,派生类虚函数返回的必须是派生类对象的指针或引用。

class Person
{
public:
	virtual Person& BuyTicket()
	{
		cout << "成人 全价" << endl;
		return *this;
	}
};

class Student : public Person
{
public:
	virtual Student& BuyTicket()
	{
		cout << "学生 半价" << endl;
		return *this;
	}
};

2.析构函数重写:如果基类析构函数为虚函数,那么派生类析构函数只要定义,无论是否添加virtual关键字都和基类析构函数构成重写。因为编译器会对析构函数的函数名进行处理,编译后析构函数的名称统一为destructor

int main()
{
	Person* p = new Person;
	Person* s = new Student;
	delete p;
	delete s;
	return 0;
}

在这里插入图片描述
以上是析构函数没有重写的情况,当使用父类指针来接收子类对象时,在释放的时候编译器并不知道该指针指向对象是父类还是子类。**仅能根据指针类型进行空间释放。这样子类的数据并未被清除干净,就会产生内存泄漏。**所以在继承体系中,我们最好将析构函数定义为虚函数

class Person
{
public:
	virtual ~Person()
	{
		cout << "~Person" << endl;
	}
};

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

int main()
{
	Person* p = new Person;
	Person* s = new Student;
	delete p;
	delete s;
	return 0;

}

在这里插入图片描述
为什么析构函数重写后就可以无视指针类型正确释放对象空间呢?多态的原理会在后文进行讲解

override 和 final

final关键字:修饰虚函数,表示该虚函数不能被继承

在C++98中,创建一个不能被继承的类需要按照以下写法:
将基类的构造函数定义为私有,这样派生类就无法调用到基类的构造函数也就无法构造派生类对象。但是这样基类自己也无法生成对象,因为构造函数隐藏,我们需要一个对象来调用构造函数,但是我们无法生成对象。这时候就可以定义一个全局的类函数来生成并返回对象,完成对象构造,并且返回对象

class Person
{
public:
	static Person Construct(int a)
	{
		return Person(a);
	}
private:
	Person(int a = 1)
		:_a(a)
	{}
protected:
	int _a;
};

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

int main()
{
	Person a = Person::Construct(10);
	//Student s; 无法构建
	return 0;
}

显然这样的方法有点麻烦,所以在C++11中添加了final这个关键字

在C++11中,类或者函数添加了final代表其无法被继承

class Person final                //表示Person类无法被继承
{
public:
	static Person* Construct(int a) 
	{
		return new Person;
	}
	
	virtual ~Person() final       //析构函数无法被继承(也就无法重写)
	{
		cout << "~Person" << endl;
	}
protect:
     int _a;
}	

class Student : public Person        //此处报错
{
public:
	virtual ~Student()               //此处报错,因为派生类的析构函数无法重写
	{
		cout << "~Student" << endl;
	}
};

override关键字(写在派生类函数后面)
作用:检查派生类的虚函数是否重写了基类的虚函数,若基类的虚函数没有写,则报错

class Person 
{
public:
	/*virtual ~Person()
	{
		cout << "~Person" << endl;
	}*/
protected:
	int _a;
};

class Student : public Person
{
public:
	virtual ~Student() override           //使用override关键字, 父类析构函数被注释, 没有构成重写,编译器报错
	{
		cout << "~Student" << endl;
	}
};

int main()
{
	return 0;
}

重载,重写,重定义区别

重载:
1.在同一作用域
2.函数名相同,参数不同
重写(覆盖):
1.一个在基类作用域,一个在子类作用域
2.两个函数都为虚函数
3.返回值相同,函数名相同,参数相同
(协同,和析构函数例外)
重定义(隐藏):
1.一个在基类作用域,一个在子类作用域
2.两个函数名相同,不构成重写就是重定义

抽象类(接口类)

概念:包含纯虚函数的类叫做抽象类(也叫接口类), 抽象类不能实例化对象,并且其派生类不对父类虚函数进行重写也无法实例化对象。纯虚函数的存在让派生类强制定义接口必须重写,另外纯虚函数更体现了接口继承

class Person 
{
public:
	virtual void func() = 0
	{
		cout << "Person::func()" << endl;		
	}
protected:
	int _a;
};

class Student : public Person
{
public:
	/*void func()
	{
		cout << "Student::func()" << endl;
	}*/
};

int main()
{
	Person* p = nullptr;
	p->func();
	return 0;
}

我们在基类加入一个函数func()其是一个纯虚函数。创建一个Person的派生类,其并没有重写基类的纯虚函数。可以看到程序可以通过编译,但是运行时会报错

编译时(成功)

在这里插入图片描述
运行程序(程序崩溃)
在这里插入图片描述

因为抽象类无法创建实例化对象,所以在抽象类中定义函数毫无意义,只需要声明就可以了
接口继承

class person
{
public:
	virtual person& buyticket()
	{
		cout << "成人 全价" << endl;
		return *this;
	}
	virtual void func()
	{
		cout << "person::func()" << endl;
	}
protected:
	int _age = 18;
	int _id = 0;
};

class student : public person
{
public:
	virtual student& buyticket()
	{
		cout << "学生 半价" << endl;
		return *this;
	}
	
private:
	virtual void func()                //将派生类的func()函数定义成保护对象
	{
		cout << "student::func()" << endl;
	}
	int _stu_id = 1;
};

void function(person& per)             //动态多态实现
{
	per.buyticket();
	per.func();  
}

int main()
{
	person peter;
	student ken;
	function(peter);                   //调用function函数
	function(ken);
	return 0;
}

在这里插入图片描述
运行程序,我们发现即便派生类的func函数是私有的,我们呢还是可以通过多态调到。这说明访问限定符并非绝对安全。因为虚函数将自己的地址放在虚表中了,我们可以通过虚表找到此函数进行调用。

派生类的重写函数的访问限定跟从基类,因为派生类的虚表就是基类的拷贝,并将其重写函数的地址对原地址进行覆盖,这就是接口继承

但并非所有情况都会去使用虚函数表查找函数地址,只有其满足多态的条件(1.必须通过基类的指针或者引用调用虚函数 2.调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写)才会使用虚表查找函数地址

int main()
{
	person peter;
	student ken;
	peter.buyticket();
	function(peter);
	function(ken);
	return 0;
}

像这样一串代码,我们将func()设置为普通函数
在这里插入图片描述

我们可以看到虽然buyticket是虚函数,但是编译器并没有通过虚函数表进行查找,让我们再看一看function函数
在这里插入图片描述
可以看到普通函数直接就可以找到其地址,但是虚函数的地址为rax,这说明在编译阶段编译器并不知道应该掉哪个地址的函数,于是其去虚函数表中查找,当运行时根据传入对象的数据来进行填充地址

多态的原理

虚函数表

#include <iostream>
using namespace std;

class Person
{
  virtual void func()
  {
    cout << "Person::func()" << endl; 
  }
  private:
    int _a;          //成员变量 4个字节
};

class Student
{
  virtual void func()
  {
    cout << "Student::func()" << endl;
  }
};
int main()
{
  int a = 0;
  int* pa = &a;
  cout << sizeof(pa) << endl;
  cout << sizeof(Person) << endl;
  return 0;
}

[clx@VM-20-6-centos polymorphic_c++]$ make
g++ -o test test.cpp -std=c++11
[clx@VM-20-6-centos polymorphic_c++]$ ./test
8        //指针占用字节数
16       //Person对象占用字节数

可以看到具有虚函数的类Person的大小并非和普通类相同,其还包含了一个指针,这个指针就指向我们的虚函数表

为了便于观察我们多增加几个成员变量和函数,在vs监视窗口中看看这个类中究竟包含了什么

#include <iostream>
using namespace std;


class person
{
public:
	virtual person& buyticket()
	{
		cout << "成人 全价" << endl;
		return *this;
	}
	virtual void func()
	{
		cout << "person::func()" << endl;
	}
protected:
	int _age = 18;
	int _id = 0;
};

class student : public person
{
public:
	virtual student& buyticket()
	{
		cout << "学生 半价" << endl;
		return *this;
	}
	virtual void func()
	{
		cout << "student::func()" << endl;
	}
private:
	int _stu_id = 1;
};

void function(person& per)
{
	per.buyticket();
}

int main()
{
	person peter;
	student ken;
	function(peter);
	function(ken);
	return 0;
}

在这里插入图片描述

_vfptr:就是我们所说的虚函数表指针 virtual function pointer
我们还可以发现,派生类的虚函数表指针和基类的虚函数表指针不同
在这里插入图片描述
我们可以发现虚函数表是一个函数指针数组,里面存储了基类和派生类各自的虚函数的地址。我们创建的对象就是根据这些地址来找到对应的函数的

那么为什么动态多态只能传递父类的指针或者是引用呢

int main()
{
	person peter;
	student ken;
	person& s_ken = ken;    //虚函数表指针不变
	person* p_ken = &ken;   //虚函数表指针不变
	person person_ken = ken; //虚函数表指针改变
	return 0;
}

在这里插入图片描述
通过小实验我们发现,当我们使用**基类的指针或者引用来对派生类对象进行切片时,并不会改变对象的虚函数表指针。但是如果我们重新创建基类对象,虚函数表指针就会变成基类的虚函数表指针。**而这个指针决定着调用那一组函数

在这里插入图片描述

void function(person per)
{
	per.buyticket();
}

这样我们就能解释为什么function处只能使用基类的指针或者引用了。若直接为基类对象的话,形参为实参的切片的拷贝,那么形参实例化一定为person类对象,那么其的虚函数表指针一定是person类的,那么不管传基类还是派生类对象都只能调用基类的函数组,就无法完成多态

虚函数表和函数地址

虚函数表中的地址和真正的函数地址可能是不同的,如果一个派生类有自己独有的虚函数,并没有构成重写,那么它也会被放在它的虚函数表中

class student : public person
{
public:
	virtual student& buyticket()
	{
		cout << "学生 半价" << endl;
		return *this;
	}
	virtual void func()
	{
		cout << "student::func()" << endl;
	}
	virtual void funb()
	{
		cout << "student::funb()" << endl;
	}
private:
	int _str_id = 3;
	
};
int main()
{
	person peter;
	student ken;
	return 0;
}

让子类增加一个虚函数funb,可以看到监视窗口中并没有funb的地址,这是因为vs的监视窗口是被处理过的。我们复制虚函数表的指针,到内存窗口中看看
在这里插入图片描述
在这里插入图片描述
可以看到第三行好像就是funb的地址,说明funb函数是有存到虚函数表中的,我们再看看内存
在这里插入图片描述
我们发现函数的地址和虚函数表中存储的地址好像不同,这是因为虚函数表中存储的地址也是一个指令,这个指令会跳转到真正的函数地址。
在这里插入图片描述

打印虚函数表

#include <iostream>
using namespace std;

typedef void(*VF_PTR)();  //将void(*)()函数指针重命名为 VF_PTR
class Base{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
private:
	int _a = 0;
};

class Derive :public Base 
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int _b = 1;
};

//子函数
void Print_vftable(VF_PTR* _vfptr)
{
    //在Vs编译器下,虚表最后会放一个nullptr,可用作判断条件
	for (int i = 0; *(_vfptr + i) != nullptr; i++)
	{
	    //打印虚表数组中的元素数据
		printf("vft[%d]: %p\n", i, *(_vfptr + i));
	}

}
//主函数
void Print_vftable(Base& b)
{
    //对象前八个或四个字节的数据就是虚表的地址
    //void** 解引用就是 指针的大小
	VF_PTR* ptr = (VF_PTR*)(*((void**)&b));
	Print_vftable(ptr);
	cout << endl << endl;
}


int main()
{
	Base b;
	Derive d;
	
	Print_vftable(b);
	Print_vftable(d);
	return 0;
}

调出监视窗口发现,重写函数func1地址被覆盖,继承函数func2地址在虚表中地址和基类相同。监视窗口中没有未被重写的派生类函数,这说明监视窗口是被处理过的。
在这里插入图片描述
我们甚至还可以通过函数地址直接调用函数

void Print_vftable(VF_PTR* _vfptr)
{
	for (int i = 0; *(_vfptr + i) != nullptr; i++)
	{
		printf("vft[%d]: %p->", i, *(_vfptr + i));
		VF_PTR f = *(_vfptr + i);    //获取每个虚表中的地址
		f();                         //调用函数
	}

}

在这里插入图片描述

多继承中的虚函数表

#include <iostream>
using namespace std;

typedef void(*VF_PTR)();


class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1 = 3;
};

int main()
{
   Derive d;
   return 0;
}  	

在这里插入图片描述
在这里插入图片描述
可以看到派生类继承了两份虚函数表, 我们再通过内存窗口看看
在这里插入图片描述
在这里插入图片描述

在上述代码中,我们的Derive对fun1()进行了重写,它本应对两个基类的虚表都完成覆盖,但是我们发现,两个虚表中func1的函数地址并不相同

int main()
{
	Derive d;
	Base1* pd1 = &d;
	Base2* pd2 = &d;
	pd1->func1();
	pd2->func1();
}

使用多态,将d的地址分别复制给pd1和pd2,并分别调用func1函数
在这里插入图片描述
可以看到两个函数最终都跳到了0x00052870(&Derive::func1)但是将pd2在调用过程中多跳了两次。
在这里插入图片描述
我们发现这两个指令对ecx进行了操作,而在前面的学习我们知道ecx是和this指针相关。其实这两个jmp就是为了修正this指针用的。因为调用成员函数编译器会自动传递this指针,当将d按Base1切片时,pd1的this指针任然指向&d。但是d按base2切片时,pd2的this指针其实在&(d + sizeof(Base1))的地方,而这个偏移量刚好也就是八个字节,所以在调用虚函数之前,编译器还会调用指令帮助我们校准this指针。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++】多态语法 的相关文章

  • React路由传参的几种方式

    react路由传值有三种方式 xff1a 1 props params 方法 xff0c 该方法可以传递一个或多个值 xff0c 但是每个值的类型都是字符串 xff0c 没法传递一个对象 xff1b 2 query方法 xff0c 该方法类
  • 文章五:Python 网络爬虫实战:使用 Beautiful Soup 和 Requests 抓取网页数据

    一 简介 本篇文章将介绍如何使用 Python 编写一个简单的网络爬虫 xff0c 从网页中提取有用的数据 我们将通过以下几个部分展开本文的内容 xff1a 网络爬虫的基本概念Beautiful Soup 和 Requests 库简介选择一
  • 无人机目标检测:使用YOLOv4在VisDrone数据集上进行目标检测任务

    在本篇博客中 我们将探讨如何使用YOLOv4在VisDrone数据集上进行无人机目标检测任务 目标检测是计算机视觉中的一个重要任务 可以用于自动驾驶汽车 无人机监测和视频分析等多种应用 YOLOv4是一种实时目标检测算法 以其速度和准确性而
  • rospy的publisher和init_node

    文章目录 1 xff0c class Publiser xff08 发布者 xff09 2 rospy init node 初始化节点 1 xff0c class Publiser xff08 发布者 xff09 废话不多说 xff0c 先
  • ros功能包

    使用ROS的功能包使用以下常见的机器视觉应用 1 xff09 摄像头标定 xff1a 摄像头本身存在光学畸变 xff0c 可以使用camera calibration功能包实现双目和单目摄像头的标定 2 xff09 基于opencv的人脸识
  • 如何彻底关闭Win10自动更新,Win10永久关闭自动更新的方法

    如何彻底关闭Win10自动更新 xff1f Win10自动更新的问题是很多用户都遇到的问题 xff0c 很多时候我们关闭了自动更新 xff0c 过一段时间系统又自动更新了 xff0c 由于win10自动更新非常顽固 xff0c 所以我们要从
  • 函数模板及库函数

    函数模板 xff08 function template xff09 是一个独立于类型的函数 xff0c 可作为一种模式 xff0c 产生函数的特定类型版本 使用函数模板可以设计通用型的函数 xff0c 这些函数与类型无关并且只在需要时自动
  • Vmware虚拟机Ubuntu的ssh远程登陆--笔记

    SSH远程登录 apt更新网路更新ssh配置了解的部分 SecureCRTPortable登陆 apt更新 版本号 xff1a Ubuntu 16 04 环境 xff1a Vmware 17 2 网路 首先 xff0c 先检查网络是否畅通
  • Docker的常用命令

    一 Docker中几个重要的概念 镜像 和容器 是docker中两个非常重要的 概念 镜像 xff08 Image xff09 xff1a Docker 将应用程序及其所需的依赖 函数库 环境 配置等文件打包在一起 xff0c 称为镜像 容
  • Linux-C语言编写-UDP服务器客户端通信流程简介(代码)

    目录 一 xff0c 服务器 1 创建数据报套接字 2 填充结构体 3 绑定服务器的ip和端口 4 接收来自客户端的消息 recvfrom 5 关闭套接字 6 详细代码 二 xff0c 客户端 1 创建数据报套接字 2 填充结构体 xff0
  • C++三阶贝塞尔曲线

    文章目录 1 贝塞尔曲线2 示意图3 c 43 43 代码实现 1 贝塞尔曲线 贝塞尔曲线阶数等于控制点个数n 1将控制点首尾相连并且取每段连线上一点P xff0c 再将每个线段上的P点连接设第一个控制点为P1 xff0c 第二个为P2 x
  • Ubuntu 图达通激光雷达可视化/获取点云

    文章目录 0 ILA 平台网页预览1 Ubuntu的安装2 安装Ubuntu对应版本ros3 激光雷达接线4 解压SDK文件5 启动ros可视化点云6 录制点云7 播放录制文件8 rosbag文件 gt pcd文件 0 ILA 平台网页预览
  • Python的while循环

    目录 一 计数器 二 while循环使用 三 不同循环的使用环境判断 xff1a 四 while循环使用break和continue 五 while的嵌套使用 一 计数器 计数器 xff0c 是一个叫法 xff0c 代表的是一个功能 用于记
  • 字符串结束符

    在C语言中 xff0c 存储一个字符串通常用一个char 数组 在C语言中 xff0c 为了方便存储 xff0c 要求在最后一个字符的后面存储一个0 xff08 一个字节 xff09 这个0称为 字符串结束符 xff0c 常用 0 表示 在
  • 一起自学SLAM算法:1.1 ROS简介

    连载文章 xff0c 长期更新 xff0c 欢迎关注 xff1a 写在前面 第1章 ROS入门必备知识 1 1 ROS简介 1 2 ROS开发环境搭建 1 3 ROS系统架构 1 4 ROS调试工具 1 5 ROS节点通信 1 6 ROS其
  • 一起自学SLAM算法:1.2 ROS开发环境搭建

    连载文章 xff0c 长期更新 xff0c 欢迎关注 xff1a 写在前面 第1章 ROS入门必备知识 1 1 ROS简介 1 2 ROS开发环境搭建 1 3 ROS系统架构 1 4 ROS调试工具 1 5 ROS节点通信 1 6 ROS其
  • 戴尔电脑恢复系统后,D盘被加密Bitlocker,要求输入48位密钥,才能打开D盘---解决过程

    一 前言 今天DELL电脑恢复系统后 xff0c D盘被加密 xff08 D盘图标上有一把黄色的锁 xff09 xff0c 鼠标双击准备打开D盘 xff0c 提示了一个密钥ID xff0c 让输入48位码解密 xff0c 被microsof
  • 一起自学SLAM算法:1.4 ROS调试工具

    连载文章 xff0c 长期更新 xff0c 欢迎关注 xff1a 写在前面 第1章 ROS入门必备知识 1 1 ROS简介 1 2 ROS开发环境搭建 1 3 ROS系统架构 1 4 ROS调试工具 1 5 ROS节点通信 1 6 ROS其
  • 一起自学SLAM算法:1.5 ROS节点通信

    连载文章 xff0c 长期更新 xff0c 欢迎关注 xff1a 写在前面 第1章 ROS入门必备知识 1 1 ROS简介 1 2 ROS开发环境搭建 1 3 ROS系统架构 1 4 ROS调试工具 1 5 ROS节点通信 1 6 ROS其
  • 一起自学SLAM算法:3.4 图像特征点提取

    连载文章 xff0c 长期更新 xff0c 欢迎关注 xff1a 写在前面 第1章 ROS入门必备知识 第2章 C 43 43 编程范式 第3章 OpenCV图像处理 3 1 认识图像数据 3 2 图像滤波 3 3 图像变换 3 4 图像特

随机推荐