剖析C++中继承、多态的底层原理

devtools/2025/3/30 20:40:28/

继承底层原理主要涉及内存布局、虚函数表、类型转换等机制。

一、内存布局:

继承的底层实现是通过内存布局来完成的。派生类对象的内存布局通常包括:

a.基类子对象:派生类对象中包含基类的所有数据成员。

b.派生类特有的数据成员。

例如:

class Base 
{
public:int a;
};class Derived : public Base 
{
public:int b;
};

Derived对象的内存布局如下:

+---------+
| Base::a |  // 基类子对象
+---------+
| Derived::b |  // 派生类特有的数据成员
+---------+

多重继承同理。在Derived对象的内存布局中多了另一个基类的对象。基类子对象按照继承的顺序排列。

二、虚函数表(vtable)

当类中有虚函数时,C++会为每个类生成一个虚函数表,通过虚表实现动态绑定(多态

虚函数表是一个函数指针数组,意味着虚函数表中存放虚函数的地址,或者说虚函数的入口。

当派生类继承基类时,派生类会继承基类的虚函数表。如果派生类重写了基类的虚函数,派生类的虚函数表中对应的条目会被更新为派生类的对应函数的地址。如果派生类没有重写基类的虚函数,派生类的虚函数表中仍然保留基类的函数地址。

每个包含虚函数的类对象中都会包含一个指向虚函数表的指针(vptr),这个指针在对象创建时被初始化,指向该类的虚函数表。派生类对象中也会包含自己的vptr,指向派生类的虚函数表,而不是基类的虚函数表。当派生类对象被创建时,vptr会被初始化为指向派生类的虚函数表。

举个例子:

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

Base类的虚函数表:

+-------------------+
| Base::foo() 地址  |
+-------------------+
| Base::bar() 地址  |
+-------------------+

Derived类的虚函数表:

+-------------------+
| Derived::foo() 地址 |  // 重写了 Base::foo()
+-------------------+
| Base::bar() 地址  |  // 未重写 Base::bar()
+-------------------+

Derived对象的内存布局:

+---------+
| vptr    |  // 指向 Derived 的虚函数表
+---------+
| Base 部分 |
+---------+

调用过程:

当ptr->foo()被调用时:

编译器通过ptr找到对象的vptr,因为ptr是一个指向Derived对象的基类指针,所以编译器通过ptr找到的是Derived对象的vptr。

接着,通过vptr找到Derived的虚函数表,调用Derived::foo().

类似地,当ptr->bar()被调用时:

编译器通过ptr找到Derived对象的vptr,通过vptr找到Derived的虚函数表

由于没有重写,因此Derived的虚函数表中存放的只有Base::bar()的地址。调用Base::bar().

几个疑惑的点:

1.怎么理解“地址晚绑定”?

其意思是:在派生类中重写了虚函数后,才能确定这个函数具体的地址。

2.怎么理解“基类指针 指向 派生类对象”?某个类型的指针不是只能指向该类型的变量吗?如果使用派生类指针指向派生类对象会有什么后果?

在C++中有一种特殊的机制,允许基类指针指向派生类对象。这种机制是通过向上转型实现的。

向上转型(Upcasting)是指将派生类指针或引用转换为基类指针或引用。这是安全的,因为派生类对象中包含基类子对象。

例如:

Derived d;
Base*ptr=&d;//向上转型

为什么基类指针可以指向派生类对象?因为派生类对象的内存布局中包含基类子对象。基类指针指向的是派生类对象中的基类子对象部分。

也就是说,当Base*ptr=&d时,ptr指向的是Derived对象中的Base子对象部分,而不是指向Derived对象中特有的数据成员。

当基类中有虚函数时,通过基类指针调用虚函数会根据对象的实际类型(派生类)调用正确的函数实现。需要说明的是,通过基类指针调用虚函数时,实际调用的是派生类中重写的虚函数。当通过基类指针调用虚函数时,程序通过对象的vptr找到虚函数表(因为创建对象的时候,vptr就被初始化指向虚函数表了),然后从虚函数表中查找对应虚函数的地址,最后调用该地址指向的函数。

如果我们向下转型(基类指针转换为派生类指针),需要使用dynamic_cast进行安全检查。

Base* ptr = new Derived;
Derived* dptr = dynamic_cast<Derived*>(ptr);  // 向下转型
if (dptr) 
{dptr->foo();
}

如果派生类指针指向派生类对象,并不会有什么负面的后果。并且派生类指针可以直接访问派生类中定义的所有成员(包括从基类继承的成员和派生类特有的成员),而基类指针只能访问派生类中从基类继承的成员。

通过基类指针调用虚函数时,会根据对象的实际类型(派生类)调用正确的函数实现。而派生类指针不需要通过虚函数表间接调用函数,直接调用派生类的成员函数。这也是它不能实现多态性的原因。编译器会根据指针的静态类型(派生类)直接绑定到派生类的函数实现。

3.虚函数表占用内存吗?为什么在Derived对象的内存布局中没有见到虚函数表?

虚函数表确实占用内存,但它并不直接存储在对象的内存布局当中。虚函数表是编译器为每个包含虚函数的类生成的一个全局数据结构,存储在程序的只读数据段。每个类都有自己的虚函数表,而不是每个对象都有自己的虚函数表。

对象的内存布局中只包含数据成员和指向虚函数表的指针,该指针指向的是类的虚函数表,而不是对象的虚函数表(类对象的内存布局中不包含虚函数表)。

#include <iostream>class Base 
{
public:virtual void foo() {std::cout << "Base::foo()" << std::endl;}int a;
};class Derived : public Base 
{
public:void foo() override {std::cout << "Derived::foo()" << std::endl;}int b;
};int main() 
{Derived d;std::cout << "Size of Derived: " << sizeof(d) << std::endl;return 0;
}

Base类的虚函数表:

+-------------------+
| Base::foo() 地址  |
+-------------------+

Derived类的虚函数表:

+-------------------+
| Derived::foo() 地址 |  // 重写了 Base::foo()
+-------------------+

Derived对象中的内存布局:

+---------+
| vptr    |  // 指向 Derived 的虚函数表
+---------+
| Base::a |  // 基类数据成员
+---------+
| Derived::b |  // 派生类数据成员
+---------+

在64位系统上,sizeof(d)的大小为16,因为vptr占用8字节(指针大小),Base::a占用4字节,Derived::b占用4字节。

虚函数表不在对象的内存布局中,这样避免了空间浪费。一个类有一个虚函数表就可以了,对象通过虚函数指针访问即可。


http://www.ppmy.cn/devtools/170712.html

相关文章

监控易运维管理软件:轻松部署,高效运维

在当今的IT环境中&#xff0c;运维管理的重要性不言而喻。一款好的运维管理软件&#xff0c;不仅能够帮助企业高效管理IT基础设施&#xff0c;还能提升运维效率&#xff0c;降低运维成本。今天&#xff0c;我要给大家详细介绍的&#xff0c;是我们公司自主研发的监控易运维管理…

Redis 跳表原理详解

一、引言 在 Redis 中&#xff0c;有序集合&#xff08;Sorted Set&#xff09;是一种非常重要的数据结构&#xff0c;它可以实现元素的有序存储和高效查找。而实现有序集合的底层数据结构之一就是跳表&#xff08;Skip List&#xff09;。跳表是一种随机化的数据结构&#xff…

我爱学算法之——滑动窗口攻克子数组和子串难题(中)

学习算法&#xff0c;继续加油&#xff01;&#xff01;&#xff01; 一、将 x 减到 0 的最小操作数 题目解析 来看这一道题&#xff0c;题目给定一个数组nums和一个整数x&#xff1b;我们可以在数组nums的左边或者右边进行操作&#xff08;x减去该位置的值&#xff09;&#…

【大模型基础_毛玉仁】3.4 Prompt 技巧

目录 3.4 Prompt 技巧3.4.1 规范Prompt 编写1&#xff09;任务说明要明确2&#xff09;上下文丰富且清晰3&#xff09;输出格式要规范4&#xff09;排版要清晰 3.4.2 合理归纳提问1&#xff09;复杂问题拆解2&#xff09;追问 3.4.3 适时使用CoT1&#xff09;何时使用CoT2&…

2025年十大AI工具对比

2025年十大AI工具对比 以下是2025年各大AI工具的详细对比&#xff0c;涵盖性能、功能、用户评价等方面&#xff0c;并以表格形式呈现。数据来源于多个权威来源&#xff0c;确保信息全面且准确。 对比表格 排名AI工具名称主要功能性能特点用户评价适用场景1DeepSeek多模态AI、…

蓝桥杯备考:BFS最短路径之Meteor Shower S流星雨

本题是一个BFS最短路问题&#xff0c;我们可以先把时刻的矩阵搞出来&#xff0c;哪些时刻哪些方块儿不能走用来剪枝 如果第一次走到永远不会被扎到的区域&#xff0c;那时候就是我们的最短距离 定义方向向量 #include <iostream> #include <queue> #include <c…

MCU vs SoC

MCU&#xff08;Microcontroller Unit&#xff0c;单片机&#xff09;和SoC&#xff08;System on Chip&#xff0c;片上系统&#xff09;是两种不同的芯片类型&#xff0c;尽管它们都实现了高度集成&#xff0c;但在设计目标、功能复杂性和应用场景上存在显著差异。以下是两者…

如何开发一个动态网页

一、使用技术 Servelet JavaServlet,Java编写的运行在服务器上的程序,用于交互式地浏览和生成动态Web内容。 二、javaweb项目的环境配置 1.创建webApplication项目 2.配置tomcat服务器 3.添加编译存放目录 在WEB-INF目录下添加classes和lib目录 配置编译路径全都放在class…