《深度探索c++对象模型》读书总结

一,导读

1.目前所有编译器对于virtual function的实现都是使用各个class专属的virtual table,大小固定,并且在程序执行前就构造好了

2.explicit:显式的(明确出现于c++程序代码)

implicit:隐式的(隐藏于程序代码背后)

二,关于对象

问题1.如果要打印出一个3d坐标的x,y,z一共有哪些方法呢?

方法一:

先声明一个坐标struct,通过函数打印

方法二:

将函数封装成一个宏

方法三:

定义一个Point的类,调用封装的方法(其实和结构体一样)

方法四:

用多层继承,每一层包含一个Point属性

方法五:

将Point类改成一个模板类,其他成员变量用泛型接收

不仅可以将成员变量泛型(参数化)同时也可以将元素个数也参数化

总结:

这几种方法越往后越能通过模板将数据类型抽象出来,同时使用继承层次更清晰

用软件工程的眼光看:一个ADT(抽象数据类型)和class hierarchy(类的层次)的数据封装,比在c程序中程序性的使用全局数据好

封装后的布局成本:加上封装后(继承和模板),布局成本增加了多少?

答:并没有增加成本,三个数据成员直接内含在每一个类对象中,而成员函数虽然含在类声明中,却不出现在对象中(成员函数不占内存),c++的布局以及存取时间上主要的额外负担是virtual引起的,包括:

1.虚函数机制:用以支持一个有效率的“执行期绑定”

2.虚基类:用以实现“多次出现在继承体系中的base class”有一个单一而被共享的实例

一般而言,并没有什么天生的理由说c++程序一定比c庞大而迟缓

c++对象模式

简单对象模式:

整个对象的成员函数和成员变量都由其类型的指针指向,成员并不放在对象中,只有指向对象的指针才放在对象中,这么做可以避免成员有不同类型,因而需要不同的存储空间所招致的问题,一个类成员的大小很容易计算出来:指针大小乘以类中声明成员的个数

简单对象模型没有被应用到实际产品中

成员表对象模型:

这个模型也没有用到c++编译器上

c++对象模型:

类对象内部:非静态成员

类对象外部:静态成员变量,静态和非静态成员变量,

而虚函数,则由两个步骤支持:

1.每个类产生出一堆指向虚函数的指针,放在一个虚函数表中(vtbl),也即每个类都维护一张虚函数表

2.每一个类对象被安插一个指针,指向相关的虚函数表,该指针成为vptr,vptr的设定和重置都有每个类的构造函数,析构函数,拷贝赋值运算符自动完成

优点:

空间和存取时间效率

缺点:

如果程序代码本身未改变,但用到的类对象的非静态数据成员改变,代码就得重新编译

虚函数机制的好处:

当类型有所增加,修改删减时,程序代码无需改变

需要多少内存才能够表现一个类对象:

1,非静态成员变量的总和大小

2.加上任何由于对齐上去的空间

3.加上为了支持virtual而由内部产生的任何额外负担


一个String是2字节,包括1字节字符指针和一个用来表示字符串长度的整数

该类的内存包含非静态成员变量和一个vptr指向虚函数表的指针

子类的对象和指针布局:

如果Bear继承ZoomAinimal

导致的结果是:

如果继承关系:ZoomAnimal->Bear->Panda

对于如下的代码:

内存对应为:

对于String类可以展示封装的非多态形式,其速度快是因为所有的函数调用都在编译时期解析完成,对象构建起来不需要设置Virtual机制,空间紧凑是因为每个类对象不需要负担传统上为了支持virtual机制而需要的额外负荷

第二章:构造函数语义学

trivial:没有用的

1.调用对象成员,如下父类Foo,子类Bar有一个变量Foo foo,那么声明一个Bar bar时,会自动调用类中对象成员foo的默认构造函数

对于类的默认构造函数的调用时编译器负责的,但是对于类中成员变量的初始化是由程序员手动在构造函数中初始化的

下面这个需要知道和理解:

         

2.带有默认构造函数的基类

如果一个没有任何构造函数的子类派生自一个带有默认构造函数的基类,那么这个子类的默认构造函数会被当做nontrivial(无用的),因此需要被合成出来

3.类声明或继承一个虚函数(该情况也会合成出默认构造函数):

4.一个类派生自一个虚基类(该情况也会合成出默认构造函数)

这四种情况会造成“编译器必须为未声明构造函数的类合成一个默认构造函数”:

1.调用对象成员

2.基类的默认构造函数

3.为每一个对象初始化其虚函数机制

4.虚基类机制

如果不是这4种情况又没有声明任何构造函数的类,我们说它们拥有的是implicit trivial default constructors(隐式而无用的默认构造函数),它们实际上并不会被合成出来

c++新手一般有两个常见的误解:

1.任何类如果没有定义默认构造函数,会被合成出来一个

2.编译器合成出来的默认构造函数会显式设定类内每一个成员数据的默认值

这两点都不是对的

2.2 拷贝构造函数的构造操作

有三种情况会调用拷贝构造函数:

一个类对象可用两种方式复制得到:

1.通过拷贝构造函数

2.通过拷贝赋值操作

2.4成员的初始化列表

不好的做法:

如果不使用初始化列表来初始化数据

那么编译器会这样处理

可见,编译器会先调用String的构造函数,再产生一个临时的String对象并赋值,拷贝临时String到String成员变量,最后再析构临时对象

推荐的做法:

采用初始化列表来初始化对象成员

如上可见,编辑器不会产生一个临时对象,带来赋值并析构的消耗

注意:

1.初始化列表变量顺序必须和成员声明的顺序一致

2.先执行初始化列表,后执行下面花括号内的逻辑

第三章 Data语意学

如果类的关系如下:

打印结果

对齐(Alignment)限制:

由于类Y和Z大小为5字节(1字节类有1字节+一个指向虚基类的指针4字节)

由于对齐方式是4的倍数,所以还得加上3字节,最后Y和Z都是8字节大小

第一种算法(A的大小):

有对齐和一个空类占1字节的算法

x(1字节)+Y(4字节)+Z(4字节)+对齐(3字节)=12字节

第二种算法(A的大小):

子类不需要父类(空类)的那一个字节,也不需要对齐

12字节减去1个字节空类减去对齐方式=8字节

第一种算法为c++标准算法

第二种算法为目前c++实现标准

不管该类被产生出多少个实例(直接产生或间接产生)静态数据成员永远只存在一份实例,甚至即使该类没有任何对象实例,其静态数据成员也已存在

3.3成员数据的存取

对于非静态成员变量的存取

编译器会这样转化:

非静态数据成员的地址:

3.4 继承与数据成员

只有继承没有多态的写法

数据布局:

对于下图这种模型:

内存布局如下:

采用多态的写法

让2d类的方法为虚函数,此时不管是p1还是p2传入的是2d坐标类还是3d坐标类,都可以弹性支持

但是这样的弹性支持势必会对2d坐标类空间和存取时间上带来额外负担,

其负担体现在:

以下是3d坐标类新的声明:

                   

多重继承的内存分布

基类和子类的对象都是从相同的地址开始,其差异只是在于子类比较大,以多容纳自身的非静态成员变量

第二遍总结书中的点:

下面的内容摘自https://www.cnblogs.com/lengender-12/p/6970496.html

1.

2.

当我们定义一个object如下:

T object;

实际上会发生什么事情呢? 如果T有一个constructor(不论是由user提供或是由编译器合成),它会被调用。这很明显,比较不明显的是,constructor的调用真正伴随了什么?

Constructor可能内带大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class T的继承体系而定。一般而言编译器所做的扩充操作大约如下:

  1. 记录在member initialization list中的data members初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序。
  2. 如果有一个member并没有出现在member initialization list中,但它有一个default constructor,那么该default constructor必须被调用。
  3. 在那之前,如果class object有virtual functions, 它们必须被设定初值,指向适当的virtual tables.
  4. 在那之前,所有上一层的base class constructors必须被调用,以base class生声明顺序为顺序(与member initialization list中的顺序没有关联):
    • 如果base class被列于member initialization list中,那么任何明确指定的参数都应该被传递进去。、
    • 如果base class没有被列于member initialization list中,而它有default constructor(或default memberwise copy constructor),那么就调用之。
    • 如果base class是多重继承下的第二或后继的base class,那么this指针必须有所调整。
  5. 在那之前,所有virtual base class constructors必须被调用,从左到右,从最深到最浅
    • 如果class被列于member initialization list中,那么如果有任何显式指定的参数,都应该传递过去。若没有列于list之中,而class有一个default constructor,亦应该调用之
    • 此外,class中的每一个virtual base class subobject的偏移位置(offset)必须在执行期可被存取
    • 如果class object是最底层(most-derived)的class,其constructors可能被调用,某些用以支持这一行为的机制必须被放进来。

vptr 初始化操作应该如何处理? vptr初始化操作在base class constructors调用操作之后,但是在程序员供应的代码或是“memeber initialization list中所列的members初始化操作”之前。

令每一个base class constructor设定其对象的vptr,使它指向相关的virtual table之后,构造中的对象就可以严格而正确地变成“构造过程所幻化出来的每一个class”的对象。也就是说,一个PVertex对象会先形成一个Point对象、一个Point3d对象、一个Vertex对象、一个Vertex3d对象,然后才成为一个PVeretex对象。在每一个base class constructors中,对象可以与constructors's class 的完整对象作比较。对于对象而言,“个体发生学”概况了“系统发生学”。constructor的执行算法通常如下:

  1. 在derived class constructor中,“所有virtual base classes”及“上一层base class”的constructors会被调用
  2. 上述完成之后,对象的vptrs被初始化,指向相关的virtual tables
  3. 如果有member initialization list的话,将在constructor体内扩展开来。这必须在vptr被设定之后才做,以免有一个virtual member function被调用。
  4. 最后,执行程序员所提供的代码

例如:已知下面这个由程序员定义的PVertex constructor:

PVertex::PVertex(float x, float y, float z)
    : _next(0), Vertex3d(x, y, z), Point(x, y)
{
    if(spyOn){
        cerr << "Within PVertex::PVertex()"
             << "size: " << size() << endl;
    }
}

它可能被扩展为:

//PVertex constructor的扩展结果
PVertex* PVertex::PVertex(PVertex *this, bool _most_derived,
            float x, float y, float z){
    //条件式调用virtual base constructor
    if(_most_derived != false)
        this->Point::Point(x, y);
    
    //无条件地调用上一层base
    this->Vertex3d::Vertex3d(x, y, z);
    
    //将相关的vptr初始化
    this->_vptr_PVertex = _vtbl_PVertex;
    this->_vptr_Point_PVertex = _vtbl_Point_PVertex;
    
    //程序员缩写代码
    if(spyOn){
        cerr << "Within PVertex::PVertex()"
                Point3d::Point3d(),
             << "size: " 
             << (*this->_vptr_PVertex[3].faddr)(this) 
             << endl;
    }
    
    //传回被构造的对象
    return this;
}

猜你喜欢

转载自blog.csdn.net/zhangxiaofan666/article/details/105363420