《C++对象模型》读书笔记
构造
Default Constructor |
对于一个类,如果没有任何用户定义的构造函数,那么会有一个default constructor被隐式声明出来,一个被隐式声明出来的default constructor是一个无用的(trivial)构造函数,但下面4种情况除外:对象成员有default constructor, 基类有default constructor,有virtual function(虚函数)以及virtual base class(虚继承). |
对象成员有default constructor -----需要调用对象成员构造 如果一个类没有任何默认构造,但它含有一个对象成员具有默认构造,那么这个类的隐式默认构造就是有用的(nontrivial),编译器需要为该class合成出一个default constructor. a. 如果对象成员有默认构造,但类没有任何构造,则编译器会生成默认构造,以调用对象成员的默认构造。 b. 如果类有默认构造,但没有调用对象成员的默认构造,那么编译器会在该默认构造中用户代码之前插入对象成员的默认构造。 c. 如果有多个对象成员,则按照声明顺序调用默认构造。 |
基类有default constructor ----需要调用基类默认构造 基类有默认构造,而派生类没有任何构造,则编译器会为派生类生成一个默认构造来调用基类构造;如果派生类有多个构造,但都没调用基类默认构造,则编译器会把基类构造插入派生类已有每个构造之中。 |
类中有virtual function或其父类有virtual function ---需要初始化指向虚表的指针vptr 如果类中有virtual function,或其父类链中有virtual function,则该类中需要有指向虚表的指针vptr.如果类没有任何构造函数,则编译器会生成一个默认的构造来完成这一功能。 |
虚继承---需要将基类的偏移指向放在虚表的负数位置p/123 类派生自一个virtual class,如果该类没有任何构造,则编译器会生成一个默认构造。 |
在合成的default constructor中,只有base class subobjects和member class objects会被初始化。所有其他的nonstatic data member 比如整数,整数指针,整数数组等等,都不会被初始化。这些初始化操作对程序员而言有需要,但对编译器则非必要,所以程序员要需要初始化他们,而不是编译器。 |
常见误区: 1. 任何class如果没有定义default constructor,就会被合成出一个来。(只有上述4种情况才会有有用的default constructor) 2. 编译器合成出来的default constructor会显示设定“class内每一个Data member的默认值”。(编译器只会调基类和成员变量的构造,而其余的是程序员的活) |
Copy Constructor |
拷贝构造的使用场合(前提是不是位逐次拷贝): 1. 定义一个类,然后直接把另一个类赋给它: X xx = x; (注,如果是xx = x那调的就是Copy assignment operator). 2. 类被当做参数。 3. 类被当做返回值。 |
编译器不总是提供Copy Constructor,如果类是位逐次拷贝(Bitwise Copy Semantics), 则不会有nontrivial(有意义的)拷贝构造;如果不是位逐次拷贝,则会提供拷贝构造。 |
编译器提供拷贝构造函数的4中情况(即非位逐次拷贝): 1. 当class内含有一个member object而后者的class声明有一个拷贝构造时(不论是显示声明还是编译器隐式提供)。 2. 当一个class继承自一个base class,而后者存在一个拷贝构造时(不论是显示声明还是编译器隐式提供)。 3. 当class声明了一个或者多个virtual function时. 4. 当class派生自一个继承串链,其中有一个或者多个virtual base classes时。 |
同一个类的不同实例间拷贝,两个实例中的虚表指针指向同一个虚表(在编译器提供的拷贝构造中完成)。 |
从派生类向父类拷贝的过程中,编译器提供的父类拷贝构造会生成一个父类的虚函数表,父类实例的虚表指针会指向这个虚表而不是指向其派生类的虚表。 |
NRV(Named Return Value)优化 编译器给函数添加一个引用类型参数,取代Named Return Value. X Bar() { X xx; …… return xx; } 优化成: Void Bar(X & _result) { _result.X::X(); …… return; } |
编译器会把变量的初始化列表中的所有初始化操作插入到构造里面的最前边。但初始化顺序是按照类中的声明顺序,而不是初始化列表中的顺序。 |
数据成员
纯空类大小为1字节,为编译器安插进去的一个char,这使得类的一个实例可以在内存中有独一无二的地址。 |
一个空类虚继承于另一个空类,其大小是4字节,因为需要指向基类的指针(按照p/121是这么布局)或者虚表指针(按照p123布局)。 |
同一个access section里面非静态数据成员在对象中排列顺序与声明顺序一致,且不需要紧挨着。(不同access section需要视编译器而定) |
多个access section(private, protected, public)并不会引入额外的负担 |
每一个static data member (class::member)都只有一个实例,存放在程序的Data Segment之中。 |
存取非静态成员,编译器需把类对象起始地址加上偏移量,而这个偏移量在编译时期即可获知。 |
Point3d * pt3d; Pt3d->x; 其执行效率在x是一个struct member, class member,单一继承和多重继承的情况下都完全相同,但是如果是虚继承,则存取速度会稍慢。 |
单一继承且没有virtual function时候的数据布局 这块要注意如果Point2d需位补齐,那么补齐的位也在这里,即: C++保证“出现在derived class中的base class subobject有其完整原样性” |
单一继承并含有虚函数情况下的数据布局 |
单一继承的基类和派生类都是从相同的地址开始,其差异只是派生类比较大。把一个派生类指定给基类(不管继承深度有多深)的指针或者引用。这个操作并不需要编译器去调停或修改地址。它很自然地可以发生,而且提供了最佳执行效率。 |
多重继承 |
虚继承使用
|
虚继承实现思想 Class内含有一个或者多个virtual base class,将被分割为两部分:一个不变区域和一个共享区域。不变区域中的数据,不管后继如何演化,总有固定的offset(从object开头算起),所以这一部分数据可以被直接存取。至于共享区域,所表现的就是virtual base class subobject这部分数据,其位置会因为每次的派生类操作而有变化,所以他们只可以被间接存取,各家编译器实现技术之间的差异就在于间接存取方法上。 |
虚继承的实现方式 1. 派生类中有一个指针指向基类 a. 虚表中的派生类中都有一个指向父类的指针 b. 共享的部分(Point2d)只有一份 2. 在虚函数表中负位置存放基类偏移
即在虚继承派生类的虚表中的负位置存放基类的偏移量 |
虚继承效率很低,最有效的形式是基类没有Data members |
&Point3d::z ---取z在class中的偏移量 &实例::z ---取z在内存中的地址 |
Float point3d::*p2 = &Point3d::x 取一个非静态数据成员的偏移,得到的结果是该成员在类中的bytes位置再加1.因为: Float Point3d::*p1 = 0; Float Point3d::*p2 = &Point3d::x,如果x是第一个变量,则两者没法区分,结果都是0 所以p2的值为1 |