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

打印 上一主题 下一主题

主题 923|帖子 923|积分 2769

接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,自动获得推文和全部的文章列表。
接下来的几篇将会解说非静态数据成员的存取分析,解说静态数据成员的环境请见上一篇:《深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)》
平凡数据成员的访问方式

接下来的几节讨论的都是非静态数据成员的环境,非静态数据成员都是存放在对象中的,类的定义中相同名称的数据成员在每个对象中都是相互独立存在的。访问非静态数据成员必须通过隐式的或者显示的类对象来访问,否则将没有权限访问。如通过显示的方式访问:
  1. class Object {
  2. public:
  3.         int x;
  4.         int y;
  5. };
  6. void foo(const Object& obj) {
  7.     int a = obj.x + obj.y;
  8. }
复制代码
或者通过隐式的方式访问:
  1. class Object {
  2. public:
  3.         void print();
  4. private:
  5.         int x;
  6.         int y;
  7. };
  8. void Object::print() {
  9.     printf("x=%d, y=%d\n", x, y);
  10. }
复制代码
print函数中可以直接访问数据成员xy,其实它是通过一个隐式的对象来访问的,这个隐式的对象编译器会把它插入到参数中,真实的函数声明会被编译器转换为下面的方式:
  1. void Object::print(Ojbect* const this) {
  2.     printf("x=%d, y=%d\n", this->x, this->y);
  3. }
复制代码
平凡数据成员在对象中的偏移值

《深度解读《深度探索C++对象模型》之C++对象的内存结构》一文中知道了对象的非静态成员的结构,由此也可以知道访问非静态数据成员是通过对象的首地址(基地址)加上非静态数据成员的偏移值得到的地址。C++标准规定,对象中的成员排列顺序必须按照类中声明的数据成员的顺序,声明在前面的将排在前面,但没有规定差别的访问权限层级(public, protected, private)哪个在前,哪个在后。这个由编译器的实现者本身决定,只要保证在同一层级中先声明的排在前面即可。如果在一个类中有声明了多个的层级,如出现多个public和多个private层级,是否将多个相同的层级归并在一起也并没有强制规定,在我的测试的编译器中,是不区分差别的层级的,是根据类中的声明顺序来排列,不管将它声明在哪个层,或者分布在差别的层级中,统统按照声明的顺序来排列。
数据成员的偏移值可以通过静态的分析方法来得到,也可以通过动态的方法来获取,如下面的步伐中,我们将每个非静态数据成员的偏移值打印出来:
  1. #include <cstdio>
  2. class Base {
  3. public:
  4.     void print() {
  5.         printf("&Base::a1 = %d\n", &Base::a1);
  6.         printf("&Base::b1 = %d\n", &Base::b1);
  7.         printf("&Base::c1 = %d\n", &Base::c1);
  8.         printf("&Base::a2 = %d\n", &Base::a2);
  9.         printf("&Base::b2 = %d\n", &Base::b2);
  10.         printf("&Base::c2 = %d\n", &Base::c2);
  11.     }
  12. public:
  13.     int a1;
  14.     static int s1;
  15. protected:
  16.     int b1;
  17.     static int s2;
  18. private:
  19.     int c1;
  20.     static int s3;
  21. private:
  22.     char a2;
  23.     static int s4;
  24. protected:
  25.     char b2;
  26.     static int s5;
  27. public:
  28.     char c2;
  29.     static int s6;
  30. };
  31. int main() {
  32.     Base b;
  33.     b.print();
  34.     return 0;
  35. }
复制代码
步伐输出结果:
  1. &Base::a1 = 0
  2. &Base::b1 = 4
  3. &Base::c1 = 8
  4. &Base::a2 = 12
  5. &Base::b2 = 13
  6. &Base::c2 = 14
复制代码
从中可以看出:

  • 静态数据成员不影响非静态数据成员的偏移值,由于他们不存储在对象中,它们也没有偏移值,获取到的只有具体的内存地址值。
  • 类中的非静态数据成员的排列是按照它们的声明顺序来的,跟声明在哪个层级没有关系,相同的层级中的成员也不会集并在一起。
通过 &Base::a1这种方式得到的是成员在对象中的偏移值,而通过 &b.a1这种方式得到的将是它的具体的内存地址值,这个内存地址也可以通过偏移值得到,即对象b的地址 &b+&Base::a1
存取平凡数据成员在编译器中的实现

独立的类即是不继续其它任何类的类,现在来分析一下独立类的非静态数据成员存取方法及效率,通过对象来存取数据成员和通过指针来存取数据成员有没有用率上的差别?从上面的分析我们已经知道,非静态数据成员在类中的声明顺序决定了它在类中的偏移值,通过偏移值可以盘算出它的内存地址,所以对象的非静态数据成员在编译期间就可以获得它的内存地址,如许就相当于跟访问一个平凡的局部变量一样,不需要通过在运行期间接地去盘算它的内存地址,从而导致运行时的效率丧失。那如果是通过指针来访问又怎样呢?下面通过一个例子,天生对应的汇编代码来分析一下,假设有一个表示三维坐标的类,类中包含有三个坐标值x,y,z
  1. class Point {
  2. public:
  3.     int x;
  4.     int y;
  5.     int z;
  6. };
  7. void bar(Point* pp) {
  8.     pp->x = 4;
  9.     pp->y = 5;
  10.     pp->z = 6;
  11. }
  12. int main() {
  13.     Point p;
  14.     p.x = 1;
  15.     p.y = 2;
  16.     p.z = 3;
  17.     bar(&p);
  18.     return 0;
  19. }
复制代码
天生对应的汇编代码:
  1. bar(Point*):                   # @bar(Point*)
  2.     push    rbp
  3.     mov     rbp, rsp
  4.     mov     qword ptr [rbp - 8], rdi
  5.     mov     rax, qword ptr [rbp - 8]
  6.     mov     dword ptr [rax], 4
  7.     mov     rax, qword ptr [rbp - 8]
  8.     mov     dword ptr [rax + 4], 5
  9.     mov     rax, qword ptr [rbp - 8]
  10.     mov     dword ptr [rax + 8], 6
  11.     pop     rbp
  12.     ret
  13. main:                          # @main
  14.     push    rbp
  15.     mov     rbp, rsp
  16.     sub     rsp, 16
  17.     mov     dword ptr [rbp - 4], 0
  18.     mov     dword ptr [rbp - 16], 1
  19.     mov     dword ptr [rbp - 12], 2
  20.     mov     dword ptr [rbp - 8], 3
  21.     lea     rdi, [rbp - 16]
  22.     call    bar(Point*)
  23.     xor     eax, eax
  24.     add     rsp, 16
  25.     pop     rbp
  26.     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爱分享并关注,以便在内容更新时直接向您推送。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

莫张周刘王

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表