首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++】你以为多态很复杂?看完这篇才发现如此简单!

【C++】你以为多态很复杂?看完这篇才发现如此简单!

作者头像
用户11960591
发布2025-12-23 15:55:38
发布2025-12-23 15:55:38
1770
举报

一、多态的认识

1.1多态的概念

多态:指同一操作作用于不同对象时,可以产生不同的行为。 多态允许通过统一的接口处理不同类的对象,提高代码的灵活性和可扩展性。

1.2多态的类型

  1. 编译时多态(静态多态):主要表现就是函数重载函数模板,他们均通过传递不同的参数,从而调用不同的函数来达到不同的状态——这称之为编译时多态。(因为他们实参传给形参的参数匹配是在编译时完成的)
  2. 动态多态(运行时多态):通过方法重写实现,当调用相同方法时,传入不同对象会触发不同的具体实现,从而表现出多种行为形态——这称之为动态多态。(重写在后面会介绍)

举个例子: 比如父类有一个买票的函数,而对于买票这个行为,当普通成年人去买票时,买的是全价票;如果是一个学生去买票,买到的就是折扣票。所以至于最终买到的是什么票是取决于传递的是什么对象来决定的!

二、多态的定义及实现

2.1多态的构成条件

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

代码语言:javascript
复制
#include<iostream>
using namespace std;

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

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

};

void Func(Person& p)
{
	p.BuyTickt();
}

int main()
{
	Person p;
	Student s;

	Func(p);//传一个Person对象
	Func(s);//传一个Student对象
	return 0;
}
在这里插入图片描述
在这里插入图片描述

我们想通过传入不同的对象,来得到不同的结果,但是发现与我们的预期不符。 原因是Func函数接受Person&类型的参数,调用BuyTickt时根据参数的静态类型(Person)决定调用哪个方法。即使传入的是Student对象,调用的仍是基类PersonBuyTickt方法。由此说明该函数编译时就已经确定了函数的地址了,不由对象决定!因此可以发现他们是不构成多态的!!!

2.2多态的实现

所以要想实现多态还必须要加上这两个重要的条件:

  • 必须是基类的指针或者引用调用虚函数。
  • 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
代码语言:javascript
复制
#include<iostream>
using namespace std;

class Person
{
public:
//在前面加上virtual变成虚函数
	virtual void BuyTickt()
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person
{
public:
//在前面加上virtual变成虚函数
	virtual void BuyTickt()
	{
		cout << "买票-打折" << endl;
	}

};

void Func(Person& p)
{
	p.BuyTickt();
}

int main()
{
	Person p;
	Student s;

	Func(p);//传一个Person对象
	Func(s);//传一个Student对象
	return 0;
}
在这里插入图片描述
在这里插入图片描述

下面画一张图来解释一下,为什么加了这两个条件以后就构成多态了:

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

说明:

  • 要实现多态效果,第一必须是基类的指针或引用。因为只有基类的指针或引用才能既指向基类对象又指向派生类对象;
  • 第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派⽣类之间才能有不同的函数,多态的不同形态效果才能达到。

注意:

  1. 虚函数: 类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。非成员函数不能加virtual修饰!
代码语言:javascript
复制
class Person
{
public:
    virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
  1. 虚函数的重写/覆盖:派生类与基类有一个完全相同虚函数,称之为派生类的虚函数重写了基类的虚函数。(完全相同指:返回类型,函数名,参数列表完全相同。)
代码语言:javascript
复制
class Person
{
public:
	
	virtual void BuyTickt()
	{
		cout << "买票-全价" << endl;
	}
};

class Student :public Person
{
public:
//=======================
//函数名,参数列表、返回值相同
//与基类的虚函数构成了重写
//=======================
	virtual void BuyTickt()
	{
		cout << "买票-打折" << endl;
	}

};

void Func(Person& p)
{
// 这⾥可以看到虽然都是Person的引用p在调⽤BuyTicket
// 但是跟p没关系,⽽是由p引用的对象决定的。
//这也就构成了运行时的多态
	p.BuyTickt();
}

注意: 在重写基类虚函数时,派生类的虚函数不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用!

2.3多态场景下的一道面试题

下面的代码输出的结果是什么? A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

代码语言:javascript
复制
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;
}

运行结果:

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

题目解析:

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

2.4析构函数的重写

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

代码语言:javascript
复制
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
//======================
//基类的析构加了virtual构成多态
//此时虽然名字不同,但编译器会特殊处理
//======================
	~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;
}
在这里插入图片描述
在这里插入图片描述

如果我们没有给基类的析构加上virtual,那么在delete p2的时候只会调用基类A的析构,而不会调用派生类B的析构,这就会导致内存泄露!!!

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

2.5override和final关键字

  • override关键字的作用: 类似于一个监视器的作用,它能显式地标记并监视派生类中重写基类虚函数的成员函数,如果没有重写或者函数名写错就会报错。
  • final关键字的作用: 禁止类被继承或禁止虚函数被重写,如果一个类或一个虚函数不想被继承或重写那么就可以加上final关键字去修饰。
代码语言:javascript
复制
//使用final修饰该类禁止继承!
class A final
{
public:

//使用final修饰禁止重写
	virtual void func() final {}
};


class Car 
{
public:
	virtual void Dirve()
	{
		cout << "Dirve()" << endl;
	}
};

class Benz :public Car 
{
public:
//===========================
//基类函数Dirve与派生类Drive不同
//使用override就能检查出来!!!
//===========================
	virtual void Drive override()  
	{ 
		cout << "Benz-舒适" << endl; 
	}
};

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

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

关键区别示例:

  • 重载: void Print(int x)void Print(string s) 共存。
  • 重写: 父类virtual void Show()子类override void Show()
  • 隐藏: 父类void Display()子类new void Display()

三、纯虚函数和抽象类

纯虚函数: 纯虚函数是在基类中声明但没有实现的虚函数,其语法形式为在函数声明后添加 = 0;例如:

代码语言:javascript
复制
virtual void functionName() = 0;

抽象类: 包含纯虚函数的基类叫做抽象类,抽象累不能示例化出对象,必须由派生类重写该函数后才能创建对象。

代码语言:javascript
复制
class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

int main()
{
	// 编译报错:error C2259: “Car”: ⽆法实例化抽象类
	Car car;
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

总结一句话: 纯虚函数使得该类变成抽象类,抽象类强制派生类实现特定功能:确保所有继承自抽象类的子类都必须覆盖/重写纯虚函数,否则子类也会成为抽象类。

四、多态的原理

4.1虚函数表指针

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

代码语言:javascript
复制
#include<iostream>
using namespace std;
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;
}
在这里插入图片描述
在这里插入图片描述

上面的运行结果是12(32位系统下),除了成员_b和成员_ch,其实还有一个指针_vfptr存在该对象里面,这个指针称之为虚函数表指针。这个指针的大小刚好占4个字节所以根据内存对齐规则算得12。

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

注意: 每一个含有虚函数的类都要有一个虚表指针! 因为一个类所有虚函数的地址都要放到该对象的虚函数表中,该表也称虚表。该指针指向这张表,调用相应的虚函数的时候该指针就加上相应的偏移量找到对应函数然后完成调用!

4.2多态是如何实现的

回到我们一开始的那段代码:从底层的角度Func函数中p->BuyTicket(),是如何作为p指向Person对象调用Person::BuyTicketp指向Student::BuyTickt的呢?

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

如上图: 满足多态条件后,底层不再是编译时通过调用对象来确定函数的地址,而是在运行时到执行的对象的虚函数表中确定对应的虚函数的地址。这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

注意: 多态不仅仅发生在派生类对象之间!多个派生类继承基类,重写虚函数后,多态也会发生在多个派生类之间。

4.3动态绑定与静态绑定

代码语言:javascript
复制
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{	
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);

	return 0;
}

对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。

代码语言:javascript
复制
//=====================================
//当BuyTicket不是虚函数,不满⾜多态时。
//这⾥就是静态绑定,编译器直接确定调⽤函数地址!
//=====================================
ptr->BuyTicket();
00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)

通过上面的反汇编代码可以看到,当PersonStudent不构成多态时编译器直接确定调用(即直接call函数的地址),所以是静态绑定!

满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。

代码语言:javascript
复制
//=====================================
// 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

这里的两个寄存器eaxedx 形成两级间接寻址:eax 先定位对象实例,edx 再定位虚函数表,最后 eax 存储目标函数地址。然后再call eax最终执行间接调用,这个就是运行时确定函数的地址动态绑定!

4.4关于虚函数表

  • 基类对象的虚函数表中存放基类所有虚函数的地址。 同类型的对象共用同一张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。
在这里插入图片描述
在这里插入图片描述
  • 派生类由两部分构成,继承下来的基类和自己的成员。一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派⽣类对象中的基类对象成员也独立的。

  • 派生类的虚函数表中包含:(1)基类的虚函数地址(2)派生类重写的虚函数地址完成覆盖(3)派生类自己的虚函数地址 三个部分。(派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址)
在这里插入图片描述
在这里插入图片描述
  • 虚函数表本质是⼀个存虚函数指针的指针数组,一般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的)
  • 虚函数存在哪的? 虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函数的地址又存到了虚表中。下面给出一些代码来对比普通函数地址和虚函数地址。
代码语言:javascript
复制
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("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}
在这里插入图片描述
在这里插入图片描述

五、总结

本篇文章我们介绍了多态: 什么是多态? 同一操作作用于不同对象时,可以产生不同的行为。 多态的类型: 静态多态和动态多态。 多态的构成条件: 基类的成员函数必须是虚函数,必须是基类的指针或引用调用。 纯虚函数和抽象类: 在虚函数后面加=0,包含纯虚函数的类叫抽象类。 多态的原理: 运行时多态是通过虚函数表指针在运行时找到对应的虚函数从而到达多态的效果。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、多态的认识
    • 1.1多态的概念
    • 1.2多态的类型
  • 二、多态的定义及实现
    • 2.1多态的构成条件
    • 2.2多态的实现
    • 2.3多态场景下的一道面试题
    • 2.4析构函数的重写
    • 2.5override和final关键字
    • 2.6重载/重写/隐藏的对比
  • 三、纯虚函数和抽象类
  • 四、多态的原理
    • 4.1虚函数表指针
    • 4.2多态是如何实现的
    • 4.3动态绑定与静态绑定
    • 4.4关于虚函数表
  • 五、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档