【C++】多态 - 从虚函数到动态绑定的核心原理

news2025/4/21 9:04:53

📌 个人主页: 孙同学_
🔧 文章专栏:C++
💡 关注我,分享经验,助你少走弯路

在这里插入图片描述

在这里插入图片描述

文章目录

    • 1. 多态的概念
    • 2. 多态的定义及实现
      • 2.1 多态的构成条件
        • 2.1.1实现多态还有两个必须重要条件:
        • 2.1.2 虚函数
        • 2.1.3 虚函数的重写/覆盖
        • 2.1.4 多态场景的一个选择题
        • 2.1.5 虚函数重写的一些其他问题
        • 2.1.6 override和final关键字
        • 2.1.7 重载/重写/隐藏的对比
    • 3. 纯虚函数和抽象类
    • 4. 多态的原理
      • 4.1 虚函数表指针
      • 4.2 多态的原理
        • 4.2.1 多态是如何实现的
        • 4.2.2 动态绑定与静态绑定
        • 4.2.3 虚函数表

1. 多态的概念

多态(polymorphism)的概念:通俗来说就是指多种形态,多态分为编译时多态(静态多态)和运行时多态(动态多态)。编译时多态(静态多态)主要是前面的函数重载和函数模板,它们传不同类型的参数就可以调用不同类型的函数,通过参数不同达到多种形态,之所以叫做编译时多态,是因为它们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。

运行时多态,具体点就是去完成某个行为(函数),传不同的对象就可以完成不同的行为,就达到多种形态。就如买票这个行为,当普通人买票时是全价买票,当学生买票时是学生票,军人买票时是优先买票。再比如,同样是动物叫的一个行为,传“猫”过去就是“喵喵”,传“狗”过去就是“汪汪”。

2. 多态的定义及实现

2.1 多态的构成条件

多态是一个继承关系下的类对象,去调用同一函数产生了不同行为。比如Student继承了PersonPerson对象买票全价,Student对象优惠买票。

2.1.1实现多态还有两个必须重要条件:
  • 必须是基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

说明:要实现多态的效果,第一必须是基类的指针或者引用,因为只有基类的指针或引用才能既指向基类的对象,又能指向派生类的对象。第二派生类必须对基类的虚函数完成重写/覆盖,重写/覆盖了基类和派生类之间才能有不同的函数,多态的不同形态效果才能达成。
在这里插入图片描述

2.1.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。❗️注意:非成员函数不能加virtual修饰。

class Person 
{
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.1.3 虚函数的重写/覆盖

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

注意:再重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写,因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数的属性),但是该种写法不是很规范,不建议这样使用。在考试题中会经常埋这样一个坑,让我们判断是否构成多态。


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

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

void Func(Person* ptr)
{
	//这里虽然都是Person指针ptr调用BuyTicket
	//但是和ptr没关系,而是由ptr指向的对象决定的
	ptr->BuyTicket();
}


int main()
{
	Person ps;
	Student st;

	Func(&ps);
	Func(&st);


	return 0;
}

//引用

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

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

void Func(Person& ptr)
{
	//这里虽然都是Person指针ptr调用BuyTicket
	//但是和ptr没关系,而是由ptr指向的对象决定的
	ptr.BuyTicket();
}


int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);


	return 0;
}
class Animal
{
public:
	virtual void talk() const
	{}
};
class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};
class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};
void letsHear(const Animal& animal)
{
	animal.talk();
}
int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
}

2.1.4 多态场景的一个选择题

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

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;
}

在这里插入图片描述

这里由this调用func,构成多态必须是父类的指针或者引用,这里是否构成多态呢?这里的thisA*,所以它是一个父类的指针。
继承它会把父类的成员拿下来是一种形象的说法,其实不会把它拿下来,它的复用是先去B里面去找有没有test,再去A里面找,如果A里面还找不到就会报错。复用的本质先去B里面找,再去A里面找。包括成员变量,如果A里面有一个_aB里面有一个_b,它不是把A里面的拷贝下来在B里面也生成一份,是B对象生成的时候会先生成一个A对象的编译器,在这个对象模型在内存里面放的时候再放B的成员,不会说是把分类的成员函数和成员对象都拷贝一份下来。
所以说第一个条件是满足的。
第二个条件是虚函数的重写,也就是funcfunc的函数名,参数类型,返回值都相同,它是父类的重写,所以也满足是虚函数。
所以是满足多态的,满足多态是指向谁调用谁,p传给了thisp是指向一个派生类的B对象的。
满足多态的情况下,调用子类重写的虚函数
在这里插入图片描述

2.1.5 虚函数重写的一些其他问题
  • 协变(了解)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。

class A {};
class B : public A {};
class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}
  • 析构函数的重写

基类的析构函数为虚函数,此时派生类的析构函数只要有定义,无论是否加virtual关键字都与基类函数构成重写,虽然基类与派生类析构函数的名字不同,看起来不符合重写规则,实际上编译器对派生类的析构函数的名称做了特殊处理,编译后析构函数的名称同一处理成destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成了重写。

class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	
	delete p1;
	delete p2;
	
	return 0;
}
2.1.6 override和final关键字

C++对虚函数的重写要求比较严格,但是有些情况写由于疏忽,比如函数名写错或者是参数写错导致无法构成重写,而这种错误是在编译期间是不会报错的,只有在程序运行时没有得到预期结果,再进行找错误就会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么就可以用final去修饰。

// error C3668: “Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法 
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override
	{ 
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	return 0;
}
// error C3248: “Car::Drive”: 声明为“final”的函数⽆法被“Benz::Drive”重写 
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};


int main()
{
	return 0;
}
2.1.7 重载/重写/隐藏的对比

注意:这个概念对比经常考!!!
在这里插入图片描述

3. 纯虚函数和抽象类

在虚函数的后面加上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义,因为要被派生类重写,但语法上可以实现),只要声明即可。包含纯虚函数的类称为抽象类,抽象类不能实例化出对象,如果派生类继承后不重写虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类必须重写虚函数,因为不重写实例化不出对象。

4. 多态的原理

4.1 虚函数表指针

下面编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

4.2 多态的原理

4.2.1 多态是如何实现的

从底层的角度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调用Person::BuyTicket,ptr指向Student对象调用Student::BuyTicket的呢?通过下图我们可以看到满足多态条件后,底层不再是编译时,通过调用函数对象确定函数地址,而是运行时到指向对象的虚表中,确定对应的虚函数的地址。

第一张图,ptr指向Person对象,调用的是Person的虚函数;第二张图ptr指向的是Student对象,调用的是Student的虚函数。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
多态:指向谁调用谁的虚函数
指向父类,运行时到指向父类对象的虚函数表中找到对应的虚函数进行调用
指向子类,运行时到指向子类对象切片出的父类的虚函数表中找到对应的虚函数进行调用。

4.2.2 动态绑定与静态绑定
  • 对不满足多态条件(指针或者引用 + 调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中,找到调用函数的地址,叫做动态绑定。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。 
 // 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 
 ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr] 
00EF2004 mov edx,dword ptr [eax] 
00EF2006 mov esi,esp 
00EF2008 mov ecx,dword ptr [ptr] 
00EF200B mov eax,dword ptr [edx] 
00EF200D call eax
 // BuyTicket不是虚函数,不满⾜多态条件。 
 // 这⾥就是静态绑定,编译器直接确定调⽤函数地址 
 ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr] 
00EA2C94 call Student::Student (0EA153Ch)
4.2.3 虚函数表
  • 基类对象的虚函数表中存放,同类型的对象公用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类各自有独立的虚表。
  • 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不再生成虚函数表指针。但是要注意的是这里继承下来的虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员,和派生类继承下来的基类对象的成员也是独立的。
  • 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。
  • 派生类的虚函数表中包括:(1)基类的虚函数地址(2)派生类重写的虚函数地址完成覆盖(3)派生类自己的虚函数地址
  • 虚函数表本质上是一个存储虚函数指针的指针数组,一般情况下这个指针数组最后面放了一个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)
  • 虚函数存在哪的?虚函数和普通函数一样,编译好是一段指令,都是存放在代码段的,只是虚函数的地址又存在虚表中。
  • 虚函数表存放在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下面的代码可以对比验证⼀下。vs下是存在代码段(常量区)
class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};
class Derive : public Base
{
public:
	// 重写基类的func1 
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }
protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);

	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Base虚表地址:%p\n", *(int*)p3);
	printf("Derive虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);

	return 0;
}

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


👍 如果对你有帮助,欢迎:

  • 点赞 ⭐️
  • 收藏 📌
  • 关注 🔔

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

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

相关文章

免费图片软件,可矫正倾斜、调整去底效果

软件介绍 有个超棒的软件要给大家介绍一下哦&#xff0c;它就是——ImgTool&#xff0c;能实现图片漂白去底的功能&#xff0c;而且重点是&#xff0c;它是完全免费使用的呢&#xff0c;功能超强大&#xff01; 软件特点及使用便捷性 这软件是绿色版本的哟&#xff0c;就像一…

Kubernetes(k8s)学习笔记(二)--k8s 集群安装

1、kubeadm kubeadm 是官方社区推出的一个用于快速部署 kubernetes 集群的工具。这个工具能通过两条指令完成一个 kubernetes 集群的部署&#xff1a; 1.1 创建一个 Master 节点$ kubeadm init 1.2 将一个 Node 节点加入到当前集群中$ kubeadm join <Master 节点的 IP 和…

【论文阅读笔记】模型的相似性

文章目录 The Platonic Representation Hypothesis概述表征收敛的依据表征收敛的原因实验依据未来发展的局限性 Similarity of Neural Network Representations Revisited概述问题背景相似性度量s的性质可逆线性变换不变性正交变换不变性各向同性缩放不变性典型度量满足的性质 …

扣子智能体1:创建Agent与写好提示词

文章目录 Agent是什么使用扣子创建智能体写好提示词生成故事发布Agent 最近学了很久多agent协同、编排工作流等与agent有关的内容&#xff0c;这里用一系列博客&#xff0c;把这些操作都一步一个脚印的记录下来。 这里我们以一个Agent为例&#xff1a;睡前灵异小故事 Agent是…

Spring源码中关于抽象方法且是个空实现这样设计的思考

Spring源码抽象方法且空实现设计思想 在Spring源码中onRefresh()就是一个抽象方法且空实现&#xff0c;而refreshBeanFactory()方法就是一个抽象方法。 那么Spring源码中onRefresh方法定义了一个抽象方法且是个空实现&#xff0c;为什么这样设置&#xff0c;好处是什么。为…

【Bluedroid】蓝牙 HID 设备信息加载与注册机制及配置缓存系统源码解析

本篇解析Android蓝牙子系统加载配对HID设备的核心流程&#xff0c;通过btif_storage_load_bonded_hid_info实现从NVRAM读取设备属性、验证绑定状态、构造描述符并注册到BTA_HH模块。重点剖析基于ConfigCache的三层存储架构&#xff08;全局配置/持久设备/临时设备&#xff09;&…

字节头条golang二面

docker和云服务的区别 首先明确Docker的核心功能是容器化&#xff0c;它通过容器技术将应用程序及其依赖项打包在一起&#xff0c;确保应用在不同环境中能够一致地运行。而云服务则是由第三方提供商通过互联网提供的计算资源&#xff0c;例如计算能力、存储、数据库等。云服务…

数字化工厂五大核心系统(PLM 、ERP、WMS 、DCS、MOM)详解

该文档聚焦数字化工厂的五大核心系统&#xff0c;适合制造业企业管理者、信息化建设负责人、行业研究人员以及对数字化转型感兴趣的人士阅读。 文档先阐述数字化工厂的定义&#xff0c;广义上指企业运用数字技术实现产品全生命周期数字化&#xff0c;提升经营效益&…

n8n 中文系列教程_02. 自动化平台深度解析:核心优势与场景适配指南

在低代码与AI技术深度融合的今天&#xff0c;n8n作为开源自动化平台正成为开发者提效的新利器。本文深度剖析其四大核心技术优势——极简部署、服务集成、AI工作流与混合开发模式&#xff0c;并基于真实场景测试数据&#xff0c;厘清其在C端高并发、多媒体处理等场景的边界。 一…

SQL注入之information_schema表

1 information_schema表介绍&#xff1a; information_schema表是一个MySQL的系统数据库&#xff0c;他里面包含了所有数据库的表名 SQL注入中最常见利用的系统数据库&#xff0c;经常利用系统数据库配合union联合查询来获取数据库相关信息&#xff0c;因为系统数据库中所有信…

Elasticsearch:使用 ES|QL 进行搜索和过滤

本教程展示了 ES|QL 语法的示例。请参考 Query DSL 版本&#xff0c;以获得等效的 Query DSL 语法示例。 这是一个使用 ES|QL 进行全文搜索和语义搜索基础知识的实践介绍。 有关 ES|QL 中所有搜索功能的概述&#xff0c;请参考《使用 ES|QL 进行搜索》。 在这个场景中&#x…

MySQL表与表之间的左连接和内连接

前言: 在上个实习生做的模块之中&#xff0c;在列表接口&#xff0c;涉及到多个表的联表查询的时候总会出现多条不匹配数据的奇怪的bug&#xff0c;我在后期维护的时候发现了&#xff0c;原来是这位实习生对MySQL的左连接和内连接不能正确的区分而导致的这种的情况。 表设置 …

【AI图像创作变现】02工具推荐与差异化对比

引言 市面上的AI绘图工具层出不穷&#xff0c;但每款工具都有自己的“性格”&#xff1a;有的美学惊艳但无法微调&#xff0c;有的自由度极高却需要动手配置&#xff0c;还有的完全零门槛适合小白直接上手。本节将用统一格式拆解五类主流工具&#xff0c;帮助你根据风格、控制…

相控阵列天线:原理、优势和类型

本文要点 相控阵列天线 &#xff08;Phased array antenna&#xff09; 是一种具有电子转向功能的天线阵列&#xff0c;不需要天线进行任何物理移动&#xff0c;即可改变辐射讯号的方向和形状。 这种电子转向要归功于阵列中每个天线的辐射信号之间的相位差。 相控阵列天线的基…

【HD-RK3576-PI】Ubuntu桌面多显、旋转以及更新Logo

硬件&#xff1a;HD-RK3576-PI 软件&#xff1a;Linux6.1Ubuntu22.04 在基于HD-RK3576-PI硬件平台运行Ubuntu 22系统的开发过程中&#xff0c;屏幕方向调整是提升人机交互体验的关键环节。然而&#xff0c;由于涉及uboot引导阶段、内核启动界面、桌面环境显示全流程适配&#x…

树莓派超全系列教程文档--(36)树莓派条件过滤器设置

树莓派条件过滤器设置 条件过滤器[all] 过滤器型号过滤器[none] 过滤器[tryboot] 过滤器[EDID*] 过滤器序列号过滤器GPIO过滤器组合条件过滤器 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 条件过滤器 当将单个 SD 卡&#xff08;或卡图像&am…

jetpack之LiveData的原理解析

前言 在一通研究下&#xff0c;我打算LiveData的解析通过从使用的方法上面切入进行LiveData的工作原理分析&#x1f60b;。感觉这样子更能让大家伙理解明白&#xff0c;LiveData的实现和Lifecycle分不开&#xff0c;并且还得需要知道LiveData的使用会用到什么样的方法。所以&a…

【微知】服务器如何获取服务器的SN序列号信息?(dmidecode -t 1)

文章目录 背景命令dmidecode -t的数字代表的字段 背景 各种场景都需要获取服务器的SN&#xff08;Serial Number&#xff09;&#xff0c;比如问题定位&#xff0c;文件命名&#xff0c;该部分信息在dmi中是标准信息&#xff0c;不同服务器&#xff0c;不同os都能用相同方式获…

51c大模型~合集119

我自己的原文哦~ https://blog.51cto.com/whaosoft/13852062 #264页智能体综述 MetaGPT等20家顶尖机构、47位学者参与 近期&#xff0c;大模型智能体&#xff08;Agent&#xff09;的相关话题爆火 —— 不论是 Anthropic 抢先 MCP 范式的快速普及&#xff0c;还是 OpenAI …

Vue3 + TypeScript,关于item[key]的报错处理方法

处理方法1&#xff1a;// ts-ignore 注释忽略报错 处理方法2&#xff1a;item 设置为 any 类型