C++三大特性—多态 “虚函数与动态绑定”

news2024/11/26 2:54:55

面向对象程序设计的核心思想是数据抽象、继承、动态绑定
通过使用数据对象,将类的接口与实现分离
使用继承,定义相似的类型并对其相似关系建模
使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象


多态

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

就比如生活中,我们需要买火车票,不同的人扮演的角色不同,买的票也不同,比如学生买学生票,小孩买未成年人票,大人买全票(举个例子)

多态的定义及实现

在继承中构成多态还有两个条件:

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数

在C++语言中,基类将类型相关的函数派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义其适合自身的版本,此时基类就将这些函数声明为虚函数(virtual function)。

class Person
{
public:
    virtual void Print(int a) const;//在函数前面加virtual,声明其为虚函数
};

虚函数重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

比如:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

   注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时
(void BuyTicket() { cout << “买票-半价” << endl; })
虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

   派生类必须在其内部对所有重新定义的虚函数进行声明,派生类可以在这样的函数之前加上virtual关键字,不是非得这么做,在C++11新标准中允许派生类显示的注明它将使用哪个成员函数改写基类的虚函数,具体措施就是在形参列表后加上override关键字

虚函数重写的两个例外:

  • 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};
  • 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态


动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

通过使用动态绑定,我们能用同一段代码分别处理Person与Student对象。

观察下面代码:

class Person
{
public:
	virtual void Print()
	{
		cout << _No<<endl;
	}
	int _No=1;
};
class Student : public Person
{
public:
	void Print() override
	{
		cout << _age<<endl;
	}
	int _age=100;
};
void Test(Person& obj)
{
	obj.Print();
}

Student继承与Person,Person里面的函数Print为虚函数,Student为这个继承来的虚函数重新定义了。

运行下面代码:

	Person obj_per;
	Student obj_stu;
	Test(obj_per);
	Test(obj_stu);

运行结果:
在这里插入图片描述
我们发现,传入的对象类型不同,打印的结果也不同。因为函数Test传入的形参obj是基类Person的一个引用(构成多态),我们既能使用基类Person的对象调用它,也能使用派生类Student的对象调用它(我们可以看这一篇关于)。因为Print是使用引用类型调用的,所以实际传入Test函数的类型决定到底该执行Print哪个版本。

	Test(obj_per);

传入的是基类Person的对象,所以调用基类版本

	Test(obj_stu);

传入的是派生类Student的对象,所以调用派生类的版本
如果我们不使用引用作为Test的形参:

void Test(Person obj)
{
	obj.Print();
}

运行结果:
在这里插入图片描述
调用的都是基类的Print函数,这个不是动态绑定

在上述过程中函数的运行版本由由实参决定,即在运行时选择函数的版本,所以动态绑定有时又被称为运行时绑定。
在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。

override 和 final (C++11)

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Person
{
public:
	virtual void Print() final
	{
		cout << _No << endl;
	}
	int _No = 1;
};
class Student : public Person
{
public:
	virtual void Print()//不能继承
	{
		cout << _age << endl;
	}
	int _age = 100;
};

结果:
在这里插入图片描述

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Person
{
public:
	virtual void Print()
	{
		cout << _No << endl;
	}
	int _No = 1;
};
class Student : public Person
{
public:
	virtual void Print() override
	{
		cout << _age << endl;
	}
	int _age = 100;
};

如果没有重写就报错:
在这里插入图片描述
如果我们上面图片中的override去掉,能够编译通过,但是基类Person与派生类Student里面的Print函数不构成动态绑定,而是构成隐藏关系,因为同名函数,不在同一作用域,派生类会隐藏掉基类里面的同名函数。
在这里插入图片描述

重载、覆盖(重写)、隐藏(重定义)的对比:

在这里插入图片描述


成员函数与继承

在C++语言中,基类必须将它的两个成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。

   对于前者,基类将其定义为虚函数,当我们使用指针或引用调用虚函数时,该调用将被动态绑定。根据指针或引用的类型不同,该调用可能执行基类的版本,也可能执行某个派生类的版本。

   任何构造函数之外的非静态函数都可以是虚函数。关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。如果基类把一个函数声明为虚函数,则该函数在派生类中隐式的也是虚函数。

   成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。


派生类中的虚函数

   派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类的版本。

   派生类可以在它覆盖的函数前使用virtual关键字,但不是非得这么做。C++11新标准允许派生类显示地注明它使用某个成员函数覆盖它继承的虚函数,具体的做法是:在形参后面、或者在const成员函数的const关键词后面、或者在引用成员函数的引用限定符后面添加一个关键词override。

  一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型返回值函数名必须与它被覆盖的函数完全一致。


虚函数与默认实参

  和其他的函数一样,虚函数也可以拥有默认实参,如果某次函数调用使用了默认实参,则该实参值由本次调用的静态类型决定。

  如果我们通过基类的引用或指针调用函数,则使用基类中定义的默认实参,即使运行的是派生类中的函数版本也是如此。此时,传入派生类函数的将是基类函数定义的默认实参。如果派生类函数依赖不同的实参,则程序结束将与我们预期不符。

  如果虚函数使用默认实参,则基类和派生类中定义的默认实参最好一致

一道关于默认实参的面试题:最终打印的结果是什么?

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};

class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

在这里插入图片描述

 我们可能会认为,这里B继承A的test函数,那么test调用函数的this指针也会改变,其实不会。是基类的函数,哪怕被继承使用仍然是基类的this指针,不会因为继承this指针类型发生改变。

比如下面代码:

class A
{
public:
	void test() { cout << _name << endl; }
 	//p访问的是基类部分,转化为基类this指针只能指向基类的成员,也就是10的
 	//_name其实是this->_name
	int _name = 10;
};

class B : public A
{
public:
	int _name = 100;
};

int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

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

 基类和派生类有相同成员变量_name,派生类会隐藏基类的_name,但是我们使用B的对象访问从A继承来的test函数运行结果是10,因为,this指针还是基类的this指针,它只有访问基类的成员权限,没有访问派生类的权限。


如何回避虚函数的机制

在某些情况,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定的版本。使用作用域运算符可以实现这一目的。

比如下面代码:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

运行:

	Student obj_stu;
	Person* obj= &obj_stu;
	obj->Person::BuyTicket();

如果按照动态绑定来,obj的静态类型是Person* 但是它的动态类型是Student,由动态类型决定它使用虚函数哪一个版本。但是我们使用了Person::,所以强制执行这个类作用域里面的,也就是Person类中的:
在这里插入图片描述

  通常情况下,只有成员函数(或友元)中的代码才需要使用作用域运算符来回避虚函数机制

  什么时候我们需要回避虚函数的默认机制?通常是当一个派生类的虚函数调用它覆盖的基类虚函数版本时。在此情况下,基类的版本通常完成继承层次中所有类型都要做的共同任务,而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。

  如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

C++多态性:
  面向对象程序设计(OOP)的核心思想是多态性。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无需在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本存在。
  当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道真正作用的对象是什么类型,如果该函数是虚函数,直到运行时才知道到底执行哪一个版本,判断的依据正是该引用或指针所绑定的对象的真实类型。
  另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论什么时候都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。
  当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型与静态类型可能会不一致


如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀

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

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

相关文章

Linux Audio (6) DAPM-3 damp的kcontrol注册过程

DAPM-3 damp的kcontrol注册过程 普通kcontrolDAMP kcontrol第一步 codec驱动add widget第二步 Mechine驱动add kcontrol damp的注册过程 普通kcontrol 定义&#xff1a; static const struct snd_kcontrol_new wm8960_snd_controls[] { SOC_DOUBLE_R_TLV("Capture Volu…

创建 ROS 的消息和服务(四)

执行命令 cd ~/catkin_ws/src/catkin_create_pkg beginner_tutorials std_msgs roscpp rospy进入刚刚那个功能包begineer什么的 cd beginner_tutorials/ mkdir msgecho "int64 num" > msg/num.msg 然后添加如下代码,按i 然后输入 <build_depend>message_…

C++:EffectiveC++:Article21:必须返回对象时,别妄想返回其Reference

Article21&#xff1a;必须返回对象时&#xff0c;别妄想返回其Reference 1. operator* 以by value 方式返回一个结果2. operator* 以 by Reference 方式返回一个结果3 定义static Rational 对象总结 本章主要介绍&#xff1a;函数返回值两种类型&#xff1a;值类型返回和引用返…

卷积神经网络的原理、结构和应用

深度学习是一种人工神经网络的应用&#xff0c;其应用范围包括自然语言处理、计算机视觉、语音识别等等。其中&#xff0c;卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;是一种应用广泛的图像识别模型&#xff0c;其用于解决计算机视觉领域…

【Linux】多种环境变量介绍与设置

文章目录 一. linux环境变量介绍1. linux中的环境变量配置文件2. 环境变量加载顺序 二. 操作环境变量1. 读取环境变量envexportecho $PATH 2. 设置环境变量2.1. export PATH&#xff1a;临时的环境变量2.2. 用户的环境变量vim ~/.bashrcvim ~/.bash_profile 2.3. 所有用户的环境…

软件详细设计总复习(二)【太原理工大学】

文章目录 二、结构型模式1. 适配器模式2. 桥接模式3. 组合模式4. 装饰模式5. 外观模式6. 代理模式 二、结构型模式 1. 适配器模式 适配器是用来将两个原本并不兼容的接口能够在一起工作。就像我们的充电线可以让手机接口和插座接口相互适应&#xff0c;完成工作。 课本上的案…

Linux防火墙iptables

文章目录 一.iptables概述二.netfilter/iptables 关系三.四表五链3.1作用3.2四表3.3五链3.4规则表的优先顺序3.5规则链的匹配顺序3.6iptables 命令行配置方法3.8常用管理选项3.9iptables安装 四、操作4.1 增加规则4.2删除规则4.3修改规则4.4查询规则 五、规则匹配5.1通用匹配5.…

IDEA快捷键总结

IDEA快捷键总结 KeyMap使用的是Eclipse 常用快捷键 Ctrl H 全局搜索Shift Shift 搜索源码Ctrl O 查看当前类或接口包含的方法&#xff0c;即自身结构。Ctrl Alt B 选中接口名&#xff0c;查看当前接口的实现类Ctrl Alt V 快速补全Ctrl Alt ↓ 复制当前行到下一行C…

广告让你不自觉地掏钱?消费者行为背后的心理学

一般来说&#xff0c;应该从广告的各个方面提升&#xff0c;比如与目标用户的需求匹配&#xff0c;产品定位&#xff0c;核心卖点&#xff0c;突出重点和价值&#xff0c;不断重复&#xff0c;等等的这些都说的很好&#xff0c;给用户提供了做这件事的足够的动机和理由。 但我…

【组合优化】基于CHHO的QoS感知的web服务组合优化【Matlab代码22#】

文章目录 【可更换其他算法&#xff0c;获取资源请见文章第7节&#xff1a;资源获取】1. Web服务2. QoS感知的Web服务组合3. 改进后的CHHO算法3.1 原始HHO算法3.2 CHHO算法 4. 优化目标5. 部分代码展示6. 仿真结果展示7. 资源获取 【可更换其他算法&#xff0c;获取资源请见文章…

rpc与grpc学习记录

文章目录 1、RPC2、gRPC多线程pythongrpc代码1、安装python需要的库&#xff1a;2、grpc编程步骤3、Demo13.1、编写 .proto文件&#xff0c;定义接口和数据类型3.2、编译 .proto文件生成存根文件3.3、编写服务器端代码&#xff1a;3.4、编写客户端代码&#xff1a;3.5、测试 1、…

docker操作2

docker操作2 文章目录 docker操作2启动新容器配置新的容器后要做的操作进入Docker容器可以显示图片的容器镜像pull 网络镜像 日志停止与删除停止删除删除image报错 在容器和宿主机之间拷贝数据创建命令别名查看docker运行容器的ipdocker image保存与导入保存image导入image 打标…

CMake的应用与实践

CMake 简介 CMake是什么&#xff1f; 全称 Cross Platform Make&#xff0c;起初为了跨平台需求&#xff0c;而后不断完善并广泛使用一款优秀的工程构建工具 特点和优势 开放源代码&#xff0c;具有BSD许可跨平台&#xff0c;支持Linux&#xff0c;Mac和Windows等不同操作系…

【C生万物】 字符串内存函数篇 (上)

欢迎来到 Claffic 的博客 &#x1f49e;&#x1f49e;&#x1f49e; &#x1f449; 专栏&#xff1a;《C生万物 | 先来学C》&#x1f448; 前言&#xff1a; 过了指针这个坎后&#xff0c;下一步就是C语言中关于字符的处理&#xff0c;这一期来讲…

chatgpt赋能Python-python5个一行

Python: 5行代码改变世界 Python是一种高级编程语言&#xff0c;以其简单易学的特性而闻名。Python的发明者Guido van Rossum在1980年代末和1990年代初创造了Python&#xff0c;旨在创建一种语言&#xff0c;既易于理解又易于使用。如今&#xff0c;Python已经成为了最受欢迎的…

【离散数学】陪集和拉格朗日定理编程题

1&#xff1a;编写一个程序能够计算有限群G的子群H的左陪集 输入一个n阶有限群G的二元运算表及相关的子群&#xff0c;输出其左陪集。 &#xff08;注意&#xff1a;按照表头元素顺序计算每个陪集&#xff0c;下图为G的二元运算表示例&#xff09; 样例1&#xff1a; 输入&…

干货 | 利用SPSS进行高级统计分析第三期

Hello&#xff0c;大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 在本期中&#xff0c;我们继续为大家介绍如何利用SPSS进行&#xff1a;单因素方差分析、多因素方差分析、重复测量方差分析等。 1. 单因素方差分析【组间实验单一因变量&#xff1b;进行差…

iptables防火墙中的SNAT和DNAT

SNAT的原理和应用 SNAT 应用环境∶局域网主机共享单个公网IP地址接入Internet &#xff08;私有IP不能在Internet中正常路由&#xff09; SNAT原理∶修改数据包的源地址。 SNAT转换前提条件∶ 局域网各主机已正确设置IP地址、子网掩码、默认网关地址Linux网关开启IP路由转发…

游资92科比到底牛在哪里?

昨天一天时间把92科比之前的一个帖子全部看完&#xff0c;从科比对情绪周期的把握来看那简直总结的已经是标准答案了&#xff0c;那么为何92科比公布了答案&#xff0c;还是有很多人“痛苦”的做不到&#xff1f; 这个问题我觉得跟退学炒股是一样的&#xff0c;退学先解决了小…

springcloud-alibaba (03)sentinel下载安装

Sentinel 一&#xff0c;下载安装&#x1f4a0;二&#xff0c;编写脚本&#x1f9ff;三&#xff0c;启动sentinel&#x1f48e;四&#xff0c;Win-访问控制台✨ 在Linux中下载安装Spring Cloud Alibaba Sentinel&#xff0c;可以按照以下步骤进行操作&#xff1a; 一&#xff0…