77 C++对象模型探索。虚函数- 从静态联编,动态联编出发,分析 虚函数调用问题探究

什么叫做单纯的类:

比较简单的类,尤其不包括 虚函数 和虚基类。

什么叫不单纯的类:

从上一章的学习我们知道,在某些情况下,编译器会往类内部增加一些我们看不见但是真实存在的成员变量,例如vptr,有了这种变量的类,我们叫做不单纯的类。

同时,这种隐藏的成员变量的增加(使用)或者赋值的时机,往往都是在执行构造函数体之前,或者拷贝构造函数体之前。

这样做的问题?

因此,很容易想到,如果在构造函数体中有memset(this,0,sizeof(Teacher)) 这样的代码,就会把 编译器往类内部增加的vptr清空。

如果在copy构造函数中,使用了memcpy(this,&tm,sizeof(Teacher)); 这样的代码,就会把tm的vptr的值 copy 到this中去,很显然,这也是有问题的。

验证此问题的存在。

//验证在构造方法和copy构造方法中使用了memset 让vptr清空;和使用memcpy 后,让this的vptr的值和tm一样的问题//要有vptr,就需要有virtual 函数
class Teacher41 {
public:Teacher41() {memset(this,0,sizeof(Teacher41));cout << "Teacher41的构造方法被执行" << endl;}Teacher41(const Teacher41 & tm) {memcpy(this,&tm,sizeof(Teacher41));cout << "Teacher41的copy构造方法被执行" << endl;}virtual void virfunc() {cout << "Teacher41 virfunc 方法被执行" << endl;}virtual ~Teacher41() {cout << "Teacher41的析构方法被执行" << endl;}};void main() {Teacher41 tea;//理论上,Teacher41在构造函数中会将vptr的值清空Teacher41 *ptea = &tea;long * temptea = (long *)ptea;long* vptr = (long *)(*temptea);cout << "断点在这里" << endl;}

我们debug看到,如上的代码看到vptr里面的值果然变成了0X00000000

如果将上面的memset 和 memcpy 的代码注释掉,debug发现,vptr的就有具体的值的了

继续验证此问题的存在

那么按照推论,这时候我们再去访问 virtual 的函数就会有问题。因为vptr指针都指向了了0X00000000,那么通过vptr查找的时候,一定会有nullpoint exception,或者访问非法路径的问题。当然也无法通过vptr找到虚函数表。

当我们执行如下的代码访问虚函数 virfunc的时候,代码居然正常运行了。

使用类对象调用虚函数:OK

	Teacher41 tea1;tea1.virfunc();//运行结果:Teacher41 virfunc 方法被执行

按照我们之前的理解,这个能正常运行的结论是不对的。

再使用指针访问虚函数:error

	Teacher41 tea;//理论上,Teacher41在构造函数中会将vptr的值清空Teacher41 *ptea = &tea;long * temptea = (long *)ptea;long* vptr = (long *)(*temptea);ptea->virfunc();cout << "断点在这里" << endl;

原因:这里就需要知道什么是静态联编,什么是动态联编。

这里虽然有虚函数指针,但是由于类对象调用,是静态联编,虽然将vptr的置为0X00000000了,但是由于静态联编是早都绑定了,不需要使用vptr,因此不会有问题发生。

什么叫联编?

在C++中,联编是指一个计算机程序的不同部分彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。

1.静态联编

静态联编是指联编工作在编译阶段完成的,这种联编过程是在程序运行之前完成的,又称为早期联编。要实现静态联编,在编译阶段就必须确定程序中的操作调用(如函数调用)与执行该操作代码间的关系,确定这种关系称为束定,在编译时的束定称为静态束定。静态联编对函数的选择是基于指向对象的指针或者引用的类型。其优点是效率高,但灵活性差。

例1 :静态联编

class A
{
public:void f() { cout << "A" << ""; }
};class B:public A
{
public:void f() { cout << "B" << endl; }
};void main()
{A a;B b;A *pa = NULL;pa = &a;pa->f();pa = &b;//会把b继承a的部分赋值给papa->f();}

该程序的运行结果为:A   A

从例1程序的运行结果可以看出,通过对象指针进行的普通成员函数的调用,仅仅与指针的类型有关,而与此刻指针正指向什么对象无关。要想实现当指针指向不同对象时执行不同的操作,就必须将基类中相应的成员函数定义为虚函数,进行动态联编。

2.动态联编:

动态联编是指联编在程序运行时动态地进行,根据当时的情况来确定调用哪个同名函数,实际上是在运行时虚函数的实现。这种联编又称为晚期联编,或动态束定。动态联编对成员函数的选择是基于对象的类型,针对不同的对象类型将做出不同的编译结果。C++中一般情况下的联编是静态联编,但是当涉及到多态性和虚函数时应该使用动态联编。动态联编的优点是灵活性强,但效率低。

动态联编规定,只能通过指向基类的指针或基类对象的引用来调用虚函数,其格式为:指向基类的指针变量名->虚函数名(实参表)或基类对象的引用名.虚函数名(实参表)

实现动态联编需要同时满足以下三个条件:

① 必须把动态联编的行为定义为类的虚函数。

② 类之间应满足子类型关系,通常表现为一个类从另一个类公有派生而来。

③ 必须先使用基类指针指向子类型的对象,然后直接或者间接使用基类指针调用虚函数

例 2 动态联编

 
#include"iostream.h"
using namespace std;classA
{public:virtual void f()//虚函数{cout<<"A"<<"";}
};classB:public A
{public:virtual void f()//虚函数{cout<<"B"<<endl;}
};void main()
{ A*pa=NULL;A a;B b;pa=&a;pa->f();pa=&b;pa->f();
}

该程序的运行结果为:A  B

从例2程序的运行结果可以看出,将基类A中的函数f定义为虚函数后,当指针指向不同对象时执行了不同的操作,实现了动态联编。

动态联编要求派生类中的虚函数与基类中对应的虚函数具有相同的名称、相同的参数个数和相同的对应参数类型、返回值或者相同,或者都返回指针或引用,并且派生类虚函数所返回的指针或引用的基类型是基类中虚函数所返回的指针或引用的基类型的子类型。

如果不满足这些条件,派生类中的虚函数将丢失其虚特性,在调用时进行静态联编。

例 3 通过指向基类的指针来调用虚函数

#include <iostream>
using namespace std;class base
{	
public:	virtual void fun1(){cout<<"base fun1"<<endl;}virtual void fun2(){cout<<"base fun2"<<endl;}void fun3(){cout<<"base fun3"<<endl;}void fun4(){cout<<"base fun4"<<endl;}};class derived:public base
{
public:	virtual void fun1(){cout<<"derived fun1"<<endl;}virtual void fun2(int x){cout<<"derived fun2"<<endl;}virtual void fun3(){cout<<"derived fun3"<<endl;}void fun4(){cout<<"derived fun4"<<endl;}
};int main(){base *pb;derived d;pb=&d;   //通过指向基类的指针来调用虚函数pb->fun1();pb->fun2();pb->fun3();pb->fun4();return 0;
}

输出结果为:

Derived fun1

base fun2

base fun3

base fun4

分析:本例中函数fun1在基类base和派生类derived中均使用了关键字virtual定义为虚函数,并且这两个虚函数具有相同的参数个数、参数类型和返回值类型。因此,当指针pb访问fun1函数时,采用的是动态联编。函数fun2在基类base和派生类derived中定义为虚函数,但这两个虚函数具有不同的参数个数,函数fun2丢失了其虚特性,在调用时进行静态联编。函数fun3在基类base中说明为一般函数,在派生类derived中定义为虚函数。在这种情况下,应该以基类中说明的成员函数的特性为标准,即函数fun3是一般成员函数,在调用时采用静态联编。函数fun4在基类base和派生类derived中均说明为一般函数,因此基类指针pb只能访问base中的成员。

例 4:通过基类对象的引用来调用虚函数

 
#include <iostream>
using namespace std;class CPoint
{
public:CPoint(double i,double j){x=i;y=j;}virtual double Area(){return 0;}private:	double x,y;};class CRectangle:public CPoint
{
public:CRectangle(double i, double j, double k, double l);double Area(){return w*h;}private:double w,h;};CRectangle::CRectangle(double i, double j, double k, double l):CPoint(i,j)
{ w=k;h=l; 
}void fun(CPoint &s)
{  cout<<s.Area()<<endl; }//通过基类对象的引用来调用虚函数int  main()
{	CRectangle rec(3, 5.2, 15, 25);	fun(rec);	return 0;
}

该程序的运行结果为:375

 例4中的成员函数Area在基类CPoint中使用了关键字virtual定义为虚函数,在派生类CRectangle中定义为一般函数,但是进行了动态联编(以基类为准),结果为15*25即375。这是因为一个虚函数无论被公有继承多少次,它仍然保持其虚特性。在派生类中重新定义虚函数时,关键字virtual可以写也可不写,但为了保持良好的编程风格,避免引起混乱时,应写上该关键字。

结论:

不要在构造函数中,直接使用memset。

不要在copy 构造函数中,直接使用memcpy。

在有虚函数的类中,只要使用了指针,或者使用了引用,都不要使用mem之类的函数,不要将整个空间清空,拷贝,移动之类的。

额外的验证 在其他地方能用这个memset 和 memcpy吗?

验证一下:

在构造函数和copy构造函数中,已经取消了 memset 和 metcpy

使用指针,发生异常

	Teacher41 tea1;Teacher41 *ptea1 = &tea1;memset(ptea1, 0, sizeof(Teacher41));ptea1->virfunc();

不使用指针,不使用引用

如下的是OK的。

	Teacher41 tea1;memset(&tea1,0,sizeof(Teacher41));tea1.virfunc();

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

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

相关文章

【shell-10】shell实现的各种kafka脚本

kafka-shell工具 背景日志 log一.启动kafka->(start-kafka)二.停止kafka->(stop-kafka)三.创建topic->(create-topic)四.删除topic->(delete-topic)五.获取topic列表->(list-topic)六. 将文件数据 录入到kafka->(file-to-kafka)七.将kafka数据 下载到文件-&g…

Linux内核中USB设备驱动实现

USB 设备驱动&#xff1a; 一、USB 描述符&#xff1a;&#xff08;存在于USB 的E2PROM里面&#xff09; 1、 设备描述符&#xff1a;struct usb_device_descriptor 2、 配置描述符&#xff1a;struct usb_config_descriptor 3、 接口描述符&#xff1a;struct usb_interfa…

【Deeplabv3+】Ubutu18.04中使用pytorch复现Deeplabv3+第三步)-----CityscapesScripts生成自己的标签

本文是在前面两篇文章的基础上&#xff0c;讲解如何更改训练数据集颜色&#xff0c;需要与前面两篇文章连起来看。 本文用于修改cityscapes数据集的标签颜色与Semankitti数据集的标签一致&#xff0c;对修改后的数据集进行训练。需要下载两个开发工具包和一个数据集&#xff0…

Git标签推送

标签默认属于本地分支&#xff0c;推送分支的时候并不会上传。需要自己手动推送 通过命令 git push origin <tagname>推送指定的标签 通过命令git push origin --tags批量推送所有的标签 在VS里打开git命令行窗口的方法&#xff1a;Git更改-操作-打开命令行提示符 对于…

1.19信息学,信息熵(wordle)

所谓均方误差实际上就是方差 分析&#xff1a;对单词进行编码后&#xff0c;采用聚类方法&#xff0c;可以将单词难度分为三类或者更多&#xff0c;如困难、一般、简单。然后对每一类的单词可视化分析&#xff0c;并描述数据得出结论。 聚类算法较多&#xff0c;在论文中可以…

Docker镜像

创建镜像有三种方法&#xff0c;分别为基于已有镜像创建、基于本地模板创建以及基于Dockerfile创建。 基于现有镜像创建 首先启动一个镜像&#xff0c;在容器里做修改 然后将修改后的容器提交为新的镜像&#xff0c;需要使用该容器的 ID 号创建新镜像 常用选项&#xff1a; -…

【Unity】【游戏开发】Pico打包后项目出现运行时错误如何Debug

【背景】 开发过程中的报错可以通过控制台查看&#xff0c;但是PICO项目这类依赖特定设备环境的应用往往存在打包后在设备端发生运行时错误。这时如何能查看到Debug信息呢&#xff1f; 【分析】 Pico也是安卓系统&#xff0c;所以这个问题就可以泛化为Unity有哪些在安卓端运…

C++实现推箱子游戏

推箱子游戏 运行之后的效果如视频所示&#xff0c;在完成游戏后播放音乐 准备工作&#xff1a;建立一个新的文件夹&#xff0c;并在文件夹中任意增加一张背景图片&#xff0c;以及各个部件的照片文件 因为这里用到了贴图技术&#xff0c;要使用graphic.h这个函数&#xff0c…

海外云手机三大优势

在全球化潮流下&#xff0c;企业因业务需求对海外手机卡等设备的需求不断攀升&#xff0c;推动了海外云手机业务的蓬勃发展。相较于自行置备手机设备&#xff0c;海外云手机不仅能够降低成本&#xff0c;还具备诸多优势&#xff0c;让我们深入探讨其中的三大黄金优势。 经济实惠…

【Linux】进程概述

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

统计学-R语言-8.1

文章目录 前言方差分析方差分析的原理什么是方差分析误差分解 单因子方差分析数学模型效应检验 练习 前言 本片开始介绍有关方差分析的知识。 方差分析 方差分析的基本原理是在20世纪20年代由英国统计学家Ronald A.Fisher在进行实验设计时为解释实验数据而首先引入的。方差分…

最新多功能PHP图床源码 /兰空图床Lsky Pro开源版v2.1/ 单纯的图床程序源码

源码介绍&#xff1a; Lsky Pro 是一个用于在线上传、管理图片的图床程序&#xff0c;中文名&#xff1a;兰空图床&#xff0c;你可以将它作为自己的云上相册&#xff0c;亦可以当作你的写作贴图库。 该程序的最初版本诞生于2017年10月&#xff0c;由ThinkPHP 5框架精心打造而…

【Linux 基础】常用基础指令(上)

文章目录 一、 创建新用户并设置密码二、ls指令ls指令基本概念ls指令的简写操作 三、pwd指令四、cd指令五、touch指令六、rm指令七、mkdir指令八、rmdir 指令 一、 创建新用户并设置密码 ls /home —— 查看存在多少用户 whoami —— 查看当前用户名 adduser 用户名 —— 创建新…

08 BGP 华为官方文档 十一条选路原则

BGP 华为官方文档 十一条选路原则 丢弃下一跳不可达的路由 1&#xff09;比较“协议首选值-pref-val”属性&#xff0c;数值越大越好&#xff0c;默认值是0&#xff0c;只在本设备生效&#xff0c;不在网络中传递 2&#xff09;比较“本地优先级-local_pref”属性&#xff0c;…

Ceph分布式存储自动化运维平台开发实践

文章目录 1. 背景介绍1.1 什么是Ceph&#xff1f;1.1.1 Ceph的核心组件1.1.2 Ceph的优势 1.2 自动化运维的需求目标 2. 平台架构设计和组件版本2.1 平台架构设计2.2 组件版本2.3 模块划分&#xff08;已经脱敏处理&#xff09;2.3.1 当前版本V1.0支持功能2.3.2 前后端代码结构t…

利用 “diart“ 和 OpenAI 的 Whisper 简化实时转录

利用 "diart" 和 OpenAI 的 Whisper 简化实时转录 工作原理 Diart 是一个基于人工智能的 Python 库&#xff0c;用于实时记录说话者语言&#xff08;即 "谁在什么时候说话"&#xff09;&#xff0c;它建立在 pyannote.audio 模型之上&#xff0c;专为实时…

微信小程序 仿微信聊天界面

1. 需求效果图 2. 方案 为实现这样的效果&#xff0c;首先要解决两个问题&#xff1a; 2.1.点击输入框弹出软键盘后&#xff0c;将已有的少许聊天内容弹出&#xff0c;导致看不到的问题 点击输入框弹出软键盘后&#xff0c;将已有的少许聊天内容弹出&#xff0c;导致看不到的问…

银行数据仓库体系实践(8)--主数据模型设计

主数据区域中保留了数据仓库的所有基础数据及历史数据&#xff0c;是数据仓库中最重要的数据区域之一&#xff0c;那主数据区域中主要分为近源模型区和整合&#xff08;主题&#xff09;模型区。上一节讲到了模型的设计流程如下图所示。那近源模型层的设计在第2.3和3这两个步骤…

85 总结一下最近遇到的一些 jar发布 相关的知识

前言 呵呵 最近有一些构建服务, 发布服务的一些需求 我们这里的服务 一般来说是 java application, spring boot application 针对发布, 当然最好是 增量发布, 尽量的减少需要传递给 发布服务器 的资源的大小 比如 我的这个 java application, 可能会存在很多依赖, 常规…

探讨Go语言在构建HTTP代理时的优势和挑战

亲爱的读者&#xff0c;让我们一起来探讨一下Go语言在构建HTTP代理时的优势和挑战。 首先&#xff0c;让我们来谈谈Go语言在构建HTTP代理时的优势。Go语言是一种高性能的编程语言&#xff0c;它具有简洁、高效的特点&#xff0c;非常适合构建高效的代理服务器。使用Go语言&…