深度探索C++对象模型(笔记)

第二章 对象

第二章第一节 类对象所占用的空间
  • 成员函数不占用类对象的内存空间
  • 一个类对象至少占用1个字节的内存空间
  • 成员变量是占用对象的内存空间
  • 成员函数 每个类只诞生 一个(跟着类走),而不管你用这个类产生了多少个该类的对象;
第二章第二节 对象结构的发展和演化
  • 非静态的成员变量(普通成员变量)跟着类对象走(存在对象内部),也就是每个类对象都有自己的成员变量;
  • 静态成员变量跟对象没有什么关系,所以肯定不会保存在对象内部,是保存在对象外面(表示所占用的内存空间和类对象无关)的。
  • 成员函数:不管静态的还是非静态,全部都保存在类对象之外。所以不管几个成员函数,不管是否是静态的成员函数,对象的sizeof的大小都是不增加的;
  • 虚函数:不管几个虚函数,sizeof()都是多了4个字节
    • 类里只要有一个虚函数(或者说至少有一个虚函数),这个类会产生一个指向虚函数表的指针
    • 类本身 指向虚函数的指针(一个或者一堆)要有地方存放,存放在一个表格里,这个表格我们就称为“虚函数表(virtual table【vtbl】)”;这个虚函数表一般是保存在可执行文件中的,在程序执行的时候载入到内存中来。虚函数表是基于类的,跟着类走的
    • 因为有了虚函数的存在,导致系统往类对象中添加了一个指针,这个指针正好指向这个虚函数表,很多资料上把这个指针叫vptr;这个vptr的值由系统在适当的时机(比如构造函数中通过增加额外的代码来给值);
  • 总结
    • 静态数据成员不计算在类对象sizeof()内;
    • 普通成员函数和静态成员函数不计算在类对象的sizeof()内
    • 虚函数不计算在类对象的sizeof()内,但是虚函数会让类对象的sizeof()增加4个字节以容纳虚函数表指针
    • 虚函数表[vtbl]是基于类的(跟着类走的,跟对象没关系,不是基于对象的)
    • 如果有多个数据成员,那么为了提高访问速度,某些编译器可能会将数据成员之间的内存占用比例进行调整。(内存字节对齐)
    • 不管什么类型指针char *p,int *q;,该指针占用的内存大小是固定的
    • 第一个基类子对象的开始地址和派生类对象的开始地址相同
    • 后续这些基类子对象的开始地址 和派生类对象的开始地址相差多少呢?就需要从开始的那些基类子对象所占用的内存空间进行相应的偏移大小
    • 你调用哪个子类的成员函数,这个this指针就会被编译器自动调整到对象内存布局中 对应该子类对象的起始地址那去
第二章第三节 this指针调整

在这里插入图片描述

  • 派生类对象 它是包含 基类子对象的。
  • 如果派生类只从一个基类继承的话,那么这个派生类对象的地址和基类子对象的地址相同
第二章第四节分析obj目标文件,构造函数语义
  • 默认构造函数(缺省构造函数):没有参数的构造函数;
  • 该类MBTX没有任何构造函数,但包含一个类类型的成员ma,而该对象ma所属于的类MATX 有一个缺省的构造函数。换句话说:编译器合成了默认的MBTX构造函数,并且在其中 安插代码,调用MATX的缺省构造函数。
第二章第五节 构造函数语义续
  • 父类带缺省构造函数,子类没有任何构造函数,那因为父类这个缺省的构造函数要被调用,所以编译器会为这个子类合成出一个默认构造函数。合成的目的是为了调用这个父类的构造函数。换句话说,编译器合成了默认的构造函数,并在其中安插代码,调用其父类的缺省构造函数。
  • 如果一个类含有虚函数,但没有任何构造函数时
    • 因为虚函数的存在编译器会给我们生成一个基于该类的虚函数表vftable。
    • 编译给我们合成了一个构造函数,并且在其中安插代码: 把类的虚函数表地址赋给类对象的虚函数表指针 (赋值语句/代码);
    • 我们可以把 虚函数表指针 看成是我们表面上看不见的一个类的成员变量
  • 编译器给我们往MBTX缺省构造函数中增加了代码:
    • 生成了类MBTX的虚函数表
    • 调用了父类的构造函数
    • 因为虚函数的存在,把类的虚函数表地址赋给对象的虚函数表指针。
  • 当我们有自己的默认构造函数时,编译器会根据需要扩充我们自己写的构造函数代码,比如调用父类构造函数,给对象的虚函数表指针赋值。编译器干了很多事,没默认构造函数时必要情况下帮助我们合成默认构造函数,如果我们有默认构造函数,编译器会根据需要扩充默认构造函数里边的代码。
  • 如果一个类带有虚基类,编译器也会为它合成一个默认构造函数
第二章第六节 拷贝构造函数语义
  • 成员变量初始化手法,比如int这种简单类型,直接就按值就拷贝过去,编译器不需要合成拷贝构造函数的情况下就帮助我们把这个事情办了,比如类A中有类类型ASon成员变量asubobj,也会递归的去拷贝类ASon的每个成员变量

  • 那编译器在什么情况下会帮助我们合成出拷贝构造函数来呢?那这个编译器合成出来的拷贝构造函数又要干什么事情呢?

    • 如果一个类A没有拷贝构造函数,但是含有一个类类型CTB的成员变量m_ctb。该类型CTB含有拷贝构造函数,那么当代码中有涉及到类A的拷贝构造时,编译器就会为类A合成一个拷贝构造函数。
    • 如果一个类CTBSon没有拷贝构造函数,但是它有一个父类CTB,父类有拷贝构造函数当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,调用父类的拷贝构造函数
    • 如果一个类CTBSon没有拷贝构造函数,但是该类声明了或者继承了虚函数,当代码中有涉及到类CTBSon的拷贝构造时,编译器会为CTBSon合成一个拷贝构造函数 ,往这个拷贝构造函数里插入语句:这个语句的含义 是设定类对象myctbson2的虚函数表指针值。虚函数表指针,虚函数表等概念。
    • 如果一个类没有拷贝构造函数,但是该类含有虚基类,当代码中有涉及到类的拷贝构造时,编译器会为该类合成一个拷贝构造函数
      C cc; C cc2 = cc;//当代码中有涉及到类的拷贝构造时
第二章第九节 拷贝构造函数,深浅拷贝
  • 当需要处理很复杂的成员变量类型的时候。因为我们增加了自己的拷贝构造函数,导致编译器本身的bitwise拷贝能力失效,所以结论:如果你增加了自己的拷贝构造函数后,就要对各个成员变量的值的初始化负责了。(在拷贝构造函数里自己申请内存,深拷贝)
第二章第十节 成员初始化列表说
  • 何时必须用成员初始化列表
    • 如果这个成员是个引用
    • 如果是个const类型成员
    • 如果你这个类是继承一个基类,并且基类中有构造函数,这个构造函数里边还有参数。
    • 如果你的成员变量类型是某个类类型,而这个类的构造函数带参数时;
  • 使用初始化列表的优势(提高效率)
    • 对于类类型成员变量xobj放到初始化列表中能够比较明显的看到效率的提升
  • 初始化列表中的代码可以看作是被编译器安插到构造函数体中的,只是这些代码有些特殊
    • 这些代码 是在任何用户自己的构造函数体代码之前被执行的。所以大家要区分开构造函数中的用户代码 和 编译器插入的初始化所属的代码
    • 这些列表中变量的初始化顺序是定义顺序,而不是在初始化列表中的顺序

第三章 虚函数

第三章第一节虚函数表指针位置分析
  • 类:有虚函数,这个类会产生一个虚函数表。
  • 类对象,有一个指针,指针(vptr)会指向这个虚函数表的开始地址。
  • 虚函数表指针位于对象内存的开头
第三章第三节 虚函数表分析

在这里插入图片描述

  • 一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的地址(虚函数表首地址)相同
  • 父类中有虚函数就等于子类中有虚函数。
  • 但不管是父类还是子类,(单继承时)都只会有一个虚函数表,不能认为子类中有一个虚函数表+父类中有一个虚函数表
  • 如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同.但仅仅是内容相同,这两个虚函数表在内存中处于不同位置,换句话来说,这是内容相同的两张表。
  • 虚函数表中每一项,保存着一个虚函数的首地址,但如果子类的虚函数表某项和父类的虚函数表某项代表同一个函数(这表示子类没有覆盖父类的虚函数),则该表项所执行的该函数的地址应该相同。

第三章第四节 多重继承虚函数分析

  • 一个类,如果它的类有多个基类(基类有虚函数),则有多个虚函数表,分别对应多个基类。子类新的虚函数加在第一个基类的虚函数表里。
  • 一个对象,如果它的类有多个基类(基类有虚函数)则有多个虚函数表指针(注意是多个虚函数表指针,而不是两个虚函数表);
  • 在多继承中,对应各个基类的vptr按继承顺序依次放置在类的内存空间中,且子类与第一个基类共用一个vptr(第二个基类有自己的vptr)(这里表述不太严禁)
  • 在这里插入图片描述
第三章第五节 vptr、vtbl创建时机-01

在这里插入图片描述

  • 实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加 为vptr赋值的代码,这是在编译期间编译器为构造函数增加的
  • 程序运行的时候,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有 给对象的vptr(成员变量)赋值的语句,自然这个对象的vptr就被赋值了
  • 实际上,虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容,就是说编译后虚函数表已经确定了。(虚函数表的地址不再发生变化了)

第四章 数据语义学

typedef放在类的最开头。编译器是对成员函数的解析,是整个A类成员变量定义完毕后才开始的
当运行一个可执行文件时,操作系统就会把这个可执行文件加载到内存;此时进程有一个虚拟的地址空间(内存空间)
在这里插入图片描述

第四章 数据成员布局
  • 普通成员变量的存储顺序 是按照在类中的定义顺序从上到下来的;比较晚出现的成员变量在内存中有更高的地址;
    类定义中pubic,private,protected的数量,不影响类对象的sizeof

  • 静态成员变量,可以当做一个全局量,但是他只在类的空间内可见;引用时用 类名::静态成员变量名静态成员变量只有一个实体,保存在可执行文件的数据段的;

  • 边界调整,字节对齐

    • 某些因素会导致成员变量之间排列不连续,就是边界调整(字节对齐),调整的目的是提高效率,编译器自动调整;调整:往成员之间填补一些字节,使用类对象的sizoef字节数凑成 一个4的整数倍,8的整数倍
  • 成员变量偏移值,就是这个成员变量的地址,离对象首地址偏移多少

  • 非静态成员变量的存取(普通的成员变量),存放在类的对象中。存取通过类对象(类对象指针)

    • 对于普通成员的访问,编译器是把类对象的首地址加上成员变量的偏移值
  • 一个子类对象,所包含的内容,是他自己的成员,加上他父类的成员的总和;从偏移值看,父类成员先出现,然后才是孩子类成员。
    在这里插入图片描述

  • 单个类带虚函数的数据成员布局

    • 类中引入虚函数时,会有额外的成本付出
    • 编译的时候,编译器会产生虚函数表,参考三章五节
    • 对象中会产生 虚函数表指针vptr,用以指向虚函数表的
    • 增加或者扩展构造函数,增加给虚函数表指针vptr赋值的代码,让vptr指向虚函数表;
    • 析构函数中也被扩展增加了虚函数表指针vptr相关的赋值代码,感觉这个赋值代码似乎和构造函数中代码相同;
      ![单个类](https://img-blog.csdnimg.cn/20200508192445516.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FzbGFkZmE=,size_16,color_FFFFFF,t_70
      在这里插入图片描述
  • 多重继承且父类都带虚函数的数据成员布局

    • 在这里插入图片描述
    • 通过this指针打印,我们看到访问Base1成员不用跳 ,访问Base2成员要this指针要偏移(跳过)8字节;
    • this指针,加上偏移值 就的能够访问对应的成员变量,this指针+偏移值

- 虚继承
- 在这里插入图片描述
- 虚基类初探
- 两个概念:(1)虚基类表 vbtable(virtual base table).(2)虚基类表指针 vbptr(virtual base table pointer)
- virtual虚继承之后,A1,A2里就会被编译器插入一个虚基类表指针,这个指针,有点成员变量的感觉
- A1,A2里因为有了虚基类表指针,因此占用了4个字节
- 虚基类表内容之5-8字节内容分析
- 虚基类表 一般是8字节,四个字节为一个单位。每多一个虚基类,虚基类表会多加4个字节
- 再增加虚继承类时,虚基类表会再多四个字节,标识相应的虚基类成员变量距离虚基类指针的偏移值
- 虚基类成员与虚基类表指针之间的偏移量
- 编译器因为有虚基类,会给A1,A2类增加默认的构造函数,并且这个默认构造函数里,会被编译器增加进去代码,给vbptr虚基类表指针赋值。
- 虚基类表内容之1-4字节内容分析(实继承一个类,再虚继承一个类时 1-4字节有值)
- 在这里插入图片描述
- 实继承的成员变量之后才是虚基类表指针
- 多继承时,子类访问数据成员是用栈底指针。
- 虚基类表指针成员变量的首地址和本对象A1首地址之间的偏移量 也就是:虚基类表指针 的首地址 - A1对象的首地址(一般是负的)
- 此时是用栈底指针 + 虚函数表中的偏移 - 一个值(只有一个实继承一个虚继承时才用栈底指针算)
- 这个值是虚函数表指针与栈底差值(最后一个变量内存后的八个字节是栈底指针
- x + 8; x = 虚函数表指针到类对象所占据最后一块内存的距离
- 就是此时虚基类对象的地址 ebp+ecx(虚函数表中的偏移值)-14h
- 结论:只有对虚基类成员进行处理比如赋值的时候,才会用到虚基类表,取其中的偏移,参与地址的计算;
- 三层结构时虚基类表分析
- 在这里插入图片描述

第五章 函数语义学

第一节 普通成员函数调用方式
  • 编译器内部实际上是将对成员函数myfunc()的调用转换成了对 全局函数的调用;
  • 成员函数有独立的内存地址,是跟着类走的,并且成员函数的地址 是在编译的时候就确定好的
  • 编译器额外增加了一个叫this的形参,是个指针,指向的其实就是生成的对象
  • 常规成员变量的存取,都通过this形参来进行
第二节 虚成员函数、静态成员函数调用方式
  • 虚成员函数(虚函数)调用方式
    • 要通过虚函数表指针查找虚函数表,通过虚函数表在好到虚函数的入口地址,完成对虚函数的调用
  • 静态成员函数调用方式
    • 静态成员函数没有this指针,这点最重要
    • 无法直接存取类中普通的非静态成员变量;
    • 静态成员函数不能在屁股后使用const,也不能设置为virtual
    • 可以用类对象调用,但不非一定要用类对象调用。
    • 静态成员函数等同于非成员函数,有的需要提供回调函数的这种场合,可以将静态成员函数作为回调函数;
第五节单继承虚函数

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200508194006427.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FzbGFkZmE=,size_16,color_FFFFFF,t_70

第六节 多继承函数深释,第二基类,虚析构必加

在这里插入图片描述
在这里插入图片描述

  • 如何成功删除用第二基类指针new出来的继承类对象
    • 我们要删除的实际是整个子类对象
    • 要能够保证Derive()对象的析构函数被正常调用
    • 编译器会调用Base2的析构函数,还是调用Derive的析构函数呢?
    • 执行delte pb2时,系统的动作会是?
      • 如果Base2里没有析构函数,编译器会直接删除以pb2开头的这段内存,一定报异常,因为这段内存压根就不是new起始的内存;
      • 如果Base2里有一个析构函数,但整个析构函数是个普通析构函数(非虚析构函数),那么当delte pb2,
        这个析构函数就会被系统调用,但是delete的仍旧是pb2开头这段内存,所以一定报异常
        。因为这段内存压根就不是new起始的内存;析构函数如果不是虚函数,编译器会实施静态绑定,静态绑定意味着你delete Base2指针时,删除的内存开始地址就是pb2的当前位置;所以肯定是错误的
      • 如果Base2里是一个虚析构函数,
      • 子类里就就算没有虚析构函数,因为Base2里 有虚析构函数,编译器也会为此给子类生成虚析构函数,为了调用基类中的虚析构函数
    • 凡是涉及到继承的,所有类都必须要写虚析构函数;
  • 类的第二个虚函数表中发现了thunk字样:
    • 一般用在多重继承中(从第二个虚函数表开始可能就 会有);用于this指针调整,调用Derive析构函数
第七节 多继承第二基类虚函数支持、虚继承带虚函数
  • 多重继承第二基类对虚函数支持的影响(this指针调整作用)
    • 子类继承了几个父类,子类就有几个虚函数表
    • this指针调整,调整的目的是干什么?
      • this指针调整的目的就是让对象指针正确的指向对象首地址,从而能正确的调用对象的成员函数或者说正确确定数据成员的存储位置。
    • 通过指向第二个基类的指针调用继承类的虚函数;
    • 一个指向派生类的指针,调用第二个基类中的虚函数
第八节 RTTI运行时类型识别回顾与存储位置介绍
  • c++运行时类型识别RTTI,要求父类中必须至少有一个虚函数;如果父类中没有虚函数,那么得到RTTI就不准确;
  • RTTI就可以在执行期间查询一个多态指针,或者多态引用的信息了;
  • RTTI能力靠typeid和dynamic_cast运算符来体现

在这里插入图片描述

第十节 指向成员函数的指针及vcall进一步谈
  • 指向成员函数的指针
    • 成员函数地址,编译时就确定好的。但是,调用成员函数,是需要通过对象来调用的;
    • 所有常规(非静态)成员函数,要想调用,都需要 一个对象来调用它;
  • 指向虚成员函数的指针及vcall进一步谈
    • vcall (vcall trunk) = virtual call:虚调用
    • 它代表一段要执行的代码的地址,这段代码去执行正确的虚函数
    • 或者我们直接把vcall看成虚函数表,如果这么看待的话,那么vcall{0}代表的就是虚函数表里的第一个函数
    • &A::myvirfunc:打印出来的是一个地址,这个地址中有一段代码,这个代码中记录的是该虚函数在虚函数表中的一个偏移值,有了这个偏移值,再有了具体的对象指针(this指针),我们就能够知道调用的是哪个虚函数表里边的哪个虚函数了;

第六章对象构造语义学

第六节 new,delete的进一步认识
		A *pa = new A(); //函数调用
		A *pa2 = new A;
  • new类对象时加不加括号的差别
    • 带括号的初始化会把一些和成员变量有关的内存清0,但不是整个对象的内存全部清0
  • new 干了两个事:一个是调用operator new(malloc),一个是调用了类A的构造函数
  • delete干了两个事:一个是调用了类A的析构函数,一个是调用operator delete(free)
第七节 new细节探秘,重载类内operator new、delete
  • 我们注意到,一块内存的回收,影响范围很广,远远不是10个字节,而是一大片
  • 分配内存这个事,绝不是简单的分配出去4个字节,而是在这4个字节周围,编译器做了很多处理,比如记录分配出的字节数等等
  • 分配内存时,为了记录和管理分配出去的内存,额外多分配了不少内存,造成了浪费;尤其是你频繁的申请小块内存时,造成的浪费更明显,更严重
  • 构造和析构函数被调用3次,但是operator new[]和operator delete[]仅仅被调用一次;(数组操作)
第八节 内存池概念
  • 内存池的概念和实现原理概述
    • malloc:内存浪费,频繁分配小块内存,则浪费更加显得明显
  • 内存池”,要解决什么问题?
    • 减少malloc的次数,减少malloc()调用次数就意味着减少对内存的浪费
    • 减少malloc的调用次数,是否能够提高程序运行效率? 会有一些速度和效率上的提升,但是提升不明显;
  • 内存池的实现原理
    • 用malloc申请一大块内存,当你要分配的时候,我从这一大块内存中一点一点的分配给你,当一大块内存分配的差不多的时候,我再用malloc再申请一大块内存,然后再一点一点的分配给你;
  • 减少内存浪费,提高运行效率;
第十节 重载全局new、delete,定位new
  • 定位new(placement new)
    • 有placement new,但是没有对应的placement delete
    • 功能:在已经分配的原始内存中初始化一个对象
      • 已经分配,定位new并不分配内存,你需要提前将这个定位new要使用的内存分配出来
      • 初始化一个对象(初始化一个对象的内存),我们就理解成调用这个对象的构造函数;定位new就是能够在一个预先分配好的内存地址中构造一个对象
    • 格式:new (地址) 类类型();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章