【C++进阶2--多态】面向对象三大特性之一,多种形态像魔法?

news/2025/1/26 15:35:22/

今天,带来C++多态的讲解。

多态和继承并用,能产生“魔法般的效果”。

*文中不足错漏之处望请斧正!


见见多态

是什么

使得父类指针或引用有多种形态。

怎么使它有多种形态呢?咱们先见见猪跑。

见见猪跑

class Base
{
public:virtual void print() { cout << "Base" << endl;}
};class Derive1 : public Base
{
public:virtual void print() { cout << "Derive1" << endl;}
};class Derive2 : public Base
{
public:virtual void print() { cout << "Derive2" << endl;}
};int main()
{Base b;Derive1 d1;Derive2 d2;Base* ptr;ptr = &b;ptr->print();ptr = &d1;ptr->print();ptr = &d2;ptr->print();return 0;
}
Base
Derive1
Derive2

父类指针,存父类对象地址,就能调用父类中的print,存子类对象地址,就能调用子类中的print,拥有了多种形态。


多态的实现

满足多态的前提:子类对父类的虚函数完成重写。

啥是虚函数,啥是重写?

虚函数

虚函数是被virtual修饰的成员函数。可以理解为一种对函数体的泛化。

在声明虚函数的域,默认有一份实例;在此域之外,你可以对虚函数“实例化”,得到新的一份实例。

其作用是实现多态性。

重写/覆盖

重写就是“实例化”虚函数,可以产生新的“虚函数实例”,会把原来的实例覆盖掉。

  • 对虚函数重写的条件:[函数名、返回值、参数]和父类的虚函数相同
  • *重写的仅仅是函数体,重写前后接口是一样的
  • 例外
    • 子类的虚函数可以不写virtual
    • 协变:返回值可以是任意父子类关系的指针/引用
class Base
{
public:virtual void print() { cout << "Base" << endl;} 
};class Derive1 : public Base
{
public:virtual void print() { cout << "Derive1" << endl;}
};class Derive2 : public Base
{
public:virtual void print() { cout << "Derive2" << endl;}
};
int main()
{Base b;Derive1 d1;Derive2 d2;Base* ptr;ptr = &b;ptr->print();ptr = &d1;ptr->print();ptr = &d2;ptr->print();return 0;
}
  • print这个虚函数默认有一份实例,它的函数体功能是打印"Base"的
  • print这个虚函数在Derive1中被重写了,这份新的实例把原来默认的覆盖了,它的函数体功能是打印”Derive1”的
  • print这个虚函数在Derive2中被重写了,这份新的实例把原来默认的覆盖了,它的函数体功能是打印”Derive2”的

如果不重写呢?

class Base
{
public:virtual void print() { cout << "Base" << endl;}
};class Derive1 : public Base
{
public:
//    virtual void print() { cout << "Derive1" << endl;}
};class Derive2 : public Base
{
public:
//    virtual void print() { cout << "Derive2" << endl;}
};

main函数不变,结果如下:

Base
Base
Base

看上面这个调用的手法,有个疑问:它是怎么知道调用哪个虚函数的实例的?

这就要说到虚函数表了。

虚函数表

有虚函数的类,其对象都会存一个虚函数表指针vptr,虚函数表vtable是干嘛的?

虚函数表是一个类的虚函数地址表,存放了这个类对某个虚函数的所有实例(对虚函数进行重写的到的真实函数)的地址。说白了,某个类对一个虚函数的实例,是用虚函数表来描述和组织起来的。

  • 为提高效率,这个可能高频访问的虚表(虚函数表)指针一般放在对象的头4/8个字节
  • 按我们的说法,虚函数表描述的是整个类对某个虚函数的实例,因此它就像类的static成员一样,属于整个类,所以存放在代码段

大概过程:

  1. 创建类对象(对象的头4/8个字节存了一个虚表指针)
  2. 调用对象的某个虚函数实例
  3. 根据虚表来找到当前类对这个虚函数的实例

普通调用和多态调用

普通调用也叫静态绑定,多态调用也叫动态绑定。

静态绑定:编译时通过调用方类型确定调用的函数
动态绑定:运行时通过父类指针指向的对象类型确定调用的函数

  • 用父类指针或引用调用被重写的虚函数是动态绑定
  • 其他都是静态绑定

在这里插入图片描述


多态中的析构函数

若在继承中出现这样的情况:

  1. 动态申请对象
  2. 子类没有重写析构函数(静态绑定)

则delete动态对象的空间时,析构调用不完全——只会根据指针类型静态绑定,只调用父类的析构。

class Base
{
public:~Base(){cout << "~Base()" << endl;delete[] _pb;}
private:int* _pb = new int[10];
};class Derive : public Base
{
public:~Derive(){cout << "~Derive()" << endl;delete[] _pd;}
private:int* _pd = new int[20];
};int main()
{Base* ptr = new Derive;delete ptr;
}
~Base()

可以看到,只会调用父类析构。

静态绑定不行,我们来动态绑定,多态上场。

class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;delete[] _pb;}
private:int* _pb = new int[10];
};class Derive : public Base
{
public:~Derive() //此处可以不用写virtual,这是子类可以不用写virtual的一种使用场景{cout << "~Derive()" << endl;delete[] _pd;}
private:int* _pd = new int[20];
};int main()
{Base* ptr = new Derive;delete ptr;
}
~Derive()
~Base()

满足了多态,调用完父类析构之后就会自动调用子类析构,成功解决。

Destructor

我们之前提到继承中的析构函数名都会被处理成Destructor,为了能够满足重写的条件。

在这里插入图片描述


继承中的对象模型

  1. 单继承(无重写)的虚函数表:
    1. 虚函数按照其声明顺序放于表中
    2. 父类的虚函数在子类的虚函数前面
  2. 单继承(有重写)的虚函数表
    1. 被重写的虚函数被放到了虚表中原来父类虚函数的位置(所以调用的时候不会跑去调用父类的,而是调用自己的)
    2. 没有被覆盖的函数依旧
  3. 多继承(无重写)的虚函数表
    1. 每个父类都有自己的虚表
    2. 子类的成员函数被放到了第一个父类的表中(所谓的第一个父类是按照声明顺序来判断的)

final和override

final

表明类不能被继承。

设计一个不能被继承的类

  1. final修饰类
class A final {};class B : public A {};int main()
{B b; //A被final修饰,无法被继承return 0;
}
  1. 构造私有化——子类想实例化必须调用父类构造。
class A
{
private:A() {}
};class B : public A {};int main()
{B b; //A的构造是私有,实例B需要调用A的析构,调不动,所以A无法被继承return 0;
}

override

override可以检查子类虚函数是否和父类的某个虚函数构成重写,或者强制要求某个函数被重写(需要重写的就都带上)。

//override:检查子类的虚函数是否完成重写
class Car
{
public:void Drive() {}
};class Benz : public Car
{
public://err:'Drive' marked 'override' but does not override any member functionsvirtual void Drive() override { cout << "Benz->comfortable" << endl; }
};int main()
{Benz mycar;mycar.Drive();return 0;
}

抽象类

先导:纯虚函数

是什么:虚函数后写上0,不需要函数体的虚函数。(可以有函数体,但不会被执行)

virtual void func() = 0;

是什么

有纯虚函数的类。

为什么

用于接口继承。

  • 接口继承:主要是为了让子类重写,形成多态
  • 实现继承:主要是为了复用函数体
class Car
{
public:virtual void Drive() = 0;
};class BMW : public Car
{
public:virtual void Drive(){cout << "驾驶乐趣+1" << endl;}
};int main()
{BMW mycar;mycar.Drive();return 0;
}

特性

  • 抽象类不能实例化出对象
  • 若想实例化对象,必须重写纯虚函数(这样就不是抽象类了)
  • 子类继承了抽象类也还是抽象类

今天的分享就到这里了,感谢您能看到这里。

这里是培根的blog,期待与你共同进步!


http://www.ppmy.cn/news/68273.html

相关文章

vue-admin-template 后台模版初始化及问题汇总

参考&#xff1a;https://segmentfault.com/a/1190000023185109?sortvotes 问题一&#xff1a;Unsupported engine 后来经过分析&#xff0c;发现&#xff0c;element-ui 的版本依赖锁定是在一个叫做package-locak.json 中&#xff0c;并且找到了相关配置。 package-lock.js…

【操作系统】《2023 南京大学 “操作系统:设计与实现” (蒋炎岩)》- 知识点目录

《2023 南京大学 “操作系统&#xff1a;设计与实现” (蒋炎岩)》 1. 操作系统概述 (操作系统的历史&#xff1b;学习建议) [南京大学2023操作系统-P1] 1.1 Z3库&#xff1a;解决逻辑定理证明问题 Z3是由微软研究院开发的一个高效的定理证明器&#xff0c;用于解决逻辑定理证…

XSD2Code++ Crack

XSD2Code Crack XSD2Code是为那些希望在将复杂的XML和JSON模式转换为NetCore时节省时间的开发人员设计的。它使用简单且灵活&#xff0c;可以很容易地集成到任何项目中&#xff0c;并适应开发人员的需求。它通过直观、可定制的用户界面&#xff0c;真正提高了生产力。使用XSD2C…

项目集战略一致性

项目集战略一致性是识别项目集输出和成果&#xff0c;以便与组织的目标和目的保持一致的绩效领域。 本章内容包括&#xff1a; 1 项目集商业论证 2 项目集章程 3 项目集路线图 4 环境评估 5 项目集风险管理战略 项目集应与组织战略保持一致&#xff0c;并促进组织效益的实现。为…

【算法】Minimum Moves to Move a Box to Their Target Location 推箱子

文章目录 Minimum Moves to Move a Box to Their Target Location 推箱子问题描述&#xff1a;分析代码 Tag Minimum Moves to Move a Box to Their Target Location 推箱子 问题描述&#xff1a; 问题 「推箱子」是一款风靡全球的益智小游戏&#xff0c;玩家需要将箱子推到仓…

设计一款全新交互的购物app

设计一款全新交互的购物app&#xff0c;基于全息影像技术实现全新的体验。 在全息影像购物场景中&#xff0c;可以利用全息影像技术提供一种全新的购物体验。以下是一个基于全息影像的购物场景生成的示例&#xff1a; 虚拟试衣镜&#xff1a;顾客站在全息影像投影区域前&#…

硬件I2C读写MPU6050代码

1、接线图 SDA接在B11,SCL接在B10 &#xff0c;软件IIC的两个引脚可以任意更改的&#xff0c;因为都是开漏输出&#xff0c;硬件接在哪个引脚上&#xff0c;程序中就对应操作哪个引脚 但是硬件IIC&#xff0c;通信引脚是不可以任意指定的&#xff0c;查表&#xff0c;由于PB6、…

第四讲:“象声词串联”写具体

有声有色&#xff0c;这个世界才会生动鲜活! 我们每天都生活在声音的世界里 象声词&#xff0c;还是事物之间的“交谈” !在一个 (组)的背后&#xff0c;藏着事物与事物之间热烈的“对话”。写作就是将“对话”内容发掘出来。 勤快的爸爸一下班就钻进了厨房。水龙头“哗啦啦 ”…