怀念夏天 发表于 2024-5-18 00:43:09

深度解读《深度探索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 , 1
mov   dword ptr , 1
mov   dword ptr , 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,
    xor   eax, eax
    mov   esi, eax
    mov   al, 0
    call    printf@PLT
    lea   rdi,
    xor   eax, eax
    mov   esi, eax
    mov   al, 0
    call    printf@PLT
    lea   rdi,
    xor   eax, eax
    mov   esi, eax
    mov   al, 0
    call    printf@PLT
    lea   rdi,
    mov   esi, 8
    mov   al, 0
    call    printf@PLT
    mov   dword ptr , 1
    mov   dword ptr , 1
    mov   dword ptr , 1
    xor   eax, eax
    lea   rcx,
    cmp   rcx, 0
    mov   qword ptr , rax       # 8-byte Spill
    je      .LBB0_2
    lea   rax,
    add   rax, 4
    mov   qword ptr , rax       # 8-byte Spill
.LBB0_2:
    mov   rax, qword ptr        # 8-byte Reload
    mov   qword ptr , rax
    mov   rax, qword ptr
    mov   dword ptr , 2
    # 略...
    ret
# 略...上面汇编代码中的第3到第7行对应的是上面C++代码的第一条printf打印语句(C++代码第17行),这里可以看到给printf函数传递了两个参数,分别通过rdi寄存器和esi寄存器,rdi寄存器保存的是第一个参数字符串,它的地址是 ( .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的地址 加上4,4是Point类的大小,然后存放在 ,再加载到rax寄存器中,然后对其赋值2(汇编代码第37行)。
通太过析汇编代码,多重继续的情况,假如是通过对象来存取数据成员,是跟独立类的存取服从是同等的,假如是通过第二及之后的基类的指针来存取,则需要调整this指针,可以看到对应的汇编代码也多了好好多行,所以服从上会有一些丧失。
假如您感爱好这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。
https://img2024.cnblogs.com/blog/3423566/202404/3423566-20240422160259297-337021502.jpg

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 深度解读《深度探索C++对象模型》之数据成员的存取服从分析(三)