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

news2024/9/30 19:30:59

什么叫做单纯的类:

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

什么叫不单纯的类:

从上一章的学习我们知道,在某些情况下,编译器会往类内部增加一些我们看不见但是真实存在的成员变量,例如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的部分赋值给pa
	pa->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.coloradmin.cn/o/1413516.html

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

相关文章

matlab appdesigner系列-图窗工具2-工具栏

工具栏&#xff0c;就是一般在任意软件界面上方的工具菜单栏 示例&#xff1a;工具菜单绘制正弦函数 操作步骤如下&#xff1a; 1&#xff09;将坐标区和工具栏拖拽到画布上 2)点击工具栏的号&#xff0c;可以看到可以添加2种工具&#xff0c;按钮工具和切换工具&#xff0c…

【JavaScript权威指南第七版】读书笔记速度

JavaScript权威指南第七版 序正文前言&#xff1a;图中笔记重点知识第1章 JavaScript简介第一章总结 第2章 词法结构注释字面量标识符和保留字Unicode可选的分号第二章总结 第3章 类型、值和变量【重要】原始类型特殊类型第三章总结 第4章 表达式与操作符表达式操作符条件式调用…

【量化交易】股市舞者:小明的撮合交易之旅

马西森AES撮合交易系统 在繁华的都市中&#xff0c;小明&#xff0c;一个普通的青年&#xff0c;刚刚赚到了人生的第一桶金——20万。这笔意外的财富&#xff0c;点燃了他对股市的强烈兴趣。他开始如饥似渴地学习金融知识&#xff0c;钻研各种交易策略。 一天&#xff0c;小…

基于 java+springboot+mybatis电影售票网站管理系统前台+后台设计和实现

基于 javaspringbootmybatis电影售票网站管理系统前台后台设计和实现 &#x1f345; 作者主页 央顺技术团队 &#x1f345; 欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; &#x1f345; 文末获取源码联系方式 &#x1f4dd; &#x1f345; 查看下方微信号获取联系方式 承…

微软 Power Apps Canvas App 画布应用将上传的附件转化为base64编码操作

微软 Power Apps Canvas App 画布应用将上传的附件结合Power Automate转化为base64编码操作 在使用canvas app的过程中&#xff0c;我们有时需要将上传的文件转换为base64存入数据库或者&#xff0c;调用外部接口传参&#xff0c;那么看下如何将文件转化为base64编码格式。 首先…

金智易表通构建学生缴费数据查询+帆软构建缴费大数据报表并整合到微服务

使用金智易表通挂接外部数据,快速建设查询类服务,本次构建学生欠费数据查询,共有3块设计,规划如下: 1、欠费明细查询:学校领导和财务处等部门可查询全校欠费学生明细数据;各二级学院教职工可查询本二级学院欠费学生明细数据。 2、大数据统计报表:从应收总额、欠费总额…

C语言编程中的陷阱与规避策略

一、引言 C语言作为一门历史悠久且广泛应用的编程语言&#xff0c;其强大的功能和灵活性深受开发者喜爱。然而&#xff0c;这种灵活性也带来了许多潜在的陷阱和难点&#xff0c;特别是对于新手来说&#xff0c;可能会在编程过程中遇到各种预料之外的问题。本文将深入探讨C语言…

自动验证码解析器:CapSolver的Chrome扩展程序自动解析器

自动验证码解析器&#xff1a;CapSolver的Chrome扩展程序自动解析器 验证码是网站实施的一种安全措施&#xff0c;通常对用户构成挑战。然而&#xff0c;随着技术的进步&#xff0c;验证码解析器已经出现&#xff0c;以简化这一过程。在本文中&#xff0c;我们将探讨专为Googl…

【华为 ICT HCIA eNSP 习题汇总】——题目集9

1、缺省情况下&#xff0c;广播网络上 OSPF 协议 Hello 报文发送的周期和无效周期分别为&#xff08;&#xff09;。 A、10s&#xff0c;40s B、40s&#xff0c;10s C、30s&#xff0c;20s D、20s&#xff0c;30s 考点&#xff1a;①路由技术原理 ②OSPF 解析&#xff1a;&…

【Unity3D日常开发】Unity3D中UGUI的Text、Dropdown输入特殊符号

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 在开发中会遇到需要显示特殊符号的情况&#xff0c;比如上标、…

机房及设备安全智慧监管AI+视频方案的设计和应用

一、背景分析 随着互联网的迅猛发展&#xff0c;机房及其配套设施的数量持续攀升&#xff0c;它们的运行状况对于企业运营效率和服务质量的影响日益显著。作为企业信息化的基石&#xff0c;机房的安全监测与管理的重要性不容忽视。它不仅关乎企业的稳定运营&#xff0c;同时也…

[docker] Docker的私有仓库部署——Harbor

一、Docker原生私有仓库—— Registry 1.1 Registry的简单了解 关于Docker的仓库分为私有库和公有仓库&#xff0c;共有仓库只要在官方注册用户&#xff0c;登录即可使用。但对于仓库的使用&#xff0c;企业还是会有自己的专属镜像&#xff0c;所以私有库的搭建也是很有必要的…

Java复习系列之阶段三:框架原理

1. Spring 1.1 核心功能 1. IOC容器 IOC&#xff0c;全称为控制反转&#xff08;Inversion of Control&#xff09;&#xff0c;是一种软件设计原则&#xff0c;用于减少计算机代码之间的耦合度。控制反转的核心思想是将传统程序中对象的创建和绑定由程序代码直接控制转移到…

Android SharedPreferences源码分析

文章目录 Android SharedPreferences源码分析概述基本使用源码分析获取SP对象初始化和读取数据写入数据MemoryCommitResultcommitToMemory()commit()apply()enqueueDiskWrite()writeToFile() 主动等待写回任务结束 总结 Android SharedPreferences源码分析 概述 SharedPrefer…

《Vue3 基础知识》 Vue2+ElementUI 自动转 Vue3+ElementPlus(GoGoCode)

前言 GoGoCode 一个基于 AST 的 JavaScript/Typescript/HTML 代码转换工具。 AST abstract syntax code 抽象语法树。 实现 第一步&#xff1a;安装 GoGoCode 插件 全局安装最新的 gogocode-cli 即可 npm i gogocode-cli -g查看版本 gogocode-cli -V相关插件说明 插件描述…

qt 坦克大战游戏 GUI绘制

关于本章节中使用的图形绘制类&#xff0c;如QGraphicsView、QGraphicsScene等的详细使用说明请参见我的另一篇文章&#xff1a; 《图形绘制QGraphicsView、QGraphicsScene、QGraphicsItem、Qt GUI-CSDN博客》 本文将模仿坦克大战游戏&#xff0c;目前只绘制出一辆坦克&#…

看懂linux内核详解实现分解

一、linux的内核管理&#xff1a;对内核的基本认识 我们所谈到的操作系统主要指内核 以上功能据没有涉及实现文本编辑、实现字处理&#xff0c;也没有服务等等。 故&#xff0c;操作系统是一种通用软件&#xff0c;是平台类软件&#xff0c;自己并不做任何工作&#xff0c;只…

[嵌入式软件][启蒙篇][仿真平台] STM32F103实现IIC控制OLED屏幕

上一篇&#xff1a;[嵌入式软件][启蒙篇][仿真平台] STM32F103实现LED、按键 [嵌入式软件][启蒙篇][仿真平台] STM32F103实现串口输出输入、ADC采集 [嵌入式软件][启蒙篇][仿真平台]STM32F103实现定时器 [嵌入式软件][启蒙篇][仿真平台] STM32F103实现IIC控制OLED屏幕 文章目…

Unity中URP下额外灯角度衰减

文章目录 前言一、额外灯中聚光灯的角度衰减二、AngleAttenuation函数的传入参数1、参数&#xff1a;spotDirection.xyz2、_AdditionalLightsSpotDir3、参数&#xff1a;lightDirection4、参数&#xff1a;distanceAndSpotAttenuation.zw5、_AdditionalLightsAttenuation 三、A…

【DevOps】Jenkins Extended E-mail 邮件模板添加自定义变量

文章目录 1、配置Jenkins邮箱2、配置告警模板1、配置Jenkins邮箱 略 2、配置告警模板 自定义变量:DYSK_PYTEST_STATUS // Uses Declarative syntax to run commands inside a container. pipeline {agent {kubernetes {cloud "kubernetes" //选择名字是kuberne…