虚函数的讲解

news2025/4/18 19:03:37

文章目录

  • 虚函数的声明与定义
  • 代码演示
    • 基类Person
    • 派生类Man
    • 派生类Woman
  • 测试代码
  • 动态绑定
  • 静态绑定
  • 访问私有虚函数
  • 总结一下通过成员函数指针调用函数的方式

虚函数的声明与定义

虚函数存在于C++的类、结构体等中,不能存在于全局函数中,只能作为成员函数存在。

代码演示

基类Person

Person.h

#pragma once
//定义一个Person类
class Person
{
public:
	virtual void speak();//声明一个虚成员函数(简称虚函数)
	virtual void eat();//声明一个虚成员函数(简称虚函数)
	void walk();//声明一个普通成员函数
};

Person.cpp

#include "Person.h"
#include <iostream>

void Person::speak()//虚函数的定义,前面不能加vitrual,否则编译不过
{
	std::cout << "Person::speak" << std::endl;
}

void Person::eat()//虚函数的定义,前面不能加vitrual,否则编译不过
{
	std::cout << "Person::eat" << std::endl;
}

void Person::walk()//普通函数的定义
{
	std::cout << "Person::walk" << std::endl;
}

派生类Man

Man .h

#pragma once
#include "Person.h"
//定义一个类Man并继承Person类
class Man :public Person
{
	//这个函数是重写了基类中的虚函数,此时这个函数也是虚函数,前面的virtual可以省略。
	void speak() override;//override 关键字只能用于虚函数,明确表明要重写基类的虚函数
	//virtual void speak() override;//等价于上面
};

Man.cpp

#include "Man.h"
#include <iostream>

void Man::speak()
{
	std::cout << "Man::speak" << std::endl;
}

派生类Woman

Woman.h

#pragma once
#include "Person.h"
//定义一个类Woman并继承Person类
class Woman :public Person
{
	void speak() override;
};

Woman.cpp

#include "Woman.h"
#include <iostream>
void Woman::speak()
{
	std::cout << "Woman::speak" << std::endl;
}

在这里插入图片描述
含有虚函数的类,在编译期间会生成一张虚函数表及虚表指针。虚函数表其实是一个指针数组,里面的元素是虚函数地址。而续表指针指向虚函数表的首地址。也可以这么认为编译器给含有虚函数的类,自动增加了一个 const void* pvtr 的指针成员变量。和一个静态的 static const void* vtable[]的指针数组;也就是说同一个类的虚函数表是共享的,同一个类下不同对象的虚表指针指向的同一个虚函数表。
在这里插入图片描述

测试代码

#include "Person.h"
#include "Man.h"
#include "Woman.h"

//test函数的声明
void test(Person* person);
void test(Person& person);

int main(int argc, char* argv[])
{
	Person person;
	test(&person);//传地址
	//test(person);

	Man man;
	test(&man);//传地址
	//test(man);

	Woman woman1;
	test(&woman1);//传地址

	Woman woman2;
	test(&woman2);//传地址

	return 0;
}

//test函数的实现,体现了多态
void test(Person* person)//指针传递
{
	person->speak();
	//person->eat();
	//person->walk();
}

void test(Person& p)//引用传递
{
	p.speak();
	p.eat();
	p.walk();
}

打印结果:
在这里插入图片描述

动态绑定

虚函数的好处就是体现了C++中多态。即当基类类型的指针或引用 指向子类的对象时,在调用虚函数时,实际调用的虚函数是子类对象中的虚函数。
就像上面的test(Person* person)函数,编译器在编译期间不确定函数内实际调用哪个类中的speak函数,需要在实际代码执行时,才能确定,指针 person 到底指向的实际对象是哪个类的,从而实现了动态绑定,即多态性。

静态绑定

所有的类中的非虚成员函数都是静态绑定的。即在编译期间就确定了实际调用哪个函数。普通成员函数的实际调用者,是根据代码里的调用者的类型来判断的,即在编译期就能确定,无法体现多态性。

注意:即使派生类没有重写基类中的虚函数,也没有自己特有的虚函数。那么派生类就会继承父类中的虚函数,即派生类也拥有虚函数表及虚表指针,只是自己的虚函数表中的虚函数都是父类的虚函数,除非自己重写过,虚函数表中的基类虚函数地址会被替换为自己重写后的。

访问私有虚函数

我们把上方的代码修改一下:

class Person
{
private:
	virtual void speak(){
		cout << "Person::speak" << endl;
	}
public:
	virtual void eat(){
		cout << "Person::eat" << endl;
	}
}

class Woman : public Person
{
public:
	void eat(){
		cout << "Woman::eat" << endl;
	}
};
int main(int argc, char *argv[])
{
	Person person;
	//person.speak();//编译报错,无法访问私有属性成员
	Woman woman;
	//woman.speak();//编译报错,同上
	//void (Person:: *fun1)(void) = &Person::speak;//编译报错,右值报错,无法通过&Person::speak获取函数地址。还是因为私有属性问题
	return 0;
}

我们想要在类的定义外部访问speak函数。通过常规的手段是访问不到的。

int main(int argc, char *argv[])
{
	typedef void (*Fun)(void);//给函数指针类型取别名
	Woman woman;
	Fun pfun1 = (Fun)*((int*)*(int*)&woman);
	Fun2 pfun2 = (Fun)*((int*)*(int*)&woman + 1);//此时pfun2 将不再是一个被类作用域限制的成员函数的指针了,而相当于一个全局函数的指针。
	
	pfun1();//输出结果:Person::speak
	pfun2();//输出结果:Woman::eat
	return 0;
}

分析一下:上面的pfun1 和 pfun2 函数指针,在执行时的输出结果。首先要明白一点,含有虚函数的类,在编译时,创建的虚表指针变量会优化为类中第一个成员属性,那么创建对象时,虚表指针所占的内存地址和对象的地址是相同的。类似于 数组首元素的地址和数组名的关系。
在这里插入图片描述
上方的图中的:有两处的地址为何可以转为(int )即int型指针,因为地址值就是整型的。虚表指针vptr的值是地址,虚函数表元素也是地址。
又因为指针的类型和指针所指向的地址存储的内容类型是关联的。另一处是转为(Fun)即函数指针。也是一个道理,函数指针指向的地址(即函数地址)存储的是真实的函数。
(int
)(int)&woman //指针指向虚函数表中第一个元素
(int*)(int)&woman + 1 //指针指向虚函数表中第二个元素
注意:这种方式能够获取私有虚函数的地址,不能获取私有普通成员函数的地址。这也是虚函数的特殊实现原理优势的。
而且这种方式最后获取的虚函数指针,不用再使用对象.* 或对象.->的动态方式来访问。直接 虚函数指针名(参数);来调用函数。

总结一下通过成员函数指针调用函数的方式

void (Woman:: *fun1)(void) = &Woman ::speak;//访问公共属性的成员普通函数或成员虚函数
Woman woman;
(woman.*fun1)();//调用speak函数
Woman & woman1 = &woman;
(woman.->fun1)();//调用speak函数

typedef void (*Fun)(void);
Fun fun = (Fun)*((int*)*(int*)&woman);//访问公共成员虚函数或者私有成员函数
fun();//调用speak函数

关于函数地址的相关博客类中成员函数及普通函数地址获取方式

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

❀My小学习之排序算法❀

目录 排序算法&#xff08;Sorting algorithm&#xff09;:) 一、定义 二、分类 三、评价标准 排序算法&#xff08;Sorting algorithm&#xff09;:) 一、定义 所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的…

【网络技术】【Kali Linux】Wireshark嗅探(一)ping和ICMP

一、实验目的 本次实验使用wireshark流量分析工具进行网络嗅探&#xff0c;旨在了解ping命令的原理及过程。 二、网络环境设置 本系列实验均使用虚拟机完成&#xff0c;主机操作系统为Windows 11&#xff0c;虚拟化平台选择Oracle VM VirtualBox&#xff0c;组网模式选择“N…

c语言的数组

#在这一篇中介绍三个数组 1.一维数组 2.字符数组 3.二维数组 1.一维数组 数组的定义&#xff1a; 类型 数组名[元素个数] ----这个为数组的定义 #include <stdio.h> int main() {int a[4];//在这里&#xff0c;我定义了一个只能装载4个整数类型元素的数组char b[5…

WPF+Halcon 培训项目实战(1-5):Halcon安装,图像处理,Halcon简单模板匹配

文章目录 前言相关链接项目专栏我个人对就业市场的评价Halcon安装实战1-4&#xff1a;Halcon基础实战5&#xff1a;模板匹配[形状匹配]实战代码 结尾 前言 为了更好地去学习WPFHalcon&#xff0c;我决定去报个班学一下。原因无非是想换个工作。相关的教学视频来源于下方的Up主…

IDEA 开发中常用的快捷键

目录 Ctrl 的快捷键 Alt 的快捷键 Shift 的快捷键 Ctrl Alt 的快捷键 Ctrl Shift 的快捷键 其他的快捷键 Ctrl 的快捷键 Ctrl F 在当前文件进行文本查找 &#xff08;必备&#xff09; Ctrl R 在当前文件进行文本替换 &#xff08;必备&#xff09; Ctrl Z 撤…

excel 函数技巧

1&#xff1a;模糊查询 LOOKUP(1,0/FIND(F1062,Sheet1!C$2:Sheet1!C$9135),Sheet1!B$2:Sheet1!B$9135) 函数含义&#xff1a;寻找F列1062行和sheet1中的C2行到C9135行进行模糊查询&#xff0c;返回该行对应的B2行到B9135行的结果。未查到返回结果0 函数公式&#xff1a; LO…

基于Mbed Studio环境下开发STM32

基于Mbed Studio环境下开发STM32 &#x1f4cd;Mbed官网&#xff1a;https://os.mbed.com/ ✨mbed OS是ARM出的一个免费开源的&#xff0c;面向物联网的操作系统。提供了一个定义良好的API来开发C应用程序&#xff1b;集成度很高&#xff0c;类似Arduino&#xff0c;目前并不兼…

Flink on K8S生产集群使用StreamPark管理

&#xff08;一&#xff09;直接部署&#xff08;手动测试用&#xff0c;不推荐&#xff09; Flink on Native Kubernetes 目前支持 Application 模式和 Session 模式&#xff0c;两者对比 Application 模式部署规避了 Session 模式的资源隔离问题、以及客户端资源消耗问题&am…

Linux操作系统基础:从入门到实践

目录 学习目标&#xff1a; 学习内容&#xff1a; 学习时间&#xff1a; 学习产出&#xff1a; Linux操作系统概述 Linux操作系统的定义和主要特点 Linux操作系统与其他操作系统的比较 Linux操作系统在不同领域的应用案例 Linux操作系统的历史 Linux操作系统的起源和发展过程 L…

浅谈WPF之ToolTip工具提示

在日常应用中&#xff0c;当鼠标放置在某些控件上时&#xff0c;都会有相应的信息提示&#xff0c;从软件易用性上来说&#xff0c;这是一个非常友好的功能设计。那在WPF中&#xff0c;如何进行控件信息提示呢&#xff1f;这就是本文需要介绍的ToolTip【工具提示】内容&#xf…

生产系统稳定上线600天!中国联通CUDB for OceanBase的开源共建和规模化应用

中国联通软件研究院架构部平台承载了上千应用的数据库需求&#xff0c;并且现存大量数据库使用过程缺少规范、缺少监控&#xff0c;同时还存在着数据库核心技术相关风险。为了实现核心技术自主可控&#xff0c;及时为用户解决线上问题、满足用户的功能需求&#xff0c;提供物美…

procise纯PL流程点灯记录

procise纯PL流程点灯记录 一、概述 此篇记录使用procise工具构造JFMQL15T 纯PL工程&#xff0c;显示PL_LED闪烁&#xff1b; 硬件说明如下&#xff1a; 时钟引脚 Pl_CLK: U2 ,IO_L14P_T2_SRCC_34 PL_LED1 : E2, IO_L17P_T2_AD5P_35 PL_LED2: D6, IO_L2N_T0_AD8N_35 PL_LED3 :…

C++11 lambda函数和包装器

目录 前言 一.lambda的引入 二、lambda函数的使用 1.一般使用 2.引用 三、包装器 1.包装普通对象 2.包装类成员对象 3.bind 前言 学习过python的同学应该对lambda函数不陌生&#xff0c;这是一个匿名函数&#xff0c;不需要写函数的名字。在不会多地方调用某个简单函数…

C# vs报错 id为XX的进程当前未运行

报错原因&#xff1a;虚拟目录端口被占用 解决方法&#xff1a;重新配置新的目录端口就行 1、选择项目属性 2、更改端口号&#xff0c;点击创建虚拟目录 3、重新生成项目

C# WPF上位机开发(MVVM模式开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 学习过vue的同学都知道mvvm这个名词。从字面上理解&#xff0c;可能有点拗口&#xff0c;但是我们可以去理解一下它的优点是什么。mvc相信大家都明…

InDesign插件-常规功能开发-添加参考线-js脚本开发-ID插件

文章目录 1.脚本执行概述2.InDesign 对象模型3.源码解析4.界面及结果5.总结 1.脚本执行概述 “脚本”面板和“脚本标签”面板概述&#xff0c;InDesign 包含两个用于脚本的面板&#xff1a;“脚本”面板和“脚本标签”面板。在“脚本”面板中可以运行脚本而不必离开 InDesign。…

Python sanic框架钉钉和第三方打卡机实现

同样还是需要开通钉钉应用这里就不错多说了 第一步:梳理逻辑流程 前提&#xff1a;打卡的机器是使用postgres数据库&#xff0c;由于因为某些原因&#xff0c;钉钉userId 我已经提前获取到了存放到数据库里。 1.用户打卡成功后&#xff0c;我们应该监听数据库进行查询&#xf…

【教学类-35-07】17号的字帖(三)年份字帖“2023”(A4竖版1份)

作品展示 前四行是一个数字的描写 后四行是合并的年份4个数字 背景需求&#xff1a; 大4班17号孩子练习数字书写&#xff0c;上一次是“17”号和大“4”&#xff0c;第3份就是年份 【教学类-35-05】17号的学号字帖&#xff08;A4竖版1份&#xff09;-CSDN博客文章浏览阅读4…

数据结构--查找

目录 1. 查找的基本概念 2. 线性表的查找 3. 树表的查找 3.1 二叉排序树 3.1.1 定义: 3.1.2 存储结构&#xff1a; 3.1.3 二叉排序树的查找 3.1.4 二叉排序树的插入 3.1.5 二叉排序树删除 3.2 平衡二叉树&#xff08;AVL 3.2.1 为什么要有平衡二叉树 3.2.2 定义 3.3 B-树 3.3.1…

Flink1.17实战教程(第五篇:状态管理)

系列文章目录 Flink1.17实战教程&#xff08;第一篇&#xff1a;概念、部署、架构&#xff09; Flink1.17实战教程&#xff08;第二篇&#xff1a;DataStream API&#xff09; Flink1.17实战教程&#xff08;第三篇&#xff1a;时间和窗口&#xff09; Flink1.17实战教程&…