图解C++对象模型

news2024/12/26 9:23:22

C++对象模型是什么

《深度探索C++对象模型》这本书中对对象模型的描述如下:

有两个概念可以解释C++对象模型: 语言中直接支持面向对象程序设计的部分。 对于各种支持的底层实现机制。

语言中直接支持面向对象程序设计的部分,包括了构造函数、析构函数、多态、虚函数等等。 对象模型的底层实现机制并未标准化,不同的编译器有一定的自由来设计对象模型的实现细节。在我看来,对象模型研究的是对象在存储上的空间与时间上的更优,并对C++面向对象技术加以支持,如以虚指针、虚表机制支持多态特性。

多态与虚函数表指针

C++中虚函数的作用主要是为了实现多态机制。多态,简单来说,是指在继承层次中,父类的指针可以具有多种形态——当它指向某个子类对象时,通过它能够调用到子类的函数,而非父类的函数。

class Base {     virtual void print(void);    }
class Drive1 :public Base{    virtual void print(void);    }
class Drive2 :public Base{    virtual void print(void);    }
Base * ptr1 = new Base; 
Base * ptr2 = new Drive1;  
Base * ptr3 = new Drive2;
ptr1->print(); //调用Base::print()
prt2->print(); //调用Drive1::print()
prt3->print(); //调用Drive2::print()

这是一种运行期多态,即父类指针唯有在程序运行时才能知道所指的真正类型是什么。这种运行期决议,是通过虚函数表来实现的。

虚函数表

当一个类本身定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数表指针(vptr)。虚函数表指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。 当vprt位于对象内存最前面时,对象的地址即为虚函数表指针地址。以下面的代码为例,在Windows平台下测试下虚函数指针(vptr)的地址。

class Base
{
public:
    Base(int i) :baseI(i){};
    virtual ~Base(){}

    virtual void print(void){ cout << "调用了虚函数Base::print()"; }
    virtual void setI(){cout<<"调用了虚函数Base::setI()";}
private:
    int baseI;
};

我们可以取得虚函数指针的地址:

Base b(1000);
int * vptrAdree = (int *)(&b);  
cout << "虚函数指针(vprt)的地址是:\t"<<vptrAdree << endl;

运行代码出结果:

我们强行把类对象的地址转换为 int* 类型,取得了虚函数表指针的地址。虚函数表指针指向虚函数表,虚函数表中存储的是一系列虚函数的地址,虚函数地址出现的顺序与类中虚函数声明的顺序一致。虚函数表指针地址值,可以得到虚函数表的地址,也即是虚函数表第一个虚函数的地址:

typedef void(*Fun)(void);
Fun vfunc = (Fun)*( (int *)*(int*)(&b));
cout << "第一个虚函数的地址是:" << (int *)*(int*)(&b) << endl;
cout << "通过地址,调用虚函数Base::print():";
vfunc();
  • 我们把虚表指针的值取出来: *(int*)(&b),它是一个地址,虚函数表的地址
  • 把虚函数表的地址强制转换成 int* : (int *)*(int*)(&b)
  • 再把它转化成我们Fun指针类型 : (Fun)*( (int *)*(int*)(&b))

这样,我们就取得了类中的第一个虚函数,我们可以通过函数指针访问它。 运行结果:

同理,第二个虚函数setI()的地址为:

 (int * )(*(int*)(&b)+1)

普通继承下的对象模型

我们接下来来研究下普通继承下的C++对象模型是怎样的(普通继承是相对于虚继承来说的)。 首先定义一个基类Base,如下:

class Base
{
public:
    Base(int i) : baseI(i){};
    virtual ~Base(){}

    int getI(){ return baseI; }

    static void countI(){};

    virtual void print(void){ cout << "Base::print()"; }

private:
    int baseI;
    static int baseS;
};

单继承

单继承的情景下,我们定义派生类Derive ,继承自Base

class Derive : public Base
{
public:
    Derive(int d) : Base(1000), DeriveI(d){};
    // overwrite父类虚函数
    virtual void print(void){ cout << "Drive::Drive_print()" ; }
    // Derive声明的新的虚函数
    virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }
    virtual ~Derive(){}
private:
    int DeriveI;
};

继承类图为:

在C++对象模型中,对于一般继承(这个一般是相对于虚拟继承而言):

  • 若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表)
  • 若子类并无overwrite父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表的最后

一般单继承下的对象模型如下:

多继承

一般多继承

一般多继承的继承类图关系如下:

单继承中(一般继承),子类会扩展父类的虚函数表。在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类overwrite了父类的函数,需要覆盖多个父类的虚函数表吗? 在多继承中,有如下规则:

  • 子类的虚函数被放在声明的第一个基类的虚函数表中。
  • overwrite时,所有基类的print()函数都被子类的print()函数覆盖。
  • 内存布局中,父类按照其继承时的声明顺序排列。

其中第二点保证了父类指针指向子类对象时,总是能够调用到真正的函数。 多继承的继承关系和对象模型图解如下:

菱形继承

菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有多份基类实例(这会带来一些问题)。为了方便叙述,我们不使用上面的代码了,而重新写一个重复继承的继承层次:

class B
{
public:
    B(int i = 1) : ib(i){}

    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }

    int ib;
};

class B1 : public B
{
public:
    B1(int i = 100 ) : ib1(i) {}

    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }

    int ib1;
};

class B2 : public B
{
public:
    B2(int i = 1000) : ib2(i) {}

    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B2::Bf2()" << endl; }

    int ib2;
};

class D : public B1, public B2
{
public:
    D(int i= 10000) : id(i){}

    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }

    int id;
};

继承类图关系如下:

根据单继承,我们可以分析出B1,B2类继承于B类时的内存布局。又根据一般多继承,我们可以分析出D类的内存布局。我们可以得出D类子对象的内存布局如下图:

从图中可以看到,由于D类间接继承了B类两次,导致D类对象中含有两个B类的数据成员ib,一个属于来源B1类,一个来源B2类。这样不仅增大了空间,更重要的是引起了程序歧义:

D d;
d.ib =1 ;               //二义性错误,调用的是B1的ib还是B2的ib?
d.B1::ib = 1;           //正确
d.B2::ib = 1;           //正确

尽管我们可以通过明确指明调用路径以消除二义性,但二义性的潜在性还没有消除,我们可以通过虚继承来使D类只拥有一个ib实体。

虚继承下的对象模型

虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承的子类,如果本身定义了新的虚函数,则编译器为其新增一个虚函数表指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。而非虚继承的情况是,直接扩展父类虚函数表。虚继承是新增虚函数表,普通继承是扩展虚函数表。
  • 子类会有一个虚基类表指针,指向一个虚基类表。虚基类表中存放的是子类的所有虚基类(按继承关系从左至右)基地址相对于这个虚基类表指针的偏移。
  • 虚继承的子类单独保留了父类的vptr与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。
  • 虚继承的子类对象中,含有四字节的虚表指针偏移值。

虚基类表

在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类表指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类表指针,那么虚基类表指针可能在实例的0字节偏移处,也可能在类实例的4字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面)。 一个类的虚基类表指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。

虚基类表的第二、第三...个条目依次为该类的最左虚继承父类、次左虚继承父类...的内存地址相对于虚基类表指针的偏移值。

简单虚继承

如果我们的B1类虚继承于B类:

class B
{
public:
    B(int i = 1) : ib(i){}

    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }

    int ib;
};

class B1 : virtual public B
{
public:
    B1(int i = 100 ) : ib1(i) {}

    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }

    int ib1;
};

继承类图关系如下:

单虚继承的对象模型如下图所示:

如果在虚继承中子类未添加新的虚函数,只是覆盖基类的虚函数,那么子类对象的首地址就不是存放虚函数表的指针,而是虚基类表指针,如下图所示:

多虚继承

如果我们的D类虚继承于B1和B2类,代码如下(具体代码省略):

class D : virtual public B1, virtual public B2 {
...
}

多虚继承的对象模型如下图所示:

菱形虚继承

如果我们有如下的菱形虚继承层次:

class B{...}
class B1: virtual public  B{...}
class B2: virtual public  B{...}
class D : public B1,public B2{...}

类图如下所示:

菱形虚拟继承下,派生类D类的对象模型又有不同的构成。在D类对象的内存构成上,有以下几点:

  • 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
  • D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
  • 编译器没有为D类生成一个它自己的vbptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
  • 超类B的内容放到了D类对象内存布局的最后。

菱形虚拟继承下的C++对象模型为:

问:菱形虚继承的子类D有几个虚函数表? 答:1、2、3个。

  • 拥有3个虚函数表:B1、B2各自虚继承了类B,且B1、B2各自新增了自己的虚函数表
  • 拥有2个虚函数表:B1、B2各自虚继承了类B,且B1、B2只有一个新增了自己的虚函数表
  • 拥有1个虚函数表:B1、B2各自虚继承了类B,但B1、B2没有新增虚函数表

相关问题解答

C++封装带来的布局成本有多大?

在C语言中,“数据”和“处理数据的操作(函数)”是分开来声明的,也就是说,语言本身并没有支持“数据和函数”之间的关联性。在C++中,我们通过类来将属性与操作绑定在一起,称为ADT,抽象数据结构。 C语言中使用struct(结构体)来封装数据,使用函数来处理数据。举个例子,如果我们定义了一个struct Point3如下:

typedef struct Point3
{
    float x;
    float y;
    float z;
} Point3;

而在C++中,我们更倾向于定义一个Point3d类,以ADT来实现上面的操作:

class Point3d
{
public:
    point3d (float x = 0.0,float y = 0.0,float z = 0.0)
        : _x(x), _y(y), _z(z){}

    float x() const {return _x;}
    float y() const {return _y;}
    float z() const {return _z;}
private:
    float _x;
    float _y;
    float _z;
};

看到这段代码,很多人第一个疑问可能是:加上了封装,布局成本增加了多少? 答案是class Point3d并没有增加成本。学过了C++对象模型,我们知道,Point3d类对象的内存中,只有三个数据成员。 上面的类声明中,三个数据成员直接内含在每一个Point3d对象中,而成员函数虽然在类中声明,却不出现在类对象(object)之中,这些函数(non-inline)属于类而不属于类对象,只会为类产生唯一的函数实例。 所以,Point3d的封装并没有带来任何空间或执行期的效率影响。 而在下面这种情况下,C++的封装额外成本才会显示出来:

  • 虚函数机制(virtual function) , 用以支持执行期绑定,实现多态。
  • 虚基类 (virtual base class) ,虚继承关系产生虚基类,用于在多重继承下保证基类在子类中拥有唯一实例。

不仅如此,Point3d类数据成员的内存布局与c语言的结构体Point3d成员内存布局是相同的。C++中处在同一个访问标识符(指public、private、protected)下的声明的数据成员,在内存中必定保证以其声明顺序出现。而处于不同访问标识符声明下的成员则无此规定。对于Point3类来说,它的三个数据成员都处于private下,在内存中一起声明顺序出现。

总结一下: 不考虑虚函数与虚继承,当数据都在同一个访问标识符下,C++的类与C语言的结构体在对象大小和内存布局上是一致的,C++的封装并没有带来空间时间上的影响。

空类构成的继承层次中,每个类的大小是多少?

今有类如下,这个空类构成的继承层次中,每个类的大小是多少?

class B{};
class B1 :public virtual  B{};
class B2 :public virtual  B{};
class D : public B1, public B2{};

int main()
{
    B b;
    B1 b1;
    B2 b2;
    D d;
    cout << "sizeof(b)=" << sizeof(b)<<endl;
    cout << "sizeof(b1)=" << sizeof(b1) << endl;
    cout << "sizeof(b2)=" << sizeof(b2) << endl;
    cout << "sizeof(d)=" << sizeof(d) << endl;
    getchar();
}

输出结果是(在64位操作系统上):

sizeof(b)=1
sizeof(b1)=8
sizeof(b2)=8
sizeof(d)=16

解析:

  • 编译器为空类安插1字节的char,以使该类对象在内存得以配置一个地址。
  • b1虚继承于b,编译器为其安插一个4字节(32为机器),8字节(64位机器)的虚基类表指针,此时b1已不为空,编译器不再为其安插1字节的char(优化)。
  • b2同理。
  • d含有来自b1与b2两个父类的两个虚基类表指针。大小为8字节(32为机器),16字节(64位机器)。

虚函数表放置在内存布局中的那个区域?

  • 虚函数表的内容是在编译时确定的。编译期间编译器就为每个类确定好了对应的虚函数表里的内容。 在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针。
  • 每个类拥有一个虚函数表,每个类的对象中的虚函数表指针所指向的都是同一个虚函数表。
  • 虚函数表的内容是固定不变的,因此它会存放在常量区。

虚函数表指针在什么时候创建?

对象构造时。类的构造函数中会首先执行vptr的初始化然后再执行其他的。

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

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

相关文章

chatgpt赋能python:Python如何快速复制上一行?

Python 如何快速复制上一行&#xff1f; 在编写Python代码时&#xff0c;经常需要快速复制上一行代码进行修改。如果只是简单的手动复制粘贴&#xff0c;会造成不必要的时间浪费并且容易出错。本文将介绍三种快速复制上一行代码的方法。 方法一&#xff1a;使用快捷键 在Pyt…

chatgpt赋能python:Python如何拟合曲线

Python如何拟合曲线 拟合曲线是数据分析中常见的一种方法。Python作为一种强大的编程语言&#xff0c;具有丰富的数据分析库和拟合曲线的功能。本文将介绍如何在Python中使用numpy、matplotlib和scipy库进行曲线拟合。 numpy库 numpy是Python中常用的数值计算库。它提供了许…

Error系列-CVE CIS-2023系统漏洞处理方案集合

问题1&#xff1a; CVE-2023-29491 Type: OS涉及到的包&#xff1a;ncurses-dev,ncurses-libs,ncurses-terminfo-base描述&#xff1a;当前系统安装的ncurses&#xff0c;存在漏洞&#xff0c;当被setuid应用程序使用时&#xff0c;允许本地用户通过在$HOME/中找到的终端数据库…

Spring Cloud Alibaba - Sentinel(二)

目录 一、Sentinel 熔断降级简介 1、基本介绍 2、熔断策略 3、熔断规则 二、Sentinel熔断策略 1、慢调用比例 2、异常比例 3、 异常数 三、热点规则 1、热点规则 2、参数例外项 四、系统规则 1、Sentinel 系统规则 一、Sentinel 熔断降级简介 1、基本介绍 除了流…

Linux 手动部署 SpringBoot 项目

Linux 手动部署 SpringBoot 项目 1. 将项目打包成 jar 包 &#xff08;1&#xff09;引入插件 <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></pl…

【Spring 项目的创建和使用】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 1. 创建 Spring 项目 2. 创建一个 普通 Maven…

chatgpt赋能python:Python如何更新pip

Python 如何更新 pip 在 Python 编程中&#xff0c;pip 是一个非常重要的工具&#xff0c;它可以帮助我们安装和管理 Python 包。然而&#xff0c;我们有时候会遇到 pip 版本过低&#xff0c;需要进行更新的情况。那么&#xff0c;如何更新 pip 呢&#xff1f; 什么是 pip p…

论文笔记(三十):Counter-Hypothetical Particle Filters for Single Object Pose Tracking

Counter-Hypothetical Particle Filters for Single Object Pose Tracking 文章概括摘要1. 简介II. 相关工作A. 机器人的物体姿态估计和跟踪B. 鲁棒性的粒子滤波 III. 背景&#xff1a;粒子滤波A. 粒子滤波B. 粒子剥夺和粒子重振IV. 反假设粒子滤波A. 反假设重取样B. 6D姿势估计…

记一次为学校流浪猫开发的小程序——航海之猫

某次刷朋友圈时&#xff0c;看到校园墙上有一个校园流浪猫救助组织在召集爱心人士加入工作小组。其中需要会做微信小程序的给学校里的猫猫做一个猫猫图鉴&#xff0c;于是就有了本次项目经历。 相关技术及工具 工具&#xff1a;Uniapp、XBuilder、微信官方开发者工具技术&…

chatgpt赋能python:Python如何把文件复制到另一个目录下

Python如何把文件复制到另一个目录下 作为一个有着10年Python编程经验的工程师&#xff0c;我可以告诉你&#xff0c;在Python编程中&#xff0c;复制文件是非常常见的任务之一。无论您是为了备份数据或将文件从一个地方传输到另一个地方&#xff0c;都需要使用文件复制操作。…

Linux中/dev/null和/dev/zero的作用

1./dev/null和/dev/zero介绍 在Linux环境中&#xff0c;我们会经常用到/dev/null和/dev/zero&#xff0c;今天为大家讲讲/dev/null和/dev/zero的作用以及使用场景。 1.1./dev/null介绍 linux中/dev/null&#xff0c;它是一种特殊的虚拟设备&#xff0c;用于写入而不是读取&a…

前端050_单点登录SSO_登录功能实现

登录功能实现 1、登录认证流程2、定义 Api 调用登录接口3、Vuex 登录信息状态管理4、提交登录触发 action5、测试1、登录认证流程 单点登录认证流程图 门户客户端要求登录时,输入用户名密码,认证客户端提交数据给认证服务器认证服务器校验用户名密码是否合法,合法响应用户…

RISC-V学习基础(六)

原子指令 RV32A有两种类型的原子操作&#xff1a; 内存原子操作&#xff08;AMO&#xff09;加载保留/条件存储&#xff08;load reserved/store conditional&#xff09; 图6.1是RV32A扩展指令集的示意图&#xff1a; 图6.2列出了它们的操作码和指令格式 AMO&#xff08;at…

chatgpt赋能python:Python如何更新包

Python如何更新包 Python是当今最流行的编程语言之一&#xff0c;它通过其丰富的生态系统和充足的工具库为许多开发者提供了便捷、快速、高效的开发体验。在Python的世界里&#xff0c;包是一种最重要的组件之一&#xff0c;因为它们可以让你轻松地扩展Python的功能&#xff0…

【跨域】如何解决跨域问题

同源策略 同源 相同协议相同域名相同端口 内容 浏览器的同源策略 - Web 安全 | MDN (mozilla.org) 浏览器页面向不同源的服务器发送ajax请求资源时&#xff0c;响应的数据会被浏览器拦截 意义 出于安全性的考虑&#xff0c;防止恶意获取数据 解决方案 JSONP 不使用aj…

Rust每日一练(Leetday0022) 最小路径和、有效数字、加一

目录 64. 最小路径和 Minimum Path Sum &#x1f31f;&#x1f31f; 65. 有效数字 Valid Number &#x1f31f;&#x1f31f;&#x1f31f; 66. 加一 Plus One &#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 P…

docker水位如何清理(容器水位清理详细分析)

docker水位过高&#xff0c;清理怕出问题&#xff1f;&#xff0c;不知道清理什么&#xff1f;怕删错了&#xff1f;进入实践 第一步准备测试数据 创建 悬空的镜像&#xff08;即REPOSITORY和TAG均为的镜像&#xff09; docker pull busybox:musl docker tag busybox:musl b…

chatgpt赋能python:Python的更新:为什么你需要及时更新并如何更新

Python的更新&#xff1a;为什么你需要及时更新并如何更新 Python是一种强大的程序设计语言&#xff0c;它具有广泛的应用&#xff0c;从数据科学&#xff0c;机器学习到Web开发&#xff0c;都有着广泛的应用。不断更新的Python版本为开发者和用户提供了最新的功能和最佳实践。…

chatgpt赋能python:Python如何在同一行输出

Python如何在同一行输出 Python是一种高级编程语言&#xff0c;广泛应用于数据科学、Web开发、人工智能等领域。其中&#xff0c;输出内容是程序必不可少的一部分。本文将介绍在Python中如何在同一行输出多个内容。 一般输出 在Python中&#xff0c;使用print函数可以将内容…