C++ 多态性——虚函数

虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程的多态。

根据类型兼容规则,可以使用派生类的对象代替基类的对象。如果基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,但是访问到的只是从基类继承来的同名的函数成员。如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,首先在基类中将这个同名函数声明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同行为,从而实现运行过程的多态。

1.一般虚函数成员

(1)一般虚函数成员的声明语法是:
virtual 函数类型 函数名(参数表);

这实际上就是在类的定义中使用virtual关键字来限定成员函数,虚函数声明只能出现在类定义中的函数原型声明中,不能出现在成员函数实现的时候。

运行过程中的多态需要满足3个条件:
(1)类之间满足类型兼容规则
(2)要声明虚函数
(3)要由成员函数来调用或者通过指针、引用来访问虚函数

【注意】虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数来处理。但将虚函数声明为内联函数也不会引起错误,因为编译器会自动忽略。

(2)普通函数成员与虚函数成员的比较

①普通函数成员

#include<iostream>
using namespace std;class A//基类A定义
{
public:void display()const//声明基类A中的成员函数为普通函数{cout << "显示类A" << endl;}
};class B :public A//公有派生类B定义
{
public:void display()const{cout << "显示类B" << endl;}
};class C :public B//公有派生类C定义
{
public:void display()const{cout << "显示类C" << endl;}
};void fun(A* p)//参数为指向基类A的对象的指针
{p->display();//"对象指针->成员名"
}int main()
{A a;//定义基类对象AB b;//定义直接基类为A类的派生类B的对象C c;//定义直接基类为B类的派生类C的对象fun(&a);//用基类A的对象的指针调用fun函数fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数return 0;
}

运行结果:
在这里插入图片描述
分析:
上述程序中,虽然基类A的指针指向了派生类B,C的对象,但是fun函数运行时,通过这个指针只能访问到派生类B和C中从基类A继承下来的成员函数display,而不是派生类B和C中自身的的同名函数display。

②虚函数成员

class A//基类A定义
{
public:virtual void display()const//声明基类A中的成员函数为虚函数{cout << "显示类A" << endl;}
};class B :public A//公有派生类B定义
{
public:void display()const//覆盖基类的虚函数{cout << "显示类B" << endl;}
};class C :public B//公有派生类C定义
{
public:void display()const//覆盖基类的虚函数{cout << "显示类C" << endl;}
};void fun(A* p)//参数为指向基类A的对象的指针
{p->display();//"对象指针->成员名"
}int main()
{A a;//定义基类对象AB b;//定义直接基类为A类的派生类B的对象C c;//定义直接基类为B类的派生类C的对象fun(&a);//用基类A的对象的指针调用fun函数fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数return 0;
}

运行结果:
在这里插入图片描述
分析:
程序中的A,B和C属于同一个类族,而且是通过公有派生而来的,因此满足类型兼容规则。同时基类A的函数成员声明为虚函数,程序中使用对象指针来访问函数成员。这样绑定过程就在运行过程中完成,实现了运行中的多态。通过基类类型的指针就可以访问到正在指向的对象的成员,这样就能够对同一类族中的对象进行统一处理,抽象程序更高,程序更加简洁、高效。

在本程序中派生类并没有显式给出虚函数的声明,这时系统就会遵循以下规则来判断一个函数成员是不是虚函数:
(1)该函数是否与基类的虚函数具有相同的名称
(2)该函数是否与基类的虚函数具有相同的参数个数及相同的对应参数类型
(3)该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值

如果从名称、参数、返回值3个方面检查之后,派生类的函数满足以上条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的其他所有重载形式。

【注意】
①用指向派生类对象的指针仍然可以调用基类中被派生类覆盖的成员函数,方法是使用“::”进行限定。例如如果把上例中的fun函数改为以下形式,其他部分不改动:

void fun(A* p)//参数为指向基类A的对象的指针
{p->A::display();//"对象指针->成员名"
}

运行结果:
在这里插入图片描述
可以看出,使用“::”进行限定之后,无论p所指向的对象的多态类型是什么,最终被调用的总是A类的display函数。在派生类的函数中,有时需要先调用基类被覆盖的函数,再执行派生类特有的操作,这时就可以使用“基类名::函数名(…)”来调用基类中被覆盖的函数。

②派生类覆盖基类成员函数时,既可以用virtual关键字,也可以不使用,二者没有差别。只要在基类中声明某成员函数是虚函数即可,派生类中的同名成员函数可以不声明为虚函数。有时候习惯于在派生类函数中也使用virtual关键字,因为这样可以清楚提示这是一个虚函数。

(3)基类的构造函数和析构函数对虚函数的调用

①当基类的构造函数调用虚函数时,不会调用派生类的虚函数。

假设有基类A和派生类B,两个类中有虚成员函数fun(),在执行派生类B的构造函数时,需要首先调用基类A的构造函数。如果A::A()调用了虚函数fun(),则被调用的是A::fun(),而不是B::fun()。这是因为当基类被构造时,对象还不是一个派生类对象。

同样,当基类被析构时,对象以及不再是一个派生类对象了,所以如果A::~A()调用了fun(),则被调用的时A::fun(),而不是B::fun()。

class A//基类A定义
{
public:A(){fun();cout << "调用基类A的默认构造函数" << endl;}A(int a):x(a){fun();cout << "调用基类A的构造函数" << endl;}virtual void fun()const//声明基类A中的成员函数为虚函数{cout << "显示类A" << endl;}~A(){cout << endl;fun();cout << "调用基类A的析构函数" << endl;}
private:int  x;
};class B :public A//公有派生类B定义
{
public:B(){}B(int b) :y(b){cout << "调用派生类B的构造函数" << endl;}virtual void fun()const//覆盖基类的虚函数{cout << "显示类B" << endl;}~B(){cout << endl;fun();cout << "调用派生类B的析构函数" << endl;}
private:int y;
};int main()
{A a(5);cout << endl;B b(3);return 0;
}

运行结果:
在这里插入图片描述
分析:

在主函数中,定义了一个基类A的对象a并进行初始化,初始化时调用基类A的构造函数,基类A的构造函数中调用虚函数fun(),虽然在基类A和派生类B中都有虚函数fun(),但是在基类A的构造函数中调用的fun()函数是基类A中的fun函数,而不是派生类B中的fun()函数。又定义了一个派生类对象b并进行初始化,初始化派生类对象b时先调用基类A的默认构造函数,再调用B类的构造函数进行初始化b对象,在调用A类默认构造函数时,A类默认构造函数中调用了虚函数fun,这里调用的虚函数fun仍然不是B类中的虚函数fun,而是A类中的虚函数fun。
这是因为当基类A被被构造时,对象还不是一个派生类对象。

②只有虚函数是多态绑定的,如果派生类需要修改基类的行为(即重写与基类函数同名的函数),就应该在基类中将相应的函数声明为虚函数。而基类中声明的非虚函数,通常代表那些不希望被派生类改变的功能,也就是不能实现多态的。一般不要重写继承而来的非虚函数,因为会导致通过基类的指针和派生类的指针会对象调用同名函数时,会产生不同的结果而引起混乱。

【注意】在重写继承来的虚函数时,如果函数有默认值形参值,不要重新定义不同的值。因为,虽然虚函数是多态绑定的,但是默认形参是静态绑定的。也就是说,通过一个指向派生类对象的基类指针,可以访问到派生类的虚函数,但是默认形参值却只能来自基类定义。例如:

class A//基类A定义
{
public:virtual void display()const//声明基类A中的成员函数为虚函数{cout << "显示类A" << endl;}
};class B :public A//公有派生类B定义
{
public:virtual void display()const//覆盖基类的虚函数{cout << "显示类B" << endl;}
};class C :public B//公有派生类C定义
{
public:virtual void display()const//覆盖基类的虚函数{cout << "显示类C" << endl;}
};void fun(A* p)//参数为指向基类A的对象的指针
{p->A::display();//"对象指针->成员名"
}int main()
{C c;//定义派生类对象A* p = &c;//基类指针p可以指向派生类对象A& r = c;//基类引用r可以作为派生类对象的别名A a = c;//调用基类A的拷贝构造函数用c构造a,a的类型是A而非Creturn 0;
}

这里,A a = c;会用C类型的对象c为A类型的对象a初始化,初始化时使用的是A类的拷贝构造函数。由于拷贝构造函数接收的是A类型的常引用,C类型的c符合类型兼容规则,可以作为参数传递给它。由于执行的是A类的拷贝构造函数,只有A类型的成员会被拷贝,C类中新增的数据成员不会被拷贝,也没有空间去存储,因此生成的对象是基类A的对象。这种用派生类对象拷贝构造基类对象的行为叫做对象切片。这时,如果用a去调用基类A的虚函数,调用的目的对象是对象切片后得到的A类对象,与C类型的c对象毫无关系,对象的类型很明确,因此无须多态绑定。

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

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

相关文章

shell脚本自动打包部署

1、安装git 2、使用Git克隆代码 3、安装Maven &#xff08;1&#xff09; tar -zxvf ** 解压文件 &#xff08;2&#xff09;修改配置 &#xff08;3&#xff09;source /etc/profile 重新加载一下文件 &#xff08;4&#xff09;mvn -version 查看版本号 已经安装成…

最小二乘问题和非线性优化

最小二乘问题和非线性优化 0.引言1.最小二乘问题2.迭代下降法3.最速下降法4.牛顿法5.阻尼法6.高斯牛顿(GN)法7.莱文贝格马夸特(LM)法8.鲁棒核函数 0.引言 转载自此处&#xff0c;修正了一点小错误。 1.最小二乘问题 在求解 SLAM 中的最优状态估计问题时&#xff0c;我们一般…

【果树农药喷洒机器人】Part2:机器人变量喷药系统硬件选型

本专栏介绍&#xff1a;免费专栏&#xff0c;持续更新机器人实战项目&#xff0c;欢迎各位订阅关注。 关注我&#xff0c;带你了解更多关于机器人、嵌入式、人工智能等方面的优质文章&#xff01; 文章目录 一、引言二、变量喷药系统总体要求2.1系统功能要求2.2系统技术要求 三…

C语言学习笔记 使用vscode外部console出现闪退-12

前言 在使用vscode的外部console时&#xff0c;会出现闪退现象&#xff0c;这是因为程序运行结束后&#xff0c;系统自动退出了终端&#xff08;终端机制决定的&#xff09;。我们可以在C程序结束后&#xff0c;使用system函数来暂停DOS终端系统&#xff0c;这样就可以完整地看…

阿里云快速部署开发环境 (Apache + Mysql8.0+Redis7.0.x)

本文章的内容截取于云服务器管理控制台提供的安装步骤&#xff0c;再整合前人思路而成&#xff0c;文章末端会提供原文连接 ApacheMysql 8.0部署MySQL数据库&#xff08;Linux&#xff09;步骤一&#xff1a;安装MySQL步骤二&#xff1a;配置MySQL步骤三&#xff1a;远程访问My…

IMV5.0

背景内容&#xff1a; 经历了多个版本&#xff0c;基础内容在前面&#xff0c;可以使用之前的基础环境&#xff1a; v1&#xff1a; https://blog.csdn.net/wtt234/article/details/132139454 v2&#xff1a; https://blog.csdn.net/wtt234/article/details/132144907 v3&#…

分布式系统监控zabbix安装部署及使用

文章目录 一.zabbix监控1.什么是zabbix2.zabbix功能3.zabbix的构成4.zabbix的3种架构4.1 C/S架构4.2 分布式架构&#xff1a;zabbix-proxy-client架构4.3 master-node-client架构 5.zabbix工作原理及数据流向6.zabbix监控模式 二.zabbix部署及图形化页面显示设置(192.168.198.1…

【Linux进阶之路】进程(上)

文章目录 前言一、操作系统加载过程二、进程1.基本概念2.基本信息①运行并观察进程②创建子进程③僵尸与孤儿进程&#xff08;父子进程衍生出来的问题&#xff09;1. 僵尸进程&#xff08;Zombie状态&#xff09;2. 孤儿进程 3.基本状态①操作系统的状态&#xff08;统一&#…

一文学透设计模式——抽象工厂模式

创建者模式 抽象工厂模式 概念 抽象工厂模式是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。 这是很多地方对于抽象工厂模式的描述&#xff0c;说实话感觉不是特别好懂。…

Tomcat的介绍和安装配置、eclipse中动态web项目的创建和运行、使用IDEA创建web项目并运行

一、Tomcat的介绍和安装配置 安装tomcat&#xff1a; 环境变量的配置&#xff1a; 配置之后重启cmd&#xff0c;执行startup命令&#xff0c;启动tomcat 在localhost:8080&#xff0c;能进入tomcat主界面&#xff0c;说明配置成功 二、eclipse中动态web项目的创建和运行 tomca…

04-5_Qt 5.9 C++开发指南_QComboBox和QPlainTextEdit

文章目录 1. 实例功能概述2. 源码2.1 可视化UI设计2.2 widget.h2.3 widget.cpp 1. 实例功能概述 QComboBox 是下拉列表框组件类&#xff0c;它提供一个下拉列表供用户选择&#xff0c;也可以直接当作一个QLineEdit 用作输入。OComboBox 除了显示可见下拉列表外&#xff0c;每个…

二进制安装K8S(单Master集群架构)

目录 一&#xff1a;操作系统初始化配置 1、项目拓扑图 2、服务器 3、初始化操作 二&#xff1a; 部署 etcd 集群 1、etcd 介绍 2、准备签发证书环境 3、master01 节点上操作 &#xff08;1&#xff09;生成Etcd证书 &#xff08;2&#xff09;创建用于存放 etcd 配置文…