多态与虚函数(补)

news2024/11/23 11:54:14

多态与虚函数(补)

  • 静态联编与动态联编的深层次理解
    • 多态底层原理
  • 示例
    • 示例一
    • 示例二
    • 示例三
    • 示例四
  • 对象与内存
  • 虚析构函数
  • 构造函数为什么不能是虚函数?

静态联编与动态联编的深层次理解

我们首先看下面一段代码

class object {
private: int value;
   public:
	   object(int x = 0) : value(x) {}
	   virtual void add() { cout << "object: :add()" << endl; }
	   virtual void fun() { cout << "object: :fun()" << endl; }
	   virtual void print() const
	   {
		   cout << "object::print()" <<endl;
	   }
};
class Base : public object {
private:
	int num;
public:
	Base(int x = 0) :object(x), num(x + 10) {}
	virtual void add() { cout << "Base: :add()" << endl; }
	virtual void fun() { cout << "Base: :fun()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }
};
class Test : public Base {
private:
	int count;
public:
	Test(int x = 0) :Base(x), count(x + 10) {}
	virtual void add() { cout << "Test: :add()" << endl; }
	virtual void print() const
	{
		cout << "Test: :print()" << endl;
	}
	virtual void show() { cout << "Test: :show()" << endl; }
};

大家可以试着画一下上面三个类的虚表,此处我就不画了
我们试着想一下在上面这三个类型的基础上运行一下代码是否可以运行通过

int main() {
	object* op = nullptr;
	Test test;
	op = &test;
	op->show();
	return 0;
}

有的同学可能会说这是可以运行通过的,因为我们的op指向了test对象,这就是错误的理解,该程序在编译时期就会出现错误,为什么呢?
我们知道编译器在编译时是按照类型识别的,而在编译识别的时候op是我们的objecct类型,而其类型中不存在所谓的show函数,所以呢就会在编译时期爆索,有的同学可能还会说但是我们的op指向了test,这是在运行时进行的,我们编译都通过不了更别提运行时了呀,要怎么运行通过呢?只能通过强转来实现,将op强转为test类型 ((Test*)op)->show();,这种方法不建议大家使用,因为会出现不确定因素导致程序崩溃,比如将以上代码改为

int main() {
	object* op = nullptr;
	object obj;
	Test test;
	op = &obj;
	((Test*)op)->show();
	return 0;
}

这样就会崩掉,因为obj中没有show函数。

多态底层原理

在上次我们讲述了多态底层是通过虚表指针查虚表来实现多态的,而虚表指针是如何查表的呢?
我们通过汇编语言来理解一下,在这里插入图片描述
这就是运行时的多态,而编译时的多态便是在编译时通过类型名绑定了对象,也就确定了要对ecx赋值的值,不存在查表这一系列操作。这也就是动态联编和静态联编的区别,注意对指针解引用再次通过对象点调用函数仍然是动态联编,((*ob).add())

示例

示例一

在这里插入图片描述
我们观察以上代码会出现什么问题?
在我们使用memset函数时对this指针进行了操作,这就使得我们该对象的虚表指针也被置为0了,也就是说虚表指针成为了野指针,导致了虚表无法被查找,无法通过指针调用虚函数。所以我们对this指针操作需要谨慎。

示例二

在这里插入图片描述
我们阅读上面代码,想一下运行结果是什么,是为什么这么调用呢?
我们在调用show函数时,函数中调用print函数使用的是this指针调用,很显然调用的是基类的print函数,但是传过去的this指针是Base指针,而在print函数中调用了add函数,这里也是使用this指针调用,而传过来的指针是Base指针,所以我们查虚表也是查的Base的虚表。
运行结果:
在这里插入图片描述

示例三

在这里插入图片描述
在这里插入图片描述
观察上面代码,想一想运行结果是什么?
很显然我们创建Base对象的时候首先会创建obj对象,所以首先会调用obj的构造函数,此时虚表指针指向obj的虚表,所以add(12)调用的是obj的add函数,然后创建完成之后创建Base对象,此时虚表指针指向Base虚表,所以此处的add查找的是Base的add,然后析构开始进入Base的析构函数(重置虚表指针),在~Base的时候调用add调用的是本类型的add函数,然后析构基类,在析构派生类的时候虚表指针已经被重置,指向了obj的虚表,所以析构基类时查找的也是obj的虚表。注意:::析构函数是静态联编,按照其类型名析构,防止编写程序时派生类对象已经析构了还通过该对象查找该类的虚表。(虚表在数据区)

示例四

在这里插入图片描述
思考一下以上代码会出现什么样的情况?
我们会发现在Base类中虚函数是私有的,我们会思考 在Base中func函数是私有的,可不可以调用。我们从程序主函数开始看,op是obj类型的,而obj中的func是共有的,所以呢编译时是可以通过的,我们将编译器给骗过去了,而运行的时候又是动态联编,因为编译时我们确定看初始值形参a=10,所以呢在动态联编的时候我们又是从虚表中进行查找不会经过private这一步,所以呢我们仍调用的是Base类型中的func函数,但是b的值变成了10,这就是我们编译时确定了的形参。输出结果:在这里插入图片描述
这个实例将动态联编和静态联编的使用达到了极致,希望大家可以都可以理解。

对象与内存

class object {
private: int value;
   public:
	   object(int x = 0) : value(x) {}
	   void func(int a = 10) {
		   cout << "object::func  a" <<a<<"  value:" << value << endl;
	   }
	   void print() const
	   {
		   cout << "object::print:" <<endl;
	   }
};
int main() {
	object* op = nullptr;
	//object obj;
	//op = &obj;
	op->print();
	return 0;
}

我们观察上面代码,才可是否可以运行?
答案是可以运行的,因为在print函数中,不存在对this指针的使用,直接输出。
在这里插入图片描述
而通过op调用func函数时就会报错,this指针为空。
我们对以上代码进行了修改如下:

class object {
private: int value;
   public:
	   object(int x = 0) : value(x) {}
	   void func(int a = 10) {
		   cout << "object::func  a" <<a<<"  value:" << value << endl;
	   }
	   virtual void print() const//2
	   {
		   cout << "object::print:" <<endl;
	   }
};
int main() {
	object obj(11);//3
	object* op = (object*)malloc(sizeof(object));//1
	(*op) = obj;//3
	new(op) object(obj);//4
	op->print();
	op->func();
	return 0;
}

我们首先知道当使用op调用print函数时会出现崩溃的情况,因为不存在this指针为nullptr,所以呢我们加入了1操作,申请了空间给op指针,这样我们就可以运行出结果,只是输出的value是随机值,因为我们只申请了空间但是没有赋值。为此我们进行了2操作,将print改成了虚函数,这样呢程序就会崩溃,因为不存在虚表指针来调用虚函数。然后我们添加了3操作,希望可以创建一个对象,通过对象与对象之间赋值来改变,但是这样呢程序仍然会崩溃,因为默认的对象与对象之间的赋值函数不会对虚表指针也进行赋值。所以我们使用了定位new,也就是通过系统操作把新的obj对象赋值给op,这样也就实现了对虚表指针的赋值,程序也就不会崩溃。

虚析构函数

首先我们给出一段代码,思考下面代码出现的问题。

class object {
private: int value;
   public:
	   object(int x = 0) : value(x) {}
	   ~object() {}
	   virtual void print(int x) //2
	   {   cout << "object::print:" <<x<<endl; }
};
class Base :public object {
	int num;
public:
	Base(int x = 0) :object(x + 10), num(x) { }
	~Base() {}
	void print(int x) { cout << "Base::print:" << x << endl; }
};
int main() {
	object* op = new Base(10);
	op->print(1);
	delete op;
	return 0;
}

上面代码我们会发现在调用print的时候调用的是Base的print函数,但是我们delete时就会只析构object对象导致出现内存泄漏,怎么解决这种问题呢?就是将obj的析构函数设置成虚析构函数,就可以实现动态析构。
根据赋值兼容规则,可以用基类的指针指向派生类的对象,如果使用基类指针指向动态创建的派生类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生类析构函数,可能存在内存泄漏问题。也就是上面代码出现的问题。
总结:在实现运行时多态,不管怎样调用析构函数都必须保证不出错,所以必须把析构函数定义为虚函数。类中没有虚函数就不要把析构函数定义为虚。

构造函数为什么不能是虚函数?

  • 构造函数的用途:创建对象,初始化对象中的属性,类型转换。
  • 在类中定义了虚函数就会有一个虚函数表,对象模型中就含有一个指向虚表的指针。在定义对象时构造函数设置虚表指针指向虚表。
  • 构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。
  • 如果构造函数可以定义为虚构造函数,使用指针调用虚析构造函数,如果编译器采用静态联编,构造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数,相当于已经实例化的对象在调用构造函数这是不允许的,对象的构造函数只能执行一次。
  • 如果指针可以调用需构造函数,通过查虚表调用构造函数,那么指针为nullptr就会出现错误。
  • 构造函数在编译时确定,如果是虚函数,编译器怎么知道你想构建是继承树上的哪一种呢?

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

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

相关文章

C learning_12 操作符前篇(算术操作符、移位操作符、位操作符、赋值操作符、单目操作符、关系操作符、逻辑操作符)

目录 算术操作符 移位操作符 移位规则 位操作符 交换两个整形变量的写法 赋值操作符 单目操作符 sizeof和数组的纠缠 和--运算符 多组输入的方案 关系操作符 逻辑操作符 算术操作符 -- 加法操作符&#xff08;&#xff09;&#xff1a;用于将两个值相加。 -- 减法操…

Python爬虫(二):Requests库

所谓爬虫就是模拟客户端发送网络请求&#xff0c;获取网络响应&#xff0c;并按照一定的规则解析获取的数据并保存的程序。要说 Python 的爬虫必然绕不过 Requests 库。 1 简介 对于 Requests 库&#xff0c;官方文档是这么说的&#xff1a; Requests 唯一的一个非转基因的 P…

存储知识点:RAID0、RAID1、RAID5、RAID10特点是什么?所需的硬盘数量分别为多少?

RAID&#xff08;Redundant Array of Independent Disks&#xff09;是一种将多个独立的硬盘组合成一个逻辑磁盘的技术&#xff0c;目的是提高性能或容错能力。RAID有不同的级别&#xff0c;常见的有RAID0、RAID1、RAID5、RAID10等。下面我们来介绍这些级别的特点和所需的硬盘数…

套接字编程简介

作者&#xff1a;V7 博客&#xff1a;https://www.jvmstack.cn 一碗鸡汤 少年辛苦终身事&#xff0c;莫向光阴惰寸功。 —— 杜荀鹤 Socket概述 在计算机中产生和接受IO流的数据源是多种多样的&#xff0c;在网络编程中&#xff0c;有一个特殊的数据源就是socket。通俗点soc…

linux的系统日志

目录 一、日志文件的产生 二、日志文件存放在哪儿 &#xff08;1&#xff09;文本日志 &#xff08;2&#xff09;二进制日志 三、日志存放规则的配置文件 四、日志轮转 五、分析和监控日志 一、日志文件的产生 日志内容&#xff1a;内核、开机引导、守护进程启动运行的…

华为和思科两种常见的网络设备如何进行ospf配置?

概述 ospf&#xff08;开放最短路径优先&#xff09;是一种基于链路状态的动态路由协议&#xff0c;它可以在网络中自动发现和维护最优的路由路径。ospf广泛应用于大型和复杂的网络环境&#xff0c;因为它具有以下优点&#xff1a; 支持分层路由&#xff0c;可以将网络划分为…

WebAssembly黑暗的一面

案例1&#xff1a;技术支持诈骗 什么是技术支持诈骗&#xff1f; 技术支持诈骗是一种电话欺诈&#xff0c;其中诈骗者声称可以提供合法的技术支持服务。该骗局可能以陌生电话开始&#xff0c;骗子通常会声称来自合法的第三方的员工&#xff0c;如“微软”或“Windows部门”。他…

YOLOv5实现目标分类计数并显示在图像上

有同学后台私信我&#xff0c;想用YOLOv5实现目标的分类计数&#xff0c;因此本文将在之前目标计数博客的基础上添加一些代码&#xff0c;实现分类计数。阅读本文前请先看那篇博客&#xff0c;链接如下&#xff1a; YOLOv5实现目标计数_Albert_yeager的博客 1. 分类实现 以co…

web 实验一 HTML基本标签实验

实验原理 通过创建HTML5网页&#xff0c;验证form内多种元素标签及其属性的作用及意义。 实验目的 理解并掌握Form表单提交必须声明的内容 理解并掌握Input元素中多种类型属性的使用方法及使用场景 理解并掌握Label元素的使用方法 理解并掌握Datalist元素的使用方法 理解并掌握…

软件测试学习——笔记一

一、软件和软件测试 1、软件和软件分类 &#xff08;1&#xff09;软件&#xff1a;程序、数据、文档——用户手册 &#xff08;2&#xff09;软件的分类 按层次划分&#xff1a;系统软件、应用软件按组织划分&#xff1a;开源软件&#xff08;代码公开&#xff09;、商业软…

RSA 加密算法在C++中的实现 面向初学者(附代码)

概述 博文的一&#xff0c;二部分为基础知识的铺垫。分别从密码学&#xff0c;数论两个方面为理解RSA算法做好了准备。第三部分是对RSA加密过程的具体介绍&#xff0c;主要涉及其密钥对&#xff08;key-pair&#xff09;的获取。前三个部分与编程实践无关&#xff0c;可以当作…

C# | 内存池

内存池 文章目录 内存池前言什么是内存池内存池的优点内存池的缺点 实现思路示例代码结束语 前言 在上一篇文章中&#xff0c;我们介绍了对象池的概念和实现方式。对象池通过重复利用对象&#xff0c;避免了频繁地创建和销毁对象&#xff0c;提高了系统的性能和稳定性。 今天我…

你真的了解索引吗

当我们学习存储算法和索引算法时&#xff0c;他们可以深入了解如何在系统中存储和查询数据。因为存储和查询数据是许多系统的核心功能之一&#xff0c;例如数据库、搜索引擎等。理解这些算法可以帮助程序员更好地设计和优化系统架构&#xff0c;提高系统的可扩展性、可用性和性…

玩转Google开源C++单元测试框架Google Test系列(gtest)之二 - 断言

一、前言 这篇文章主要总结gtest中的所有断言相关的宏。 gtest中&#xff0c;断言的宏可以理解为分为两类&#xff0c;一类是ASSERT系列&#xff0c;一类是EXPECT系列。一个直观的解释就是&#xff1a; 1. ASSERT_* 系列的断言&#xff0c;当检查点失败时&#xff0c;退出当前…

大数据之光:Apache Spark 实用指南 大数据实战详解【上进小菜猪大数据】

上进小菜猪&#xff0c;沈工大软件工程专业&#xff0c;爱好敲代码&#xff0c;持续输出干货。 本文将深入探讨Apache Spark作为一种强大的大数据处理框架的基本概念、特点和应用。我们将详细介绍Spark的核心组件&#xff0c;包括Spark Core、Spark SQL、Spark Streaming和Spa…

百子作业 —— 中国邮递员问题

题目 严老师和宋老板去勘测武威市区的道路网&#xff0c;每一条路都需要勘测&#xff0c;且需要两人合作.武威市区可以近似地看成六横六纵组成的道路网&#xff0c;自西向东依次为学府路、民勤路、西关路、中关路、富民路、滨河路&#xff1b;自北向南依次为雷海路、宣武路、祁…

Redis基本数据类型及使用(2)

书接上回&#xff0c;这节讲讲其余的基本数据结构使用 集合&#xff0c;有序集合以及遍历和事务的使用 Set集合&#xff0c;无序不重复的成员 表现形式&#xff1a; key1string1string2key2string1string2 常用的基本操作&#xff1a; sadd key string1 [string2..]添加1…

第二十届宁波大学程序设计竞赛(同步赛)

A-0-1翻转_第二十届宁波大学程序设计竞赛&#xff08;同步赛&#xff09; (nowcoder.com) 思路&#xff1a; 我们观察发现&#xff0c;奇数位与偶数位的1每次操作一定时同时增加或者减少的&#xff0c;我们无法做到同时删除奇数位的两个1.。不满足相等则情况无解那么&#xf…

【谷粒商城之订单服务-支付】

本笔记内容为尚硅谷谷粒商城订单服务支付部分 目录 一、支付宝沙箱 沙箱环境 二、公钥、私钥、加密、加签、验签 1、公钥私钥 2、加密和数字签名 3、对称加密和非对称加密 三、内网穿透 四、整合支付 1、导入支付宝SDK依赖 2、封装工具类和PayVo 3、前端访问支付接…

python汉诺塔编程代码

汉诺塔问题是一个经典的递归问题。以下是使用Python实现汉诺塔的一个简单方法&#xff1a; python def hanoi(n, source, target, auxiliary): if n > 0: # 把 n-1 个盘子从 source 移动到 auxiliary hanoi(n-1, source, auxiliary, target) # 把第 n 个盘子从 source 移动到…