【C++】多态

作者:yuuki233233日期:2026/2/12

前言:
在面向对象编程的学习脉络中,继承机制让代码的复用和层级设计成为可能,但仅靠继承无法完全体现对象行为的灵活性。比如我们通过继承定义了Person基类,以及StudentSoldier等派生类后,若想让不同对象执行 “买票” 这一相同名称的行为时展现出不同逻辑(普通人全价、学生打折、军人优先),单纯的继承语法无法高效实现这种 “一个接口,多种实现” 的需求。而多态作为继承的延伸与升华,恰好解决了这一问题 —— 它让继承体系下的不同对象,对同一行为能做出符合自身特性的响应,是面向对象编程中实现代码扩展性、灵活性的核心特性。

多态

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

一、多态的概念

多态就是多种形态。多态分为:静态多态、动态多态,编译时多态(静态多态)主要就是我们前面学的函数重载和函数模板,它们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为它们实参传给形参的参数匹配是在编译时完成的。我们把编译时归为静态,运行时归为动态。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票;军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是”(>ω<)喵“,传狗对象过去,就是"汪汪"。

二、多态的定义及实现

2.1 多态的构成条件

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

2.1.1 实现多态还有两个必要条件

  1. 必须时基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖

说明:要实现多态效果

  1. 必须是基类的指针或引用,因为只有基类的指针或引用才能即指向基类对象又指针指向派生类对象
  2. 派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.1.2虚函数

类成员函数前加virtual修饰,该成员函数被称为虚函数。注意:非成员函数不能加virtual修饰。

1class Person
2{
3public:
4	virtual void BuyTicket() { cout << "买票-全价" << endl;}
5}
6

2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖:

  1. 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数
  2. 派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不规范,不建议这样使用,不过在考试选择题种,经常会故意买这个坑,让你判断是否构成多态
1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class A
6{
7public:
8	virtual void func() { cout << "A" << endl; }
9};
10
11class B : public A // 派生类B继承基类A
12{
13public:
14	virtual void func() { cout << "B" << endl; } // 符合重写规则:虚函数+返回值/函数名/参数列表一致
15};
16
17void test(A* a) // 基类指针作为函数参数
18{
19	a->func();
20}
21
22int main()
23{
24	A a;
25	B b;
26	test(&a); // 传入基类对象地址,调用A::func
27	test(&b); // 传入派生类对象地址,调用B::func
28	return 0;
29}
30

示例 2:动物叫的多态场景

1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class Animal
6{
7public:
8	virtual void talk() const
9	{
10	}
11};
12
13class Cat : public Animal
14{
15public:
16	virtual void talk() const
17	{
18		cout << "miao~" << endl;
19	}
20};
21
22class Dog : public Animal
23{
24public:
25	virtual void talk() const
26	{
27		cout << "wang~" << endl;
28	}
29};
30
31void func(Animal* a) // 基类指针作为函数参数
32{
33	a->talk();
34}
35
36int main()
37{
38	Cat cat;
39	Dog dog;
40	func(&cat); // 调用Cat::talk,输出miao~
41	func(&dog); // 调用Dog::talk,输出wang~
42	return 0;
43}
44

2.1.4 多态场景选择题解析

题目:以下程序输出的结果是什么()
选项:A->0 B->1 A->1 B->0

1class A
2{
3public:
4	virtual void func(int val = 1) { cout << "A->" << val << endl; }
5	virtual void test() { func(); }
6};
7
8class B : public A
9{
10public:
11	void func(int val = 0) { cout << "B->" << val << endl; }
12};
13
14int main(int argc, char* argv[])
15{
16	B* p = new B;
17	p->test();
18
19	return 0;
20}
21

解析

  1. B继承AB::func重写了A::func(虽未加virtual,但继承后func仍为虚函数,满足重写规则);
  2. pB类型指针,调用test()时,test()A中定义的虚函数,未被B重写,因此执行A::test()
  3. A::test()中调用func(),此时满足多态条件(隐式以基类指针 / 引用调用虚函数),实际调用B::func
  4. 虚函数的默认参数遵循 “静态绑定” 规则,即默认参数由基类虚函数的定义决定,因此valA::func的默认值1
  5. 最终输出:B->1

2.1.5 虚函数重写的一些其他问题

(1)协变

派生类重写基类虚函数时,返回值类型可不同:基类虚函数返回基类对象的指针 / 引用,派生类虚函数返回派生类对象的指针 / 引用,该情况称为 “协变”(实际开发中意义不大,理解即可)。

1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class A {}; // 基类A
6class B : public A {}; // 派生类B继承A
7
8class Animal
9{
10public:
11	virtual A* func()
12	{
13		return nullptr;
14	}
15};
16
17class Cat : public Animal
18{
19public:
20	virtual B* func() // 协变:返回值为派生类指针,仍构成重写
21	{
22		cout << "miao~" << endl;
23		return nullptr;
24	}
25};
26
27class Dog : public Animal
28{
29public:
30	virtual B* func() // 协变:返回值为派生类指针,仍构成重写
31	{
32		cout << "wang~" << endl;
33		return nullptr;
34	}
35};
36
37void test(Animal* a)
38{
39	a->func();
40}
41
42int main()
43{
44	Cat cat;
45	Dog dog;
46	test(&cat); // 输出miao~
47	test(&dog); // 输出wang~
48	return 0;
49}
50
(2)析构函数的重写

基类析构函数为虚函数时,派生类析构函数无论是否加virtual,都与基类析构函数构成重写。原因:编译器对析构函数名称做特殊处理,编译后统一为destructor,因此满足 “函数名相同” 的重写规则。

核心作用:避免继承体系下使用基类指针释放派生类对象时的内存泄漏。

1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class A
6{
7public:
8	virtual ~A() // 基类析构函数设为虚函数
9	{
10		cout << "~A" << endl;
11	}
12};
13
14class B : public A
15{
16public:
17	~B() // 自动与基类析构函数构成重写
18	{
19		cout << "~B()->delete:" << _p << endl;
20		delete _p;
21	}
22
23protected:
24	int* _p = new int(10); // 动态分配内存
25};
26
27int main()
28{
29	A* p1 = new A;
30	A* p2 = new B;
31
32	delete p1; // 调用A::~A
33	delete p2; // 调用B::~B + A::~A,避免内存泄漏
34	return 0;
35}
36

上面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只会调用A的析构函数,就会导致内存泄漏。

2.1.6 override和final关键字

C++11 提供这两个关键字,解决虚函数重写的 “隐性错误”(如函数名 / 参数写错导致未构成重写,编译不报错但运行不符合预期):

  • override:修饰派生类虚函数,强制检测是否与基类虚函数构成重写,未构成则编译报错;
  • final:修饰基类虚函数,禁止派生类重写该虚函数。
1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class Car
6{
7public:
8	virtual void Drive() // 注意:原代码笔误为Dirve,修正为Drive
9	{
10	}
11};
12
13class Benz : public Car
14{
15public:
16	virtual void Drive() override // 检测是否重写Car::Drive,写错则报错
17	{
18		cout << "Benz" << endl;
19	}
20};
21
22class BMW : public Car
23{
24public:
25	void Drive() override // 即使不加virtual,override也会检测重写规则
26	{
27		cout << "BMW" << endl;
28	}
29};
30
31void func(Car* c)
32{
33	c->Drive();
34}
35
36int main()
37{
38	Benz b1;
39	BMW b2;
40	func(&b1); // 输出Benz
41	func(&b2); // 输出BMW
42	return 0;
43}
44
1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class Car
6{
7public:
8	virtual void Drive() final // 禁止派生类重写Drive
9	{
10		cout << "Car::Drive" << endl;
11	}
12};
13
14class Benz :public Car
15{
16public:
17	// 编译报错:无法重写被final修饰的虚函数Car::Drive
18	virtual void Drive() { cout << "Benz" << endl; }
19};
20
21int main()
22{
23	return 0;
24}
25

2.1.7 重载/重写/隐藏的对比

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

特性重载重写隐藏
作用范围同一类中继承体系中(基类 + 派生类)继承体系中(基类 + 派生类)
函数名必须相同必须相同通常相同(也可不同)
参数列表必须不同必须相同可相同 / 不同
virtual 修饰无要求基类函数必须加,派生类建议加无要求
绑定方式静态绑定(编译时)动态绑定(运行时)静态绑定(编译时)

三、纯虚函数和抽象函数

在虚函数后加 =0,该虚函数称为纯虚函数。纯虚函数无需定义实现(语法上可实现,但无意义,因需被派生类重写),仅需声明。

包含纯虚函数的类称为抽象类,抽象类无法实例化对象;若派生类继承抽象类后未重写纯虚函数,派生类也会成为抽象类。纯虚函数的核心作用是 “强制派生类重写虚函数”,确保继承体系下的派生类都实现该行为。

1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3using namespace std;
4
5class Animal
6{
7public:
8	virtual void talk() = 0; // 纯虚函数,Animal成为抽象类
9};
10
11class Cat :public Animal
12{
13public:
14	virtual void talk() override // 重写纯虚函数
15	{
16		cout << "miao~" << endl;
17	}
18};
19
20class Dog :public Animal
21{
22public:
23	virtual void talk() override // 重写纯虚函数
24	{
25		cout << "wang~" << endl;
26	}
27};
28
29int main()
30{
31	// Animal animal; // 编译报错:抽象类无法实例化
32	Animal* pCat = new Cat;
33	pCat->talk(); // 输出miao~
34
35	Animal* pDog = new Dog;
36	pDog->talk(); // 输出wang~
37
38	delete pCat;
39	delete pDog;
40	return 0;
41}
42

四、多态的原理

4.1 虚函数表指针

问题:以下 32 位程序的运行结果是什么()
选项:A. 编译报错 B. 运行报错 C.8 D.12

1class Base
2{
3public:
4	virtual void Func1()
5	{
6		cout << "Func1()" << endl;
7	}
8protected:
9	int _b = 1;
10	char _ch = 'x';
11}
12
13int main()
14{
15	Base b;
16	cout << sizeof(b) << endl;
17	
18	return 0;
19}
20

答案:D(12 字节)

解析
包含虚函数的类实例化对象时,对象中会额外包含一个虚函数表指针(_vfptr(通常放在对象内存布局的最前方)。32 位系统下指针占 4 字节,int _b占 4 字节,char _ch占 1 字节(内存对齐后补 3 字节),总计:4(虚表指针) + 4(_b) + 4(_ch + 对齐) = 12 字节。

虚函数表指针指向的虚函数表(虚表) 是一个存储虚函数地址的指针数组,类中所有虚函数的地址都会存入该数组;同一个类的所有对象共享同一张虚表。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2 多态的原理

4.2.1 多态是如何实现的

满足多态条件时,函数调用的地址不再是编译时确定(静态绑定),而是运行时通过 “对象的虚表指针→虚表→对应虚函数地址” 的流程动态确定(动态绑定):

  • 基类指针 / 引用指向基类对象时,通过基类对象的虚表指针找到基类虚表,调用基类虚函数;
  • 基类指针 / 引用指向派生类对象时,通过派生类对象的虚表指针找到派生类虚表,调用派生类重写后的虚函数。
    第一张图,ptr指向Person对象,调用的时Person的虚函数。第二张图,ptr指向Student对象,调用的是Student的虚函数。

示例:买票行为的多态底层逻辑

1#define _CRT_SECURE_NO_WARNINGS
2#include<iostream>
3#include<string>
4using namespace std;
5
6class Person
7{
8public:
9	virtual void BuyTicket() { cout << "全价" << endl; }
10private:
11	string _name;
12};
13
14class Student : public Person
15{
16public:
17	virtual void BuyTicket() { cout << "打折" << endl; }
18private:
19	string _id;
20};
21
22class Soldier : public Person
23{
24public:
25	virtual void BuyTicket() { cout << "优先" << endl; }
26private:
27	string _codename;
28};
29
30void Func(Person* ptr)
31{
32	// 动态绑定:运行时根据ptr指向的对象,从虚表中找BuyTicket的地址
33	ptr->BuyTicket();
34}
35
36int main()
37{
38	Person ps;
39	Func(&ps); // 调用Person::BuyTicket(基类虚表)
40
41	Student d;
42	Func(&d); // 调用Student::BuyTicket(派生类虚表,覆盖基类地址)
43
44	Soldier s;
45	Func(&s); // 调用Soldier::BuyTicket(派生类虚表,覆盖基类地址)
46
47	return 0;
48}
49

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4.2.2 动态绑定与静态绑定

  • 静态绑定:不满足多态条件的函数调用,编译时确定函数地址(如普通函数、非虚函数、非基类指针 / 引用调用虚函数);
  • 动态绑定:满足多态条件的函数调用,运行时从对象的虚表中获取函数地址(基类指针 / 引用 + 调用虚函数)。
1// 动态绑定(满足多态):ptr->BuyTicket()
200EF2001 mov eax, dword ptr[ptr]    // 取ptr指向的对象地址(含虚表指针)
300EF2004 mov edx, dword ptr[eax]    // 取虚表指针(对象首4字节)
400EF2006 mov esi, esp
500EF2008 mov ecx, dword ptr[ptr]
600EF200B mov eax, dword ptr[edx]    // 从虚表中取第一个虚函数地址(BuyTicket)
700EF200D call eax                   // 调用该地址对应的函数
8
9// 静态绑定(不满足多态,如BuyTicket非虚函数):ptr->BuyTicket()
1000EA2C91 mov ecx, dword ptr[ptr]
1100EA2C94 call Student::BuyTicket(0EA153Ch) // 编译时直接确定函数地址
12

4.2.3 虚函数表

  • 基类对象的虚表存放基类所有虚函数地址;派生类虚表包含:基类虚函数地址(未重写的) + 派生类重写后的虚函数地址(覆盖原基类地址) + 派生类自身新增虚函数地址;
  • 同类型对象共享一张虚表,不同类型(基类 / 派生类)对象的虚表相互独立;
  • 虚函数表本质是指针数组,VS 编译器会在数组末尾放0x00000000作为结束标记(g++ 编译器无此标记);
  • 虚函数本身存放在代码段(与普通函数一致),仅地址存入虚表;
  • 虚函数表在 VS 下存放在代码段(常量区),C++ 标准未强制规定,不同编译器实现不同。

【C++】多态》 是转载文章,点击查看原文


相关推荐


上下文工程(Context Engineering)
uzong2026/2/4

本文整理自 LangChain 博客《Context Engineering for Agents》」更自然 blog.langchain.com/context-eng… 随着大语言模型(LLM)在智能体(Agent)系统中的广泛应用,如何有效管理有限的上下文窗口(Context Window)成为关键挑战。 1. 为什么需要上下文工程? 大语言模型的上下文窗口类似于计算机的 RAM——它是模型的“工作记忆”,但容量有限。当智能体执行长期任务时,会不断累积以下类型的上下文: ● 指令(Inst


墨梅博客 1.2.0 发布与 AI 开发实践 | 2026 年第 4 周草梅周报
草梅友仁2026/1/25

本文在 草梅友仁的博客 发布和更新,并在多个平台同步发布。如有更新,以博客上的版本为准。您也可以通过文末的 原文链接 查看最新版本。 前言 欢迎来到草梅周报!这是一个由草梅友仁基于 AI 整理的周报,旨在为您提供最新的博客更新、GitHub 动态、个人动态和其他周刊文章推荐等内容。 开源动态 本周依旧在开发 墨梅 (Momei) 中。 您可以前往 Demo 站试用:demo.momei.app/ 您可以通过邮箱 admin@example.com,密码momei123456登录演示用管理


Monorepo入门
Hyyy2026/1/17

1. Monorepo 介绍 核心价值:把“需要一起演进的一组项目”放在同一个版本空间里,从而让跨项目改动(API 变更、重构、升级)能在一次提交里完成并验证 Monorepo 是把多个相关项目/包放在同一个 Git 仓库中管理的策略,有助于跨项目联动修改、内部包共享更顺畅、统一规范与 CI、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。 Monorepo 提倡了开放、透明、共享的组织文化,这种方法已经被很多大型公司广泛使用,如 Google、Facebook 和 Mic


HarmonyOS一杯冰美式的时间 -- FullScreenLaunchComponent
猫猫头啊2026/1/9

一、前言 最近在开发中,我们的元服务需要被其他应用通过FullScreenLaunchComponent拉起,我只能说当时上了5.0的当,FullScreenLaunchComponent也是Beta版本的!在实际开发中作为碰了几次灰,踩了不少坑,觉得有必要分享下,故有了此篇文章。 该系列依旧会带着大家,了解,开阔一些不怎么热门的API,也可能是偷偷被更新的API,也可以是好玩的,藏在官方文档的边边角角~当然也会有一些API,之前是我们辛辛苦苦的手撸代码,现在有一个API能帮我们快速实现的,希望


设计模式和设计原则-中高级架构思路-面向接口编程
自由生长20242025/12/31

历史文章参见 设计模式-23种设计模式的说法-掘金 每日知识-设计模式-状态机模式-掘金 每日知识-设计模式-观察者模式 - 掘金 cpp笔记第3篇-C++多线程单例模式单例模式 - 掘金 今天讲讲面向接口编程的核心思想,它可以看到各种设计模式的一种杂糅。 面向接口编程的核心思想 以实际的代码举例子,我最近在写一个安卓的笔记程序,使用到了面向接口的编程方法,下面我以具体的类举例来说明面向接口编程的思想,以及后文解释,面向接口编程可以体现哪些设计模式。 一、依赖接口,而不是具体实现 // ❌ 面


你以为 Props 只是传参? 不,它是 React 组件设计的“灵魂系统”
白兰地空瓶2025/12/22

90% 的 React 初学者,都低估了 Props。 他们以为它只是“从父组件往子组件传点数据”。 但真正写过复杂组件、设计过通用组件的人都知道一句话: Props 决定了一个组件“好不好用”,而不是“能不能用”。 这篇文章,我们不讲 API 清单、不背概念, 而是围绕 Props 系统的 5 个核心能力,一次性讲透 React 组件化的底层逻辑: Props 传递 Props 解构 默认值(defaultProps / 默认参数) 类型校验(PropTypes) children 插


前端跨页面通讯终极指南⑥:SharedWorker 用法全解析
一诺滚雪球2025/12/14

前言 前面的文章已经介绍了postMessage、localStorage、messageChannel、broadcastChannel以及window.name。今天要介绍一种“多页面协同”场景的工具——SharedWorker。 不同于普通Worker只能被单个页面独占,SharedWorker能被同一域名下的多个页面共享,实现高效的“多页面数据中枢”。本文就带你了解SharedWorker跨页面通讯的核心用法。 1. 什么是SharedWorker? 在介绍SharedWorker之前,


从 Oracle 到 KingbaseES:破解迁移痛点,解锁信创时代数据库新可能
倔强的石头_2025/12/5

提起 Oracle,它在传统数据库领域可是标杆般的存在,长久以来一直撑起了众多企业的核心业务,可这两年情形发生了改变,Oracle的授权费用很高,运维成本又让人头疼,再加上信创政策对于合规有着强硬的要求,于是很多企业便开始把目光转向国产数据库。金仓数据库 KingbaseES 是国产数据库中的佼佼者,凭借其高适配性和高性能的基础,成了人们更换 Oracle 的首要选择,不过要告诉大家的是,迁移之路并非易走,“报错”频繁出现,存在适配性障碍,成本难以控制住……这些难点真真切切地成为了企业在执行迁移


基于 YOLOv8 的水体污染目标检测系统 [目标检测完整源码]
我是杰尼2026/2/21

基于 YOLOv8 的水体污染目标检测系统 [目标检测完整源码] 一、背景:水体监控为什么需要“计算机视觉”? 在水资源保护与环境治理领域,“看得见问题”往往是治理的第一步。然而在真实场景中,水体监控长期面临以下现实挑战: 水域面积大、人工巡检成本高 污染物种类多、形态变化大 人工判读主观性强、难以量化 传统传感器难以识别“视觉型污染” 随着无人机、高清摄像头、遥感设备的普及,水体数据获取已不再是瓶颈,真正的难点转移到了如何从海量影像中自动识别污染风险。 基于此,本文介绍一套 以 YOLOv


OpenClaw安装技能的三种方式
可夫小子2026/3/1

OpenClaw安装技能的三种方式 💡 大家好,我是可夫小子,《小白玩转ChatGPT》专栏作者,关注AI编程、AI自动化和自媒体。 OpenClaw很强大,但也需要搭配很多技能才能发挥最大的作用,它的安装方式没有像Claude Code那样复杂,但也还有一些操作过程。在OpenClaw里面,可以通过WebUI的界面,很直观的管理安装的技能。 但如果安装技能呢?今天分享常用的三种方式。 通过官方openclaw命令来配置 通过openclaw config 命令,选择Local→Ski

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客