深入探索C++对象模型

来源:tracylee

链接: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函数放在虚函数表中,通过虚指针进行查找调用。基类对象数据成员和虚指针均存放在继承类的对象中。


---------------------------------------------------------------------------

上一章讲过了关于类对象内存分布,对于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对象的初始化


其成员初始化既可以使用成员初始化列表,也可以使用普通的方式,但还是普通方式效率太低。


普通初始化方式:首先将成员默认构造,然后产生一个临时对象使用复制构造参数对象,最后使用赋值方式初始化;


而成员初始化列表则会在用户代码之前按照成员声明的顺序并按相应的方式进行初始化。


注意:


成员初始化列表中的顺序不决定初始化顺序,成员初始化顺序由成员声明的顺序而定;


成员初始化列表中的内容先于用户代码执行

------------------------------------------------------------------------

在学习完类对象的构造后,下面就需要学习类数据成员和函数成员的存取。


编译器对于类对象的处理方式:(1)对于空类,编译器为该类添加一个char类型的成员,用来唯一标识该类在内存的位置(2)使用对齐机制,当一个类的内存字节数不足4的倍数将自动补充,目的是为了寻址的方便


有些编译器对于空类的处理进行了优化处理,仅当该空类被继承的时候,空类对象在子类对象中不占用任何内存,单独空类的大小仍是1个字节,这样可能会避免对齐机制,优化了C++对象模型的内存空间。


但是对于非空类,不处理和优化处理对于类对象的内存空间没有任何改变。


例如:


class A{};

class X:public virtual A{};

class Y:public virtual B{};

Class Z:public X,public Y{}


两种处理机制得到的各个类的大小:


未优化                                    优化后

   1                                            1

   8                                            4

   8                                            4

   12                                          8 

---------------------------------------------


一、数据成员深度探索


1、数据成员在类中声明的位置尽量靠前,虽然C++标准没有规定声明顺序,但是为了防范全局变量和局部成员变量产生二义性,所以尽量在声明函数前声明全部变量。


inline函数在未定义之前不去去决断其中使用的变量,但若声明之后立即定义,则会判断函数前声明的变量。


2、nostatic数据成员是在对象存储空间中存放的,static数据成员存在在静态存储区中,每个类只用一份实例(除了模板类)。nostatic数据成员在内存中的布局要求是:在同一访问权限段(public private protected)中,晚声明的成员的地址比早声明的成员的地址高即可,不要求相邻。


3、编译器为了实现某种机制为类对象添加的成员,如vptr指针,它的存放位置C++标准并没有限制,可以放在对象首部,也可以放在尾部。放在首部方便了对虚函数调用,放在尾部可以与C语言中的结构体相兼容,各有好处,视编译器而定。


4、数据成员的存取


考虑通过对象存取成员和通过对象指针存取成员有什么区别?


(1)static数据成员的存取


由于static数据成员存储在程序的静态存储区中,当通过类对象、对象指针或者类::方式存取该成员是,编译器将内部转化为对静态变量的存取。因为static数据成员并不存储在类对象空间中,所以对静态成员的存取不需要经过对象,因此通过对象和指针存取static变量没有任何差异。


若取static数据成员的地址,将会得到该成员在内存中的实际地址,而且其指针类型和普通指针类型是相同的,而nostatic数据成员则有所区别


static int a;为一个A类中的一个static成员,声明一个指向它的指针应该这样声明:int *p=&A::a;指针的使用也和普通指针相同。


(2)nostatic数据成员的存取


nostatic数据成员的存取必须通过对象或指向对象的指针,因为nostatic数据成员的地址依赖于对象的存储地址,编译器会在存取nostatic成员时加上this指针(指向对象的起始地址),通过this指针和nostatic成员在对象中的offset值存取该成员。


换句话说,nostatic数据成员的物理地址可表示为this+offset。


所以,取某个类中的nostatic数据成员地址得到的是该成员在对象中存储offset,即&A::b转化成指针类型就是int A::*p=&A::b;想要使用p还需要通过对象才能完成如a.*p==a.b;


 注意:&A::b的到的值在编译器端将会自动加1,也就是说真实的offset=&A::b-1,这样做的目的是为了区别空的指向数据成员指针(0)和非空指向数据成员指针。当调用指针的时候首先将指针值减1,如果是空的成员指针的话将不能调用。这样就可以把空指针(0)和指向首部成员的指针(0)分开


例如int A::*P1=0;int A::*p2=&A::b;则p2的值是1(假设b放在对象首部,vptr在尾部),当调用时想将p2-1+this获取成员地址,空指针则是-1。


1)单一继承(非虚拟继承):子类总是把基类对象放在子类对象的首部,然后才放子类自己的成员。因此,子类通过对象或者通过对象指针访问基类成员不会存在间接性,基类成员在编译期就可以确定其offset值(基类成员在基类中的offset值和在子类中的offset值是一样的)。因为基类对象在子类对象的首部,这样当基类指针被子类赋值时,基类指针仍然指向基类对象起始地址。


当存在多态时,编译器会自动根据vptr的位置修改数据成员的offset值


2)多重继承(非虚拟继承):此种情况较上述情况麻烦,需要编译器进行地址转换。基类按照继承生命的顺序在继承类中排列,因此第一个基类的地址不需要转化,直接复制即可。而处于中间的基类的地址就需要通过this+中间类的大小才能够确定,这个工作由编译器完成。地址转化的时候首先判断子类地址是否为零。


这种情况下,存取基类对象中的成员在编译器是就确定了offset值,因此通过对象和指针访问基类对象不会存在差异


3)虚拟继承:虚拟继承使得继承类无论继承多少个虚拟基类,都会只包含一个虚拟基类对象。那么,虚拟基类对象在继承类对象内存分布中就只存在一个基类对象实例。现在通用的是将虚拟基类对象放在继承类对象的尾部,继承类其他成员在虚拟基类对象上面。



那么,虚拟基类对象在每个继承类中的offset值是不同的,因此如果通过对象指针存取基类对象成员,不能在编译期确定基类成员的offset值。但是,通过对象存取基类成员时是在编译期确定offset值。


比如:


Vertex3D v3d;  

Point3D *p=&v3d;


那么p->_x的步骤是:v3d的this指针指向Point3D子对象的地址传递给p,然后通过p查询虚表找到虚拟基类对象的地址,再根据offset进行相应的修改即可存取_x。


如果还按照之前的offset值,_X在Point3D中的offset是13,但是_x在Vertex3D中offset不是13,这样就可能提取错误的值,因此,对虚拟基类对象成员的提取将通过虚表间接获得基类对象的地址进行转换。


在虚拟继承时,使用对象和对象指针存取基类对象成员会产生差异,使用对象指针存取基类对象成员时,必须等到运行期进行判断指针指向的真正类型才能确定offset值


-------------------------------------------------------------------------

类中函数的深度探索


类中包含的函数主要有三种:static成员函数、nostatic成员函数、virtual成员函数。


C++类中数据成员和成员函数的命名机制:


数据成员的命名:在每个数据成员命名的时候编译器将该成员所属的类名也添加上,用来标志这个成员的来源范围。这样,继承类就可以与子类用相同的名字命名其成员,这样就不会产生二义性和冲突,但是对外界而言,通过继承类对象访问该名字,只会获取继承类的数据成员,因为根据命名查找机制,两个名字属于不同的作用域,继承类中的成员覆盖了子类中的成员,想要调用必须显示调用子类成员或在继承类作用域中使用using。(函数名相同)


成员函数的命名:在同一个类中允许函数重载,重载的机制就是对函数名+参数名编码获得唯一的编码识别。


(1)static成员函数


首先,说明static成员函数的几条限制:1)static成员函数只能使用static数据成员;2)static成员函数不能设为const、virtual、inline函数,只能保持一份实例;3)static成员函数不使用this指针,因此不必需要通过对象来访问,虽然很多时候是这样使用的。


static成员函数在内存空间中只有一份实例,如果取函数的地址,得到的是真实的内存地址,其函数指针是与普通函数相同的函数指针,而不是指向成员函数的指针,例如


static int foo();       int (*p)()=&A::foo; 使用方法与普通函数指针相同。


因此,对于static成员函数的调用与调用非成员函数的效率是相同的。


(2)nostatic成员函数


虽然nostatic成员函数也是存放在对象外的内存空间中,且取其函数地址也是真实的内存地址,但是调用nostatic成员函数也需要通过对象,也就是说需要this指针,因为nostatic成员函数中可以直接使用类数据成员。


编译器将this指针作为参数传进nostatic成员函数中去,然后通过this指针访问数据成员。例如:


int foo();A a;a.foo();编译器将转化为foo(&a);


编译器将成员函数转化为了非成员函数,是的对成员函数的调用不会有异于非成员函数。


成员函数的指针的使用也必须通过对象调用,例如


int (A::*p)()=&A::foo;(a.*p)();相当于p(&a)调用,也可以由其继承类对象调用。


(3)virtual函数


虚拟成员函数地址存放在虚表中,而获得到虚表的访问权仍然需要通过对象的地址。


指向虚拟成员函数的指针获取到的虚拟成员函数的地址实际上是虚拟函数在虚表中slot编号,通过该编号可以获取到虚拟成员函数的地址,也正是因为获取得到的是编号而非真正的内存地址,才能实现多态机制。


1)单一继承


单一继承情况下,子类会继承父类的vptr指针和虚表,不会产生新的vptr,这时,子类中若存在不同与父类的虚拟函数,将会添加到虚表的尾部,子类若修改了父类中虚拟函数的定义,将会在虚表中相应的位置进行覆盖指向新的函数地址,也就是说,子类的虚表是在父类虚表的基础上扩展修改得到的。





2)多重继承


多重继承中牵扯到this指针调整的问题,也就是第二个及以后的父类地址的问题。C++中将继承类的虚函数写到第一个父类的虚表中,而第一个父类的虚表也就成为主虚表,其他的则成为次虚表。


在继承类中修改的虚函数将会覆盖所有的虚表中原虚函数,凡是继承类中存在的虚函数都会在主虚表中出现,对于没有修改的虚函数,其实际地址存放位置仍然在原父类的虚表中,调用的时候需要进行this指针调整。




如图,Derived a;a.mumble();的调用将会在主虚表中查到4号函数,然后将this指针调整到Base2位置,同多Base2 子对象调用mumble()函数。


再例如,Base2 *p=new Derived; delete p;也会产生this指针的调整,必须调用正确的析构函数,this指针必须回到真个Derived对象的地址首部。


因此,对于多重继承主要是要考虑this指针的调整问题。


3)虚拟继承


虚拟继承的子类将会产生一个vptr和一个虚表来存储子类中虚拟函数和其子类对象的地址。


虚拟继承也需要进行this指针的调整,如同多重继承一样,但是this指针的调整更加复杂,建议不要在虚拟基类中定义nostatic成员,这样会使虚拟子类对象的offset值的确定变得过于复杂。(在不同子类中,虚拟子对象的offset是不相同的)。


-------------------------------------------------------------------------

虚拟继承下的对象构造:


由于虚拟基类对象在子类中只能保持一个实例,那么,子类构造的时候调用父类的构造函数的时候必须保证虚拟基类对象不能够重复构造。那么,C++规定虚拟基类对象的构造只能是最外层的子类进行构造,浅层次的子类将不会在进行构造,保证了虚拟基类对象的唯一性。


在虚拟继承体系下,子类的构造函数中必须做一个判断,设置一个标准位,用来判断虚拟基类对象是否已经构建,然后将该标志为传递给浅层次的子类,那么虚拟基类将不会再次构造。例如,编译器会为子类构造函数内部设置标志位


Point3D::Point3D(Point3D *this,bool _most_derived)

{

  if(_most_derived!=fales)Point();//如果是最外层子类,构建虚拟基类对象

  Point2D(false);

  Vertex(false);//将false传入说明其父类不是最外层,将不会构建虚拟基类对象

}


继承体系下的对象构造:


必须首先将父类对象构造再构造子类对象内容。在子类构造函数中调用父类构造函数的方法可以是在成员初始化列表中显示调用构造函数,如果没有在成员初始化列表中进行构造,那么,编译器会在子类构造函数中扩充调用父类默认构造函数进行构造父类对象。


----------------------------------------------------------------------


Vptr的深入探索


在前面我们知道,Vptr必须在构造对象的时候进行初始化设置,使它指向正确的类的虚表地址。那么,在什么时候进行vptr的设置呢?


C++标准规定构造函数中内容执行的顺序:


1、首先调用虚拟基类(若有首先调用虚拟基类构造函数)或基类们的构造函数,构建基类子对象;  

2、设置该对象的vptr,使其指向适应的虚拟函数表;  

3、执行成员初始化列表中的成员初始化;  

4、执行用户程序内容。


因此,当遇到在构造函数或析构函数中调用虚函数问题的时候,答案就会很明确了。


在基类构造函数中调用虚函数,将不会使用多态机制,即不会调用其子类中的虚函数,因为在基类构造的时候,vptr的设置仍指向基类的虚表,而子类还未完成构造,vptr还未指向子类的虚表,因此,此时不会使用多态机制,仍然调用基类中的虚函数实例。而在构造函数中使用成员函数,成员函数中调用虚函数时也不适用多态机制。只有在非构造函数中调用虚函数时才会使用多态机制。


同理,析构函数中内容的顺序正好相反:


1、调用子类析构函数中实体,完成用户程序中内容的退栈;  

2、析构释放子类中不同于基类的成员;  

3、调整设置vptr,使其指向基类相对应的虚表;  

4、完成基类的析构。


在基类析构函数中调用虚函数,也不会实现多态机制,因为子类已经析构完毕,vptr指向基类的虚表。


----------------------------------------------------------------------


赋值函数的深入探索:


未显示定义的赋值函数,编译器将视情况为类合成赋值函数,条件和合成复制构造函数的相同,只有当复制不适合bitmise的时候才会很成默认构造函数。


注意:复制构造函数时进行vptr的设定,而赋值函数不会进行vptr的设置,也就是说当以子类对象赋值给父类对象时,将不会改变父类对象的vptr指向,因为父类对象在构造的嘶吼已经进行了设定。


赋值函数需要进行自我识别:


加上一句,防止自我复制


if(this==&参数对象)return *this;


另外,赋值函数不能使用成员初始化列表,只有构造函数才能使用,这样就会导致,虚拟继承情况下,使用赋值函数复制对象时,会在被赋值的对象中出现多个虚拟基类对象的现象。


例如:


类A,B虚拟继承类base,C继承A和B,那么C的赋值函数就会这样写:


C& operator=(const C& c)

{

   if(this==&c)return *this;

  A::operator=(c);

  B::operator=(c);//导致出现两份虚拟基类对象实例

  //C自己的成员的复制

  return *this;

}


建议:尽量不要使用赋值函数进行虚拟继承子类对象的复制。


-------------------------------------------------------------------

对象数组的构造:


对象数据的构造一般有两种方式:静态和动态


(1)静态分配


以string类为例,string a[10];就是以静态形式构造数据,这样的数组的个数是确定的不能修改的。


像这样的数组怎么进行构造和析构呢?


编译器在构造数组的时候会生成一个使用默认构造函数的数组构造函数arr_new(char *p,sizeof(string),int num,构造函数地址,析构函数地址);同样也会生成数组析构函数,形式类似。arr_del(char *p,sizeof(string),int num,析构函数地址);


若数组构造中间出现异常,该函数必须保证已构造的对象析构,然后释放内存。


如果数组对象不使用默认构造函数构造对象,必须显示构造,否则,未显示构造的对象将会使用上述函数进行默认构造。


(2)动态分配


使用new表达式进行操作,string *a=new string[10];


new表达式分为两个步骤:首先通过内存分配所用类型大小的空间,然后再该空间上调用相应的构造函数进行构造,上述语句使用默认构造函数。


delete 表达式则释放指针所指的内存(首先析构),大小按照指针类型的大小计算。与数组相对应的为delete[] a;


这样就可能造成一定错误:


当使用基类指针指向一个子类数组,则释放的时候将可能会产生错误


Base *p=new Derived[10];    

delete[] p;


我们知道,new出来的数组是根据Derived对象大小*10的内存空间,而delete 则是根据指针类型的大小进行析构和释放内存的,且使用类似与静态分配时的arr_del函数进行析构释放内存,这样调用的就会是基类的析构函数和基类对象的大小。除了第一个元素,其他元素的析构都会错误的进行。


因此建议:不要使用基类指针指向一个子类数组。


上诉问题根据编译器而不同,微软的编译器可以支持使用基类指针释放子类数组,但是基于cfront的编译器g++将会出现错误,它会将指针类型的大小和析构函数传入它生成的arr_del函数进行析构释放,导致内存错误。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值