深入探索C++对象模型(1)
http://www.cnblogs.com/tracylee/archive/2012/12/18/2822431.html
在实际生产中,遇到一个复杂的类,如果能看出这个类的内存模型结构,那么以后的操作基本就没有难度的;所以说,学会分析一个类的内存模型,是每一个C++程序员必须要会的知识。 (1)C++类封装和C中的结构体的区别 C++的类封装是在C语言中的结构体的基础上构建起来的,C结构体只允许存在数据,而不会存在对数据的操作。C++语言中延承C语言中的结构体,但增加的对数据的操作,即成员函数;类是对结构体的进一步封装,使某些数据成员对外不可见,称为私有成员。 类和结构体最大的区别就是:结构体成员均是public类型的。 那么,类和结构体的布局成本有没有区别呢?对于只有数据成员的类和结构体在内存的布局是相同的,没有增加成本。而members functions 虽然含在class的声明之内,却不出现在object之中。每一个non-inline member fuction 只会诞生一个函数实体,调用时链接至函数体。至于每一个"拥有零个或一个定义"的inline function则会在其每一个调用者(模块)身上产生一个函数实体。 在C++中,有两种class data members:static和nonstatic,以及三种class member functions:static、nonstatic和virtual。在C++对象模型中,非静态数据成员被配置在每一个class object之内,静态数据成员则被存在个别的class object之外,静态和非静态的成员函数也被放在个别的class object之外。 c++在布局及存取时间上主要的额外负担是由virtual引起。包括: 1.virtual function 机制 用以支持一个有效率的"执行期绑定"
2.virtual base class 用以实现"多次出现在集成体系中的base class,
有一个单一而被共享的实体。
无论是含有虚函数还是虚拟继承,类都会产生一个虚函数表,同时每一个对象都会含有一个Vptr指针指向虚拟函数表。
虚拟函数的实现 1.每一个class产生一堆指向虚函数的指针,放在表格之中,这个表格被称为virtual table(vtbl); 2.每一个class object被安插一个指针,指向相关的virtual table。这个指针通常被称为vptr。vptr的设定和重置都由每一个类的构造函数、析构函数和赋值操作符重载函数自动完成且一旦完成初始化不能修改。每一个类所关联的type_info object(主要用于RTTI)也经由virtual table被指出来,通常放在表格的第一个slot。 多重继承模型: C++最初采用的继承模型时间基类对象中的数据成员直接存放在继承类对象内存中,包括基类对象的虚函数表和虚指针(貌似现在也是这样的模型),这样就可以对基类成员进行最有效率的存取,然而缺点是如果基类成员有任何的修改将会导致基类对象和继承类对象的重新编译。 有人提出一种间接存取模型:在继承类对象中放置一指针Bptr,该指针指向继承类的基类表,类似与虚函数表,基类表中每一项都指向继承类对象的基类对象。这样的好处是,基类对象的任何改变吗都不会影响到继承类对象的内存布局,缺点是导致对基类对象成员的存取效率低,随着继承深度的增加,间接性越强,效率越低。 因此又有人提出,在继承类对象中放置与基类相等数目的指针指向每个基类,这样存取时间就恒久不变了,但是这样无疑增加了继承类的内存开销。 目前使用的多重继承模型仍为第一种,继承类对象中含有所有基类对象数据成员和虚指针,同时继承基类的虚函数表共享为继承类的虚函数表。 虚拟继承模型: 虚拟继承保证继承类中只含有一个基类对象,继承类中会产生虚指针指向虚表中offset指向继承类中的基类对象。只有直接虚继承的继承类对象才会产生Vptr。 目前,关于虚拟继承的继承类的内存布局有三种模型: 1、原始模型:在每个继承类对象中设置指针,用以指向继承类中的基类对象。优点:通过指针能够保证继承类中只有一个基类对象,可以直接存取基类对象。缺点:若继承类有多个虚拟基类,则需要多个指针指向基类们,会消耗较多的内存。 2、有人提出像虚函数表一样,提供一个虚拟基类表virtual base class table,表中每个slot指向一个基类对象的地址。在继承类对象中,设置一个虚拟基类表指针指向基类表。优点:继承类减少了因虚拟基类指针而产生的开销,且继承类不会因为虚拟基类的增加而改变内存模型。缺点:存取基类对象数据成员会导致效率低下。 3、现在最常用的模型:扩充原Virtual table,在虚表负offset位置指向虚基类对象,因此,只要是虚继承就会产生虚指针和虚表。 虚拟多继承,采用“钻石”继承结构,其每个类的内存分布如下图:其中虚指针的位置可以在顶部或尾部。 上述内容是类对象内存模型的介绍: (1)无虚函数和虚继承:nostatic数据成员存放在对象中,static数据成员存放在对象外属于整个类。nostatic和static函数成员均放在对象外。 (2)有虚函数和虚继承:virtual函数放在虚函数表中,通过虚指针进行查找调用。基类对象数据成员和虚指针均存放在继承类的对象中。
深入探索C++对象模型(2)http://www.cnblogs.com/tracylee/archive/2012/12/18/2824125.html
上一章讲过了关于类对象内存分布,对于nostatic数据将会放在对象内存空间中,static数据成员和nostatic、static函数成员将不会放在对象内存中,对于虚拟继承和含有虚函数的类来说,将会在对象内存中增加一个虚表指针,指向该类的虚表,其中虚表中将会存放虚函数的地址和虚拟基类的地址。一个类中只含有一个共享虚表(继承基类的虚表也是继承类的虚表,一般继承类的虚函数会存放在第一个基类的虚表中方便提取),对象中可以含有多个虚指针,继承至基类,除了直接虚拟继承的继承类才会产生新的vptr和虚表用于指示虚拟基类的位置。 下面是如何构建类对象,即构造函数的深入探索。 首先强调两个C++新手容易陷入的误区: (1)编译器会为没有显示声明构造函数的类合成默认构造函数;-------------并不是所有的都合成默认构造函数,只有只有四种情况下才会合成,其他情况下不会合成。 (2)编译器合成出来的默认构造函数会为每个数据成员赋予默认值。------------编译器不负责对数据成员的赋值工作,并不是编译器所需的,而应该是程序员完成的工作。(除非其有默认构造函数的subobject和memeber在构造时会初始化)
那么,哪四种情况会导致编译器为类合成一个默认的构造函数呢? (1)类成员存在默认构造函数的,在合成默认构造函数时必须调用该成员的默认构造函数或者是扩充到已有的默认构造函数中; (2)所继承的基类存在默认构造函数的,编译器会合成默认构造函数,在构造函数中调用基类默认构造函数; (3)含有虚函数的类,编译器会合成默认构造函数,主要是因为要初始化虚表指针; (4)虚继承的类,编译器会合成默认构造函数,也是因为要初始化虚表指针,指向虚拟基类对象。 当已经显示定义了构造函数,即使不是默认的构造函数,编译器也不会为类合成默认构造函数。如果显示定义的构造函数没有完成一些默认构造的功能,编译器将会扩充显示定义的构造函数,调用能够调用的默认构造函数比如成员默认构造函数或基类默认构造函数。
---------------------------------------------------------------------------------------------------------------------------------------------------------- 复制构造函数深度探索: 复制构造函数只要用于三个方面:用一个对象的内容构造出一个新对象;参数以值传递的形式传递时;以一个对象作为返回值时。在这三种情况下将会调用复制构造函数,有的情况下需要产生临时对象。 例如:string s=a;将不会产生临时对象,string s=string(a);将会产生临时对象t,然后在调用复制构造函数构造s;string s(a);也不会产生临时对象。 当参数和返回值以值传递的形式,一般情况下都会产生临时对象存储数据,当在编译器NRV优化(下面细讲)的情况下,返回值可能不要产生临时对象,而是直接操作,例如: T operator+(T &a,T &b); Tc=a+b; 在编译器优化的情况下,operator +函数极有可能是这个样子的: //编译器内部伪码 void operator +(T &a,T &b,T &c) { //直接对c进行操作,这种情况下不需要产生构造对象 } 复制构造的两种方式:bitwise和memeberwise,前者是单纯的位拷贝,后者则是以成员为单位进行递归拷贝(所谓递归拷贝就是当成员为一个类对象时将会按照该对象的成员进行拷贝知道结束)。另外,复制构造函数也会对数组的所有成员进行拷贝。 当类中没有显示定义的复制构造函数的时候,编译器会不会合成一个复制构造函数呢?要根据该类的复制构造形式而定,bitwise形式不会合成复制构造函数,memberwise形式会合成。 以下有四种情况会导致类不是bitwise形式,会在无显示定义复制构造函数的时候合成默认复制构造函数: (1)类成员对象含所有显示复制构造函数(无论是编译器合成的还是自定义的,有时候为了实现NRV优化会自定义复制构造函数),编译器会为这样的类合成默认复制构造函数; (2)类的基类对象含有复制构造函数,编译器会构造默认复制构造函数,构造的时候将会调用扩充基类复制构造函数; (3)类中含有虚函数的,编译器将会构造默认复制构造函数,这样可以保证当基类对象被继承类对象初始化的时候,基类对象的vptr指向正确的虚函数表,而不能单纯的复制vptr那么容易,否则将会造成基类对象的vptr指向继承类的虚表了; (4)虚拟继承的类,编译器将会构造默认复制构造函数,同样是当基类对象被子类对象初始化时,必须保证指向虚拟基类对象的指针被正确设置,因为虚拟基类对象在不同子类内存中的位置是不同的,在虚表中对应offset值是不同的,所以必须保证vptr所指向的虚表的正确性,因此编译器将会为虚拟继承的类合成默认复制构造函数。 --------------------------------------------------------------------------------------------------------------------------------------------- NRV编译器优化:前提是编译器提供该服务,且类中存在复制构造函数,无论是编译器合成的还是自定义的,否则优化无从谈起,因为trival复制构造函数就是最有效率的构造方法,有些类为了使用NRV优化甚至强行提供显示复制构造函数。 NRV优化主要用在返回值为对象的函数身上,主要方法是减少临时对象的数目来提高效率。 优化前: X bar()//将会产生临时对象存储返回的值 { X xx; //处理 return xx; } 优化后: void bar(X &_result)//这样在指定函数返回对象是将不会产生临时对象 { X xx; //处理xx //调用复制构造函数 _result.X::X(xx); return; } X yy=bar();相当于bar(yy); 但是,当bar()单独使用的时候,仍然会产生一个临时对象存储结果,因为函数没有返回的对象。 而NRV优化又是在上述的基础上直接对_result上处理,不需要在函数内部产生一个xx对象,然后再调用复制构造函数,这样就减省了复制构造的消耗,但是会使用默认构造函数。 void bar(X &_result)//NRV优化 { _result.X::X(); //对_result直接进行处理 return; }
------------------------------------------------------------------------------------------------------------------------------------------------- 成员初始化表的使用: 有以下三种情况必须使用成员初始化表: (1)调用基类构造函数或者基类复制构造函数 (2)类对象中的引用初始化 (3)类中const对象的初始化 其成员初始化既可以使用成员初始化列表,也可以使用普通的方式,但还是普通方式效率太低。 普通初始化方式:首先将成员默认构造,然后产生一个临时对象使用复制构造参数对象,最后使用赋值方式初始化; 而成员初始化列表则会在用户代码之前按照成员声明的顺序并按相应的方式进行初始化。 注意: 成员初始化列表中的顺序不决定初始化顺序,成员初始化顺序由成员声明的顺序而定; 成员初始化列表中的内容先于用户代码执行
|