【C++】多态

前言:

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

多态

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

一、多态的概念

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

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

二、多态的定义及实现

2.1 多态的构成条件

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

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

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

说明:要实现多态效果

  1. 必须是基类的指针或引用,因为只有基类的指针或引用才能即指向基类对象又指针指向派生类对象
  2. 派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到

2.1.2虚函数

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

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

2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖:

  1. 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数
  2. 派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不规范,不建议这样使用,不过在考试选择题种,经常会故意买这个坑,让你判断是否构成多态
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class A
{
public:
	virtual void func() { cout << "A" << endl; }
};

class B : public A // 派生类B继承基类A
{
public:
	virtual void func() { cout << "B" << endl; } // 符合重写规则:虚函数+返回值/函数名/参数列表一致
};

void test(A* a) // 基类指针作为函数参数
{
	a->func();
}

int main()
{
	A a;
	B b;
	test(&a); // 传入基类对象地址,调用A::func
	test(&b); // 传入派生类对象地址,调用B::func
	return 0;
}

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

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Animal
{
public:
	virtual void talk() const
	{
	}
};

class Cat : public Animal
{
public:
	virtual void talk() const
	{
		cout << "miao~" << endl;
	}
};

class Dog : public Animal
{
public:
	virtual void talk() const
	{
		cout << "wang~" << endl;
	}
};

void func(Animal* a) // 基类指针作为函数参数
{
	a->talk();
}

int main()
{
	Cat cat;
	Dog dog;
	func(&cat); // 调用Cat::talk,输出miao~
	func(&dog); // 调用Dog::talk,输出wang~
	return 0;
}

2.1.4 多态场景选择题解析

题目 :以下程序输出的结果是什么()

选项:A->0 B->1 A->1 B->0

cpp 复制代码
class A
{
public:
	virtual void func(int val = 1) { cout << "A->" << val << endl; }
	virtual void test() { func(); }
};

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

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

	return 0;
}

解析

  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)协变

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

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class A {}; // 基类A
class B : public A {}; // 派生类B继承A

class Animal
{
public:
	virtual A* func()
	{
		return nullptr;
	}
};

class Cat : public Animal
{
public:
	virtual B* func() // 协变:返回值为派生类指针,仍构成重写
	{
		cout << "miao~" << endl;
		return nullptr;
	}
};

class Dog : public Animal
{
public:
	virtual B* func() // 协变:返回值为派生类指针,仍构成重写
	{
		cout << "wang~" << endl;
		return nullptr;
	}
};

void test(Animal* a)
{
	a->func();
}

int main()
{
	Cat cat;
	Dog dog;
	test(&cat); // 输出miao~
	test(&dog); // 输出wang~
	return 0;
}
(2)析构函数的重写

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

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

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

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); // 动态分配内存
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1; // 调用A::~A
	delete p2; // 调用B::~B + A::~A,避免内存泄漏
	return 0;
}

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

2.1.6 override和final关键字

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

  • override:修饰派生类虚函数,强制检测是否与基类虚函数构成重写,未构成则编译报错;
  • final:修饰基类虚函数,禁止派生类重写该虚函数。
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Car
{
public:
	virtual void Drive() // 注意:原代码笔误为Dirve,修正为Drive
	{
	}
};

class Benz : public Car
{
public:
	virtual void Drive() override // 检测是否重写Car::Drive,写错则报错
	{
		cout << "Benz" << endl;
	}
};

class BMW : public Car
{
public:
	void Drive() override // 即使不加virtual,override也会检测重写规则
	{
		cout << "BMW" << endl;
	}
};

void func(Car* c)
{
	c->Drive();
}

int main()
{
	Benz b1;
	BMW b2;
	func(&b1); // 输出Benz
	func(&b2); // 输出BMW
	return 0;
}
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Car
{
public:
	virtual void Drive() final // 禁止派生类重写Drive
	{
		cout << "Car::Drive" << endl;
	}
};

class Benz :public Car
{
public:
	// 编译报错:无法重写被final修饰的虚函数Car::Drive
	virtual void Drive() { cout << "Benz" << endl; }
};

int main()
{
	return 0;
}

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

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

三、纯虚函数和抽象函数

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

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

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Animal
{
public:
	virtual void talk() = 0; // 纯虚函数,Animal成为抽象类
};

class Cat :public Animal
{
public:
	virtual void talk() override // 重写纯虚函数
	{
		cout << "miao~" << endl;
	}
};

class Dog :public Animal
{
public:
	virtual void talk() override // 重写纯虚函数
	{
		cout << "wang~" << endl;
	}
};

int main()
{
	// Animal animal; // 编译报错:抽象类无法实例化
	Animal* pCat = new Cat;
	pCat->talk(); // 输出miao~

	Animal* pDog = new Dog;
	pDog->talk(); // 输出wang~

	delete pCat;
	delete pDog;
	return 0;
}

四、多态的原理

4.1 虚函数表指针

问题 :以下 32 位程序的运行结果是什么()

选项:A. 编译报错 B. 运行报错 C.8 D.12

cpp 复制代码
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;
}

答案: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的虚函数。

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

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	virtual void BuyTicket() { cout << "全价" << endl; }
private:
	string _name;
};

class Student : public Person
{
public:
	virtual void BuyTicket() { cout << "打折" << endl; }
private:
	string _id;
};

class Soldier : public Person
{
public:
	virtual void BuyTicket() { cout << "优先" << endl; }
private:
	string _codename;
};

void Func(Person* ptr)
{
	// 动态绑定:运行时根据ptr指向的对象,从虚表中找BuyTicket的地址
	ptr->BuyTicket();
}

int main()
{
	Person ps;
	Func(&ps); // 调用Person::BuyTicket(基类虚表)

	Student d;
	Func(&d); // 调用Student::BuyTicket(派生类虚表,覆盖基类地址)

	Soldier s;
	Func(&s); // 调用Soldier::BuyTicket(派生类虚表,覆盖基类地址)

	return 0;
}

4.2.2 动态绑定与静态绑定

  • 静态绑定:不满足多态条件的函数调用,编译时确定函数地址(如普通函数、非虚函数、非基类指针 / 引用调用虚函数);
  • 动态绑定:满足多态条件的函数调用,运行时从对象的虚表中获取函数地址(基类指针 / 引用 + 调用虚函数)。
cpp 复制代码
// 动态绑定(满足多态):ptr->BuyTicket()
00EF2001 mov eax, dword ptr[ptr]    // 取ptr指向的对象地址(含虚表指针)
00EF2004 mov edx, dword ptr[eax]    // 取虚表指针(对象首4字节)
00EF2006 mov esi, esp
00EF2008 mov ecx, dword ptr[ptr]
00EF200B mov eax, dword ptr[edx]    // 从虚表中取第一个虚函数地址(BuyTicket)
00EF200D call eax                   // 调用该地址对应的函数

// 静态绑定(不满足多态,如BuyTicket非虚函数):ptr->BuyTicket()
00EA2C91 mov ecx, dword ptr[ptr]
00EA2C94 call Student::BuyTicket(0EA153Ch) // 编译时直接确定函数地址

4.2.3 虚函数表

  • 基类对象的虚表存放基类所有虚函数地址;派生类虚表包含:基类虚函数地址(未重写的) + 派生类重写后的虚函数地址(覆盖原基类地址) + 派生类自身新增虚函数地址;
  • 同类型对象共享一张虚表,不同类型(基类 / 派生类)对象的虚表相互独立;
  • 虚函数表本质是指针数组,VS 编译器会在数组末尾放0x00000000作为结束标记(g++ 编译器无此标记);
  • 虚函数本身存放在代码段(与普通函数一致),仅地址存入虚表;
  • 虚函数表在 VS 下存放在代码段(常量区),C++ 标准未强制规定,不同编译器实现不同。
相关推荐
寻寻觅觅☆8 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
YJlio8 小时前
1.7 通过 Sysinternals Live 在线运行工具:不下载也能用的“云端工具箱”
c语言·网络·python·数码相机·ios·django·iphone
fpcc8 小时前
并行编程实战——CUDA编程的Parallel Task类型
c++·cuda
l1t8 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划9 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿9 小时前
Jsoniter(java版本)使用介绍
java·开发语言
ceclar1239 小时前
C++使用format
开发语言·c++·算法
山塘小鱼儿9 小时前
本地Ollama+Agent+LangGraph+LangSmith运行
python·langchain·ollama·langgraph·langsimth
探路者继续奋斗9 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd