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

【C++入门】虚函数与多态

文章目录

  • 前言
    • 虚函数是什么?
      • 如何使用虚函数?
    • 纯虚函数是什么?
    • 虚函数与普通函数的区别
    • 虚表
      • 虚表是什么?
      • 含有虚表的类内存结构图
      • 如何找到虚表的地址?
      • 示例代码
      • 代码解释
    • 多态是什么?
      • 如何使用多态?
      • 为什么要使用多态?
    • 多态遇到的所有情况
  • 总结


前言

C++ 是一种功能强大的编程语言,以其面向对象编程(OOP)特性而闻名。虚函数和多态是 C++ 面向对象编程中的两个重要概念,它们使得代码更灵活、更具扩展性。本篇文章将介绍虚函数与多态,包括其定义、使用方法及其在实际编程中的作用。


虚函数是什么?

虚函数是一种在基类中使用 virtual 关键字声明的函数,它允许在运行时通过基类指针或引用调用子类的重写版本。这种机制使得 C++ 能够实现多态性。

如何使用虚函数?

在基类中声明虚函数,并在子类中重写该虚函数。基类指针或引用可以指向子类对象,并在运行时调用子类的重写版本。

#include <iostream>class Base {
public:virtual void show() {std::cout << "Base show" << std::endl;}
};class Derived : public Base {
public:void show() override {std::cout << "Derived show" << std::endl;}
};int main() {Base* ptr = new Derived();ptr->show();  // 调用的是 Derived 的 show 方法delete ptr;return 0;
}

纯虚函数是什么?

纯虚函数是一种没有实现的虚函数,必须在派生类中实现。它使用 = 0 语法来声明。声明纯虚函数的类称为抽象类,不能直接实例化。

class AbstractBase {
public:virtual void pureVirtualFunction() = 0; // 纯虚函数
};class ConcreteDerived : public AbstractBase {
public:void pureVirtualFunction() override {std::cout << "ConcreteDerived implementation of pureVirtualFunction" << std::endl;}
};int main() {// AbstractBase obj; // 错误,不能实例化抽象类ConcreteDerived obj;obj.pureVirtualFunction(); // 调用派生类实现的纯虚函数return 0;
}

虚函数与普通函数的区别

  1. 调用时机

    • 普通函数:在编译时确定调用的函数。
    • 虚函数:在运行时确定调用的函数(动态绑定)。
  2. 多态性

    • 普通函数:不支持多态性。
    • 虚函数:支持多态性,可以通过基类指针或引用调用派生类的重写版本。

虚表

虚表是什么?

在 C++ 中,虚表(vtable)是一个由编译器维护的数据结构,用于实现虚函数的动态绑定。当一个类中包含虚函数时,编译器会为这个类创建一个虚表。虚表是一个指针数组,其中每个指针指向该类的虚函数实现。当通过基类指针或引用调用虚函数时,程序会通过虚表找到实际要调用的函数。

每个包含虚函数的类对象都有一个隐藏的指针,称为虚表指针(vptr),它指向该对象所属类的虚表。因此,通过虚表指针,程序可以在运行时动态地确定调用哪个函数。

含有虚表的类内存结构图

有下面这样的两个类:

class Base {
public:virtual void func1();int baseData;
};class Derived : public Base {
public:void func1() override;void func2();int derivedData;
};

它的内存结构图如下

+-----------------+
| Derived object  |
+-----------------+
| vptr ---------->+-------------------------+
| derivedData     |                         |
+-----------------+                         |
| baseData        |                         |
+-----------------+                         ||||+-----------------+   +-----------------+| Virtual Table   |   | Virtual Table   || for Derived     |   | for Base        |+-----------------+   +-----------------+| func1()         |   | func1()         || Derived::func1  |   | Base::func1     |+-----------------+   +-----------------+| func2()         || Derived::func2  |+-----------------+

如何找到虚表的地址?

找到虚表地址的方法涉及使用一些底层的 C++ 技巧和对内存布局的理解。以下是一个简单的示例代码,用于展示如何找到一个类对象的虚表地址并打印虚表中的内容。

示例代码

#include <iostream>class Base {
public:virtual void func1() {std::cout << "Base::func1" << std::endl;}virtual void func2() {std::cout << "Base::func2" << std::endl;}
};class Derived : public Base {
public:void func1() override {std::cout << "Derived::func1" << std::endl;}void func2() override {std::cout << "Derived::func2" << std::endl;}
};typedef void(*FuncPtr)();void printVTable(FuncPtr* vtable) {std::cout << "Virtual Table Address: " << vtable << std::endl;for (int i = 0; vtable[i] != nullptr; ++i) {std::cout << "Function " << i << " address: " << vtable[i] << std::endl;}
}int main() {Derived obj;// 获取虚表指针的地址FuncPtr* vptr = *reinterpret_cast<FuncPtr**>(&obj);printVTable(vptr);return 0;
}

代码解释

  1. 类定义

    • Base 类中定义了两个虚函数 func1func2
    • Derived 类继承自 Base 并重写了这两个虚函数。
  2. 函数指针类型

    • typedef void(*FuncPtr)(); 定义了一个函数指针类型 FuncPtr,用于指向虚函数。
  3. 打印虚表函数

    • printVTable 函数接受一个虚表指针数组并打印虚表地址和其中每个函数的地址。
  4. main 函数

    • 创建一个 Derived 类对象 obj
    • 使用 reinterpret_cast 将对象的地址转换为虚表指针数组的指针,从而获取虚表地址。
    • 调用 printVTable 函数打印虚表内容。

多态是什么?

多态性是面向对象编程的一种特性,它允许通过基类指针或引用在运行时调用不同子类的重写方法。多态性使得代码更灵活和可扩展。

如何使用多态?

通过基类指针或引用指向子类对象,并调用基类中的虚函数。在运行时,根据实际指向的对象类型调用相应的重写方法。

#include <iostream>class Animal {
public:virtual void sound() {std::cout << "Some generic animal sound" << std::endl;}
};class Dog : public Animal {
public:void sound() override {std::cout << "Bark" << std::endl;}
};class Cat : public Animal {
public:void sound() override {std::cout << "Meow" << std::endl;}
};void makeSound(Animal* animal) {animal->sound(); // 根据实际对象类型调用相应的 sound 方法
}int main() {Dog dog;Cat cat;makeSound(&dog); // 输出 BarkmakeSound(&cat); // 输出 Meowreturn 0;
}

为什么要使用多态?

  1. 代码复用:多态性允许通过统一的接口调用不同子类的方法,提高代码的复用性和可维护性。
  2. 灵活性:多态性使得程序在运行时根据实际情况调用不同的方法,提高了代码的灵活性和扩展性。
  3. 可扩展性:可以在不修改已有代码的情况下添加新的子类,并在运行时通过多态性使用它们。

多态遇到的所有情况

  1. 通过基类指针或引用调用子类方法

    • 使用基类指针或引用指向子类对象,调用虚函数实现多态性。
  2. 对象切片问题

    • 如果将子类对象赋值给基类对象,会发生对象切片问题,子类的附加属性和方法会被切掉。
    Base baseObj = derivedObj; // 对象切片,丢失 Derived 部分
    
  3. 多重继承中的多态

    • C++ 支持多重继承,可以通过虚继承解决多重继承中的菱形继承问题,使多态性依然有效。
  4. 虚析构函数

    • 在有多态的情况下,基类通常需要一个虚析构函数,以确保通过基类指针删除派生类对象时,正确调用派生类的析构函数。
    class Base {
    public:virtual ~Base() {std::cout << "Base destructor" << std::endl;}
    };class Derived : public Base {
    public:~Derived() {std::cout << "Derived destructor" << std::endl;}
    };int main() {Base* ptr = new Derived();delete ptr;  // 正确调用 Derived 的析构函数return 0;
    }
    

总结

虚函数和多态是 C++ 中面向对象编程的重要特性,通过虚函数可以实现多态性,使得代码更加灵活和可扩展。虚函数允许在运行时通过基类指针或引用调用子类的重写方法,而纯虚函数则强制派生类实现某些方法,使得抽象类无法实例化。多态性使得程序可以在运行时根据实际对象类型调用相应的方法,提升了代码的复用性、灵活性和可扩展性。然而,在使用多态时,需要注意对象切片问题和虚析构函数的使用,以确保正确的对象管理和方法调用。掌握虚函数和多态的概念和应用,对于提高 C++ 编程水平和开发效率至关重要。

相关文章:

【C++入门】虚函数与多态

文章目录 前言虚函数是什么&#xff1f;如何使用虚函数&#xff1f; 纯虚函数是什么&#xff1f;虚函数与普通函数的区别虚表虚表是什么&#xff1f;含有虚表的类内存结构图如何找到虚表的地址&#xff1f;示例代码代码解释 多态是什么&#xff1f;如何使用多态&#xff1f;为什…...

wpf中轮询显示图片

本文的需求是&#xff0c;在一个文件夹中&#xff0c;放一堆图片的集合&#xff0c;然后在wpf程序中&#xff0c;按照定时的方式&#xff0c;循序显示照片。 全部代码 1.声明一个PictureInfo类 namespace WpfApp1 {public class PictureInfo{public string? FileName { get; …...

CSA笔记9-磁盘管理(2)

分区挂载 挂载&#xff1a;将该文件系统中的内容与指定的目录关联起来&#xff0c;使得你可以通过该目录来访问文件系统中的文件和目录。 mount 命令用来挂载文件系统 #挂载/dev/sda1和/dev/sda2 [rootlocalhost ~]# mkdir test{1..2} [rootlocalhost ~]# ll test1 te…...

Python入门第三课

# 入门第三课 # 关键字 if and or in not in ! car g print(car g) print(car dd) if car ! hh:print("wlcome to here ") age 33 print(age 33) print(age 44) age1 44 if age >0 and age1 > 0:print("nihao") if age >0 or age1 > …...

java计算器,输入公式和对应变量的值

目标&#xff1a;最近想写个东西&#xff0c;本质就是一个计算器&#xff0c;我们可以输入公式&#xff08;例如&#xff1a;ab&#xff09;&#xff0c;然后把公式的值&#xff08;a:10,b:20&#xff09;也输入进去。最后得到结果。核心&#xff1a;这个想法核心部分就是给一个…...

加密货币赋能跨境电商:PayPal供应链金融服务如何引领行业新趋势

跨境电商行业近年来呈现出爆发式增长&#xff0c;随着全球化贸易壁垒的降低和数字经济的快速发展&#xff0c;越来越多的商家和消费者跨越国界进行交易。根据eMarketer的数据&#xff0c;全球跨境电商交易额在2023年已超过4万亿美元&#xff0c;并预计在未来几年内仍将保持两位…...

redis面试(二)List链表数据

list 列表 我们总是说List为列表&#xff0c;其实在真正的数据结构来说&#xff0c;redis是自己基于c语言来实现的双向链表数据结构&#xff0c;主要的逻辑就是每个节点都可以指向下一个节点&#xff0c;这个结构就属于链表数组结构。 每个节点中的属性如下&#xff1a; type…...

SpringDataJPA(三):多表操作,复杂查询

一、Specifications动态查询 有时我们在查询某个实体的时候&#xff0c;给定的条件是不固定的&#xff0c;这时就需要动态构建相应的查询语句&#xff0c;在Spring Data JPA中可以通过JpaSpecificationExecutor接口查询。相比JPQL,其优势是类型安全,更加的面向对象。 import …...

嵌入式硬件面试题集萃:从基础到进阶

基础问题 问题: 解释什么是微控制器&#xff0c;以及它与微处理器的区别。 答案: 微控制器是具有集成内存和输入/输出外设的微型计算机。与通用微处理器相比&#xff0c;微控制器通常用于控制特定应用&#xff0c;而不是执行通用计算任务。 问题: 什么是数字逻辑门&#xff0c…...

easyui-datebox 只显示月份选择,默认开启月份,隐藏日期选择框

如果你使用 ​​easyui-datebox​​​ 并希望隐藏日期选择框,只显示月份选择,可以通过一些自定义代码来实现。虽然 EasyUI 没有直接提供这种功能,但可以通过自定义 ​​formatter​​​ 和 ​​parser​​​ 方法,以及修改 ​​onShowPanel​​ 事件来实现这个功能。 以下…...

【数据结构】队列(链表实现 + 力扣 + 详解 + 数组实现循环队列 )

Hi~&#xff01;这里是奋斗的明志&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f331;&#x1f331;个人主页&#xff1a;奋斗的明志 &#x1f331;&#x1f331;所属专栏&#xff1a;数据结构 &#x1f4da;本系列文章为个人学…...

02 Go语言操作MySQL基础教程_20240729 课程笔记

概述 如果您没有Golang的基础&#xff0c;应该学习如下前置课程。 Golang零基础入门Golang面向对象编程Go Web 基础Go语言开发REST API接口_20240728 基础不好的同学每节课的代码最好配合视频进行阅读和学习&#xff0c;如果基础比较扎实&#xff0c;则阅读本教程巩固一下相…...

相交链表 - 力扣(LeetCode)C语言

160. 相交链表 - 力扣&#xff08;LeetCode&#xff09; (点击前面链接即可查看题目) 一、题目 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始…...

【Python】基础学习技能提升代码样例3:JSON文本处理

对json的处理&#xff0c;无非是编码和解码两部分 编码&#xff1a;将python数据结构转换为json字符串解码: 将json字符串转换为python数据结构 另外&#xff0c;还有.json文件的读写 一、编码 json.dumps(obj, *, skipkeysFalse, ensure_asciiTrue, check_circularTrue, a…...

最新Yiso智云搜索引擎系统源码/开源PHP源码/修复版

源码简介&#xff1a; 最新Yiso智云搜索引擎系统源码/开源PHP源码/修复版。Yiso 是一个性能非常好的搜索引擎&#xff0c;不仅免费开源&#xff0c;还能当作收录网址的平台来用呢&#xff01;只需要输入关键词&#xff0c;就能轻松找到相关的搜索结果内容。 1、Yiso 用的是自…...

Anconda 快速常用命令简洁版

目的&#xff1a;简单清楚的使用基本的conda 命令 可能需求 查看项目中的虚拟环境及依赖是否满足需求操作新环境来满足项目或者论文的实现 Anconda 常用命令 conda 查看基础命令1. 进入Anaconda 环境2. 查看版本3.查看有哪些虚拟环境4.激活虚拟环境5. 进入虚拟环境查看6. 退出…...

Android 系统启动动画

一、接着我们把 bootanimation.zip 动画文件 预制到 /system/media/ 目录下&#xff1a; 二、目录/system/media/bootanimation.zip PRODUCT_COPY_FILES \$(LOCAL_PATH)/bootanimation.zip:/system/media/bootanimation.zipPRODUCT_ARTIFACT_PATH_REQUIREMENT_WHITELIST \ /…...

解决antd打开modal时页面自动跳到顶部问题

问题原因&#xff1a;antd的样式中有一行&#xff0c;如下样式代码&#xff0c;这行代码导致了在本来有滚动条的页面底部触发modal弹出时&#xff0c;会自动滚动到页面顶部。 html {overflow-y: scroll; } 解决办法&#xff1a;删除这行代码、或者将html的overflow-y属性改成…...

什么是等保测评2.0,等保测评如何定级

在信息化时代&#xff0c;网络安全已成为国家安全的重要组成部分。为了应对日益复杂的网络安全形势&#xff0c;我国推出了网络安全等级保护制度&#xff0c;其中等保测评是评估信息系统安全防护能力的关键环节。本文将深入探讨等保2.0的测评流程和定级标准&#xff0c;以揭示其…...

【嵌入式英语教程--6】C语言中的数组与指针

C语言中的数组与指针 英文原文 Arrays and pointers are fundamental concepts in the C programming language. An array is a collection of elements of the same data type stored in contiguous memory locations. Arrays can be used to store and manipulate sequence…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动

一、前言说明 在2011版本的gb28181协议中&#xff0c;拉取视频流只要求udp方式&#xff0c;从2016开始要求新增支持tcp被动和tcp主动两种方式&#xff0c;udp理论上会丢包的&#xff0c;所以实际使用过程可能会出现画面花屏的情况&#xff0c;而tcp肯定不丢包&#xff0c;起码…...

反向工程与模型迁移:打造未来商品详情API的可持续创新体系

在电商行业蓬勃发展的当下&#xff0c;商品详情API作为连接电商平台与开发者、商家及用户的关键纽带&#xff0c;其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息&#xff08;如名称、价格、库存等&#xff09;的获取与展示&#xff0c;已难以满足市场对个性化、智能…...

k8s从入门到放弃之Ingress七层负载

k8s从入门到放弃之Ingress七层负载 在Kubernetes&#xff08;简称K8s&#xff09;中&#xff0c;Ingress是一个API对象&#xff0c;它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress&#xff0c;你可…...

可靠性+灵活性:电力载波技术在楼宇自控中的核心价值

可靠性灵活性&#xff1a;电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中&#xff0c;电力载波技术&#xff08;PLC&#xff09;凭借其独特的优势&#xff0c;正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据&#xff0c;无需额外布…...

linux 下常用变更-8

1、删除普通用户 查询用户初始UID和GIDls -l /home/ ###家目录中查看UID cat /etc/group ###此文件查看GID删除用户1.编辑文件 /etc/passwd 找到对应的行&#xff0c;YW343:x:0:0::/home/YW343:/bin/bash 2.将标红的位置修改为用户对应初始UID和GID&#xff1a; YW3…...

AI书签管理工具开发全记录(十九):嵌入资源处理

1.前言 &#x1f4dd; 在上一篇文章中&#xff0c;我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源&#xff0c;方便后续将资源打包到一个可执行文件中。 2.embed介绍 &#x1f3af; Go 1.16 引入了革命性的 embed 包&#xff0c;彻底改变了静态资源管理的…...

Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?

在大数据处理领域&#xff0c;Hive 作为 Hadoop 生态中重要的数据仓库工具&#xff0c;其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式&#xff0c;很多开发者常常陷入选择困境。本文将从底…...

C++使用 new 来创建动态数组

问题&#xff1a; 不能使用变量定义数组大小 原因&#xff1a; 这是因为数组在内存中是连续存储的&#xff0c;编译器需要在编译阶段就确定数组的大小&#xff0c;以便正确地分配内存空间。如果允许使用变量来定义数组的大小&#xff0c;那么编译器就无法在编译时确定数组的大…...

scikit-learn机器学习

# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...

Golang——7、包与接口详解

包与接口详解 1、Golang包详解1.1、Golang中包的定义和介绍1.2、Golang包管理工具go mod1.3、Golang中自定义包1.4、Golang中使用第三包1.5、init函数 2、接口详解2.1、接口的定义2.2、空接口2.3、类型断言2.4、结构体值接收者和指针接收者实现接口的区别2.5、一个结构体实现多…...