首页 > 代码库 > 深入探索C++对象模型-5

深入探索C++对象模型-5

虚拟继承下的对象构造:

  由于虚拟基类对象在子类中只能保持一个实例,那么,子类构造的时候调用父类的构造函数的时候必须保证虚拟基类对象不能够重复构造。

  那么如何保证基类对象的唯一性?

  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还未指向子类的虚表,因此,此时不会使用多态机制,仍然调用基类中的虚函数实例。这个是值得我们注意的,同时,对于这个,在《Effective C++》里也有相关的说明!

 

而在构造函数中使用成员函数,成员函数中调用虚函数时也不适用多态机制。只有在非构造函数中调用虚函数时才会使用多态机制。

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

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

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

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

4、完成基类的析构。

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


 

 

赋值函数的深入探索:

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

注意:复制构造函数时进行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函数进行析构释放,导致内存错误。

 

深入探索C++对象模型-5