C++虚函数食用笔记

虚函数定义与作用:

virtual关键字声明虚函数,虚函数可被派生类override(保证返回类型与参数列表,名字均相同),从而通过基类指针调用时,实现多态的功能

virtual关键字:

将函数声明为虚函数

override关键字:

告诉编译器该函数为重写的虚函数,若重写失败,报错,防止出现疏忽导致虚函数未重写的情况

final关键字:

声明给虚函数时,表明该虚函数不可再被派生类重写

声明给类时,表明该类不可再被继承

class Base{
public:virtual void print() const{cout << "Base" << endl;}
};class Derived final : public Base{//注意此处final的位置
public:void print() const override final{ //此处const override final的顺序不可调换cout << "Derived" << endl;}
};

指针的动态类型与静态类型:

指针/引用的定义类型为其静态类型

指针/引用指向的对象类型为其动态类型

当调用非虚函数时,函数的匹配取决于指针/引用的静态类型

当调用虚函数时,函数的匹配取决于指针/引用的动态类型

例子:

class Base{
public:virtual void print(){cout << "Base" << endl;}
};class Derived:public Base{
public:void print()override{cout << "Derived" << endl;}
};int main(){Base *p_base{new Derived};p_base->print(); //调用Derived::print()Base obj_base = *p_base;obj_base.print();//调用Base::print()return 0;
}

输出:

Derived
Base

特殊的虚函数重写:协变返回类型

当虚函数的返回类型为一系列的基类/派生类的指针/引用时,重写的虚函数返回类型可以不一样

例子:

class A{
public:void print(){cout << "A" << endl;}
};
class B:public A{
public:void print(){cout << "B" << endl;}
};class Base{
public:virtual A& get(){cout << "return A" << endl;return *(new A{});}
};class Derived:public Base{
public:B& get() override{cout << "return B" << endl;return *(new B{});}
};int main(){Base *p_base{new Derived};p_base->get();Base obj_base = *p_base;obj_base.get();return 0;
}

输出:

return B
return A

综合的虚函数调用例子:

#include <iostream>
using namespace std;class A{
public:void print(){cout << "A" << endl;}virtual void vprint(){cout << "A" << endl;}
};
class B:public A{
public:void print(){cout << "B" << endl;}void vprint()override{cout << "B" << endl;}
};class C{
private:A m_a{};
public:virtual A& get(){return m_a;}
};class D:public C{
private:B m_b{};
public:B& get() override{return m_b;}
};int main(){C *p_C{new D{}};p_C->get().print(); //静态调用print(),所以调用的是p_C的静态类型对应的get(),返回A&p_C->get().vprint(); //动态调用vprint(), 所以调用的是p_C的动态类型对应的get(),返回B&return 0;
}

输出:

A
B

虚析构函数:

在类的析构函数前加上virtual关键字,可将其变为虚析构函数,此后的派生类写自己的析构函数时,相当于重写基类的虚函数,派生类的析构函数默认成为虚函数

如果一个类会被继承的话,那么应当将其析构函数写成虚析构函数,以避免内存泄漏

不使用虚析构函数的继承:

class Base{
public:~Base(){cout << "~Base()" << endl;}
};class Derived:public Base{
public:~Derived(){cout << "~Derived()" << endl;}
};int main(){Base* p{new Derived{}};delete p;return 0;
}

输出:

~Base()

可以看到delete只调用了Base的析构函数,从而导致Derived部分分配的内存未被清空,发生内存泄漏

使用虚析构函数的继承:

class Base{
public:virtual ~Base(){cout << "~Base()" << endl;}
};class Derived:public Base{
public:~Derived() override{cout << "~Derived()" << endl;}
};int main(){Base* p{new Derived{}};delete p;return 0;
}

输出:

~Derived()
~Base()

指针正确调用了Derived的虚析构函数,而该析构函数又调用了~Base(),从而正确的清空了分配的内存

纯虚函数:

在虚函数声明后面加上=0,使其成为纯虚函数

class A{
public:virtual void func() = 0;void func2(){}
};

纯虚函数也可以有定义,如写在类外面的定义:

void A::func(){cout << "I'm a pure virtual function" << endl;
}

抽象类:

只要包含纯虚函数的类就称为抽象类(如上述的A类),抽象类不可被实例化

继承自抽象类的派生类需要重写其所有纯虚函数,否则该派生类也是抽象类

接口类:

不包含任何属性和成员函数,只包含纯虚函数的类称为接口类

利用虚函数修改派生类的operator<<

为了使用ostream,我们通常将operator<<写成友元函数,但友元函数不能是虚函数,因此无法被派生类重写

但我们又不想每定义一个派生类就新添加一个友元operator<<

因此,我们可以定义一个辅助print()虚函数,然后用operator<<来调用这个虚函数,从而达到多态的目的

class Base{
public:virtual ostream& print(ostream& out){out << "Base" << endl;}friend ostream& operator<<(ostream& out,Base& obj){return obj.print(out);}
};class Derived:public Base{
public:ostream& print(ostream& out) override{out << "Derived" << endl;}
};int main(){Derived d{};cout << d << endl;return 0;
}

输出:

Derived

在cout<<d的时候,由于没有与Derived匹配的<<运算符,因此编译器将d隐式转换为Base,然后传入operator<<(ostream& out,Base& obj)里,从而通过Base&调用Derived对应的虚函数print(),实现多态的目的

虚函数的实现原理:

结构

当一个类内包含虚函数,那么编译器就会为这个类分配一个数组,数组里存了若干个指针(虚函数指针,vfptr),每个指针指向对应的虚函数的地址,我们把这个数组称作虚函数表(__vtable)。
当该类实例化为对象时,编译器会在该对象头部插入一个指针,该指针指向虚函数表,我们把这个指针称作虚函数表指针(__vptr),虚函数表指针的初始化在构造函数之前。

结构如图所示:

运行:

当我们通过类指针调用虚函数的时候,编译器并不会在编译期就根据函数签名来确定调用的函数的地址(静态绑定),而是在运行期让类指针通过__vptr找到vtable,并通过调用的函数签名,确定在vtable的偏移量,从而找到对应的vfptr,通过vfptr找到需要调用的函数的地址,进而调用该函数,此谓动态绑定

继承规则:

当我们发生继承、虚函数重写、添加虚函数时,

那么vtable以及__vptr的分配规则如下:

每个含有虚函数的基类的子对象的首地址都会有对应的__vptr

重写虚函数:修改对应__vptr所指向的vtable的对应位置(下标)内的vfptr,

新添虚函数: 在首个继承的基类的vtable后添加vfptr

例子:

当我们有如下代码的继承关系时:

class Base1{
public:virtual void func1(){}virtual void print1(){}void test1(){}
};class Base2{
public:virtual void func2(){}virtual void print2(){}void test2(){}
};class Derived:public Base1,public Base2{
public:void func1() override{}void print2() override{}virtual void f_derived(){}
};

Base1,Base2各有两个虚函数和一个非虚函数,Derived各重写了基类的一个虚函数,以及新添加了一个自己的虚函数

结构如图所示:

可以看到

在Derived obj内,

虚函数表内vfptr_func1_b和vfptr_print2_b对应的位置被替换成了vfptr_func1_d和vfptr_print2_d

新添加的f_derived对应的vfptr_f_d被添加到了__vptr1指向的vtable的末尾

值得注意的是

Derived的vtable和Base1,Base2的vtable是独立开来的,因此这里总共有三个虚函数表

如果Derived没有重写任何虚函数,其仍会生成一个独立的vtable

多态实现原理:

当我们用基类指针指向不同的派生类,调用其相同的虚函数时,若其虚函数被重写过,则会产生不同的效果,此谓多态

那么多态是如何实现的呢:

用上述的Base1,Base2,Derived举例

调用重写的虚函数:

比如我们现在有

Base2* p{new Derived{}};
p->print2();

由于我们使用了Base2指针指向Derived对象,那么该指针会指向Derived中的Base2子对象的首地址,也就是__vptr2所在处,当我们调用print2()时,指针会通过__vptr2找到vtable,然后由print2()的函数签名,编译器会让指针偏移一个指针偏移量,从而找到vfptr_print2_d,进而调用Derived::print2(),这也就是为什么要修改对应位置的vfptr的原因,因为vtable就是根据位置来找对应的虚函数的

调用新添的虚函数:

当我们使用Base1指针指向Derived对象时,也是同样的道理:

Base1* p2{new Derived{}};
p2->f_derived();

当我们调用f_derived()时,编译器是用__vptr1去找该函数的,这也就是为什么vfptr_f_d会加到__vptr1的末尾的原因,也就是说Derived与其Base1的子对象是共用一个vtable的,也因此,p2无法调用在Base2定义的虚函数

指向不同对象时的调用:

当我们用Base1指针指向Base1对象时,其__vptr指向的是Base1的vtable(图右上角),因此调用时只会调用Base1定义的虚函数

总结:

多态的根本原理是不同类的__vtpr指向的vtable不同,从而在运行时索引的时候找到不同的函数并运行

this指针调整:

编译器在运行时动态调整this指针的功能,具体实现原理这里不再赘述

例子1:

在上述例子中,我们再加入一个派生类:

class Derived2:public Derived{
public:
};int main(){Derived* p{new Derived2{}};p->print2();return 0;
}

上述代码肯定是可运行的,但按上述运行原理,Derived*所用的__vptr是不可能找到print2()的,也就是说其使用了__vptr2,那么编译器是怎么实现的呢?

在这里,编译器用到了一种叫做this指针调整的功能,在调用虚函数表指针之前,编译器先让p向下偏移到了Base2子对象首地址的位置,进而使用__vptr2正确调用print2()

例子2:

若Derived有虚析构函数,那么该虚析构函数只会放在__vptr1所指向的vtable内,此时,若用Base2指针指向Derived对象,在调用虚析构函数时,编译器会先将this指针偏移到Base1子对象的首地址处,进而使用__vptr1正确地调用虚析构函数

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

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

相关文章

运算放大器相关的电路

1运算放大器介绍 解释&#xff1a;运算放大器本质就是一个放大倍数很大的元件&#xff0c;就如上图公式所示 Vp和Vn相差很小但是放大后输出还是会很大。 运算放大器不止上面的三个引脚&#xff0c;他需要独立供电&#xff1b; 如图比较器&#xff1a; 解释&#xff1a;Vp&…

华为OD机试真题——通信系统策略调度(用户调度问题)(2025B卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 B卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

Ubuntu 系统默认已安装 python,此处只需添加一个超链接即可

步骤 1&#xff1a;确认 Python 3 的安装路径 查看当前 Python 3 的路径&#xff1a; which python3 输出类似&#xff1a; /usr/bin/python3 步骤 2&#xff1a;创建符号链接 使用 ln -s 创建符号链接&#xff0c;将 python 指向 python3&#xff1a; sudo ln -s /usr/b…

深度学习-分布式训练机制

1、分布式训练时&#xff0c;包括train.py的全部的代码都会在每个gpu上运行吗&#xff1f; 在分布式训练&#xff08;如使用 PyTorch 的 DistributedDataParallel&#xff0c;DDP&#xff09;时&#xff0c;每个 GPU 上运行的进程会执行 train.py 的全部代码&#xff0c;但通过…

yarn的介绍

### Yarn 的基本概念 Yarn 是 Hadoop 生态系统中的一个重要组成部分&#xff0c;它是一种分布式资源管理框架&#xff0c;旨在为大规模数据处理提供高效的资源管理和调度能力。以下是关于 Yarn 的一些核心概念&#xff1a; #### 1. **Yarn 的定义** Yarn 是一个资源调度平台&a…

Spring-messaging-MessageHandler接口实现类ServiceActivatingHandler

ServiceActivatingHandler实现了MessageHandler接口&#xff0c;所以它是一个MessageHandler&#xff0c;在spring-integration中&#xff0c;它也叫做服务激活器&#xff08;Service Activitor&#xff09;&#xff0c;因为这个类是依赖spring容器BeanFactory的&#xff0c;所…

快速入门深度学习系列(2)----损失函数、逻辑回归、向量化

针对深度学习入门新手目标不明确 知识体系杂乱的问题 拟开启快速入门深度学习系列文章的创作 旨在帮助大家快速的入门深度学习 写在前面&#xff1a; 本系列按照吴恩达系列课程顺序发布(说明一下为什么不直接看原笔记 因为内容太多 没有大量时间去阅读 所有作者需要一次梳理…

KingBase问题篇

安装环境 操作系统&#xff1a;CentOS7 CPU&#xff1a;X86_64架构 数据库&#xff1a;KingbaseES_V008R006C009B0014_Lin64_install.iso 项目中遇到的问题 Q1. 执行sql中有字符串常量&#xff0c;且用双引号包裹&#xff0c;执行报错 A1. 默认KingBase不认双引号&#xff0…

濒危仙草的重生叙事:九仙尊米斛花节如何以雅集重构中医药文化IP

五月的霍山深处,层峦叠翠之间,中华仙草霍山米斛迎来一年一度的花期。九仙尊以“斛韵雅集,春野茶会”为主题,举办为期半月的米斛花文化节,融合中医药文化、东方美学与自然体验,打造一场跨越古今的沉浸式文化盛宴。活动涵盖古琴雅集、书法创作、茶道冥想、诗歌吟诵、民族歌舞等多…

LeetCode100.1 两数之和

今天晚上看了许多关于未来计算机就业的视频&#xff0c;有种正被贩卖焦虑的感觉&#xff0c;翻来覆去下决定先做一遍leetcode100给自己降降温&#xff0c;打算每周做四题&#xff0c;尽量尝试不同的方法与不同的语言。 一开始想到的是暴力解法&#xff0c;两层循环。数据量为1e…

python制造一个报错

以下是用Python制造常见错误的示例及解析&#xff0c;涵盖不同错误类型&#xff0c;便于理解调试原理&#xff1a; 一、语法错误 (SyntaxError) # 错误1&#xff1a;缺少冒号 if Trueprint("这行不会执行")# 错误2&#xff1a;缩进错误 def func(): print("未对…

idea整合maven环境配置

idea整合maven 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是springboot的使用。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性。【帮帮志系列文章】&#xff1a;每个知识点&#xff0c;都是写出代码…

Node.js中那些常用的进程通信方式

文章目录 1 什么是子进程?2 核心方法详解2.1 `child_process.spawn(command, [args], [options])`2.2 `child_process.exec(command, [options], callback)`2.3 `child_process.execFile(file, [args], [options], callback)`2.4 `child_process.fork(modulePath, [args], [op…

Vue3吸顶导航的实现

吸顶导航实现 【实现目标】&#xff1a; 在Layout页面中&#xff0c;浏览器上下滚动时&#xff0c;距离顶部距离大于80px吸顶导航显示&#xff0c;小于则隐藏。 【实现过程】&#xff1a; 通过layout接口获取分类列表内容并使用categorystore进行状态管理&#xff0c;获取到…

双向长短期记忆网络-BiLSTM

5月14日复盘 二、BiLSTM 1. 概述 双向长短期记忆网络&#xff08;Bi-directional Long Short-Term Memory&#xff0c;BiLSTM&#xff09;是一种扩展自长短期记忆网络&#xff08;LSTM&#xff09;的结构&#xff0c;旨在解决传统 LSTM 模型只能考虑到过去信息的问题。BiLST…

2025年Flutter项目管理技能要求

在2025年&#xff0c;随着Flutter技术的广泛应用和项目复杂度的提升&#xff0c;项目管理的重要性愈发凸显。Flutter项目管理不仅需要技术能力&#xff0c;还需要良好的沟通、协调、规划和执行能力。本文将详细探讨2025年Flutter项目管理应具备的技能要求&#xff0c;帮助项目管…

OpenCV CUDA模块中逐元素操作------数学函数

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 在OpenCV的CUDA模块中&#xff0c;确实存在一系列用于执行逐元素数学运算的函数&#xff0c;包括指数、对数、平方根等。这些函数对于高级图像处…

PhpStudy | PhpStudy 工具安装 —— Kali Linux 系统安装 PhpStudy

&#x1f31f;想了解这个工具的其它相关笔记&#xff1f;看看这个&#xff1a;[网安工具] 服务器环境配置工具 —— PhpStudy 使用手册 笔者备注&#xff1a;演示虽然是 Kali Linux&#xff0c;但其实 Linux 系列都可以参考此流程完成安装。 在前面的章节中&#xff0c;笔者简…

第6讲、全面拆解Encoder、Decoder内部模块

全面拆解 Transformer 架构&#xff1a;Encoder、Decoder 内部模块解析&#xff08;附流程图小测验&#xff09; 关键词&#xff1a;Transformer、Encoder、Decoder、Self-Attention、Masked Attention、位置编码、残差连接、多头注意力机制 Transformer 自 2017 年诞生以来&am…

游戏引擎学习第283天:“让‘Standing-on’成为一个更严谨的概念

如果同时使用多个OpenGL上下文&#xff0c;并且它们都有工作负载&#xff0c;GPU或GPU驱动程序如何决定调度这些工作&#xff1f;我注意到Windows似乎优先处理活动窗口的OpenGL上下文&#xff08;即活动窗口表现更好&#xff09;&#xff0c;挺有意思的…… 当多个OpenGL上下文…