C++类对象多态基础语法【超详细】

文章目录

  • 前言
  • 1. 虚函数
    • 1.1 现象
    • 1.2 多态
    • 1.3 析构函数
    • 1.4 override和final
    • 1.5 重载、隐藏、重写对比
  • 2. 抽象类
    • 2.1 抽象类特性
    • 2.2 抽象类的应用场景
  • 3. 多态实现的底层原理
  • 4. 静态绑定和动态绑定
  • 5. 总结

前言

多态是面向对象三大特性之一,也是细节最多的语法之一。学习类对象的多态,我们不仅仅要看到基础语法,也要体会到底层原理。从而我们结合类与对象的其它性质,才能更好地体会到多态的魅力。

下面小编会和大家一起探讨多态的基础语法细节!

关于虚函数,还有涉及很多很多的问题(例如:构造中的多态调用、初始化顺序……),小编会在一篇讲解习题的文章中谈到!

注:本文章的测试用例均在VS2022 x86环境下进行

1. 虚函数

小编会由现象引入多态

1.1 现象

来看下面一个例子,例1:

#include<iostream>
#include<string>
using namespace std;class animal
{
public:animal(const string& name):_name(name){}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}virtual void call(){cout << "name is : " << _name << " call : meow" << endl;}
};class dog : public animal
{
public:dog(const string& name = ""):animal(name){}virtual void call(){cout << "name is : "<< _name << " call : growl" << endl;}
};int main()
{cat c("Lucy");dog d("Juck")animal* ptr1 = &c;animal *ptr2 = &d;ptr1->call();ptr2->call();return 0;
}

说明:
上面代码定义了一个animal的类,其中成员是animal的名字和叫声。
dogcat来继承这个animal的类,并且重写了叫声方法。
创建了一个catdog对象并且由animal的指针来接受这两个指针。然后调用call方法,观察现象!

在这里插入图片描述
结果是我们可以正常得到catdogcall函数调用结果!!

这就是多态

1.2 多态

多态的字面意思:对于同一种行为,不同对象去完成产生了不同的结果(形态)!

  • 多态:是在不同继承的类对象,去调用同一个函数产生了不同的行为。

根据刚刚的例1,我们可以得到的事实是:

  1. catdog继承于同一个基类animal
  2. animal中用virtual声明了函数call
  3. catdog中都对函数call再进行了重写

看到的现象

  1. 同一个类型的animal*指针指向了不同的对象,调用同一个call函数,产生了不同的结果。

接下来我们正式步入多态语法的讲解

  • 多态的两个条件

    1. 基类指针或者引用,指向派生类。(原因后面会谈到)
    2. 被调用的函数一定是虚函数,派生类必须对虚函数进行重写
  • 虚函数在类里virtual关键字修饰的函数被称为虚函数。

    • 注意:这个virtual关键字和继承的虚拟继承没有关系
  • 重写(也叫覆盖)

    1. 函数必须是虚函数
    2. 函数名完全和基类相同、函数参数列表完全和基类相同、函数返回值和基类相同。(三同)
    • 需要注意的是:

      • virtual关键字可以在派生类的时候不用声明,但是基类必须显示声明
      • 协变:返回值可以不同。但是必须满足父子关系的同类型的返回值。例如:基类返回基类的指针,派生类虚函数就可以返回派生类的指针(不可以是引用)。(只要满足父子关系即可,并不用管是什么类型)
      • 派生类方法的访问权限与基类方法的访问权限只会影响当前类型的访问
        建议:基类的访问可以公有。
      • 了解:派生类方法的抛出的异常不能大于基类方法的异常范围。

下面我们来进行解析

  1. 必须是指针或者引用。那如果是基类对象呢?

例2:

#include<iostream>
#include<string>
using namespace std;
class animal
{
public:animal(const string& name):_name(name){}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}virtual void call(){cout << "name is : " << _name << " call : meow" << endl;}
};int main()
{cat c("Lucy");animal a = c; //用基类的对象得到对象ca.call(); //尝试调用call()函数return 0;
}

在这里插入图片描述
没有产生预期的结果!(底层解析的时候会告诉读者为什么)

这就给我们提示:普通对象的调用看的是类型。(千万不要和隐藏弄混了)

  1. 一定是需要调用虚函数。如果不是调用虚函数,那么就是满足继承体系中的隐藏(重定义)关系了!

例3:

#include<iostream>
#include<string>
using namespace std;
class animal
{
public:animal(const string& name):_name(name){}void func(){//无关键字virtual声明—>普通的成员函数cout << "1" << endl;}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}void func(){cout << "2" << endl;}void call(){cout << "name is : " << _name << " call : meow" << endl;}
};int main()
{cat c("Lucy");animal* ptr = &c;ptr->func(); //普通调用ptr->call(); //多态调用return 0;
}

上面代码func函数的调用构成普通的隐藏关系。

  1. 协变演示:

例4:

#include<iostream>
#include<string>
using namespace std;
class A
{};class B : public A
{};class animal
{
public:animal(const string& name):_name(name){}virtual A* call(){cout << "name is : " << _name << " call : sound" << endl;A* ptr = new A; //仅仅由于演示return ptr;}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}virtual B* call(){cout << "name is : " << _name << " call : meow" << endl;B* ptr = new B;return ptr;}
};int main()
{cat c("Lucy");animal* ptr1 = &c;ptr->call();return 0;
}

小编是为了演示而newAB对象,大家不要写出这样的代码。

在这里插入图片描述

  1. 访问权限问题

例5:

#include<iostream>
#include<string>
using namespace std;
class animal
{
public:animal(const string& name):_name(name){}virtual void call(){cout << "name is : " << _name << " call : sound" << endl;	}
protected:string _name;
};class cat : public animal
{
public:cat(const string& name = ""):animal(name){}private:virtual void call(){cout << "name is : " << _name << " call : meow" << endl;}
};int main()
{cat c("Lucy");animal* ptr1 = &c;ptr->call(); //没有影响return 0;
}

上面代码的animalcall方法访问权限为public,而catcall方法访问权限为private,但是不影响animal*ptr调用。

1.3 析构函数

说到多态,不得不提及一个默认成员函数了:析构函数

来看下面一个场景:

例6(错误示例):

#include<iostream>
using namespace std;
class Base
{
public:~Base(){cout << "~Base()" << endl;}
};class Derived : public Base
{
public:~Derived(){cout << "~Derived()" << endl;}
};int main()
{Base *ptr = new Derived;delete ptr;return 0;
}

说明:
上面代码Derived继承Base类,并且由Base指针指向newDerived对象,然后delete该指针指向的对象。
来看运行结果:
在这里插入图片描述
我们明明是想调用Derived的析构函数,为什么会调用Base的呢?

  • 下面我们来分析一下:

    1. delete调用析构 + 销毁空间。
    2. 上面我们谈到:普通函数的调用只能看类型。(1.2的例3)
    3. BaseDerived的析构函数很显然没有被声明为一个虚函数,那么调用就被当作普通函数来对待。
    4. 那么对于一个Base的指针来说,如果以该指针类型来调用析构函数的话,就只会调用Base类型的析构函数。

如果想要解决这个问题:

  • 析构函数要被处理为统一的名字 — 满足三同。这个任务已经帮助我们完成了,统一被处理为:destructor
  • 析构函数要被声明为虚函数

例7:

#include<iostream>
using namespace std;class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;}
};class Derived : public Base
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}
};int main()
{Base *ptr = new Derived;delete ptr;return 0;
}

来看运行结果:

在这里插入图片描述

  • 注意

    编译默认生成的析构函数不会为你声明为一个虚函数。如果涉及以上想要使用delete的场景,还请显示声明析构函数为虚函数

1.4 override和final

C++11提出这两个关键字,更好地规范了虚函数的重写

  1. override

    • 用途:声明在派生类函数的参数列表后。检查派生类虚函数是否重写了基类的某个虚函数。如果没有重写,编译器会报错。

例8:

#include<iostream>
using namespace std;class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;}virtual void func(){cout << "Base" << endl;}
};class Derived : public Base
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}virtual void func() override //声明override,并且完成了重写:三同{cout << "Derived" << endl;}
};int main()
{Base *ptr = new Derived;ptr->func();delete ptr;return 0;
}
  1. final

    • 用途:修饰虚函数,表示该虚函数不能再被重写。

例9(错误样例,代码final检查错误):

#include<iostream>
using namespace std;
class Base
{
public:virtual ~Base(){cout << "~Base()" << endl;}virtual void func() final //声明{cout << "Base" << endl;}
};class Derived : public Base
{
public:virtual ~Derived(){cout << "~Derived()" << endl;}virtual void func(){cout << "Derived" << endl;}
};

在这里插入图片描述

1.5 重载、隐藏、重写对比

这三个是比较容易混淆的概率,小编在这里为大家对比一下

名称特性
重载1、两个函数在同一作用域下 2、函数名和参数列表相同
隐藏(重定义)1、两个函数分别在父子类域中 2、函数名相同
重写(覆盖)1、两个函数分别在父子类域中 2、三同(协变例外)3、两个函数都是虚函数
  • 我们可以得到一个小结论

    • 父子类域中的两个同名函数,不是构成重写就是隐藏

2. 抽象类

有时候我们描述一个事物,但是这个事物在现实生活中是“抽象”的。那么对于它的方法而言就是一个抽象的方法,但是这个事物可以得到延展。例如:形状。

  • 抽象类:含有纯虚函数的类被称为“抽象类”。

    • 纯虚函数

      是在基类中声明的函数,它在基类中没有定义,但要求任何该类的派生类都要重写自己的实现方法

      在基类中实现纯虚函数的方法是:在虚函数的方法函数原型后面添加= 0

例10:

#include<iostream>
using namespace std;
class shape //该类为抽象类
{
public:virtual double area() const = 0 // 声明为纯虚函数并且const修饰{}
};class circle : public shape 
{
public:virtual double area() const //const修饰指针也是参数列表中的一环{//业务处理}
};

上面代码中,就是一个抽象类shape,由circle继承这个抽象类,并且重写方法area

2.1 抽象类特性

  1. 抽象类是不能实例化对象出来的

    我们可以理解为:类似这样的抽象类在现实生活中也找不到实体!

    • 也就是意味着:抽象类只能作为其它类的基类

    在这里插入图片描述

  2. 抽象类的本身类型不能作为函数参数或者函数返回值,也不能作为显示类型转换的类型

    原因同上:抽象类不能示例化出对象!

  3. 如果派生类没有重写该纯虚函数,那么该派生类也不能实例化出对象。

    在这里插入图片描述4. 可以定义抽象类类型指针或者引用来指向其派生类。这里小编就不再列举例子了。

2.2 抽象类的应用场景

  • 纯虚函数常用于定义接口规范,强制派生类实现特定功能。抽象基类仅提供接口声明,不包含具体实现,确保所有派生类遵循统一的接口标准。抽象类体现了一种:接口继承的理念。

我们可以将抽象类用于描述一些抽象的事物

  1. 图像
  2. 游戏角色
  3. ……

这些还是需要大家实战体会。

3. 多态实现的底层原理

关于这个问题,小编打算另起一文,如下:

C++类对象多态底层原理及扩展问题

在这篇文章中小编会和大家探讨:

  1. 多态调用的原理:解析虚函数指针,虚函数表,多态成立的两个条件
  2. 拓展虚函数在虚函数表中的存放位置:单继承和多继承
  3. ……

4. 静态绑定和动态绑定

上面链接那篇文章谈到了一个话题:多态调用的消耗。实际上这是一个动态绑定的。汇编层面上的差异我们已经可以得知了。

  • 静态绑定(前期绑定):

    • 在程序编译期间就已经确定了程序的行为,这就是静态绑定
    • 例如:函数重载也是一种静态多态
  • 动态绑定(后期绑定):

    • 在程序运行期间根据具体的类型确定程序的行为,这就是动态绑定
    • 例如:虚函数重写多态调用动态多态

5. 总结

我们总结一下这个部分的知识点

  1. virtual关键字声明虚函数,建议析构函数声明为虚函数。
  2. 构成多态的两个条件:a、重写 b、指针和引用调用。
  3. 重载、隐藏、重写对比。
  4. 什么是抽象类?具体的性质?
  5. 多态调用的底层原理?虚函数指针、虚函数表?
  6. 了解静态多态和动态多态 。

完……

  • 希望这篇文章能够帮助你!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/88601.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/88601.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flask 入门到实战(3):用 SQLAlchemy 优雅操作数据库

深入理解 Flask ORM&#xff1a;用 SQLAlchemy 优雅操作数据库一、前言&#xff1a;什么是 ORM&#xff1f;为什么要用它&#xff1f; 传统数据库操作要写 SQL&#xff0c;比如&#xff1a; SELECT * FROM users WHERE id 1;而使用 ORM 后&#xff0c;你可以这样写&#xff1a…

源表=电源+数字表?一文看懂SMU源表 2025-04-14

源表(Source Meter Unit, SMU)广泛用于半导体器件、材料、医疗、发光器件与光通信等行业,测量器件的伏安(I-V)特性曲线、绝缘材料的电阻值(电阻率)、电容的绝缘电阻(漏电流)、光电器件的暗电流或者L-I-V等。 源表的名称已经清晰的告诉我们,它包含了高精度电源输出和…

单片机STM32F103:DMA的原理以及应用

STM32F103系列微控制器&#xff08;基于ARM Cortex-M3内核&#xff09;集成了**DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;**控制器&#xff0c;用于在存储器与外设、存储器与存储器之间高效传输数据&#xff0c;减少CPU的干预&#xff0c;从而…

Webview 中可用的 VS Code 方法

在 VS Code Webview 的 HTML 中&#xff0c;不能直接调用 VS Code 的 API&#xff08;如 vscode.window.showInformationMessage&#xff09;&#xff0c;但可以通过 acquireVsCodeApi() 获取一个受限的 vscode 对象&#xff0c;用于与插件主程序通信。以下是详细说明和示例&am…

Qt:布局管理器Layout

目录 布局管理器 QVBoxLayout QHBoxLayout QGirdLayout QFormLayout Spacer 布局管理器 在以往的界面操作上&#xff0c;都是程序员手动拖动控件来布局&#xff0c;这种方式有一些不足之处&#xff0c;比如不能很好的把握控件之间的距离&#xff0c;以及控件的大小&…

【Java编程动手学】深入剖析Java网络编程:原理、协议与应用

文章目录一、引言二、计算机网络基础1、计算机网络的概念2、网络地址的重要性三、套接字编程&#xff1a;网络通信的基石1、套接字的概念2、TCP通信编程示例四、TCP通信编程&#xff1a;可靠的数据传输1、TCP协议的特点2、实际应用中的TCP通信五、UDP通信编程&#xff1a;高效的…

vue3.2 前端动态分页算法

文章目录背景思路页面情况核心代码小结效果背景 1. 后台接口只是动态返回一个数组的数据&#xff0c;前端需要根据数据量的大小判断是否需要分页&#xff0c;页面高度固定2. 页面根据页数大小有不同的展示a. 只有一页 头部 内容 统计 尾部b. 多页i. 第一页 头部 内容 尾…

UC浏览器PC版自2016年后未再更新不支持vue3

win uc浏览器&#xff0c;点击页面触发异常。UC浏览器PC版自2016年后未再更新&#xff08;最新版本停留在Chromium 50内核&#xff09;。其内置内核版本较低&#xff08;如Trident/Blink旧版&#xff09;&#xff0c;无法支持Vue 3等现代前端框架的语法特性&#xff08;如ES6、…

亚古数据:澳大利亚公司的ABN和ACN号码是什么?

在跨国商业的迷宫中&#xff0c;了解目标市场的公司注册细节是一项不可或缺的技能。对于与中国企业有业务往来的朋友们来说&#xff0c;澳大利亚这片充满机遇的土地上&#xff0c;两个缩写——ABN与ACN&#xff0c;如同解锁合作之门的密钥&#xff0c;显得尤为重要。今天&#…

LangChain框架 Prompts、Agents 应用

目录 (Prompts)提示作用 Prompts 常见操作 基础 PromptTemplate 使用 Few-shot 提示模板 ChatPromptTemplate (对话提示模板) (Agents)代理作用 Agents 常见操作 基础 Agent 使用 自定义工具 Agent 高级应用示例 带记忆的对话代理 使用本地模型的代理 结构化输出代…

模拟实现unordered_map

1.定义unordered_map 是 C 标准库中的哈希表容器&#xff0c;特点是无序存储、平均 O (1) 时间复杂度的插入 / 查找 / 删除操作。其核心原理是通过哈希函数将关键字映射到哈希桶&#xff08;bucket&#xff09;&#xff0c;再通过链表或红黑树处理哈希冲突。2.实现原理1. 哈希表…

史上最详细Java并发多线程(面试必备,一篇足矣)

第一章&#xff1a;线程基础 1.1 线程与进程 进程&#xff1a;系统资源分配的基本单位&#xff0c;拥有独立的内存空间 线程&#xff1a;CPU调度的基本单位&#xff0c;共享进程内存空间 关系&#xff1a;一个进程可包含多个线程&#xff0c;线程切换成本远低于进程 1.2 线程的…

【DataFlow】数据合成流水线工具

1.整体解读 核心思想&#xff1a;以数据为中心的AI&#xff08;Data-Centric AI&#xff09; DataFlow 的核心目标是通过一系列自动化“流水线”&#xff08;Pipelines&#xff09;来处理和生成高质量的数据&#xff0c;从而提升大语言模型&#xff08;LLM&#xff09;在特定领…

Hangfire 调用报错解决方案总结

System.ArgumentNullException: 值不能为 null 错误在使用 Hangfire 时确实是一个常见问题&#xff0c;特别是在配置 Hangfire 服务器时。问题分析这个错误通常发生在以下情况&#xff1a;没有正确配置 Hangfire 服务器队列配置缺失或不正确连接字符串配置问题解决方案要点正确…

MySQL的使用

MySQL的使用一、mysql中的周边命令1. 检查版本2. 查看字符集3. 查看客户端连接4. 查看最后一条警告消息二、数据库、数据表的管理1. 语法规则2. 数据库2.1 查看数据库2.2 创建数据库2.3 选择数据库2.4 查看创建数据库命令2.5 创建库时添加字符集2.6 修改数据库字符集2.7 删除数…

2025Nginx最新版讲解/面试

维护系统多服务器部署&#xff0c;将我们请求代理到各个服务器。代理正向代理&#xff0c;代理对象是我们的客户端&#xff0c;目标对象不知道我们用户。VPN就是典型的正向代理。反向代理&#xff0c;代理对象是服务端&#xff0c;用户不知道服务端具体信息。而这正是Nginx所做…

JAVASCRIPT 前端数据库-V8--仙盟数据库架构-—-—仙盟创梦IDE

老版本 在v1 版本中我们讲述了 基础版的应用 JAVASCRIPT 前端数据库-V1--仙盟数据库架构-—-—仙盟创梦IDE-CSDN博客 接下载我们做一个更复杂的的其他场景 由于&#xff0c;V1查询字段必须 id 接下来我们修改了了代码 JAVASCRIPT 前端数据库-V2--仙盟数据库架构-—-—仙盟创…

UNIX 域套接字实现本地进程间通信

&#x1f680; 使用 UNIX 域套接字 (AF_UNIX) 实现高效进程通信 在 Linux 和其他类 UNIX 系统中&#xff0c;进程间通信 (IPC) 的方法有很多种&#xff0c;例如管道、消息队列、共享内存等。然而&#xff0c;当你的应用程序需要在 同一台机器上的不同进程间进行高效、低延迟的数…

【Axure教程】中继器间图片的传递

中继器在Axure中可以作为图片保存的数据库&#xff0c;在实际系统中&#xff0c;我们经常需要将选择数据库的图片添加到其他图片列表中&#xff0c;所以今天就教大家&#xff0c;怎么在Axure中实现中继器之间的图片传递&#xff0c;包含将一个中继器中的图片列表传递到另一个中…

专题:2025云计算与AI技术研究趋势报告|附200+份报告PDF、原数据表汇总下载

原文链接&#xff1a;https://tecdat.cn/?p42935 关键词&#xff1a;2025, 云计算&#xff0c;AI 技术&#xff0c;市场趋势&#xff0c;深度学习&#xff0c;公有云&#xff0c;研究报告 云计算和 AI 技术正以肉眼可见的速度重塑商业世界。过去十年&#xff0c;全球云服务收…