c++ ---深度探索C++对象模型读书笔记1-2

  1. 简单的对象模型:一个class object 的大小等于指针大小,乘以类成员的数量,原因是因为类对象中只保存类成员的slot。成员本身并不存在对象中,只有指向成员的指针存在对象中,是因为成员的类型不同,申请的内存不同的存储空间。
  2. 表格驱动对象模型:把所有与memebers相关的信息抽出来,放在一个data member table 和一个member function table中,class object 本身则内含指向这两个表格的指针。
  3. C++ 对象模型:非静态成员被配置在每一个class object中,静态数据成员被存在所有类对象之外,静态和非静态函数也被存在类对象之外,虚函数:
  1. 在每一个class 产生出一堆指向虚函数的指针,放在表格中,这个表格就是vtbl;
  2. 每一个类对象被添加一个指针,指向相关的vtbl,通常这个指针成为vptr。Vptr的设定和重置都由每一个类的构造和析构,拷贝赋值运算符自动完成。每个类所关联的type_info object(用于支持RTTI)也由vtbl指出来,通常放在表格的第一个slot处。

c++ ---深度探索C++对象模型读书笔记1-2

  1. 虚继承:

c++ ---深度探索C++对象模型读书笔记1-2

虚继承可以解决掉菱形继承的问题。在虚继承的情况下,base class 不管在继承串链中被派生多少次,永远只会存在一个实体(称为subobject).ru iostream 之中就只有virtual iosbase class 的一个实体。

  1. c++以下列方法支持多态:
  1. 经由一组隐含的转化操作,父类指针指向子类对象。
  2. 经由虚函数机制。
  3. 经由dynamic_cast和typeid运算符

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

5.多少内存表现一个class object的总和大小:

非静态成员的总和大小,以及内存对齐的补充。加上为了支持virtual而由内部产生的任何额外负担。

 

  1. 指向不同类型之各指针间的差异,既不在其指针的表示法不同,也不在其内容(代表一个地址)不同 ,而是在其所寻址出来的object类型不同。一个类型为void* 的指针只能够含有一个地址,而不能够通过它操作所指的object。

 

2.1 Default Constructor的建构操作  

构造函数:

       什么时候需要有用的默认构造(编译器需要的时候):

      C++ stardand 将这些合成的默认构造成为隐式有用的默认构造,用来满足编译器的需要,像类成员char* str,不是member class object,则其初始化需要程序员完成; 至于没有存在下面这四种情况而又没有声明任何constructor的类,我们称为他们已拥有隐式无用的默认构造,实际上并不会被合成。

              1.带有default constructor 的member class object

           A.如果一个class 没有任何constructor ,但它内含一个member object ,而后者有default constructor, 那么这个类的隐含的默认构造函数就是有用的,编译器需要为此个类合成出一个default constructor ,不过这个合成操作只有才constructor 真正需要被调用时才会发生。被合成的默认构造只满足编译器的需要,而不是满足程序的需要。

           B.如果class A内含一个或者一个以上的member class objects 那么class A的每一个构造必须调用每一个成员类的默认构造函数,编译器会扩张已存在的构造,在其中安插一些编译器附加的一些代码,使得用户代码在被执行之前,先调用必要的默认构造函数。

 

              2.带有default constructor 的base class

           A.如果一个没有任何constructors的class 派生自“带有default constructor”的base class,那么这个子类的默认构造函数就是有用的。并因此会被合成出来,它将调用上一层基类的默认构造函数(根据继承顺序)。

         B.如果设计者提供多个constructors,但都不是default constructor,编译器会扩张每一个构造函数,将“用以调用所有必要之默认构造”的程序代码加进去,但是它不会合成一个新的默认构造,因为用户提供了构造函数,如果还存有上面的成员类对象默认构造要加,就加在所有被继承的基类构造的后边。

 

3.带有virtual function的类

  1. class声明或者继承一个虚函数。
  2. class 派生自一个继承链,其中有一个或者更过的虚基类。

 

4.带有一个virtual base class的类:

c++ ---深度探索C++对象模型读书笔记1-2

 

c++ ---深度探索C++对象模型读书笔记1-2

__vbcX表示编译器产生的指针,指向virtual base class X, 它是在class object 建构期间被完成的。对于class所定义的每一个constructor ,编译器会安插那些“允许每一个virtual base class”的执行期存取操作的码,如果class 没有声明任何contructors,编译器必须为它合成一个default constructor。

 

C++新手一般有两个误解:1)任何class如果没有定义default constructor,就会被合成出来一个。2)编译器合成出来的default constructor会明确设定class内每一个data member的默认值。

2.2 Copy Constructor 的建构操作

拷贝构造函数:

       三种情况会以一个object的内容作为另一个class object 的初值:

  1. 明确的以一个对象的内容最为另一个类对象的初值 X xx = x;
  2. 被作为参数传递给函数时
  3. 当函数返回一个class object时

缺省的按成员初始化(default memberwise initialization):

       如何class 没有提供一个缺省的显示拷贝构造,那么在以一个相同类的对象对另一个对象做初值时, 其内部就是利用给的编译器的缺省按成员初始化完成的,就是把每一个内建的或者派生的数据成员的值,从某一个对象拷贝一份到另一个对象上,但是其并不会拷贝其中的member class object ,而是以递归的方式施行memberwise initialization.,即同样的方式对待类内的成员类对象。

       Default constructors 和copy constructor 在必要的时候才由编译器产生出来。

一个class object 可以从两种方式复制得到,一种是初始化,另一种是被指定(assignment),从概念上而言,两个操作分别是 copy constructor 和 copy assignment operator完成的。

 

隐式copy constructor 也分有用的和无用的,只有有用的拷贝构造才会被合成到程序中,判断无用的标准就是在于class 是否展现出所谓的(bitwise copy scmantics)位逐次拷贝。

(bitwise copy scmantics)位逐次拷贝:

源类中的成员变量 中的每一位 都逐次 复制到 目标类中。

如果class中出现了bitwise copy semantics的时候,default constructors和copy constructors 编译器就不会为我们产生出来。那么没有default constructors和copy constructors的时候,我们的类怎么产生呢。这时候,就是通过  bitwise copy 来搞定了,也就是 将 源类中的成员变量中的每一位都逐次复制到 目标类 中,这样我们的类就构造出来了。

这时候,这种bitwise copy构造的类 就会存在一个问题,就是对于指针变量来说,源类中的指针变量保存的是开辟的空间,而目标类中的的指针变量,是通过逐位复制的方式得到的,这样,目标类中的指针变量保存的地址和源类的是一样的。当源类释放空间之后,目标类中的指针变量指的是一堆无意义的空间,这样,当目标类中的指针变量再释放空间的时候,就会报错。

c++ ---深度探索C++对象模型读书笔记1-2

 c++ ---深度探索C++对象模型读书笔记1-2

c++ ---深度探索C++对象模型读书笔记1-2

c++ ---深度探索C++对象模型读书笔记1-2

注意:在被合成的拷贝构造中,那些类中的不是类类型的成员 ,比如整数,指针,数组等,也会被复制。

不要位逐次拷贝,以下四种情况不展示位逐次拷贝。

  1. 当class 内含一个member object 而后者的class 声明有一个copy constructor时(不论是被class 设计者明确地声明,就像前面的String那样,或者被编译器合成出的)
  2. 当class 继承自一个base class 而后者存在一个copy constructor时(不论是被class 设计者明确地声明,就像前面的String那样,或者被编译器合成出的)
  3. 当一个类声明一个或者和多个virtual functions
  4. 当class派生自一个继承链,其中有一个或者多个virtual base classes

前两种情况,编译器必须将member 或者base class 的copy constructors 调用操作 安插到被合成的copy constructor 中。

 

重新设定Virtual Table的指针:

编译期间的两个程序扩张操作(只要有一个class声明了一个或者多个virtual function就会如此):

  1. 增加一个virtual function table(vtbl),内含每一个有作用的virtual function 的地址。
  2. 将一个指向virtual function table 的指针(vptr),安插在每一个class object 内。

 

显然,编译器对于每一个新产生的class object 的vptr 必须要正确的设定好初值,当编译器导入一个vptr到class之中时, 该class就不再展现bitwise semantics了,而是需要合成一个copy constructor, 用来将vptr适当的初始化。

继承自同一虚基类的类,生成的对象之间拷贝vptr是安全的。

当一个base class object 以其derived class 的object 内容做初始化操作时,其vptr复制操作也必须保证安全,虽然赋值操作会发生切割行。通过虚基类对象调用其虚函数,调用的是虚基类的实体而非子类实体。如果是虚基类对象指针或者引用,它的值又是子类的地址,那么经由虚基类对象指针调用的同名函数,才是子类的实体。也就是说合成出来的拷贝构造会明确设定object的vptr 指向虚基类的虚表,而不是直接从右值子类对象中拷贝vptr现值。

 

处理virtual base class subobject :

       Virtual base class 的存在需要特别处理,一个class object 如果以另一个object 作为初值,而后者有一个virtual base class subobject ,那么也会使“bitwise copy semantics”失效。

每个编译器基本上都会在执行期之前安排好“derived class object”中的virtual base class subobject的内存位置,但是如果使用“bitwise copy semantics”会破坏这个位置,所以必须合成出copy constructor 中做出仲裁。在合成的代码中,调用父类的默认构造,对子类的vptr初始化,并定位出子中父类的部分。如果是虚基类的实例被其子类的对象赋值,这种编译器就会生成copy constructor 来设定virtual base class pointer/offset的初值,对每一个members执行必要的memberwise初始化,以及其他的内存相关工作。

 

2.3程序化语义学:

NRV(named return value)原理如下:

X bar()

{

    X xx;

    //处理x

    return x;

}

A.明确的初始化操作

X x0;

 

void foo_bar() {

X x1(x0);

X x2 = x0;

X x3 = X(x0);

}

程序转化过程如下:

  1. 重新定义,剔除初始化。
  2. Class 的copy constructor 会被加入在其后。

B.参数的初始化:(值传)

       把一个class object 作为参数传递给函数或者作为函数返回值,相当于“=”形式的初始化。

参数传入时要求将局部实体【形参】以memberwise的方式将实参作为初值,编译器的策略就是导入暂时性的object,并调用copy constructor 将它初始化,然后将暂时性的object交给函数。Class object 的destructor则被用来在函数完成时,清理这个暂时性的object。

       另一种方式“拷贝构建”,把实参直接构建在它应该存在的位置上,该位置视函数的活动范围的不同记录在程序的堆栈中。在函数返回之前,这个局部实体【形参】如果有destructor,则调用,进行回收清理。

 

C.返回值的初始化:

       返回值的从局部变量拷贝出来,是一个双阶段的转化:

  1. 首次函数加上一个类型为class object的引用参数,是用来放置“拷贝构建”而得到的返回值。
  2. 在return 之前安插一个copy constructor调用操作,以便将欲传回的object的内容当做上述新增参数的初值。

最后一个转化操作重新改写函数,使函数不传回任何值,是void。

 

经过NRV优化后如下:

void bar(X &__result)

{

    //

    __result..X::X();

    //直接处理__result,代替x

    return ;

}

注意:启动NRV优化的条件是:需要手动写一个copy constructor

 

memcpymemset都只有在“class 不含任何由编译器产生的内部members(主要值得vptr)”时才能有效运行。如果class中声明有virtual functions或者含有一个virtual base class,那么使用上述函数将会导致那些“被编译器产生的内部members”的初值被改变。

 

copy constructor要,还是不要?

    copy constructor的应用,迫使编译器多少对你的程序做出部分转化。尤其是当一个函数以值传递的方式传会一个class object,而该class有一个copy constructor(不管是你写的,还是编译器默认创建的),这将导致深奥的程序转换。

2.4成员们的初始化队伍

初始化class members的初值,要么在member initialization list ,要么在constructor函数中,除了下列四种情况,选哪个都差不多。

必须使用 member initialization list 的情况:

  1. 初始化一个reference member 时;
  2. 初始化一个const member 时;
  3. 当调用一个base class 的constructor,而它拥有一组参数时;
  4. 当调用一个member class 的constructor 而它拥有一组参数时;

需要注意以下几点:

1:初始化列表的真正顺序是由class中的member声明次序决定的,而不是由初始化列表中的排列次序决定。(顺便说一下,内存中的顺序未必是真的是声明的顺序)。不然可能出现一些初始化顺序相关的问题。

2:只有当class中有的class 对象时,才起到作用,例如: 

  1. class Word{
  2.     String _name;
  3.     int _cnt;
  4. public:
  5.     Word(){
  6.         _name = 0;
  7.         _cnt = 0;
  8.         }
  9. }

那么c++对constructor的扩张如下:

//C++伪代码

  1. Word::Word(){
  2.     //调用default constructor
  3.     _name.String::String();
  4.     //产生临时对象temp
  5.     String* temp = String(0);
  6.     //memberwise 地拷贝_name;
  7.     _name.String::operator = ( temp );
  8.     //销毁临时对象temp
  9.     temp.String::~String();
  10.     _cnt = 0;
  11. }

那么,使用初始化列表的结果是什么样的呢?C++伪代码:

  1. Word::Word:_name(0)
  2. {
  3.     _cnt = 0;
  4. }
  5. Word::Word(/*this pointer goes here*/)//C++伪代码
  6. {
  7.     //调用String(int) constructor
  8.     _name.String::String(0);
  9.     _cnt = 0;
  10. }

可以看到编译器会一一操作初始化列表,以适当的顺序才构造函数中安插初始化操作,并且在任何explicit user code 之前。