《深度探索C++对象模型》读书笔记

`

C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括:虚函数机制和虚基类机制(即用以实现“多次出现在继承体系中的base class,有一个单一而被共享的尸体”)

C++对象模式:在C++中有两中成员数据:static和nonstatic以及三种成员函数:static、nonstatic和virtual

 

C++编译器的采用的对象模型:

Nonstatic成员对象属于每一个类的实例对象(一对一的关系),static类型的成员对象属于类(不属于对象),被该类的所有对象所共享。然后nonstatic和static 成员函数被放置在所有对象之外。虚函数则用下面两个步骤进行实现:
1、每个class产生出一堆指向虚函数的指针,放在一个表格中,这个表格称之为虚函数表

  1. 每一个类的实例对象被添加了一个指针,指向了该对象所属类的虚函数表。通常这个指针称之为虚函数指针。虚函数指针的设定和重置都由该类的构造函数、析构函数和拷贝赋值函数自动完成(后面会详细介绍)。每一个类所关联的对象类型信息(用来支持RTTI)也都经由虚函数表被指出来,通常是放虚函数表的第一个slot。

在虚拟继承(即菱形继承或叫钻石继承)的情况下,虚基类不管在继承串链中被派生多少次,永远只会存在一个实体。

C++中凡是处于同一个作用域的数据,必定保证以其声明顺序出现在内存布局中。但是被放置在多个作用域的各个数据,排列顺序就不一定了。

优先使用组合而非继承

多态只能通过指针或者引用来实现,即基类类型的指针或引用指向子类

多态只存在以一个公有继承体系中。

C++以下列方法支持多态:

  1. 经由一组隐含的转化操作:例如把一个子类指针转化为英指向父类类型的指针:

Base* ps = new Derived();

  1. 经由虚函数机制:ps->虚函数名();
  2. 经由dynamic_cast和typeid运算符:

if (Base* pc = dynamic_cast<Base*>(ps)  )......

 

多态的主要用途是经由一个共同的接口来影响类型的封装,这个接口通常被定义在一个抽象的基类中。

 

对象占用内存的大小探讨:

  1. 起nonstatic 成员数据的总和大小
  2. 加上内存对齐而填补上的空间(内存对齐可能会发生在空间计算大小的过程中,也可能会发生在集合体边界之间)
  3. 为了支持virtual而由内部产生的任何额外负担(虚函数指针)

 

指向不同类型值的各指针之间的差异:既不在其指针表示法的不同,也不在其内容(代表一个地址)不同,而是在其所寻址出来的对象类型不同。也就是说:“指针类型”会教导编译器如何解释某个特定地址中的内存内容及其大小

转型(cast)其实是一种编译指令。大部分情况下它并不会改变一个指针所含的真正地址,它只影响“被指出之内存的大小和其内容”的解释方式

 

一个指针或者引用之所以支持多态:是因为他们并不会引发内存中任何与“类型有关的内存委托操作”:会受到改变的知识他们所指向的内存的“大小和内容解释方式”而已

一个子类对象赋值给父类对象时,就会进行内存切割,将子类对象中含有的父类部分放进父类对象中,子类中多余的内存部分就会忽略不再有效。编译器会在编译期间解析一个“通过该对象而触发的虚函数调用操作”。正常的虚函数调用都是在运行时期完成。这就是为什么对象不会触发多态机制

 

全局变量的内存保证会在程序加载的时候被清零。局部变量配置于程序的对战中,堆对象配置于自由空间中,都不一定会被清零,它们的内容是内存上次被使用后留下的垃圾值。

在编译期间的两个扩张操作:

  1. 虚函数表(就是一个函数指针数组)在编译期间会被扩张出来,里面存放类的虚函数地址
  2. 在每一个类的对象中,一个额外的虚函数指针会被编译器合成出来,虚函数指针指向的是虚函数表的地址
  3. 子类重写父类虚函数时,会将虚函数表对应的内容改成子类自身的虚函数地址

 

全局对象的构造发生在main函数之前,全局对象的析构发生在main函数结束之前

没有初始值的全局变量/对象会被自动初始化为该类型的零值

什么时候初始化(以下内容摘自文章:https://www.cnblogs.com/catch/p/4314256.html,侵删

根据 C++ 标准,全局变量的初始化要在 main 函数执行前完成,常识无疑,但是这个说法有点含糊,main 函数执行前到底具体是什么时候呢?是编译时还是运行时?答案是既有编译时,也可能会有运行时(seriously), 从语言的层面来说,全局变量的初始化可以划分为以下两个阶段(c++11 N3690 3.6.2)

  1. static initialization: 静态初始化指的是用常量来对变量进行初始化,主要包括 zero initialization 和 const initialization,静态初始化在程序加载的过程中完成,对简单类型(内建类型,POD等)来说,从具体实现上看,zero initialization 的变量会被保存在 bss 段,const initialization 的变量则放在 data 段内,程序加载即可完成初始化,这和 c 语言里的全局变量初始化基本是一致的
  2. dynamic initialization:动态初始化主要是指需要经过函数调用才能完成的初始化,比如说:int a = foo(),或者是复杂类型(类)的初始化(需要调用构造函数)等。这些变量的初始化会在 main 函数执行前由运行时调用相应的代码从而得以进行(函数内的 static 变量除外)。

需要明确的是:静态初始化执行先于动态初始化! 只有当所有静态初始化执行完毕,动态初始化才会执行。显然,这样的设计是很直观的,能静态初始化的变量,它的初始值都是在编译时就能确定,因此可以直接 hard code 到生成的代码里,而动态初始化需要在运行时执行相应的动作才能进行,因此,静态初始化先于动态初始化是必然的。

初始化的顺序

对于出现在同一个编译单元内的全局变量来说,它们初始化的顺序与他们声明的顺序是一致的(销毁的顺序则反过来),而对于不同编译单元间的全局变量,c++ 标准并没有明确规定它们之间的初始化(销毁)顺序应该怎样,因此实现上完全由编译器自己决定,一个比较普遍的认识是:不同编译单元间的全局变量的初始化顺序是不固定的,哪怕对同一个编译器,同一份代码来说,任意两次编译的结果都有可能不一样[1]。

因此,一个很自然的问题就是,如果不同编译单元间的全局变量相互引用了怎么办?

当然,最好的解决方法是尽可能的避免这种情况(防治胜于治疗嘛),因为一般来说,如果出现了全局变量引用全局变量的窘况,那多半是程序本身的设计出了问题,此时最应该做的是回头重新思考和修改程序的结构与实现,而不是急着穷尽技巧来给错误的设计打补丁。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值