深度解读《深度探索C++对象模型》之数据成员的存取效率分析(三)

这篇具有很好参考价值的文章主要介绍了深度解读《深度探索C++对象模型》之数据成员的存取效率分析(三)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。

前面两篇请通过这里查看:

深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)

深度解读《深度探索C++对象模型》之数据成员的存取效率分析(二)

这一节讲解具体继承的情况,具体继承也叫非虚继承(针对虚继承而言),分为两种情况讨论:单一继承和多重继承。

单一继承

在上面的例子中,所有的数据都封装在一个类中,但有时可能由于业务的需要,需要拆分成多个类,然后每个类之间具有继承关系,比如可能是这样的定义:

class Point {
	int x;
};
class Point2d: public Point {
	int y;
};
class Point3d: public Point2d {
	int z;
};

对于这样的单一继承关系,在前面的文章《深度解读《深度探索C++对象模型》之C++对象的内存布局》中已经分析过了。一般而言,Point3d类的内存布局跟独立声明的类的内存布局没什么差别,除非在某些情况下,编译器为了内存对齐而进行填充,造成空间占用上会变大的情况,但对于存取效率而言没什么影响,因为在编译期间就已经确定好了它们的偏移值。完善上面的例子,在main函数中定义Point3d的对象,然后访问各个成员,看看对应的汇编代码。

int main() {
    printf("&Point2d::y = %d\n", &Point2d::y);
    printf("&Point3d::y = %d\n", &Point3d::y);
    Point3d p3d;
    p3d.x = p3d.y = p3d.z = 1;

    return 0;
}

上面两行打印代码输出的都是4,再看看第5行代码对应的汇编代码:

mov     dword ptr [rbp - 8], 1
mov     dword ptr [rbp - 12], 1
mov     dword ptr [rbp - 16], 1

生成的汇编代码跟独立类的汇编代码没有区别,这说明单一继承的存取效率跟没有继承关系的类的存取效率是一样的。

多重继承

或许业务需要,继承关系不是上面的单一继承关系,而是需要改成多重继承关系,多重继承下对象的存取效率是否会受影响?我们来看一个具体的例子:

#include <cstdio>

class Point {
public:
    int x;
};
class Point2d {
public:
    int y;
};
class Point3d: public Point, public Point2d {
public:
    int z;
};

int main() {
    printf("&Point2d::y = %d\n", &Point2d::y);
    printf("&Point3d::x = %d\n", &Point3d::x);
    printf("&Point3d::y = %d\n", &Point3d::y);
    printf("&Point3d::z = %d\n", &Point3d::z);
    Point3d p3d;
    p3d.x = p3d.y = p3d.z = 1;
    Point2d* p2d = &p3d;
    p2d->y = 2;

    return 0;
}

输出结果是:

&Point2d::y = 0
&Point3d::x = 0
&Point3d::y = 0
&Point3d::z = 8

第1、2行输出是0很正常,因为对于Point2d类来说只有一个成员y,也没有继承其他类,所以y的偏移值是0,第2行输出的是x的偏移值,它从Point类继承而来,排在最前面,所以偏移值也是0。但为什么第3行输出也是0?难道不应该是4吗?从第4行的输出看到z的偏移值是8,说明前面确实有两个成员在那里了。其实这里应该是编译器做了调整了,因为Point2d是第二基类,访问第二基类及之后的类时需要调整this指针,也就是将Point3d对象的起始地址调整为Point2d的起始地址,一般是将Point3d的地址加上前面子类的大小,如 &p3d+sizeof(Point) 。来看看上面代码生成的汇编代码:

main:                           # @main
    # 略...
    lea     rdi, [rip + .L.str]
    xor     eax, eax
    mov     esi, eax
    mov     al, 0
    call    printf@PLT
    lea     rdi, [rip + .L.str.1]
    xor     eax, eax
    mov     esi, eax
    mov     al, 0
    call    printf@PLT
    lea     rdi, [rip + .L.str.2]
    xor     eax, eax
    mov     esi, eax
    mov     al, 0
    call    printf@PLT
    lea     rdi, [rip + .L.str.3]
    mov     esi, 8
    mov     al, 0
    call    printf@PLT
    mov     dword ptr [rbp - 8], 1
    mov     dword ptr [rbp - 12], 1
    mov     dword ptr [rbp - 16], 1
    xor     eax, eax
    lea     rcx, [rbp - 16]
    cmp     rcx, 0
    mov     qword ptr [rbp - 32], rax       # 8-byte Spill
    je      .LBB0_2
    lea     rax, [rbp - 16]
    add     rax, 4
    mov     qword ptr [rbp - 32], rax       # 8-byte Spill
.LBB0_2:
    mov     rax, qword ptr [rbp - 32]       # 8-byte Reload
    mov     qword ptr [rbp - 24], rax
    mov     rax, qword ptr [rbp - 24]
    mov     dword ptr [rax], 2
    # 略...
    ret
# 略...

上面汇编代码中的第3到第7行对应的是上面C++代码的第一条printf打印语句(C++代码第17行),这里可以看到给printf函数传递了两个参数,分别通过rdi寄存器和esi寄存器,rdi寄存器保存的是第一个参数字符串,它的地址是 [rip + .L.str].L.str是字符串存储在数据段中的位置标签,rip+这个标签可以取得它的偏移地址,以下的 .L.str.1、.L.str.2.L.str.3都是字符串的位置标签),esi是第二个参数,这里的值被设为0了。

第8到12行汇编代码对应的是C++代码中的第二条printf打印语句,同样地,给rdi寄存器设置字符串的地址,给esi寄存器设置值为0。第13到第17行对应的是第三条printf打印语句,第18到第21行就是对应C++代码中的第四条printf打印语句,可以看到编译器在编译期间已经确定好了它们的偏移值为0, 0, 0, 8

第22到24行对应的C++的第22行代码,是对对象的成员进行赋值,可以看到通过对象来存取数据成员跟独立的类存取数据成员是一样的,已经知道了每个成员的内存地址了,所以存取的效率跟独立的类的存取效率没有差别。

汇编代码的第25行到37行对应C++的第23、24行代码,是将Point3d的地址转换成父类Point2d的指针类型,通过父类Point2d的指针来访问数据成员。前面提到过的将子类转换成第2及之后的基类时会进行this指针的调整,这里就是具体的实现。相当于伪代码:Point2d* p2d = &p3d+sizeof(Point),其实这里应该还需要判断下p3d是否为0,所以正确应该是:Point2d* p2d = &p3d ? &p3d+sizeof(Point) : 0。上面的第26到29行即是判断是否为0,如果为0则跳转到第33行,如果不为0则将p3d的地址 [rbp - 16] 加上44Point类的大小,然后存放在 [rbp - 32] ,再加载到rax寄存器中,然后对其赋值2(汇编代码第37行)。

通过分析汇编代码,多重继承的情况,如果是通过对象来存取数据成员,是跟独立类的存取效率是一致的,如果是通过第二及之后的基类的指针来存取,则需要调整this指针,可以看到对应的汇编代码也多了好好多行,所以效率上会有一些损失。

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。
深度解读《深度探索C++对象模型》之数据成员的存取效率分析(三)文章来源地址https://www.toymoban.com/news/detail-855442.html

到了这里,关于深度解读《深度探索C++对象模型》之数据成员的存取效率分析(三)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 深度解读《深度探索C++对象模型》之C++虚函数实现分析(一)

    接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。 假如有这样的一段代码,代码中定义了一个Object类,类中有一个成员函数print,通过以下的两种调用方式调用: 请问这

    2024年04月23日
    浏览(93)
  • 深度解读《深度探索C++对象模型》之C++虚函数实现分析(二)

    接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。 第一篇请从这里阅读: 深度解读《深度探索C++对象模型》之C++虚函数实现分析(一) 这一篇主要讲解多重继承情况下

    2024年04月24日
    浏览(37)
  • C++类和对象-C++对象模型和this指针->成员变量和成员函数分开存储、this指针概念、空指针访问成员函数、const修饰成员函数

    #includeiostream using namespace std; //成员变量 和 成员函数 分开储存的 class Person { public:     Person() {         mA = 0;     }     //非静态成员变量占对象空间     int mA;     //静态成员变量不占对象空间     static int mB;     //函数也不占对象空间,所有函数共享一个函数实例

    2024年02月20日
    浏览(47)
  • [C++]类与对象(下) -- 初始化列表 -- static成员 -- 友元 -- 内部类,一篇带你深度了解。

      目录 1、再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表 1.2.1 初始化列表的意义 1.3 explicit 2、static成员 2.1 问题引入 2.2 特性 3、友元 3.1 友元函数 3.2 友元类 4、内部类 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。 我们构造函

    2024年02月12日
    浏览(46)
  • C++奇迹之旅:探索类对象模型内存的存储猜想

    上回我们学习了类的定义,初步了解了什么是类?类的定义,以及类的三个访问限定符: public , private , protected ,本小节将讲解类的实例化,类对象模型的猜想存储,及三种简单类的计算。 在 C++ 中,类的实例化是指创建一个类的对象。当我们定义了一个类之后,就可以根据

    2024年04月12日
    浏览(39)
  • C++:常成员变量、常成员函数、常对象

    常成员变量: 1.用const修饰,可位于类型前后,若是成员变量类型为指针则只可位于类型后。 即:int  *const  p; 2.只能通过构造函数的初始化表对常成员变量进行初始化。 3.常成员所在类中的所有构造函数都必须对常成员变量初始化(通过初始化表)。 4.常成员变量可以被访

    2024年02月11日
    浏览(50)
  • 【C++】类和对象(中篇)----->六大默认成员函数

    目录 一、类的6个默认成员函数 二、构造函数  1、概念   2、特性 三、析构函数  1、概念  2、特性 四、拷贝构造函数  1、概念  2、特征 五、赋值运算符重载  1、运算符重载  2、值运算符重载    2.1 赋值运算符重载格式    2.2 赋值运算符只能重载成类的成员函数不能

    2024年02月12日
    浏览(43)
  • C++ ------类和对象详解六大默认成员函数

    如果我们定义一个类,然后这个类中什么也没有。那么这里的类就什么也没有吗?其实不然,任何类在里面什么都不写时,编译器都会生成6个默认成员函数。 用户没有显式实现,编译器会生成的成员函数称为默认成员函数。 六个默认成员函数 我们来看一个Date类 观察上述代

    2024年02月15日
    浏览(55)
  • 【C++系列P5】‘类与对象‘-三部曲——[对象&特殊成员](3/3)

     前言 大家好吖,欢迎来到 YY 滴 C++系列 ,热烈欢迎! 【 \\\'类与对象\\\'-三部曲】的大纲主要内容如下 : 如标题所示,本章是【 \\\'类与对象\\\'-三部曲】三章中的第三章节——对象成员章节,主要内容如下: 目录 一.const成员/成员函数 一.用const修饰this指针的好处——含权限知识点

    2024年02月06日
    浏览(44)
  • 【C++入门到精通】C++入门 —— 类和对象(初始化列表、Static成员、友元、内部类、匿名对象)

    目录 一、初始化列表 ⭕初始化列表概念 ⭕初始化列表的优点 ⭕使用场景 ⭕explicit 二、Static成员 ⭕Static成员概念 🔴静态数据成员: 🔴静态函数成员: ⭕使用静态成员的优点 ⭕使用静态成员的注意事项 三、友元 ⭕友元的概念 ⭕类友元 ⭕函数友元  四、内部类 ⭕

    2024年02月14日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包