C++ 虚函数virtual的引入和应用

        来回顾一下使用引用或指针调用方法的过程。请看下面的代码:
BrassPlus ophelia;        // 子类对象
Brass * bp;                // 基类指针
bp = &ophelia;            // 让基类指针指向子类对象
bp->ViewAcct();            // ViewAcct() 如果基类和子类都有这个函数,那么这里应该调用哪一个呢?

        如果在基类中没有将 ViewAcct()声明为虚的,则 bp-ViewAcct()将根据指针类型(Brass*)调用 Brass::ViewAcct()。指针类型在编译时已知,因此编译器在编译时,可以将 ViewAcct()关联到Brass::ViewAcct()。总之,编译器对非虚方法使用静态联编。


        然而,如果在基类中将 ViewAcct()声明为虚的,则 bp->ViewAcct()根据对象类型(BrassPlus)调用BrassPlus::ViewAcct()。在这个例子中对象类型为 BrassPlus,但通常只有在运行程序时才能确定对象的类型。所以编译器生成的代码将在程序执行时,根据对象类型将 ViewAcct()关联到 Brass::ViewAcct()或 BrassPlus::ViewAcct()。总之,编译器对虚方法使用动态联编。


        在大多数情况下,动态联编很好,因为它让程序能够选择为特定类型设计的方法。因此,您可能会问:
1. 为什么有两种类型的联编? 
2. 既然动态联编如此之好,为什么不将它设置成默认的?
3. 动态联编是如何工作的?

        下面来看看这些问题的答案。

        如果动态联编让您能够重新定义类方法,而静态联编在这方面很差,为何不摒弃静态联编呢?原因有两个-效率和概念模型。


        首先来看效率。为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的对象类型,这增加了额外的处理开销。例如,如果类不会用作基类,则不需要动态联编。同样,如果派生类不重新定义基类的任何方法,也不需要使用动态联编。在这些情况下,使用静态联编更合理,效率也更高。由于静态联编的效率更高,因此被设置为 C++的默认选择。Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。仅当程序设计确实需要虚函数时,才使用它们。


                接下来看概念模型。在设计类时可能包含一些不在派生类重新定义的成员函数。不将该类函数设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚的。

提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法:否则,设置为非虚方法。

        当然,设计类时,方法属于哪种情况有时并不那么明显。与现实世界中的很多方面一样,类设计并不是一个线性过程。

        虚函数的工作原理?

        通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表 (virtual function tablevbl)。

        虚函数表中存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址:如果派生类没有重新定义虚函数,该 vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到 vtbl中。 注意无论类中包含的函数是1个还是10个,都只需要在对象中添加1地址成员,只是表的大小不同而已。


        调用虚函数时,程序将查看存储在对象中的 vtbl地址,然后转向相应的函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。

        总之,使用虚函数时,在内存和执行速度方面有一定的成本,包括:
1. 每个对象都将增大,增大量为存储地址的空间;
2. 对于每个类,编译器都创建--个虚函数地址表(数组);
3. 对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。
虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

        有关虚函数注意事项
        我们已经讨论了虚函数的一些要点。
1. 在基类方法的声明中使用关键字 virtal可该方法在基类以及所有的派生类(包括从派生类派生出来的类)中是虚的。
2. 如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,而不使用为引用或指针类型定义的方法。这称为动态联编或晚期联编。这种行为非常重要,因为这样基类指针或引用可以指向派生类对象。
3. 如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

        对于虚方法,还需要了解其他一些知识,其中有的已经介绍过。下面来看看这些内容。

1. 构造函数
        构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。


2. 析构函数
        析构函数应当是虚函数,除非类不用做基类。例如,假设 Employee 是基类, Singer 是派生类,并添加一个char*成员,该成员指向由new分配的内存。当Singer 对象过期时,必须调用~Singer()析构函数来释放内存。

        如果使用默认的静态联编,delete语句将调用~Employee()析构函数。这将释放由_Singer 对象中的Employee 部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚的,则上述代码将先调用~Singer 析构函数释放Singer 组件指向的内存然后,用~Employee()构函数来释放由Employec组件指向的内存。
这意味着,即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使它不执行任何操作:
        virtual -BaseClass()

顺便说一句,给类定义一个虚析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。
提示:通常应给基类提供一个虚析构函数,即使它并不需要析构函数。

3. 友元
        友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。如果由于这个原因引起了设计问题,可以通过让友元函数使用虚成员函数来解决。

4. 没有重新定义

        如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

5. 重新定义将隐藏方法
class Dwelling
{
    public:
        virtual void showperks(int a) const;
};

class Hovel : public Dwelling
{
    public:
        virtual void showperks() const;
        ...
};

这将导致问题,如:
    Hovel trump;
    trump.showperks();        // 是有效的
    trump.showperks(5);        // 是无效的

        新定义将showperks()定义为一个不接受任何参数的函数。重新定义不会生成函数的两个成灾版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载。如果在派生类中重新定义函数,将不是使用相同的函数特征标覆盖基类声明,而是隐藏同名的基类方法,不管参数特征标如何。


        这引出了两条经验规则,第一,如果重新定义继承类方法,应确保与原来的原型完全相同,但如哦返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针(这种例外是新出现的)。这种特性被称为返回类型协变。因为允许返回类型随类类型的变化而变化。

示例源码:

// Len2024_0106_01.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
using namespace std;

class Base
{
private:
	int m_nNum1;
	int m_nNum2;

public:
	Base(int num1, int num2)
	{
		m_nNum1 = num1;
		m_nNum2 = num2;
	}
	virtual void Show()
	{
		int data = m_nNum1 + m_nNum2;
		cout << m_nNum1 << "+" << m_nNum2 << "=" << data << endl;
	}
};

class Child : public Base
{
private:
	int m_nNum3;
	int m_nNum4;

public:
	Child(int num1, int num2, int num3, int num4) :Base(num1, num2) 
	{
		m_nNum3 = num3;
		m_nNum4 = num4;
	}

	virtual void Show()
	{
		int data = m_nNum3 + m_nNum4;
		cout << m_nNum3 << "+" << m_nNum4 << "=" << data << endl;
	}
};

class Grandson :public Child
{
private:
	int m_nNum5;
	int m_nNum6;
public:
	Grandson(int num1, int num2, int num3, int num4, int num5, int num6) :Child(num1, num2, num3, num4)
	{
		m_nNum5 = num5;
		m_nNum6 = num6;
	}

	virtual void Show()
	{
		int data = m_nNum5+ m_nNum6;
		cout << m_nNum5 << "+" << m_nNum6 <<"="<< data<< endl;
	}
};

int main()
{
	Base* base1 = new Base(11, 21);
	base1->Show();  // 调用基类的show

	cout << endl;
	Base* base2 = new Child(101, 201, 301, 401);
	base2->Show(); // 调用Child类的show

	cout << endl;
	Base* base3 = new Grandson(1001, 2001, 3001, 4001, 5001, 6001);
	base3->Show();  调用Grandson类的show
}

执行结果:

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

tp8/6 插件PhpOffice\PhpSpreadsheet导入表格

一、安装 composer require phpoffice/phpspreadsheet 官网&#xff1a;phpoffice/phpspreadsheet - Packagist 二、代码 <?php namespace app\services\upload\model; use app\services\BaseServices; use \PhpOffice\PhpSpreadsheet\Spreadsheet; use \PhpOffice\Php…

计算机Java项目|基于SpringBoot+Vue的图书个性化推荐系统

项目编号&#xff1a;L-BS-GX-10 一&#xff0c;环境介绍 语言环境&#xff1a;Java: jdk1.8 数据库&#xff1a;Mysql: mysql5.7 应用服务器&#xff1a;Tomcat: tomcat8.5.31 开发工具&#xff1a;IDEA或eclipse 二&#xff0c;项目简介 图片管理系统是一个为学生和…

Mac打包Unix可执行文件为pkg

Mac打包Unix可执行文件为pkg 方式一&#xff1a;通过packages页面打包 1.下载packages app Distribution&#xff1a;自定义化更高&#xff0c;包括修改安装页面的内容提示 我这里主要演示Distribution模式的项目&#xff1a;通过unix可执行文件postinstall.sh脚本实现通过ma…

浅谈冒泡排序

手写一个冒泡排序的代码。 1.数组 let arr [10, 2, 50, 23, 30, 56, 3]; 2.排序的思路 里层的循环: for (var i 0; i < arr.length; i) {if (arr[i] < arr[i 1]) {var temp arr[i];arr[i] arr[i 1];arr[i 1] temp;} 用途&#xff1a; [2, 10, 23, 30, 50, 3, …

腾讯云取消免费10G CDN流量包:免费CDN时代结束

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 免费送了7-8年的腾讯云10G免费流量包&#xff0c;从2024年开始&#xff0c;停止赠送了!自此&#xff0c;国内绝大多数互联网大厂的CDN都开收费了! 大概从2016年开始&#xff0c;腾讯云为了抢夺CDN客户&#xff0…

基于JavaWeb+SSM+Vue四六级词汇微信小程序系统的设计和实现

基于JavaWebSSMVue四六级词汇微信小程序系统的设计和实现 源码获取入口KaiTi 报告Lun文目录前言主要技术系统设计功能截图订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 KaiTi 报告 &#xff08;1&#xff09;课题背景 伴随着社会的快速发展, 现代社…

[NAND Flash 5.2] SLC、MLC、TLC、QLC、PLC NAND_闪存颗粒类型

依公知及经验整理&#xff0c;原创保护&#xff0c;禁止转载。 专栏 《深入理解NAND Flash》 <<<< 返回总目录 <<<< 前言 闪存最小物理单位是 Cell, 一个Cell 是一个晶体管。 闪存是通过晶体管储存电子来表示信息的。在晶体管上加入了浮动栅贮存电子…

odoo16 销售模块易错的几个操作

odoo16 销售模块易错的几个操作 据168Report调研团队最新报告“全球定制服装市场报告2023-2029”显示&#xff0c;预计2029年全球定制服装市场规模将达到1082.4亿美元&#xff0c;未来几年年复合增长率CAGR为7.8%。一个普通定制的小皮袄竟月销二十多万件&#xff0c;比我们做定…

HTML的简单介绍

文章目录 1. HTML1.1 HTML 基础认识1.2 快速生成代码框架1.3 HTML 基础标签 1. HTML 1.1 HTML 基础认识 什么是HTML呢&#xff1f; HTML叫做超文本标记语言。超文本&#xff1a;例如图片&#xff0c;视频&#xff0c;文本&#xff0c;声音&#xff0c;表格&#xff0c;链接等…

前端--基础 常用标签-超链接标签 外部链接( herf 和 target)

目录 超链接标签 &#xff1a; 超链接的语法格式 &#xff1a; 超链接的属性 &#xff1a; 超链接的分类 &#xff1a; 外部链接 &#xff1a; 超链接标签 &#xff1a; # 在 HTML 标签中&#xff0c;<a> 标签用于定义超链接&#xff0c;作用是从一个页面…

【面试高频算法解析】算法练习6 广度优先搜索

前言 本专栏旨在通过分类学习算法&#xff0c;使您能够牢固掌握不同算法的理论要点。通过策略性地练习精选的经典题目&#xff0c;帮助您深度理解每种算法&#xff0c;避免出现刷了很多算法题&#xff0c;还是一知半解的状态 专栏导航 二分查找回溯&#xff08;Backtracking&…

【C语言:可变参数列表】

文章目录 1.什么是可变参数列表2.可变参数列表的分析与使用2.1使用2.2分析原理2.3分析原码 1.什么是可变参数列表 对于一般的函数而言&#xff0c;参数列表都是固定的&#xff0c;而且各个参数之间用逗号进行分开。这种函数在调用的时候&#xff0c;必须严格按照参数列表中参数…

计算机网络(7):网络安全

网络安全问题 计算机网络上的通信面临以下的四种威胁: (1)截获(interception)攻击者从网络上窃听他人的通信内容。 (2)中断(interruption)攻击者有意中断他人在网络上的通信。 (3)篡改(modification)攻击者故意篡改网络上传送的报文。 (4)伪造(fabrication)攻击者伪造信息在网…

uniapp中uview组件库CircleProgress 圆形进度条丰富的使用方法

目录 #内部实现 #平台差异说明 #基本使用 #设置圆环的动画时间 #API #Props 展示操作或任务的当前进度&#xff0c;比如上传文件&#xff0c;是一个圆形的进度环。 #内部实现 组件内部通过canvas实现&#xff0c;有更好的性能和通用性。 #平台差异说明 AppH5微信小程…

web——德州扑克

1.此案例只用于学习 2.未接入游戏规则 HTML代码部分 <!DOCTYPE html> <html><head><meta charset"utf-8"><meta name"viewport" content"widthdevice-width"><meta name"Poker Skin" content&quo…

软件测试|深入学习 Docker Logs

简介 Docker 是一种流行的容器化技术&#xff0c;它能够帮助用户将应用程序及其依赖项打包成一个可移植的容器。Docker logs 是 Docker 提供的用于管理容器日志的命令&#xff0c;本文将深入学习 Docker logs 的使用和管理&#xff0c;帮助用户更好地监测和解决容器问题。 Do…

Python如何生成个性二维码

Python-生成个性二维码 一、问题描述 通过调用MyQR模块来实现生成个人所需二维码。 安装&#xff1a; pip install myqr 二、代码实现 1.普通二维码 from MyQR import myqr # 普通二维码 myqr.run(wordshttp://www.csdn.net/mayi0312,save_nameqrcode.png ) 效果图&#…

e2studio开发STHS34PF80人体存在传感器(1)----获取人体存在状态

e2studio开发STHS34PF80人体存在传感器.1--获取人体存在状态 概述视频教学样品申请完整代码下载主要特点硬件准备接口最小系统图新建工程工程模板保存工程路径芯片配置工程模板选择时钟设置UART配置UART属性配置设置e2studio堆栈e2studio的重定向printf设置R_SCI_UART_Open()函…

百度地图打点性能优化(海量点、mapv)

文章目录 百度地图打点性能优化&#xff08;海量点、mapv&#xff09;原因优化方法数据获取方面页面加载方面 参考资料 百度地图打点性能优化&#xff08;海量点、mapv&#xff09; 原因 在百度地图api中&#xff0c;默认的点是下图的红点 而这种点位比较多的时候&#xff0c…

关键字:super关键字

在 Java 中&#xff0c;super 关键字主要有以下两种用法&#xff1a; 在子类中调用父类的构造方法&#xff1a;当创建子类对象时&#xff0c;可以使用 super 关键字来显式调用父类的构造方法。这可以用于初始化父类的成员变量或执行父类的其他初始化操作。下面是一个示例代码&…