[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.hqwc.cn/news/491161.html

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

相关文章

迪萧科技有限公司邀您参观2024生物发酵展

参展企业介绍 浙江迪萧科技有限公司位于浙江杭州&#xff0c;是一家专注于膜技术的国家高新企业。公司针对食品饮料、医药保健等领域的过程分离与控制、产品提取及浓缩、废料资源化利用等提供全方案解决服务。坚持以“顾客至上、优质服务、卓越品质”为原则。为客户企业提供清…

C# 使用onnxruntime部署夜间雾霾图像的可见度增强

目录 介绍 模型信息 效果 项目 代码 下载 C# Onnx 使用onnxruntime部署夜间雾霾图像的可见度增强 介绍 github地址&#xff1a;GitHub - jinyeying/nighttime_dehaze: [ACMMM2023] "Enhancing Visibility in Nighttime Haze Images Using Guided APSF and Gradien…

HTML+CSS+JS:轮播组件

效果演示 一个具有动画效果的卡片元素和一个注册表单,背景为渐变色,整体布局简洁美观。 Code <div class="card" style="--d:-1;"><div class="content"><div class="img"><img src="./img/果果k_01.jpg…

如何在Linux系统中进行高级的软件包管理

软件包管理是在Linux系统中进行软件安装、更新和卸载的重要过程之一。它通过打包软件并自动处理依赖关系&#xff0c;极大简化了软件的管理过程。在Linux中有多种包管理工具可用&#xff0c;本文将介绍常用的RPM和DEB包管理工具&#xff0c;以及它们对应的包管理器YUM和APT。 软…

VIO第5讲:后端优化实践

VIO第5讲后端优化实践&#xff1a;逐行手写求解器 文章目录 VIO第5讲后端优化实践&#xff1a;逐行手写求解器1 非线性最小二乘求解流程1.1 H矩阵不满秩的解决办法1.2 H矩阵的构建1.2.1 确定维度1.2.2 构建海塞矩阵 1.3 初始化μ—LM算法1.4 求解线性方程1.4.1 非SLAM问题—求逆…

第6.4章:StarRocks查询加速——Colocation Join

目录 一、StarRocks数据划分 1.1 分区 1.2 分桶 二、Colocation Join实现原理 2.1 Colocate Join概述 2.2 Colocate Join实现原理 三、应用案例 注&#xff1a;本篇文章阐述的是StarRocks-3.2版本的Colocation Join 官网文章地址&#xff1a; Colocate Join | StarRoc…

波奇学Linux:进程通信管道

进程通信 管道&#xff1a;基于文件级别的单向通信 创建父子进程&#xff0c;使得进程的struct file*fd_array[]的文件描述符指向同一个struct file文件&#xff0c;这个文件是内存级文件。 父进程关写端&#xff0c;子进程再关闭读端。实现单向通信 子进程写入&#xff0c;父进…

利用psutil库检查脚本是否在运行

摘要 如果要判断某一脚本是否在运行&#xff0c;可以通过psutil库获取所有进程的cmdline&#xff0c;并判断指定的文件名是否在cmdline中。 目录 1.psutil库简介 2.检查代码及说明 2.1检查思路 2.2异常捕获 2.3执行方法 1.psutil库简介 psutil 是一个跨平台&#xff08;…

力扣随笔之寻找重复数(中等287)

思路1&#xff1a;暴力解法&#xff0c;根据要求不修改数组且只用常量级O(1)的额外空间&#xff0c;我们写两层嵌套循环&#xff0c;寻找重复的数;可以解决部分问题&#xff0c;但会超出时间限制无论Java还是C; Java实现&#xff1a; class Solution {public int findDuplicat…

LeetCode--代码详解 235.二叉搜索树得最近公共祖先

235.二叉搜索树得最近公共祖先 题目 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有根树 T 的两个结点 p、q&#xff0c;最近公共祖先表示为一个结点 x&#xff0c;满足 x 是 p、q 的祖先且 x 的深度尽可…

设计模式-结构型模式-桥接模式

桥接模式&#xff08;Bridge Pattern&#xff09;&#xff1a;将抽象部分与其实现部分分离&#xff0c;使它们都可以独立地变化。它是一种对象结构型模式&#xff0c;又称为柄体&#xff08;Handle and Body&#xff09;模式或接口&#xff08;Interface&#xff09;模式。桥接…

linux 0.11 调试c代码

我们可以通过实验楼实验环境 来调试linux0.11的c代码。 cd ~/oslab/ tar -zxvf hit-oslab-linux-20110823.tar.gz -C ~ cd ~/oslab/linux-0.11/ make cd ~/oslab/ nohup ./dbg-c & nohup terminator & ls在新的窗口执行 ./rungdb,进入调试状态。 输入 set disassemb…