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

C++多态与虚函数的原理解析

一,派生类指针与基类指针的互相转换

1,基本原理

参考文章《C++继承关系中,深度解析类内存布局与多态的实现》。可以知道,派生类的内存布局中包含基类子对象。那么可以通过指针的偏移完成派生类指针与基类指针的互相转换。

  • 派生类指针可以隐式/显式转换为基类指针

(派生类对象一定包含基类子对象,一定可以转换,所以可以隐式转换,更可以显式转换)

  • 基类指针可以显式转换为派生类指针

(基类对象不一定是派生类的子对象,转换时需要开发人员显式进行转换,由开发人员保证该基类对象属于某派生类对象)

  • 派生类的多重继承的基类指针之间不可以互相转换

(这种转换时的指针偏移不明确,在不同的多重继承情况下,被不同的派生类同时多重继承时,可能有不同的偏移量,并且从业务角度这种转换没有现实意义)

2,代码验证
#include <iostream>
#include <string>class N1
{int m_A;
};class N2
{int m_A;
};class N3 : public N1, public N2
{int m_A;
};
int main() {// 派生类指针可以隐式/显式转换为基类指针N3* n3_1 = new N3;N1* n1_1 = n3_1;N2* n2_1 = static_cast<N2*>(n3_1);// 基类指针可以显式转换为派生类指针N3* n3_2 = static_cast<N3*>(n1_1);//基类指针之间不可以互相转换//编译器报错误 E0171	类型转换无效//N2* n2 = static_cast<N2*>(n1_1);std::cout << "派生类指针地址:" << (int)n3_1 << std::endl;std::cout << "基类N1指针地址:" << (int)n1_1 << ",派生类地址相同,说明派生类对象与N1子对象的起始地址相同" << std::endl;std::cout << "基类N2指针地址:" << (int)n2_1 << ",派生类地址相差4字节,说明派生类对象与N2子对象的起始地址相差了一个N1子对象的大小" << std::endl;
}

运行后输出:

3,不明确的转换

当派生类的内存布局中有多个同类型的基类子对象时,进行派生类与基类的指针转换时会报转换不明确的错误。

#include <iostream>
#include <string>class N1
{int m_A;
};class N2 : public N1
{int m_A;
};class N3 : public N1, public N2
{int m_A;
};
int main() {N3* n3_1 = new N3;// 错误	C2594:从“N3 * ”到“N1* ”的转换不明确//N1* n1_1 = n3_1;//N1* n1_2 = static_cast<N1*>(n3_1);
}

使用虚继承可以解决这个问题,使用虚继承可保证派生类对象只创建一个基类子对象的实例。可以保证基类指针到派生类指针之间的转换关系是明确且唯一的。

在虚继承中,从派生类到基类指针的转换,可以在编译时确定,因为通过派生类指针可以访问到派生类虚基类指针,再根据虚基类指针访问到虚基类表,虚基类表中可以确定派生类带虚基类的指针偏移量。但这一过程不可逆。因为从虚基类的角度,访问不到虚基类表指针,也无法访问到虚基类表。所以会导致无法在编译时通过static_cast静态的确定偏移量,所以,使用static_cast对虚基类指针到派生类指针的转换会报编译错误。

为了解决这个问题,可以使用dynamic_cast在运行时通过RTTI进行转换。这需要借助多态技术,在基类中定义一个虚函数,这会在虚基类对象中,维护一个虚函数表指针,通过虚函数表,可以获取到从基类对象到派生类对象的偏移量,那么就可以完成从虚基类对象的指针到派生类对象的指针的转换了。

#include <iostream>
#include <string>class N1
{int m_A;virtual void M1() {};
};class N2 : virtual public N1
{int m_A;
};class N3 : virtual public N1, public N2
{int m_A;
};
int main() {N3* n3_1 = new N3;// 使用虚继承可保证派生类对象只创建一个基类子对象的实例N1* n1_1 = n3_1;N1* n1_2 = static_cast<N1*>(n3_1);// 错误:E0288	无法将指向基类 "N1" 的指针转换为指向派生类 "N3" 的指针 -- 基类是虚拟的//N3* n3_2 = static_cast<N3*>(n1_2);// 通过dynamic_cast,在运行时通过RTTI进行转换N3* n3_2 = dynamic_cast<N3*>(n1_2);std::cout << "派生类地址:" << (int)n3_1 << std::endl;std::cout << "虚基类地址:" << (int)n1_1 << std::endl;std::cout << "虚基类动态转换为派生类的地址:" << (int)n3_2 << std::endl;
}

上述代码中,N3的内存布局为:

ConsoleApplication1.cppclass N3        size(20):+---0      | +--- (base class N2)0      | | {vbptr}4      | | m_A| +---8      | m_A+---+--- (virtual base N1)
12      | {vfptr}
16      | m_A+---N3::$vbtable@:0      | 01      | 12 (N3d(N2+0)N1)N3::$vftable@:| -12   // 虚函数表指针在整个对象中的偏移量0      | &N1::M1     // 函数地址
vbi: class  offset o.vbptr  o.vbte fVtorDispN1      12       0       4 0

二,虚函数与虚函数表

虚函数会在类内部生成虚函数表指针,虚函数表指针指向虚函数表。虚函数表是编译器在编译时生成的静态数据结构,它存储在程序的可执行文件中,并在程序加载时映射到内存的只读数据段。所有同类型的类对象共享同一个虚函数表。

在有虚函数的起始类对象中,会创建虚函数表指针。

  • 若派生类未重写基类虚函数,派生类的虚函数表中保留基类函数的地址(指向基类实现)。
  • 若派生类重写了基类虚函数,派生类的虚函数表中对应条目更新为派生类函数的地址(覆盖基类地址)。
  • 若派生类新增虚函数,新虚函数的地址会追加到虚函数表末尾。

虚函数表的存在,导致无论通过派生类指针还是通过基类指针去调用虚函数,调用的函数地址和传入的this指针都是一样的。函数地址就是重写的函数地址,this指针就是重写函数所在的类的指针。

下面根据代码做详细说明:

#include <iostream>
#include <string>class N1
{
public:int m_A;virtual void M1() {std::cout << "&N1::M1(),m_A:" << m_A << std::endl;}virtual void M2() = 0;void M3(){std::cout << "&N1::M3(),m_A:" << m_A << std::endl;}
};class N2 : public N1
{
public:int m_A;void M1() override { std::cout << "&N2::M1(),m_A:" << m_A << std::endl;}void M2() override{std::cout << "&N2::M2(),m_A:" << m_A << std::endl;}
};class N3 :public N2
{
public:int m_A;void M2() override{std::cout << "&N3::M2(),m_A:" << m_A << std::endl;}void M3(){std::cout << "&N3::M3(),m_A:" << m_A << std::endl;}
};int main()
{// 初始化N3 n3;n3.m_A = 3;n3.N2::m_A = 2;n3.N2::N1::m_A = 1;std::cout << "通过作用域调用函数:" << std::endl;// 通过作用域运算符(如n3.N2::M1())调用虚函数时,会绕过虚函数表,直接进行静态绑定。n3.M1();     // 调用函数&N2::M1(),this指针是N2子对象的指针。n3.N2::M1(); // 调用函数&N2::M1(),this指针是N2子对象的指针。n3.N2::N1::M1(); // 调用函数&N1::M1(),this指针是N1子对象的指针。n3.M2(); // 调用函数&N3::M2(),this指针是N3对象的指针。std::cout << std::endl << "通过虚函数表调用函数:" << std::endl;N1* n1ptr = &n3;n1ptr->M1(); // 通过虚函数表,调用函数&N2::M1(),this指针是N2子对象的指针。n1ptr->M2(); // 通过虚函数表,调用函数&N3::M2(),this指针是N3对象的指针。n1ptr->M3(); // 函数地址静态绑定,调用函数&N1::M3(),this指针是N1对象的指针。
}

N3的虚函数表的情况如下:

ConsoleApplication1.cppclass N3        size(16):+---0      | +--- (base class N2)0      | | +--- (base class N1)0      | | | {vfptr}4      | | | m_A| | +---8      | | m_A| +---
12      | m_A+---N3::$vftable@:| &N3_meta|  0   // 从虚函数表指针到整个派生类对象指针之间的偏移量0      | &N2::M11      | &N3::M2N3::M2 this adjustor: 0

三、纯虚函数与抽象类

纯虚函数不需要有函数实现,纯虚函数声明为:

virtual 返回值类型 函数名(参数列表) = 0;

含有纯虚函数的类称为抽象类。抽象类不能实例化为对象。继承自抽象类的派生类需要重写这个纯虚函数,否则也是抽象类。

四、虚析构函数

在将派生类指针赋值给基类指针对象时,在使用delete删除基类指针时,编译器会将整个派生类给清理掉。但是不会调用派生类的析构函数,所以,需要写虚析构函数。

1,delete操作会释放掉完整分配的内存大小

C++在使用基类指针清理内存时,会将整个派生类对象的内存清理掉。

验证代码为:

#include <iostream>
#include <vector>
#include <cstdlib> // 包含 malloc 和 free// 使用链表避免递归分配问题
struct AllocationRecord {void* ptr;size_t size;AllocationRecord* next;
};AllocationRecord* alloc_head = nullptr;void* operator new(size_t size) {// 使用 malloc 而不是 ::operator new 避免递归void* ptr = malloc(size);// 创建记录节点(使用 malloc 避免递归)AllocationRecord* rec = static_cast<AllocationRecord*>(malloc(sizeof(AllocationRecord)));rec->ptr = ptr;rec->size = size;rec->next = alloc_head;alloc_head = rec;std::cout << "[ALLOC] " << size << " bytes at " << ptr << "\n";return ptr;
}void operator delete(void* ptr) noexcept {if (!ptr) return;// 查找并移除记录AllocationRecord** prev = &alloc_head;AllocationRecord* current = alloc_head;size_t freed_size = 0;while (current) {if (current->ptr == ptr) {freed_size = current->size;*prev = current->next;free(current); // 释放记录节点break;}prev = &(current->next);current = current->next;}std::cout << "[FREE] " << freed_size << " bytes at " << ptr << "\n";free(ptr); // 释放实际内存
}// 原始类定义保持不变
class N1 {
public:int m_A;virtual void M1() = 0;virtual void M2() = 0;
};class N2 : public N1 {
public:int m_B;void M1() override { std::cout << "N2::M1\n"; }void M2() override { std::cout << "N2::M2\n"; }
};class N3 : public N2 {
public:int m_C;void M1() override { std::cout << "N3::M1\n"; }
};int main() {std::cout << "Sizes:\n";std::cout << "N1: " << sizeof(N1) << " bytes\n";std::cout << "N2: " << sizeof(N2) << " bytes\n";std::cout << "N3: " << sizeof(N3) << " bytes\n\n";N1* n1Ptr = new N3;std::cout << "\nDeleting...\n";delete n1Ptr;// 清理剩余的分配记录(如果有)while (alloc_head) {AllocationRecord* temp = alloc_head;alloc_head = alloc_head->next;free(temp);}
}
2,虚析构函数

delete操作不会调用派生类的析构函数。C++在使用基类指针清理内存时,会将整个派生类对象的内存清理掉。但不会调用派生类的析构函数。使用虚析构可以解决这个问题。使用虚析构时,会调用所有的虚析构的函数。

使用虚析构时,需要将最底层基类的析构函数增加virtual关键字。这样在整个派生结构中的所有析构函数都会被调用。

虚析构的常用的定义:

    virtual ~类名(){std::cout << "N1的析构函数" << std::endl;}
// 虚析构,使用默认实现
virtual ~类名() = default;
class N1
{
public:int m_A;// 纯虚析构,对应的,类为抽象类virtual ~N1() = 0;
};// 纯虚析构,会将类编程抽象类,但是仍然需要析构函数的实现,否则编译器会报错。
N1::~N1()
{
}

http://www.dtcms.com/a/320732.html

相关文章:

  • MySQL的触发器:
  • 虹科技术分享 | LIN总线译码功能与LIN控制交流发电机(二)
  • 灌区信息化智能管理系统解决方案
  • 计算机视觉CS231n学习(5)
  • AI开发平台行业全景分析与战略方向建议
  • C++归并排序
  • 使用 Python GUI 工具创建安全的密码短语
  • tmi8150b在VM=3.3v电压下,如何提高转速,记录
  • 高性能 Vue 应用运行时策略
  • 仓颉编程语言的match表达式
  • 《算法导论》第 12 章 - 二叉搜索树
  • 【量子计算】量子计算驱动AI跃迁:2025年算法革命的曙光
  • conda pip uv与pixi
  • SpringCloud(4)-多机部署,负载均衡-LoadBalance
  • ASP.NET三层架构成绩管理系统源码
  • HBase的异步WAL性能优化:RingBuffer的奥秘
  • 深度虚值期权合约有什么特点?
  • InfoNCE 损失
  • 企微消息机器人推送配置-windows+python
  • 【ros-humble】2.自定义通讯接口发布者python,qt使用(话题)
  • 关于csdn导入和导出
  • USB2.0协议学习-基础知识
  • day070-Jenkins自动化与部署java、前端代码
  • linux安装mysql8.0,二进制码安装
  • 《Graph machine learning for integrated multi-omics analysis》
  • ChipCamp探索系列 -- 1. Soft-Core RISC-V on FPGA
  • 【全栈自动驾驶与异构加速】系统学习计划
  • React 状态管理入门:从 useState 到复杂状态逻辑
  • 【MongoDB】查询条件运算符:$expr 和 $regex 详解,以及为什么$where和$expr难以使用索引
  • 使用pybind11封装C++API