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

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

时间:2024-04-21 11:44:35浏览次数:13  
标签:静态数据 int 成员 mov C++ rbp Base 深度 存取

接下来我将持续更新“深度解读《深度探索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

标签:静态数据,int,成员,mov,C++,rbp,Base,深度,存取
From: https://www.cnblogs.com/isharetech/p/18148736

相关文章

  • C++ STL -- HashTable
    HashTable一般常用的unordered_set、unordered_map都是基于哈希表实现的,哈希表主要需要注意的是哈希冲突,迭代器等基础哈希映射使用哈希函数将键映射到索引的数据结构。即将原始数组索引通过哈希函数映射到一个哈希值上,从而将一个大范围索引,减小到一个小的固定范围哈希冲突......
  • 【爆款推荐】初中中考阅读理解难题一网打尽!句子结构深度解析+答案揭秘,助你轻松冲刺高
    PDF格式公众号回复关键字:ZKYDT008原文1TheGiantStudentFarmers’Marketisthelargeststudent-runfarmers’marketinthecountry,isn’tit?解析1TheGiantStudentFarmers’Market主语,is是,thelargeststudent-runfarmers’market表语,inthe......
  • FreeLearning C/C++ 译文集翻译完成
    C++高级编程C++高级编程秘籍QtCreator应用开发C++游戏编程入门指南C++编程入门指南Boost.AsioC++网络编程BoostC++应用开发秘籍第二版C++数据结构与算法设计原理C++Qt5GUI编程C++高性能编程C++反应式编程C++系统编程秘籍C++研讨会C++现代嵌入式......
  • js,php,C++ 压缩算法不一致
    参考:https://yushuangqi.com/blog/2015/golang-php-gzencode-difrent.html压缩的数据:这是要压缩的数据aaaaaaaaaaaaaaaaaaa2222222222222222222222222222222顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶顶fffffffffffffffffffgggggggggggggggggggeeeeeeeeeeeeee对应的三种语言的最后数......
  • 数据结构与算法学习(1)——DFS(深度优先搜索)
    DFS基础深度优先搜索算法(英语:Depth-First-Search,缩写为DFS)是一种用于遍历或搜索树或图的算法。这个算法会尽可能深地搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发......
  • 深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)
    接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。在《深度解读《深度探索C++对象模型》之C++对象的内存布局》这篇文章中已经详细分析过C++的对象在经过封装后,在各种情况下的内存布局以......
  • C++ STL -- list
    listlist是一种基于双向链表的数据结构,适用于需要在序列中执行频繁插入和删除操作的场景特性本质上是一个双向链表,允许在序列的两端和中间执行插入和删除操作只能通过迭代器访问元素,即访问元素的时间复杂度为\(O(n)\)动态内存管理,内部通过类似指针的节点实现元素存储,一个节......
  • C++ int main(int argc, char *argv[])的参数
    一般来说intmain有两种写法 第一种就是不带参数的intmain(){return-1;} 第二种就是带有参数的intmain(intargc,char*argv[]){return-1;}这里argc是参数的个数,实际调用函数时不用手动传入,其是根据*argv参数列表内的个数进行统计实际传入的参数都存入*a......
  • 【爆款推荐】初中中考阅读理解难题一网打尽!句子结构深度解析+答案揭秘,助你轻松冲刺高
    PDF格式公众号回复关键字:ZKYDT007原文1WhereareMarkTwain’squotationsalwaysused?解析1Where哪里,are,MarkTwain’squotationsMarkTwain’s语录,alwaysused?总是被使用,被引用马克·吐温的名言总是被用在哪些地方?2MarkTwainisoneofthemostquote......
  • GDExtension的C++示例
    GDExtension的C++示例本文按照官方文档,进行c++的GDExtension​插件开发,主要进行文档进行复刻,同时对文档中未涉及步骤进行补充什么是GDExtension除了GDScript​和C#​这两种脚本语言外,Godot​引擎可以执行其他编程语言编写的代码。目前有两种方式实现:C++模块与GDExtension简单......