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

这篇具有很好参考价值的文章主要介绍了深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

接下来我将持续更新“深度解读《深度探索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爱分享并关注,以便在内容更新时直接向您推送。文章来源地址https://www.toymoban.com/news/detail-855039.html

到了这里,关于深度解读《深度探索C++对象模型》之数据成员的存取效率分析(一)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++类和对象-C++对象模型和this指针->成员变量和成员函数分开存储、this指针概念、空指针访问成员函数、const修饰成员函数

    #includeiostream using namespace std; //成员变量 和 成员函数 分开储存的 class Person { public:     Person() {         mA = 0;     }     //非静态成员变量占对象空间     int mA;     //静态成员变量不占对象空间     static int mB;     //函数也不占对象空间,所有函数共享一个函数实例

    2024年02月20日
    浏览(43)
  • [C++]类与对象(下) -- 初始化列表 -- static成员 -- 友元 -- 内部类,一篇带你深度了解。

      目录 1、再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表 1.2.1 初始化列表的意义 1.3 explicit 2、static成员 2.1 问题引入 2.2 特性 3、友元 3.1 友元函数 3.2 友元类 4、内部类 在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。 我们构造函

    2024年02月12日
    浏览(43)
  • C++奇迹之旅:探索类对象模型内存的存储猜想

    上回我们学习了类的定义,初步了解了什么是类?类的定义,以及类的三个访问限定符: public , private , protected ,本小节将讲解类的实例化,类对象模型的猜想存储,及三种简单类的计算。 在 C++ 中,类的实例化是指创建一个类的对象。当我们定义了一个类之后,就可以根据

    2024年04月12日
    浏览(36)
  • C++:常成员变量、常成员函数、常对象

    常成员变量: 1.用const修饰,可位于类型前后,若是成员变量类型为指针则只可位于类型后。 即:int  *const  p; 2.只能通过构造函数的初始化表对常成员变量进行初始化。 3.常成员所在类中的所有构造函数都必须对常成员变量初始化(通过初始化表)。 4.常成员变量可以被访

    2024年02月11日
    浏览(47)
  • C++ ------类和对象详解六大默认成员函数

    如果我们定义一个类,然后这个类中什么也没有。那么这里的类就什么也没有吗?其实不然,任何类在里面什么都不写时,编译器都会生成6个默认成员函数。 用户没有显式实现,编译器会生成的成员函数称为默认成员函数。 六个默认成员函数 我们来看一个Date类 观察上述代

    2024年02月15日
    浏览(50)
  • 【C++】类和对象(中篇)----->六大默认成员函数

    目录 一、类的6个默认成员函数 二、构造函数  1、概念   2、特性 三、析构函数  1、概念  2、特性 四、拷贝构造函数  1、概念  2、特征 五、赋值运算符重载  1、运算符重载  2、值运算符重载    2.1 赋值运算符重载格式    2.2 赋值运算符只能重载成类的成员函数不能

    2024年02月12日
    浏览(40)
  • 【C++系列P5】‘类与对象‘-三部曲——[对象&特殊成员](3/3)

     前言 大家好吖,欢迎来到 YY 滴 C++系列 ,热烈欢迎! 【 \\\'类与对象\\\'-三部曲】的大纲主要内容如下 : 如标题所示,本章是【 \\\'类与对象\\\'-三部曲】三章中的第三章节——对象成员章节,主要内容如下: 目录 一.const成员/成员函数 一.用const修饰this指针的好处——含权限知识点

    2024年02月06日
    浏览(42)
  • 【C++入门到精通】C++入门 —— 类和对象(初始化列表、Static成员、友元、内部类、匿名对象)

    目录 一、初始化列表 ⭕初始化列表概念 ⭕初始化列表的优点 ⭕使用场景 ⭕explicit 二、Static成员 ⭕Static成员概念 🔴静态数据成员: 🔴静态函数成员: ⭕使用静态成员的优点 ⭕使用静态成员的注意事项 三、友元 ⭕友元的概念 ⭕类友元 ⭕函数友元  四、内部类 ⭕

    2024年02月14日
    浏览(44)
  • 【C++】类和对象(下篇)--->再识构造函数,static成员,友元,内部类,匿名对象

    目录 一、构造函数  1、构造函数体赋值  2、初始化列表  3、explicit 二、Static 成员  1、概念  2、特性 三、友元  1、友元函数  2、友元类 四、内部类  1、概念  2、特性 五、匿名对象 六、拷贝对象时的一些编译器优化 在创建对象时,编译器通过调用构造函数,给对

    2024年02月13日
    浏览(40)
  • 【C++类和对象】类有哪些默认成员函数呢?(下)

    ヾ(๑╹◡╹)ノ\\\" 人总要为过去的懒惰而付出代价 ヾ(๑╹◡╹)ノ\\\" 如果一个类中什么成员都没有,简称为空类。 空类中并不是什么都没有,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。 ​​​​​​​​ 默认成员函数:用户没有显式实现,编译器会生成

    2024年02月12日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包