[C++]虚函数用法

讲虚函数之前先讲讲面向对象的三大特性:封装、继承、多态。

1、封装

封装是指将数据(属性)和操作数据的方法(函数)封装在一个单元中,这个单元就是类。封装的主要目的是隐藏类的内部实现细节,只暴露必要的接口给外部使用者。

优点:

  • 信息隐藏: 封装可以将类的内部细节隐藏起来,不暴露给外部,提高了安全性和防止误用。
  • 简化接口: 封装通过提供清晰的接口简化了类的使用,使用者只需关注如何使用接口而不需要了解内部实现。
  • 提高可维护性: 内部实现的修改不会影响外部使用者,从而提高了代码的可维护性。

示例:

#include <iostream>
#include <string>class Student {
private:std::string name;int age;public:// 构造函数Student(const std::string& n, int a) : name(n), age(a) {}// 获取姓名std::string getName() const {return name;}// 设置年龄void setAge(int a) {if (a >= 0) {age = a;}}// 显示学生信息void displayInfo() const {std::cout << "Name: " << name << ", Age: " << age << std::endl;}
};int main() {Student student("Alice", 20);// 使用公有接口获取和设置信息student.setAge(21);std::cout << "Student Name: " << student.getName() << std::endl;student.displayInfo();return 0;
}

2、继承

继承允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。通过继承,子类可以获得父类的特征,并可以添加新的特征或修改继承的特征。

优点:

  • 代码重用: 继承允许在不重复编写代码的情况下扩展和修改现有类,提高了代码的重用性。
  • 层次结构: 继承可以创建类的层次结构,使得代码更有组织性和可扩展性。
  • 多态性支持: 继承是多态性的基础,通过基类指针或引用调用派生类的方法实现多态行为。

示例:

#include <iostream>
#include <string>// 基类
class Animal {
protected:std::string name;public:Animal(const std::string& n) : name(n) {}void eat() {std::cout << name << " is eating." << std::endl;}
};// 派生类
class Dog : public Animal {
public:Dog(const std::string& n) : Animal(n) {}void bark() {std::cout << name << " is barking." << std::endl;}
};int main() {Dog myDog("Buddy");myDog.eat();  // 继承自基类myDog.bark(); // 派生类自己的方法return 0;
}

3、多态

多态性是指同一个操作可以作用于不同类型的对象,并且可以根据对象的类型执行不同的行为。多态性通过虚函数和函数重载实现。

  • 编译时多态性(静态多态性): 通过函数重载实现,编译器在编译时根据函数参数的类型和数量来选择调用合适的函数。这种多态性是在编译时解析的。
  • 运行时多态性(动态多态性): 通过虚函数和继承实现,允许在运行时根据对象的实际类型来调用适当的函数。这种多态性是在运行时解析的。

优点:

  • 灵活性: 多态性允许在不同的情境下以通用的方式处理不同类型的对象,提高了代码的灵活性。
  • 可扩展性: 可以轻松地添加新的派生类而不影响现有的代码,增加了系统的可扩展性。
  • 简化接口: 多态性简化了代码的接口,允许使用者按统一的方式与不同类型的对象交互。

示例:

#include <iostream>
#include <vector>class Shape {
public:virtual void draw() {std::cout << "Drawing a shape." << std::endl;}
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing a circle." << std::endl;}
};class Square : public Shape {
public:void draw() override {std::cout << "Drawing a square." << std::endl;}
};int main() {std::vector<Shape*> shapes;shapes.push_back(new Circle());shapes.push_back(new Square());for (Shape* shape : shapes) {shape->draw(); // 多态性:根据对象的实际类型调用适当的方法}// 释放内存for (Shape* shape : shapes) {delete shape;}return 0;
}

讲了这么多,进入今天主题吧,C++实现多态的虚函数。

在C++中,函数继承的方法可以让我们快速开发,为了满足多态和泛型编程,C++允许用户使用虚函数来完成运行时解析,与一般的编译时解析也有着本质区别。

4、虚函数在内存中的分布

对于C++了解的人都应该知道虚函数是通过一个虚函数表来实现的。在这个表中,主要是一个类的虚函数的地址表,这个表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样的话,在有虚函数的类的实例中,这个表被分配在这个实例的内存中。当我们用父类的指针操作其子类的时候,这个虚表就非常重要了,它指明了实际所应该调用的函数。

class A {
public:virtual void v_a(){}virtual ~A(){}int64_t _m_a;
};
int main()
{A* a = new A();return 0;
}

定义一个类A,那么它在内存中分布的情况是什么样的呢?接下来一起看看

  • 首先在主函数的栈帧上有一个 A 类型的指针指向堆里面分配好的对象 A 实例。
  • 对象 A 实例的头部是一个 vtable 指针,紧接着是 A 对象按照声明顺序排列的成员变量。(当我们创建一个对象时,便可以通过实例对象的地址,得到该实例的虚函数表,从而获取其函数指针。)
  • vtable 指针指向的是代码段中的 A 类型的虚函数表中的第一个虚函数起始地址。
  • 虚函数表的结构其实是有一个头部的,叫做 vtable_prefix ,紧接着是按照声明顺序排列的虚函数。
  • 注意到这里有两个虚析构函数,因为对象有两种构造方式,栈构造和堆构造,所以对应的,对象会有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行 delete 函数,会自动被回收。
  • typeinfo 存储着 A 的类基础信息,包括父类与类名称,C++关键字 typeid 返回的就是这个对象。
  • typeinfo 也是一个类,对于没有父类的 A 来说,当前 tinfo 是 class_type_info 类型的,从虚函数指针指向的vtable 起始位置可以看出。

5、虚函数表实现原理

虚函数表是一个指向虚函数的指针数组,每个带有虚函数的类都有一个对应的虚函数表。

虚函数指针

其本质就是一个指向函数的指针,与普通的函数指针并没有什么大的区别。它指向程序员自己定义的虚函数,当子类调用虚函数的时候,实际上就是通过调用这个虚函数指针从而找到接口。

虚函数指针是一个真实存在的数据类型,在对象实例化的时候,放在这个对象地址的首位,目的就是为了保证运行的快速性。与对象的成员函数不一样的是,虚函数指针对外部是完全不可见的,除非直接访问地址或者是debug模式,否则它是不能被外部调用的。

只有拥有虚函数的类才能拥有虚函数指针,每个虚函数都会对应一个虚函数指针。那么,拥有虚函数的类都会产生额外的开销,并且也会在一定程度上影响程序的运行速度。

虚函数表

当一个类包含虚函数时,编译器会在该类的对象中添加一个指向虚函数表的指针。这个指针通常位于对象的内存布局的开头(虚指针),它们按照一定的顺序组织起来就会构成一个表状结构,叫做虚函数表。虚函数表本身是一个全局的、类特定的数组,其中包含了该类中所有虚函数的地址。

先来定义一个基类:

class Panent
{
public:virtual void A(){cout<<"Panent::A"<<endl;}virtual void B(){cout<<"Panent::B"<<endl;}virtual void C(){cout<<"Panent::C"<<endl;}
};

对于基类Base的虚函数表记录的只有自己定义的虚函数。

下来再看看子类:

class Children: public Panent
{
public:virtual void A(){cout<<"Children::f"<<endl;}virtual void B1(){cout<<"Children::B1"<<endl;}virtual void C1(){cout<<"Children::C1"<<endl;}
}

最常见的继承,就是子类对基类的虚函数进行覆盖继承。

此时的虚函数表:

基函数的表项仍然会保留,而得到正确继承的虚函数的指针将会被覆盖,而子类自己的虚函数将跟在表后。

当多继承的时候,表项将会增多,顺序将会体现为继承的顺序,那么子类的虚函数就跟在第一个表项后。

C++中一个类是公用一个虚函数表的,基类有基类的虚函数表,子类有子类的虚函数表,这样极大的节省了内存。

虚表指针

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,在编译阶段,编译器在类中添加了一个指针 __vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表,__vptr一般在对象内存分布的最前面。

虚表指针的初始化确实发生在构造函数的调用过程中, 但是在执行构造函数体之前,即进入到构造函数的"{“和”}"之前。 为了更好的理解这一问题, 我们可以把构造函数的调用过程细分为两个阶段,即:

  • 进入到构造函数体之前。在这个阶段如果存在虚函数的话,虚表指针被初始化。如果存在构造函数的初始化列表的话,初始化列表也会被执行。
  • 进入到构造函数体内。这一阶段是我们通常意义上说的构造函数。

带缺省参数的虚函数

当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。

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

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

相关文章

Java内部类的使用与应用

内部类的使用与应用 1. 内部类用法 普通内部类&#xff1a; 实例化内部类对象需要先实例化外部类对象&#xff0c;然后再通过OuterClassName.new InnerClassName()方式实例化内部类。内部类对象在创建后会与外部类对象秘密链接&#xff0c;因此无法独立于外部类创建内部类对象…

JWT学习笔记

了解 JWT Token 释义及使用 | Authing 文档 JSON Web Token Introduction - jwt.io JSON Web Token (JWT&#xff0c;RFC 7519 (opens new window))&#xff0c;是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准&#xff08;(RFC 7519)。该 token 被设计为紧凑…

微服务-微服务Spring Security OAuth 2实战

1. Spring Authorization Server 是什么 Spring Authorization Server 是一个框架&#xff0c;它提供了 OAuth 2.1 和 OpenID Connect 1.0 规范以及其他相关规范的实现。它建立在 Spring Security 之上&#xff0c;为构建 OpenID Connect 1.0 身份提供者和 OAuth2 授权服务器产…

高等数学(极限)

目录 一、数列 二、极限 2.1 讲解 2.2 例题 一、数列 按照一定次数排列的一列数&#xff1a; 其中 叫做通项。 对于数列&#xff0c;如果当n无限增大时&#xff0c;其通项无限接近于一个常数A&#xff0c;则称该数列以A为极限或称数列收敛于A&#xff0c;否则称数列为发散…

OpenCV(2)

1.OpenCV的模块 其中core、highgui、imgproc是最基础的模块&#xff0c;该课程主要是围绕这几个模块展开的&#xff0c;分别介绍如下&#xff1a; core模块实现了最核心的数据结构及其基本运算&#xff0c;如绘图函数、数组操作相关函数等。highgui模块实现了视频与图像的读取…

【JVM】计数器引用和可达性分析

&#x1f4dd;个人主页&#xff1a;五敷有你 &#x1f525;系列专栏&#xff1a;JVM ⛺️稳中求进&#xff0c;晒太阳 C/C的内存管理 在C/C这类没有自动垃圾回收机制的语言中&#xff0c;一个对象如果不再使用&#xff0c;需要手动释放&#xff0c;否则就会出现内存泄漏…

一文get,最容易碰上的接口自动化测试问题汇总

本篇文章分享几个接口自动化用例编写过程遇到的问题总结&#xff0c;希望能对初次探索接口自动化测试的小伙伴们解决问题上提供一小部分思路。 sql语句内容出现错误 空格&#xff1a;由于有些字段判断是变量&#xff0c;需要将sql拼接起来&#xff0c;但是在拼接字符串时没有…

对象池模板

概述 对象池的引入也是嵌入式开发的常用方法&#xff0c;也是内存预分配的一种&#xff0c;主要是用来隐藏全局对象的跟踪&#xff0c;通常预内存分配是通过数组来实现。 CMake配置 cmake_minimum_required(VERSION 3.5.1)project(objpool)add_executable(objpool objpool.cp…

C语言《数据结构与算法》安排教学计划课设

背景&#xff1a; 10、安排教学计划 (1) 问题描述。 学校每学期开设的课程是有先后顺序的&#xff0c;如计算机专业&#xff1a;开设《数据结构》课程之前&#xff0c;必须先开设《C语言程序设计》和《离散数学》课程&#xff0c;这种课程开设的先后顺序称为先行、后继课程关…

在使用nginx的时候快速测试配置文件,并重新启动

小技巧 Nginx修改配置文件后需要重新启动&#xff0c;常规操作是启动在任务管理器中关闭程序然后再次双击nginx.exe启动&#xff0c;但是使用命令行就可以快速的完成操作。 将cmd路径切换到nginx的安装路径 修改完成配置文件后 使用 nginx -t校验nginx 的配置文件是否出错 …

海豚调度DolphinScheduler入门学习

DS简介&#xff1a; DolphinScheduler 是一款分布式的、易扩展的、高可用的数据处理平台&#xff0c;主要包含调度中心、元数据管理、任务编排、任务调度、任务执行和告警等模块。其技术架构基于 Spring Boot 和 Spring Cloud 技术栈&#xff0c;采用了分布式锁、分布式任务队列…

vue3 实现 el-pagination页面分页组件的封装以及调用

示例图 一、组件代码 <template><el-config-provider :locale"zhCn"><el-pagination background class"lj-paging" layout"prev, pager, next, jumper" :pager-count"5" :total"total":current-page"p…

深度学习基础(四)医疗影像分析实战

之前的章节我们初步介绍了卷积神经网络&#xff08;CNN&#xff09;和循环神经网络&#xff08;RNN&#xff09;&#xff1a; 深度学习基础&#xff08;三&#xff09;循环神经网络&#xff08;RNN&#xff09;-CSDN博客文章浏览阅读1.2k次&#xff0c;点赞17次&#xff0c;收…

机器学习基础(六)TensorFlow与PyTorch

导语&#xff1a;上一节我们详细探索了监督与非监督学习的结合使用。&#xff0c;详情可见&#xff1a; 机器学习基础&#xff08;五&#xff09;监督与非监督学习的结合-CSDN博客文章浏览阅读4次。将监督学习和非监督学习结合起来&#xff0c;就像将两种不同的艺术形式融合&a…

1298 - 摘花生问题

题目描述 Hello Kitty 想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地(如下图)&#xff0c;从西北角进去&#xff0c;东南角出来。地里每个道路的交叉点上都有种着一株花生苗&#xff0c;上面有若干颗花生&#xff0c;经过一株花生苗就能摘走该它上面所有…

消息中间件篇之RabbitMQ-消息重复消费

一、导致重复消费的情况 1. 网络抖动。 2. 消费者挂了。 消费者消费消息后&#xff0c;当确认消息还没有发送到MQ时&#xff0c;就发生网络抖动或者消费者宕机。那当消费者恢复后&#xff0c;由于MQ没有收到消息&#xff0c;而且消费者有重试机制&#xff0c;消费者就会再一次消…

python print 格式化输出详解

print 输出字符串和数字 以下实例中列举了print打印各种类型的示例 示例如下, print("qayrup") # 直接输出字符串print(100) # 输出数字str qayrup print(str) # 输出变量L [1,2,a] # 输出列表类型变量 print(L) t (1,2,a…

Folx GO+ 5.27 Mac上优秀好用的下载工具

Folx Pro 5 for Mac是Mac平台上公认的最好的下载工具&#xff0c;功能可以与迅雷相媲美。目前Folx Pro 5 特别版正式上线&#xff0c;新版的Folx整体界面更加简洁漂亮&#xff0c;支持HTTP FTP下载&#xff0c;torrent种子下载&#xff0c;高速下载&#xff0c;定时下载&#x…

C语言中strstr函数的使用!

strstr函数的作用是什么&#xff1f; 查找子字符串 具体直接看下面的这段代码我相信你必明白 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int main() { char *p1 "abcdefghijklmnopqrstuvwxyz"; char* p2 "abc"; char* r…

Open CASCADE学习|提取面的内外环线

在Open CASCADE中&#xff0c;区分内环和外环主要基于面的参数域内环线方向的定义。具体来说&#xff0c;在面的参数域内&#xff0c;沿着环线正方向前进时&#xff0c;如果左侧为面内、右侧为面外&#xff0c;那么该环线被视为外环&#xff1b;反之&#xff0c;如果左侧为面外…