当前位置: 首页 > news >正文

浅谈C++的继承与多态(静态绑定、动态绑定和虚函数等)

今天我们来谈谈C++的继承与多态😊😊😊,本篇的关键内容如下:

  • 继承的本质及其原理
  • 派生类的构造和析构过程
  • 重载、隐藏和覆盖
  • 类的向下或向上转型
  • 静态绑定与动态绑定
  • 虚函数对类的影响
  • 虚析构函数

下面,我们将对这几个有关于C++继承与多态的关键内容进行详述的论述

浅谈C++的继承与多态

    • 一、继承的本质及其原理
    • 二、派生类的构造和析构过程
    • 三、重载、隐藏和覆盖
    • 四、类的向下或向上转型
    • 五、静态绑定与动态绑定
    • **一个问题:thinking::thinking::thinking::一个类声明了虚函数后,当调用该虚函数时,一定是动态绑定吗?** **不一定 !**
    • 六、虚析构函数
    • **什么时候把基类的析构函数声明为虚函数呢?**
    • 七、虚函数对类的影响

一、继承的本质及其原理

在C++中,继承是一种面向对象编程思想的概念,允许一个类继承另一个类的属性和方法,同时也可以在此基础上添加新的属性和方法。究其本质,即:

  • 代码的复用
  • 在基类中给所有派生类提供一个统一的接口,让派生类重写,满足开闭原则

二、派生类的构造和析构过程

在定义的派生类中,由于派生类是从基类的基础上继承过来的,对应继承而来的成员的初始化和清理是由基类的构造函数和析构函数负责,而派生类的构造和析构函数则负责初始化和清理派生类中特定的部分,所以在派生类调用构造函数需要初始化基类里的成员变量时,不能直接指定,而是需要通过基类的构造函数来进行初始化,如下:

class Base
{
public:Base(int d) :ma(d) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }
protected:int ma;
};class Derive :public Base
{
public://Derive(int d = 20) :ma(d), mb(d) {cout << "Derive()" << endl;}//此句会出现报错:"ma" 不是类 "Derive" 的非静态数据成员或基类Derive(int d = 20) :Base(d), mb(d) {cout << "Derive()" << endl;}~Derive() { cout << "~Derive()" << endl; }
private:int mb;
};

这里我们给出一段关于派生类构造和析构过程的主要描述:

  • 派生类调用基类的构造函数,初始化从基类继承而来的成员
  • 调用派生类自己的构造函数,初始化派生类自己特有的成员
  • 调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆空间、文件等)
  • 调用基类的构造函数,释放派生类内存中从基类继承而来的成员可以占用的外部资源(堆空间、文件等)
int main()
{Derive m(20);  //可以自己允许一遍,查看程序允许结果return 0;
}

三、重载、隐藏和覆盖

重载: 一组函数重载,必须处在同一个作用域中,且函数名相同,参数列表不同
隐藏: 隐藏其实就是隐藏作用域,即,在继承结构中,派生类的同名成员,把基类的同名成员隐藏掉了
覆盖: 如果派生类中定义的方法与在基类继承而来的方法,在返回值、函数名、参数列表都相同的前提下,且基类的实现方法为virtual虚函数,那么派生类的这个方法就会被自动的处理成虚函数,本质上是虚函数表上的虚函数地址的覆盖,详见后文!

若我们在上面的Base类和Derive中添加方法 void show() ,在派生类调用show函数时会优先在自己类中寻找对应的函数方法,即会打印Derive::show(),基类的show方法被隐藏了

class Base
{...void show() { cout << "Base::show()" << endl; }...
};
class Derive :public Base
{void show() { cout << "Derive::show()" << endl; }...
};int main()
{Derive m(20);m.show();  //优先找派生类自己show成员return 0;
}

四、类的向下或向上转型

在C++中,派生类对象转化为基类对象、基类对象转化为派生类对象的情形可以描述为:

  • 向上转型: 派生类对象(指针) -> 基类对象(指针) YES
  • 向下转型: 基类对象(指针) -> 派生类对象(指针) NO

由于派生类是由基类继承而来的,其给它分配地址空间大于基类的地址空间,故可以将派生类对象(指针)转化为基类对象(指针),即Base* pb = &m,而不可以可以将基类类对象(指针)转化为派生类对象(指针),即Derive* pd = &m;,如下图所示:
在这里插入图片描述
对于 派生类 -》 基类: 相当于切片,红色部分内存在基类指针访问不了,也不会被访问
对于 基类 -》 派生类: 派生类申请的地址空间大于基类,一旦派生类对象/指针访问红色部分时,由于基类对象是没有这块空间的,直接会发生非法访问问题
在这里插入图片描述

五、静态绑定与动态绑定

静态绑定和动态绑定是C++中两种不同的绑定方式

  • 静态绑定: 在编译时进行的绑定。它根据函数调用时的静态类型来确定要调用的函数。静态绑定适用于非虚函数,它会默认绑定到函数定义中的相应代码。
  • 动态绑定: 在运行时进行的绑定。它根据函数调用时的动态类型来确定要调用的函数。动态绑定适用于虚函数,它允许在运行时根据实际对象类型来调用相应的函数。这样可以实现多态性,即不同对象调用同名函数时可以执行不同的操作。
#include <iostream>class Base {
public:void print() {std::cout << "Base class" << std::endl;}virtual void display() {std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:void print() {std::cout << "Derived class" << std::endl;}void display() {std::cout << "Derived class" << std::endl;}
};int main() {Base* d = new Derived();d->print(); // 静态绑定,调用Base类的print函数/*mov eax, dword ptr[pb]mov ecx, dword ptr[eax]call ecx;(虚函数地址)  动态(运行时期)的绑定(函数的调用)*/d->display(); // 动态绑定,调用Derived类的display函数return 0;
}

d->print() 调用的是静态绑定,因为 print() 函数在基类中不是虚函数。即使 d 指向的对象实际类型是 Derived 类,编译器也会根据指针类型(Base*)来静态绑定调用基类的 print() 函数。所以输出为 Base class

d->display() 这里调用的是动态绑定,因为 display() 函数是虚函数。在运行时,实际调用的是指向的对象的类型的版本,即RTTI (run-time type information) ,即 Derived 类中的 display() 函数。所以输出为 Derived class

一个问题🤔🤔🤔:一个类声明了虚函数后,当调用该虚函数时,一定是动态绑定吗? 不一定 !

动态绑定的实现通常涉及虚函数表(vtable)的使用。每个包含虚函数的类都会有一个虚函数表,其中包含了指向虚函数的指针。子类会继承父类的虚函数表,并在其自己的虚函数表中重写父类的虚函数指针。这样,当通过指针或引用调用虚函数时,会根据对象的实际类型找到对应的虚函数指针,并调用正确的函数。
然而,如果直接通过对象调用虚函数,编译器会根据对象的静态类型来进行绑定,即根据对象的声明类型来调用虚函数,而这就是静态绑定,因为编译器在编译时已经确定了应该调用哪个函数,不需要在运行时根据对象的实际类型进行判断。因此,在通过指针或引用调用虚函数时,会进行动态绑定,根据对象的实际类型确定调用哪个函数;而在直接通过对象调用虚函数时,会进行静态绑定,根据对象的声明类型确定调用哪个函数
在这里插入图片描述

六、虚析构函数

虚析构函数是一种特殊的析构函数,用于实现多态性。它允许在基类指针指向派生类对象时,使用基类指针来调用派生类对象的析构函数,从而正确地释放派生类对象的资源,我们先来看看下面几个问题

我们来看看虚函数所依赖的条件:

  • 虚函数能产生地址,存储在vftable虚函数表中
  • 对象必须存在(vfptr -> vftable -> 虚函数地址)

哪些函数不能声明为虚函数?

  • 构造函数:构造函数在对象创建和销毁的过程中是特殊的,在构造函数中的所有函数都是静态绑定的,不能声明为virtual
  • 静态成员函数static:虚函数是通过对象访问的,而静态成员函数没有 this 指针,是直接通过类名访问的,不能被继承和覆盖
  • 内联函数:内联函数在编译时会直接将函数体嵌入到函数调用点,没有函数调用的开销。而虚函数是通过虚函数表来确定的,无法进行内联

什么时候把基类的析构函数声明为虚函数呢?

当基类指针(或引用)指向在堆上通过new创建的派生类对象时,派生类对象也会分配一份外部空间。如果基类的析构函数没有声明为虚函数,在准备使用delete删除基类指针时,会发生静态绑定。这样,基类对象会调用基类的析构函数,而派生类对象则无法调用其自己的析构函数,导致内存泄漏。

class Base
{
public:Base(int d) :ma(d) { cout << "Base()" << endl; }~Base() { cout << "~Base()" << endl; }virtual void show() { cout << "Base::show()" << >endl; }
protected:int ma;
};class Derive :public Base
{
public:Derive(int d) :Base(d), mb(d), ptr(new int(d)){cout << "Derive()" << endl;}// 基类的析构函数是virtual,那么派生类的析构>函数自动变成virtual~Derive(){delete ptr;cout << "~Derive()" << endl;}void show() { cout << "Derive::show()" << endl; }private:int mb;int* ptr;
};int main()
{Base* pb = new Derive(10);pb->show();delete pb;return 0;
}     

在将基类的析构函数声明为虚函数后,当使用delete删除基类指针时,由于基类的析构函数是虚函数,会发生动态绑定。这样,派生类的析构函数会自动成为虚析构函数。在执行delete时,通过虚函数表指针(vfptr)找到虚函数表(vftable),将基类在虚函数表上的虚析构函数覆盖为派生类的虚析构函数。这样就会先调用派生类的析构函数,再调用基类的虚构函数进行释放。

七、虚函数对类的影响

  1. 一个类里面定义了虚函数,那么编译阶段,编译器会给这个类生成唯一的虚函数表 vftable 主要存放的是RTTI指针虚函数地址,当程序运行时,每一张虚函数表都会加载到内存的.rodate区(只读,不能写)

  2. 一个类中定义了虚函数,那么这个类定义的对象在程序运行时,内存中开始的部分多存储一个vfptr虚函数指针,指向相同类型的虚函数表vftable,一个类型定义的n个对象,它们的 vfptr 指向的都是同一张虚函数表

  3. 一个类里的虚函数个数,不影响对象内存的大小(vfptr),影响的是虚函数表的大小

  4. 如果派生类中的方法和基类继承而来的某个方法的返回值、函数名、参数列表相同,而基类的方法是virtual虚函数,那么派生类的这个方法会自动被处理成虚函数,将虚函数表中的原来的虚函数地址覆盖成派生类的虚函数地址


🌻🌻🌻以上就是有关浅浅谈C++的继承与多态(静态绑定、动态绑定和虚函数等)的内容,如果聪明的你浏览到这篇文章并觉得文章内容对你有帮助,请不吝动动手指,给博主一个小小的赞和收藏 🌻🌻🌻

http://www.lryc.cn/news/324490.html

相关文章:

  • 【无人机综合考试题】
  • JS精度计算的几种解决方法,1、转换成整数计算后再转换成小数,2、toFixed,3、math.js,4、bignumber.js,5、big.js
  • v77.递归
  • Spring Cloud微服务功能及其组件详细讲解
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • 4635: 【搜索】【广度优先】回家
  • Uibot6.0 (RPA财务机器人师资培训第1天 )RPA+AI、RPA基础语法
  • 【吊打面试官系列】Redis篇 -Redis集群的主从复制模型是怎样的?
  • 高效的二进制列化格式 MessagePack 详解
  • 鸿蒙Harmony应用开发—ArkTS-if/else:条件渲染
  • JAVA 100道题(14)
  • STM32+ESP8266水墨屏天气时钟:简易多级菜单(数组查表法)
  • 数学建模综合评价模型与决策方法
  • window下安装并使用nvm(含卸载node、卸载nvm、全局安装npm)
  • Mysql——基础命令集合
  • 记录一次流相关故障
  • linux源配置:ubuntu、centos;lspci与lsmod命令区别
  • 面试算法-88-反转链表
  • 如何在个人Windows电脑搭建Cloudreve云盘并实现无公网IP远程访问
  • 一文详解Rust中的字符串
  • Mysql中用户密码修改
  • day14-SpringBoot 原理篇
  • ChatGPT论文指南|揭秘8大ChatGPT提示词研究技巧提升写作效率【建议收藏】
  • P1563 [NOIP2016 提高组] 玩具谜题
  • 【数据库】数据库语言
  • javascript单例模式字面量定义的接口和匿名函数定义的接口;他们之间访问私有变量和私有函数之间的区别
  • 啥是大语言模型LLM
  • vue3之路由导航故障
  • Dr4g0n
  • 蓝桥杯每日一题:扫雷