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

深度,解读,探索,c++,对象,模型,数据,成员,存取,效率,分析 · 浏览次数 : 62

小编点评

**深度解读《深度探索C++对象模型》之数据成员的存取分析** **非静态数据成员的访问方式:** * 通过隐式访问:通过类对象访问非静态数据成员,例如:class Object {public:\tvoid print();private:\tint x;\tint y;};void Object::print() { printf(\"x=%d, y=%d\\", x, y);} * 通过显示访问:通过类对象访问非静态数据成员,例如:class Object {public:\tvoid print();private:\tint x;\tint y;};void Object::print() { printf("x: %d, y: %d\n", x, y); } **效率对比:** * 通过对象访问: ```c++ pp->x = 4; pp->y = 5; pp->z = 6; ``` * 通过指针访问: ```c++ printf("x: %d, y: %d, z: %d\n", *(int*)(rdi + 4), *(int*)(rdi + 8), *(int*)(rdi + 12)); ``` **结论:** 通过对象访问非静态数据成员比通过指针访问效率更高。这是因为通过对象访问不需要计算成员地址,直接通过指针访问则需要计算成员地址。

正文

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

接下来的几篇将会讲解非静态数据成员的存取分析,讲解静态数据成员的情况请见上一篇:《深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)》

普通数据成员的访问方式

接下来的几节讨论的都是非静态数据成员的情况,非静态数据成员都是存放在对象中的,类的定义中相同名称的数据成员在每个对象中都是相互独立存在的。访问非静态数据成员必须通过隐式的或者显示的类对象来访问,否则将没有权限访问。如通过显示的方式访问:

class Object {
public:
	int x;
	int y;
};
void foo(const Object& obj) {
    int a = obj.x + obj.y;
}

或者通过隐式的方式访问:

class Object {
public:
	void print();
private:
	int x;
	int y;
};
void Object::print() {
    printf("x=%d, y=%d\n", x, y);
}

print函数中可以直接访问数据成员xy,其实它是通过一个隐式的对象来访问的,这个隐式的对象编译器会把它插入到参数中,真实的函数声明会被编译器转换为下面的方式:

void Object::print(Ojbect* const this) {
    printf("x=%d, y=%d\n", this->x, this->y);
}

普通数据成员在对象中的偏移值

《深度解读《深度探索C++对象模型》之C++对象的内存布局》一文中知道了对象的非静态成员的布局,由此也可以知道访问非静态数据成员是通过对象的首地址(基地址)加上非静态数据成员的偏移值得到的地址。C++标准规定,对象中的成员排列顺序必须按照类中声明的数据成员的顺序,声明在前面的将排在前面,但没有规定不同的访问权限层级(public, protected, private)哪个在前,哪个在后。这个由编译器的实现者自己决定,只要保证在同一层级中先声明的排在前面即可。如果在一个类中有声明了多个的层级,如出现多个public和多个private层级,是否将多个相同的层级合并在一起也并没有强制规定,在我的测试的编译器中,是不区分不同的层级的,是根据类中的声明顺序来排列,不管将它声明在哪个层,或者分布在不同的层级中,统统按照声明的顺序来排列。

数据成员的偏移值可以通过静态的分析方法来得到,也可以通过动态的方法来获取,如下面的程序中,我们将每个非静态数据成员的偏移值打印出来:

#include <cstdio>

class Base {
public:
    void print() {
        printf("&Base::a1 = %d\n", &Base::a1);
        printf("&Base::b1 = %d\n", &Base::b1);
        printf("&Base::c1 = %d\n", &Base::c1);
        printf("&Base::a2 = %d\n", &Base::a2);
        printf("&Base::b2 = %d\n", &Base::b2);
        printf("&Base::c2 = %d\n", &Base::c2);
    }
public:
    int a1;
    static int s1;
protected:
    int b1;
    static int s2;
private:
    int c1;
    static int s3;
private:
    char a2;
    static int s4;
protected:
    char b2;
    static int s5;
public:
    char c2;
    static int s6;
};

int main() {
    Base b;
    b.print();
    return 0;
}

程序输出结果:

&Base::a1 = 0
&Base::b1 = 4
&Base::c1 = 8
&Base::a2 = 12
&Base::b2 = 13
&Base::c2 = 14

从中可以看出:

  • 静态数据成员不影响非静态数据成员的偏移值,因为他们不存储在对象中,它们也没有偏移值,获取到的只有具体的内存地址值。
  • 类中的非静态数据成员的排列是按照它们的声明顺序来的,跟声明在哪个层级没有关系,相同的层级中的成员也不会合并在一起。

通过 &Base::a1这种方式得到的是成员在对象中的偏移值,而通过 &b.a1这种方式得到的将是它的具体的内存地址值,这个内存地址也可以通过偏移值得到,即对象b的地址 &b+&Base::a1

存取普通数据成员在编译器中的实现

独立的类即是不继承其它任何类的类,现在来分析一下独立类的非静态数据成员存取方法及效率,通过对象来存取数据成员和通过指针来存取数据成员有没有效率上的差别?从上面的分析我们已经知道,非静态数据成员在类中的声明顺序决定了它在类中的偏移值,通过偏移值可以计算出它的内存地址,所以对象的非静态数据成员在编译期间就可以获得它的内存地址,这样就相当于跟访问一个普通的局部变量一样,不需要通过在运行期间接地去计算它的内存地址,从而导致运行时的效率损失。那如果是通过指针来访问又如何呢?下面通过一个例子,生成对应的汇编代码来分析一下,假设有一个表示三维坐标的类,类中包含有三个坐标值x,y,z

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

void bar(Point* pp) {
    pp->x = 4;
    pp->y = 5;
    pp->z = 6;
}
int main() {
    Point p;
    p.x = 1;
    p.y = 2;
    p.z = 3;
    bar(&p);

    return 0;
}

生成对应的汇编代码:

bar(Point*):                   # @bar(Point*)
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     rax, qword ptr [rbp - 8]
    mov     dword ptr [rax], 4
    mov     rax, qword ptr [rbp - 8]
    mov     dword ptr [rax + 4], 5
    mov     rax, qword ptr [rbp - 8]
    mov     dword ptr [rax + 8], 6
    pop     rbp
    ret
main:                          # @main
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     dword ptr [rbp - 4], 0
    mov     dword ptr [rbp - 16], 1
    mov     dword ptr [rbp - 12], 2
    mov     dword ptr [rbp - 8], 3
    lea     rdi, [rbp - 16]
    call    bar(Point*)
    xor     eax, eax
    add     rsp, 16
    pop     rbp
    ret

从汇编代码中可以看到,在main函数中,汇编代码的第18到第20行就是对应上面C++代码的第15到第17行, [rbp - 16] 存放的是局部变量Point p的地址,也是成员x的地址,因为成员x是排在最前面,偏移值为0,也就是跟对象p的地址是一样的。成员y的偏移值是4,所以基地址加上4即 [rbp - 12] ,以此类推,成员z的地址是 [rbp - 8] ,可见成员变量的地址在编译期间就已确定了的。然后在第21行代码将对象p的地址存放在rdi寄存器中,将它作为调用bar函数的参数,传递给bar函数,第22行即调用bar函数。

然后看下通过指针的方式来访问数据成员是怎样的?在bar函数的汇编代码中,将传递过来的参数rdi寄存器(存放着对象p的地址)的值先存放在栈空间中的 [rbp - 8] 位置,然后再加载到rax寄存器(第4、5行),之后的第6到第10行是分别给数据成员赋值,可以看到通过指针存取数据成员也是通过偏移值来算出成员的具体地址的,地址在编译期间就已确定,所以跟通过对象来存取是一样的,所以两者的效率是一样的,不存在差别。

如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,以便在内容更新时直接向您推送。
image

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

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

C++对象在经过类的封装后,存取对象中的数据成员的效率是否相比C语言的结构体访问效率要低下?本篇将从C++类的不同定义形式来一一分析C++对象的数据成员的访问在编译器中是如何实现的,以及它们的存取效率如何?

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

本系列深入分析编译器对于C++虚函数的底层实现,最后分析C++在多态的情况下的性能是否有受影响,多态究竟有多大的性能损失。

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

本系列深入分析编译器对于C++虚函数的底层实现,最后分析C++在多态的情况下的性能是否有受影响,多态究竟有多大的性能损失。

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

本系列深入分析编译器对于C++虚函数的底层实现,最后分析C++在多态的情况下的性能是否有受影响,多态究竟有多大的性能损失。

深度解读《深度探索C++对象模型》之默认构造函数

C++的默认构造函数的作用是什么?什么时候会需要一个默认构造函数,以及默认构造函数从哪里来?这篇文章将从编译器的角度来分析这个问题。

Python生成器深度解析:构建强大的数据处理管道

# 前言 生成器是Python的一种核心特性,允许我们在请求新元素时再生成这些元素,而不是在开始时就生成所有元素。它在处理大规模数据集、实现节省内存的算法和构建复杂的迭代器模式等多种情况下都有着广泛的应用。在本篇文章中,我们将从理论和实践两方面来探索Python生成器的深度用法。 ## 生成器的定义

Django ORM:最全面的数据库处理指南

**深度探讨Django ORM的概念、基础使用、进阶操作以及详细解析在实际使用中如何处理数据库操作。同时,我们还讨论了模型深入理解,如何进行CRUD操作,并且深化理解到数据库迁移等高级主题。为了全面解读Django ORM,我们也讨论了其存在的不足,并对其未来发展进行了展望。这篇文章旨在帮助读者全

编织人工智能:机器学习发展历史与关键技术全解析

>本文全面回顾了机器学习的发展历史,从早期的基本算法到当代的深度学习模型,再到未来的可解释AI和伦理考虑。文章深入探讨了各个时期的关键技术和理念,揭示了机器学习在不同领域的广泛应用和潜力。最后,总结部分强调了机器学习作为一种思维方式和解决问题的工具,呼吁所有参与者共同探索更智能、更可持续的未来,同时

OpenCV实战:从图像处理到深度学习的全面指南

> 本文深入浅出地探讨了OpenCV库在图像处理和深度学习中的应用。从基本概念和操作,到复杂的图像变换和深度学习模型的使用,文章以详尽的代码和解释,带领大家步入OpenCV的实战世界。 # 1. OpenCV简介 ## 什么是OpenCV? ![file](https://img2023.cnblo

深入解析React DnD拖拽原理,轻松掌握拖放技巧!

>我们是[袋鼠云数栈 UED 团队](http://ued.dtstack.cn/),致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。 >本文作者:霁明 # 一、背景 ## 1、业务背景 业务中会有一些需要实现拖拽的场景,尤其是偏视觉方向以及移动端