C++ | 多态

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

回顾:上一篇我们结束了 继承,接下来这篇文章让我们进入到新的面向对象特性 多态 的学习,体会新的设计思路吧~

放个目录

  • [一 概念](#一 概念)
    • [1.1 静态多态](#1.1 静态多态)
    • [1.2 动态多态](#1.2 动态多态)
  • [二 多态的定义及实现](#二 多态的定义及实现)
  • [三 纯虚函数和抽象类](#三 纯虚函数和抽象类)
    • [3.1 语法格式](#3.1 语法格式)
    • [3.2 介绍](#3.2 介绍)
    • [3.3 上代码](#3.3 上代码)
  • [四 多态的原理](#四 多态的原理)

一 概念

多态分为 编译时多态(静态多态)和 运行时多态(动态多态)。

1.1 静态多态

即编译时多态。

  1. 主要指 函数重载 和 函数模板。
  2. 传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。
  3. 这里的参数匹配是在编译时完成的。

1.2 动态多态

即运行时多态。

  • 要完成某个函数,传不同的对象就会完成不同的⾏为。

二 多态的定义及实现

2.1 多态的构成条件

  1. 一个继承关系下的类对象,去调⽤同⼀函数,产生不同行为。
  2. 必须是 基类的指针/引用 调⽤虚函数。
  3. 被调用的虚函数,完成了虚函数重写/覆盖。

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)重载
  1. 两个函数在同个作用域。
  2. 函数名相同,参数不同。
(2)重写
  1. 俩函数分别在基类和派生类俩作用域。
  2. 函数名,参数,返回值相同(协变为特殊情况)。
  3. 两个函数都必须是虚函数。
(3)隐藏
  1. 俩函数分别在基类和派生类俩作用域。
  2. 函数名/变量名 相同。
  3. 俩函数不构成重写。

三 纯虚函数和抽象类

3.1 语法格式

cpp 复制代码
virtual void fun() = 0;

3.2 介绍

  1. 纯虚函数只需要声明即可(当然想定义也可以定义),后续会被派生类重写。
  2. 包含纯虚函数的类叫抽象类,抽象类不能实例化出对象。
  3. 所以派生类不重写纯虚函数,就不能被实例化。

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 虚函数表

  1. 基类和派生类有各自的虚函数表,同类型对象共⽤⼀张虚表。
  2. 派生类的虚函数表指针 继承自 基类,但跟基类不是同一个指针(各自指向各自作用域的虚函数)。
  3. 派生类重写的虚函数,该虚函数指针(继承下来后)会在虚表中被 重写的虚函数地址 覆盖。
  4. 派生类虚表包含:
    ① 继承下来的基类虚函数地址
    ② 派⽣类重写的虚函数地址(覆盖继承下来的)
    ③ 派⽣类自己的虚函数地址
  5. 虚表是一个存虚函数指针的指针数组,vs下数组最后会放一个0x00000000标记。
  6. 虚函数和普通函数一样,编译好后存在代码段。
  7. vs下,虚表存在代码段。

多态 的学习就到这里,下一篇我们就要开始学习新的容器相关知识啦,也是今天更出来~


相关推荐
我是无敌小恐龙1 小时前
Java SE 零基础入门 Day05 类与对象核心详解(封装+构造方法+内存+变量)
java·开发语言·人工智能·python·机器学习·计算机视觉·数据挖掘
逻辑驱动的ken1 小时前
Java高频面试考点14
开发语言·数据库·算法·哈希算法
小灰灰搞电子2 小时前
Python self关键字详解及其应用
开发语言·python
故事还在继续吗2 小时前
C++17关键特性
开发语言·c++·算法
Rabitebla2 小时前
【数据结构】消失的数字+ 轮转数组:踩坑详解
c语言·数据结构·c++·算法·leetcode
8486981192 小时前
Cursor 用 Java + Vue3 做了一个可落地的酒店管理系统(HMS),支持多门店、RBAC、财务结算,源码开源!
java·开发语言·开源
格林威2 小时前
面阵相机 vs 线阵相机:堡盟与Basler选型差异全解析 +C# 实战演示
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
Queenie_Charlie2 小时前
关于二叉树(2)
数据结构·c++·二叉树·简单树结构
枫叶丹42 小时前
【HarmonyOS 6.0】AVCodec Kit 视频解码器平滑停用机制详解
开发语言·华为·音视频·harmonyos