C++基础知识(七:多态)

简介: 多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。


目录

多态概念与实践

1. 多态的基础

2. 函数重载(静态多态)

3. 动态多态

4. 虚析构函数

【1】多态的前提

【2】虚函数(virtual)

【4】虚析构函数

示例:


多态概念与实践

多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。

1. 多态的基础

  • 继承与多态:继承是实现多态的前提条件。通过继承,子类可以继承父类的属性和方法,并在此基础上进行扩展或重写,从而实现不同类之间相同接口的多种行为。

2. 函数重载(静态多态)

  • 定义:在同一作用域内,函数名相同但参数列表不同(类型、数量或顺序不同),实现不同的功能,这就是静态多态,编译器在编译时期就能确定调用哪个函数。

3. 动态多态

  • 虚函数:通过在基类中声明虚函数(使用virtual关键字),允许子类重写(override)该函数,实现运行时的多态性。虚函数的关键在于,它允许通过基类指针或引用调用子类的实现。
  • 虚函数表(V-Table):每个包含虚函数的类都有一个虚函数表,存储虚函数的地址。对象实例包含一个指向虚函数表的指针,该指针在对象创建时初始化。当子类重写了虚函数,子类的虚函数表会在相应位置存储子类的函数地址。
  • 实现机制:在运行时,通过基类指针调用虚函数时,实际调用的是该指针所指向对象的虚函数表中的函数地址,从而实现动态绑定,达到多态的效果。

4. 虚析构函数

  • 重要性:当使用基类指针删除派生类对象时,如果没有虚析构函数,只会调用基类的析构函数,导致派生类特有的资源无法被正确释放。因此,为保证多态对象能被正确销毁,基类的析构函数应声明为虚函数。
  • 工作原理:虚析构函数确保通过基类指针删除对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数,从而彻底释放派生类的资源。

【1】多态的前提

  1. 继承:多态的实现通常基于类的继承关系。一个类(子类)继承自另一个类(父类),子类可以继承父类的属性和方法,并且可以覆盖或扩展父类的行为。
  2. 虚函数:在基类中定义虚函数(使用virtual关键字),是实现动态多态的关键。虚函数允许子类重写父类中的同名函数,这样在运行时,根据对象的实际类型来决定调用哪个版本的函数。
  3. 父类指针或引用:通过父类的指针或引用来指向子类的对象,这是多态调用的常见方式。这样,即使使用的是父类的接口,也能调用到子类重写后的方法,实现动态行为。
  4. 方法重写:子类在继承父类的过程中,可以重写(override)父类中的虚函数,提供自己的实现。这是多态表现不同行为的基础。

在子类中重写父类的虚函数就是函数重写的过程,可以实现多态

【2】虚函数(virtual)

只要基类中是虚函数,后面的所有子类中该函数都是虚函数

常规来说,在继承时,给父类中的函数加上virtual关键字,定义成一个虚函数,

在子类中,可以对父类中的虚函数进行函数重写(override)

只要有虚函数的类,都会有一个虚函数表和一个虚(函数表)指针

虚指针是指向虚函数表的指针;

虚函数表,存储所有的虚函数的信息

虚函数表:保存所有虚函数的入口地址,每一个包含虚函数的类都会有一张虚函数表

如果发生继承关系,子类先复制父类的虚函数表,如果子类对某个虚函数重写,就去更改虚函数表中,该函数的入口地址

虚函数表指针:指向虚函数表的指针,父类中有一个虚函数表指针,子类中的虚函数表指针是从父类中继承下来的虚函数表指针,指向子类的虚函数表(虚函数表指针存在类中的第一个位置)

image.gif 编辑

【4】虚析构函数

由于实现多态,需要使用父类的指针,指向子类的空间,父类指针可以操作的空间,只有父类自己的部分,所以,在delete父类指针时,并不会释放调子类的空间

解决方法:给基类(父类)的析构函数前面加上virtual关键字,只要基类是虚析构函数,后面继承的所有子类都是虚析构函数,虚析构函数会引导父类的指针,释放掉子类的空间

示例:

#include <iostream>
using namespace std;
class Person {
public:
    virtual ~Person() { cout << "Person的析构" << endl; }
    virtual void play() { cout << "吃饭" << endl; }
    virtual void fun() { cout << "fun" << endl; }
};
class Student : public Person {
public:
    void play() override { cout << "打游戏" << endl; }
    ~Student() { cout << "Student的析构" << endl; }
};
class SubStudent : public Student {
public:
    void play() override { cout << "第二次继承" << endl; }
    void fun() override { cout << "fun的第二次继承" << endl; }
};
int main() {
    Person *p = new Student;
    p->play(); // 通过父类指针调用子类重写的play方法
    delete p;  // 正确释放资源,由于虚析构函数的存在,Student的析构也会被调用
    // 下面是注释掉的代码示例,展示了多态的其他用法
    // Person *p1 = new SubStudent;
    // p1->play();  // 第二次继承
    // p1->fun();   // fun的第二次继承
    return 0;
}

image.gif

image.gif 编辑

可以看到这里虽然是Person类型的指针但是调用出来的函数确实student的play()。

相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
40 2
C++入门12——详解多态1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
81 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
55 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
5月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
56 2
|
5月前
|
C语言 C++ 开发者
C++基础知识(一:命名空间的各种使用方法)
C++在C的基础上引入了更多的元素,例如类,类的私密性要比C中的结构体更加优秀,引用,重载,命名空间,以及STL库,模板编程和更多的函数,在面向对象的编程上更加高效。C语言的优势则是更加底层,编译速度会更快,在编写内核时大多数都是C语言去写。 在C++中,命名空间(Namespace)是一种组织代码的方式,主要用于解决全局变量、函数或类的命名冲突问题。命名空间提供了一种封装机制,允许开发者将相关的类、函数、变量等放在一个逻辑上封闭的区域中,这样相同的名字在不同的命名空间中可以共存,而不会相互干扰。
109 0
|
5月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
|
5月前
|
编译器 程序员 C++
【C++高阶】掌握C++多态:探索代码的动态之美
【C++高阶】掌握C++多态:探索代码的动态之美
45 0