c++ ---深度探索C++对象模型读书笔记1-2
- 简单的对象模型:一个class object 的大小等于指针大小,乘以类成员的数量,原因是因为类对象中只保存类成员的slot。成员本身并不存在对象中,只有指向成员的指针存在对象中,是因为成员的类型不同,申请的内存不同的存储空间。
- 表格驱动对象模型:把所有与memebers相关的信息抽出来,放在一个data member table 和一个member function table中,class object 本身则内含指向这两个表格的指针。
- C++ 对象模型:非静态成员被配置在每一个class object中,静态数据成员被存在所有类对象之外,静态和非静态函数也被存在类对象之外,虚函数:
- 在每一个class 产生出一堆指向虚函数的指针,放在表格中,这个表格就是vtbl;
- 每一个类对象被添加一个指针,指向相关的vtbl,通常这个指针成为vptr。Vptr的设定和重置都由每一个类的构造和析构,拷贝赋值运算符自动完成。每个类所关联的type_info object(用于支持RTTI)也由vtbl指出来,通常放在表格的第一个slot处。
- 虚继承:
虚继承可以解决掉菱形继承的问题。在虚继承的情况下,base class 不管在继承串链中被派生多少次,永远只会存在一个实体(称为subobject).ru iostream 之中就只有virtual iosbase class 的一个实体。
- c++以下列方法支持多态:
- 经由一组隐含的转化操作,父类指针指向子类对象。
- 经由虚函数机制。
- 经由dynamic_cast和typeid运算符
if (circle *pc = dynamic_cast<circle*>(ps)...
5.多少内存表现一个class object的总和大小:
非静态成员的总和大小,以及内存对齐的补充。加上为了支持virtual而由内部产生的任何额外负担。
- 指向不同类型之各指针间的差异,既不在其指针的表示法不同,也不在其内容(代表一个地址)不同 ,而是在其所寻址出来的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的类
- class声明或者继承一个虚函数。
- class 派生自一个继承链,其中有一个或者更过的虚基类。
4.带有一个virtual base class的类:
__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 的初值:
- 明确的以一个对象的内容最为另一个类对象的初值 X xx = x;
- 被作为参数传递给函数时
- 当函数返回一个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构造的类 就会存在一个问题,就是对于指针变量来说,源类中的指针变量保存的是开辟的空间,而目标类中的的指针变量,是通过逐位复制的方式得到的,这样,目标类中的指针变量保存的地址和源类的是一样的。当源类释放空间之后,目标类中的指针变量指的是一堆无意义的空间,这样,当目标类中的指针变量再释放空间的时候,就会报错。
注意:在被合成的拷贝构造中,那些类中的不是类类型的成员 ,比如整数,指针,数组等,也会被复制。
不要位逐次拷贝,以下四种情况不展示位逐次拷贝。
- 当class 内含一个member object 而后者的class 声明有一个copy constructor时(不论是被class 设计者明确地声明,就像前面的String那样,或者被编译器合成出的)
- 当class 继承自一个base class 而后者存在一个copy constructor时(不论是被class 设计者明确地声明,就像前面的String那样,或者被编译器合成出的)
- 当一个类声明一个或者和多个virtual functions时
- 当class派生自一个继承链,其中有一个或者多个virtual base classes时。
前两种情况,编译器必须将member 或者base class 的copy constructors 调用操作 安插到被合成的copy constructor 中。
重新设定Virtual Table的指针:
编译期间的两个程序扩张操作(只要有一个class声明了一个或者多个virtual function就会如此):
- 增加一个virtual function table(vtbl),内含每一个有作用的virtual function 的地址。
- 将一个指向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);
}
程序转化过程如下:
- 重新定义,剔除初始化。
- Class 的copy constructor 会被加入在其后。
B.参数的初始化:(值传)
把一个class object 作为参数传递给函数或者作为函数返回值,相当于“=”形式的初始化。
参数传入时要求将局部实体【形参】以memberwise的方式将实参作为初值,编译器的策略就是导入暂时性的object,并调用copy constructor 将它初始化,然后将暂时性的object交给函数。Class object 的destructor则被用来在函数完成时,清理这个暂时性的object。
另一种方式“拷贝构建”,把实参直接构建在它应该存在的位置上,该位置视函数的活动范围的不同记录在程序的堆栈中。在函数返回之前,这个局部实体【形参】如果有destructor,则调用,进行回收清理。
C.返回值的初始化:
返回值的从局部变量拷贝出来,是一个双阶段的转化:
- 首次函数加上一个类型为class object的引用参数,是用来放置“拷贝构建”而得到的返回值。
- 在return 之前安插一个copy constructor调用操作,以便将欲传回的object的内容当做上述新增参数的初值。
最后一个转化操作重新改写函数,使函数不传回任何值,是void。
经过NRV优化后如下:
void bar(X &__result)
{
//
__result..X::X();
//直接处理__result,代替x
return ;
}
注意:启动NRV优化的条件是:需要手动写一个copy constructor
memcpy和memset都只有在“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 的情况:
- 初始化一个reference member 时;
- 初始化一个const member 时;
- 当调用一个base class 的constructor,而它拥有一组参数时;
- 当调用一个member class 的constructor 而它拥有一组参数时;
需要注意以下几点:
1:初始化列表的真正顺序是由class中的member声明次序决定的,而不是由初始化列表中的排列次序决定。(顺便说一下,内存中的顺序未必是真的是声明的顺序)。不然可能出现一些初始化顺序相关的问题。
2:只有当class中有的class 对象时,才起到作用,例如:
- class Word{
- String _name;
- int _cnt;
- public:
- Word(){
- _name = 0;
- _cnt = 0;
- }
- }
那么c++对constructor的扩张如下:
//C++伪代码
- Word::Word(){
- //调用default constructor
- _name.String::String();
- //产生临时对象temp
- String* temp = String(0);
- //memberwise 地拷贝_name;
- _name.String::operator = ( temp );
- //销毁临时对象temp
- temp.String::~String();
- _cnt = 0;
- }
那么,使用初始化列表的结果是什么样的呢?C++伪代码:
- Word::Word:_name(0)
- {
- _cnt = 0;
- }
- Word::Word(/*this pointer goes here*/)//C++伪代码
- {
- //调用String(int) constructor
- _name.String::String(0);
- _cnt = 0;
- }
可以看到编译器会一一操作初始化列表,以适当的顺序才构造函数中安插初始化操作,并且在任何explicit user code 之前。