
🦌云深麋鹿
专栏 :C++ | 用C语言学数据结构 | Java

回顾:上一篇我们结束了 继承,接下来这篇文章让我们进入到新的面向对象特性 多态 的学习,体会新的设计思路吧~
放个目录
- [一 概念](#一 概念)
-
- [1.1 静态多态](#1.1 静态多态)
- [1.2 动态多态](#1.2 动态多态)
- [二 多态的定义及实现](#二 多态的定义及实现)
-
- [2.1 多态的构成条件](#2.1 多态的构成条件)
-
- [2.1.1 虚函数](#2.1.1 虚函数)
- [2.1.2 虚函数重写/覆盖 构成条件](#2.1.2 虚函数重写/覆盖 构成条件)
- [2.1.3 选择题](#2.1.3 选择题)
- [2.1.4 虚函数重写的问题](#2.1.4 虚函数重写的问题)
- [2.1.5 override 和 final 关键字](#2.1.5 override 和 final 关键字)
- [2.1.5 重载/重写/隐藏](#2.1.5 重载/重写/隐藏)
- [三 纯虚函数和抽象类](#三 纯虚函数和抽象类)
-
- [3.1 语法格式](#3.1 语法格式)
- [3.2 介绍](#3.2 介绍)
- [3.3 上代码](#3.3 上代码)
- [四 多态的原理](#四 多态的原理)
-
- [4.1 虚函数表指针](#4.1 虚函数表指针)
- [4.2 多态原理](#4.2 多态原理)
一 概念
多态分为 编译时多态(静态多态)和 运行时多态(动态多态)。
1.1 静态多态
即编译时多态。
- 主要指 函数重载 和 函数模板。
- 传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。
- 这里的参数匹配是在编译时完成的。
1.2 动态多态
即运行时多态。
- 要完成某个函数,传不同的对象就会完成不同的⾏为。
二 多态的定义及实现
2.1 多态的构成条件
- 一个继承关系下的类对象,去调⽤同⼀函数,产生不同行为。
- 必须是 基类的指针/引用 调⽤虚函数。
- 被调用的虚函数,完成了虚函数重写/覆盖。
2.1.1 虚函数
语法格式:
cpp
virtual void fun(){}
2.1.2 虚函数重写/覆盖 构成条件
派生类虚函数 与 基类虚函数的返回值类型、函数名字、参数列表完全相同。
cpp
class Person {
public:
virtual void BuyTicket() {
cout << "Full Price." << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "Half Price." << endl;
}
};
这里 Student类 继承 Person类。
测试:
cpp
wyzy::Person* p1 = new wyzy::Person();
wyzy::Person* p2 = new wyzy::Student();
p1->BuyTicket();
p2->BuyTicket();
运行:

派生类也可以不加virtual关键字修饰(因为派生类函数被继承下来就带有虚函数属性),但是不规范。
2.1.3 选择题
多态场景下,程序输出结果?
cpp
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()
{
B* p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
这里需要注意:
重写,重写的是函数体部分。
运行:

改下代码
cpp
class A
{
public :
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; }
};
void TestAB(void)
{
B* p = new B;
p->test();
}
- 不构成多态,直接调用派生类函数。
运行:

2.1.4 虚函数重写的问题
(1)协变(不常用)
与基类返回值类型不同。
cpp
class A
{
public :
virtual A* func() {
cout << "A" << endl;
return this;
}
};
class B : public A
{
public :
virtual B* func() {
cout << "B" << endl;
return this;
}
};
(2)析构函数的重写
- 析构函数编译后名称统一会处理成destructor,因此符合重写条件。
派生类的析构函数不加virtual,就不构成重写,这样就调用不到这个析构函数,会造成内存泄漏。
2.1.5 override 和 final 关键字
(1)override
可帮助我们检查是否成功构成重写。
cpp
class A
{
public :
A* func() {
cout << "A" << endl;
return this;
}
};
class B : public A
{
public :
B* func() override{
cout << "B" << endl;
return this;
}
}
编译报错:

(2)final
适用于 类 和 非静态虚函数。
- 可修饰虚函数,让它不被继承。
cpp
class A
{
public :
virtual void func() final{
cout << "A" << endl;
}
};
class B : public A
{
public :
virtual void func() {
cout << "B" << endl;
}
};
编译报错:

- 修饰静态成员会报错。
cpp
virtual static void func() final{
cout << "A" << endl;
}

2.1.5 重载/重写/隐藏
(1)重载
- 两个函数在同个作用域。
- 函数名相同,参数不同。
(2)重写
- 俩函数分别在基类和派生类俩作用域。
- 函数名,参数,返回值相同(协变为特殊情况)。
- 两个函数都必须是虚函数。
(3)隐藏
- 俩函数分别在基类和派生类俩作用域。
- 函数名/变量名 相同。
- 俩函数不构成重写。
三 纯虚函数和抽象类
3.1 语法格式
cpp
virtual void fun() = 0;
3.2 介绍
- 纯虚函数只需要声明即可(当然想定义也可以定义),后续会被派生类重写。
- 包含纯虚函数的类叫抽象类,抽象类不能实例化出对象。
- 所以派生类不重写纯虚函数,就不能被实例化。
3.3 上代码
cpp
class A {
public:
virtual void func() = 0;
};
class B : public A {};
void TestA01(void) {
A* p = new B;
p->func();
}
编译报错:

四 多态的原理
4.1 虚函数表指针
上题目:编译为32位程序的运行结果?
cpp
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main(){
cout << sizeof(Base) << endl;
return 0;
}
运行:

为什么是16字节不是8字节类?
cpp
class Base
{
public:
void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main(){
cout << sizeof(Base) << endl;
return 0;
}
这样就是8字节:

原因
- 多出来一个指针:虚函数表指针。
- ⼀个类所有虚函数的地址要放到虚函数表中,这个表简称虚表。
4.2 多态原理
4.2.1 多态的实现
上代码:
cpp
class Person {
public:
virtual void BuyTicket() {
cout << "Full Price." << endl;
}
};
class Student : public Person {
public:
virtual void BuyTicket() {
cout << "Half Price." << endl;
}
};
class Soldier : public Person {
public:
virtual void BuyTicket() {
cout << "Priority." << endl;
}
};
Student 和 Soldier 继承自 Person,各自有各自的虚函数表。
测试:
cpp
Person* p1 = new Person();
Person* p2 = new Student();
Person* p3 = new Soldier();
p1->BuyTicket();
p2->BuyTicket();
p3->BuyTicket();
运行:

满足多态场景下:
- 具体执行逻辑,跟什么类型指针没关系。
- 运行时,到指向对象的虚表中找到对应虚函数。
4.2.2 动态绑定与静态绑定
(1)静态绑定
不满足多态场景下,普通函数编译时确定调⽤函数的地址,称为静态绑定。
写个普通函数:
cpp
class Soldier : public Person {
public:
// ...
void func() {
cout << " S " << endl;
}
};
int main(){
Soldier* p1 = new Soldier();
p1->func();
return 0;
}
调试转到反汇编:

(2)动态绑定
满足多态场景下,函数运行时到指向对象的虚函数表中找到调⽤函数的地址,称为动态绑定。
以上面的代码举例,调试转到反汇编:


4.2.3 虚函数表
- 基类和派生类有各自的虚函数表,同类型对象共⽤⼀张虚表。
- 派生类的虚函数表指针 继承自 基类,但跟基类不是同一个指针(各自指向各自作用域的虚函数)。
- 派生类重写的虚函数,该虚函数指针(继承下来后)会在虚表中被 重写的虚函数地址 覆盖。
- 派生类虚表包含:
① 继承下来的基类虚函数地址
② 派⽣类重写的虚函数地址(覆盖继承下来的)
③ 派⽣类自己的虚函数地址 - 虚表是一个存虚函数指针的指针数组,vs下数组最后会放一个0x00000000标记。
- 虚函数和普通函数一样,编译好后存在代码段。
- vs下,虚表存在代码段。
多态 的学习就到这里,下一篇我们就要开始学习新的容器相关知识啦,也是今天更出来~

