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

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

时间:2024-04-20 12:00:51浏览次数:25  
标签:静态数据 lea s1 深度 C++ Base rip printf 存取

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

《深度解读《深度探索C++对象模型》之C++对象的内存布局》这篇文章中已经详细分析过C++的对象在经过封装后,在各种情况下的内存布局以及增加的成本。本文将进一步分析C++对象在封装后,数据成员的存取的实现手段及访问的效率。在这里先抛出一个问题,然后带着问题来一步一步分析,如下面的代码:

class Point {};
Point p;
Point *pp = &p;
p.x = 0;
pp->x = 0;

上面的代码中,对数据成员x的存取成本是什么?通过对象p来存取成员x和通过对象的指针pp来存取成员x的效率存在差异吗?要搞清楚这个问题,得看具体的Point类的定义以及成员x的声明方式。Point类可能是一个独立的类(也就是没有从其他类继承而来),也可能是一个单一继承或者多重继承而来的类,甚至也有可能它的继承父类中有一个是虚拟基类(virtual base class),成员x的声明可能是静态的或者是非静态的。下面的几节将根据不同的情况来一一分析。

类对象的数据成员的存取效率分析系列篇幅比较长,所以根据不同的类的定义划分为几种情形来分析,这篇先来分析静态数据成员的情况。

静态数据成员在编译器里的实现

在前面的文章中说过,类中的静态数据成员是跟类相关的,而非跟具体的对象有关,它存储在对象之外,具体的存储位置是在程序中的数据段中。它其实跟一个全局变量没什么区别,在编译期间编译器就已经确定好了它的存储位置,所以能够确定它的地址。看一下下面的代码:

#include <cstdio>

int global_val = 1;

class Base {
public:
    int b1;
    static int s1;
};
int Base::s1 = 1;

int main() {
    static int static_var = 1;
    int local_var = 1;
    Base b;
    printf("&global_val = %p\n", &global_val);
    printf("&static_var = %p\n", &static_var);
    printf("&local_var = %p\n", &local_var);
    printf("&b.b1 = %p\n", &b.b1);
    printf("&b.s1 = %p\n", &b.s1);

    return 0;
}

程序输出的结果:

&global_val = 0x102d74000
&static_var = 0x102d74008
&local_var = 0x16d0933f8
&b.b1 = 0x16d0933f4
&b.s1 = 0x102d74004

可以看到全局变量global_val和局部静态变量static_var以及类中的静态数据成员s1的地址是顺序且紧密排列在一起的,而且跟其他的两个局部变量的地址相差较大,说明这几个都是一起存储在程序的数据段中的。类中的非静态数据成员b1跟局部变量local_var一样,是存放在栈中的。

可以进一步看看生成的汇编代码,看一下是怎么存取静态数据成员的,下面节选部分的汇编代码:

main:                            # @main
    # 略...
    lea     rdi, [rip + .L.str]
    lea     rsi, [rip + global_val]
    mov     al, 0
    call    printf@PLT
    lea     rdi, [rip + .L.str.1]
    lea     rsi, [rip + main::static_var]
    mov     al, 0
    call    printf@PLT
  	# 略...
    lea     rdi, [rip + .L.str.4]
    lea     rsi, [rip + Base::s1]
    mov     al, 0
    call    printf@PLT
    # 略...
    ret
global_val:
    .long   1        # 0x1

Base::s1:
    .long   1        # 0x1

main::static_var:
    .long   1        # 0x1

从汇编代码中看到,global_valBase::s1main::static_var是定义在数据段中的,在代码中直接使用它们的地址,如:

lea rsi, [rip + Base::s1]

则是将Base::s1的地址加载到rsi寄存器中,作为参数传递给printf函数。这也证明了它跟全局变量,普通的静态变量是没有区别的。结论就是,类中的静态数据成员的存取方式是直接通过一个具体的地址来访问的,跟全局变量毫无区别,所以效率上也跟访问一个全局变量一样。

通过不同方式存取静态数据成员的效率差异

访问类的静态数据成员可以通过类名来访问,如Base::s1,也可以通过对象来访问,如b.s1,甚至是通过指针来访问,如pb->s1。那么这几种访问方式有什么差别?或者说是否有效率上的损失?其实这几种访问方式本质上没有任何差别,编译器会转换成如Base::s1一样的方式,后面的两种方式只是语法上的方便而已,看一下汇编代码就一目了然。把上面的例子多余的代码删除掉,只留下Base类,然后main函数中增加几行打印,如下:

Base b;
Base *pb = &b;
printf("&Base::s1 = %p\n", &Base::s1);
printf("&b.s1 = %p\n", &b.s1);
printf("&pb->s1 = %p\n", &pb->s1);

输出的结果当然是同一个地址了,下面是节选的汇编代码:

lea     rdi, [rip + .L.str]
lea     rsi, [rip + Base::s1]
mov     al, 0
call    printf@PLT
lea     rdi, [rip + .L.str.1]
lea     rsi, [rip + Base::s1]
mov     al, 0
call    printf@PLT
lea     rdi, [rip + .L.str.2]
lea     rsi, [rip + Base::s1]
mov     al, 0
call    printf@PLT

可以看到C++中的几行不同的访问方式在汇编代码中都转换为同样的代码:

lea rsi, [rip + Base::s1]

继承而来的静态数据成员的存取分析

我们已经知道类中的静态数据成员是跟对象无关的,所有的对象都共享同一个静态数据成员。但是如果继承而来的静态数据成员又是怎样的呢?假如定义一个Derived类,它是Base类的派生类,那么静态数据成员s1的情况又是如何?其实无论继承多少次,静态数据成员都只有一份,无论是Derived类还是Base类,它们都共享同一个静态数据成员s1,可以通过下面的例子来验证一下:

#include <cstdio>

class Base {
public:
    int b1;
    static int s1;
};
int Base::s1 = 1;

class Derived: public Base {};

int main() {
    Derived d;
    printf("&d.s1 = %p\n", &d.s1);
    printf("d.s1 = %d\n", d.s1);
    d.s1 = 2;

    Base b;
    printf("&b.s1 = %p\n", &b.s1);
    printf("b.s1 = %d\n", b.s1);

    return 0;
}

程序输出的结果:

&d.s1 = 0x10028c000
d.s1 = 1
&b.s1 = 0x10028c000
b.s1 = 2

可以看到通过Derived类的对象dBase类的对象b访问到的都是同一个地址,通过对象d修改s1后,通过对象b可以看到修改后的值。

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

标签:静态数据,lea,s1,深度,C++,Base,rip,printf,存取
From: https://www.cnblogs.com/isharetech/p/18147543

相关文章

  • 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简单......
  • C++字符串常见混淆方案
    正文将字符串转换成等效int数组std::vector<uint32_t>convert_wstring_to_int_array(constwchar_t*str){std::vector<uint32_t>vec;for(size_ti=0;i<wcslen(str);i+=2){uint32_tval=(uint32_t)str[i]<<16&0xffff0000;i......
  • 为什么有很多出名开源的C/C++方面的高性能网络库,比如libevent,boost-asio,有些企业还要
    为什么有很多出名开源的C/C++方面的高性能网络库,比如libevent,boost-asio,有些企业还要自己写?    我个人很倾向用著名的开源软件来完成功能需求,但是发现在实际开展中很多人会反对开源,而要求自己实现一套,我不知道是我考虑太少,还是他们太武断。 因为KPI的原因更多......
  • 美国政府敦促开发者:停止使用 C、C++
    美国政府敦促开发者:停止使用C、C++开源Linux​ ​关注他 5人赞同了该文章整理|屠敏出品|CSDN(ID:CSDNnews)“C、C++不安全,新应用开发时就别用了,旧应用应该采取迁移行动”,近日,美国白宫国家网络主任办公室(ONCD)在一份主题为《回到基础构件:通......
  • 音视频开发是不是C++开发中最难的细分方向?
    音视频开发是不是C++开发中最难的细分方向?     关注者611被浏览599,438关注问题​写回答​邀请回答​好问题7​3条评论​分享​  查看全部67个回答luluce不关心国事的程序猿(不会QT)。已关注......
  • C++六种内存序详解
    前言要理解C++的六种内存序,我们首先须要明白一点,处理器读取一个数据时,可能从内存中读取,也可能从缓存中读取,还可能从寄存器读取。对于一个写操作,要考虑这个操作的结果传播到其他处理器的速度。并且,编译器的指令重排和CPU处理器的乱序执行也是我们需要考虑的因素。 我们先看......
  • [8] UE C++ Mario
    创建了盒子,定义了盒子的碰撞位置能在if里面直接声明赋值局部变量但不能赋值成员变量friend关键词应用导入类的时候如果是灰色就删掉,并且查看头文件 ......