*注:上述三个构造成员函数指:默认构造函数,复制构造函数,赋值操作符函数
• 以下列例子进行讨论:
Eg:
class Point
{
public:
Point(int i = 0, int j = 0) :x(i), y(j),{}
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
private:
int x, y;
};
定义类 Line,其有两个 Point 类型的数据成员,Point 类型数据成员拥有四种上述成员函数,在 Line 类型没有定义四种成员函数的情况下,编译器需要合成对应的 trival 成员函数
class Line
{
public:
Line(int i = 0, int j = 0, int k = 0, int m = 0);
Line(const Point&, const Point&);
void draw();
private:
Point _begin, _end;
};
• 对每一个显示构造函数都会被扩充以调用两个数据成员对象的构造函数。
• inline 析构函数也将被合成出来,并按照成员声明顺序的逆序调用数据成员对象的析构函数
• 对赋值操作符函数需要特别注意:
1.大部分编译器合成的赋值操作符函数不会有如下的相等条件过滤
Eg:
Line& operator=(Line * this, const Line & rhs)
{
if(this == &rhs) //相等条件过滤
return *this;
//调用数据成员的赋值操作符函数
this->_begin.Point::operator=(rhs._begin);
this->_end.point::operator=(rhs._end);
return *this;
}
因此,对同一个对象的赋值操作将会做多余的拷贝操作,但由于合成的赋值操作符函数未伴随任何资源释放行为,因此该操作是安全的
2.对程序员自行定义的赋值操作符函数,若存在资源释放操作,则未加相等条件过滤是危险的
Eg:
string& operator=(const string &rhs)
{
//首先释放 this->str 所占资源,未加相等条件过滤,若 &rhs == this ,则将出现访问错误,是危险的
delete[] str;
str = new char[strlen(rhs.str) + 1];
}
• 以下列例子进行讨论:
Eg:
class Point
{
public:
Point(int i = 0, int j = 0) :x(i), y(j), {}
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
private:
int x, y;
};
class Point3d : public virtual Point
{
public:
Point3d(int i = 0, int j = 0, int k = 0) : Point(i, j), z(k){}
Point3d(const Point3d&);
Point3d & operator=(const Point3d);
~Point3d();
int z;
};
假设同时存在如下类继承关系:
class Vertex : virtual public Point{…};
class Vertex3d : public Point3d, public Vertex{…};
class Pvertex : public Vertex{…};
可以看出类 Point 是继承体系中的虚基类,Point 子对象应该由继承体系中的最底层的类来负责构造与维护,即:对Pvertex 对象,Point 子对象由其进行构造
• 有虚基类的类的构造函数在调用虚基类的构造函数的过程中,应当条件式的测试传进来的参数,然后决定调用或不调用相关的虚基类的构造函数
Eg: (以下是对Point3d构造函数的扩充内容)
Point3d * Point3d::Point3d(Point3d * this, bool __most_derived, int x, int y, int z)
{
if(__most_derived)
this->Point::Point(x,y);
this->__vptr_Point3d = __vtbl_Point3d;
this->__vptr_Point3d_Point = __vtbl_Point3d_point;
this->z = z;
return this;
}
• 在更深层的继承情况下,总是会把子类的 __most_derived 参数设为false,压制了子类对象中对虚基类的构造函数的调用操作
Eg:(以下是对Vertex3d构造函数的扩充内容)
Vertex3d* Vertex3d::Vertex3d(Vertex3d * this, bool __most_derived, int i, int j, int k)
{
if(__most_derived)
this->Point::Point(i,j);
//将子类的 __most_derived 参数设置为 false, 抑制其对虚基类的构造函数的调用
this->Point3d::Point3d(false,i,j,k);
this->Vertex::Vertex(false,i,j,k);
//安插vptrs
//用户代码
return this;}
• 关于虚基类的构造函数是否被调用的判断:
§ 当一个完整的类对象被定义出来时,在该类中调用其虚基类的构造函数
§ 当对象只是某个完整的类对象的子对象时,将不会调用其虚基类的构造函数
• 另一种处理方法:将每个构造函数分裂为两个,一个针对完整的对象,另一个针对子对象。
1. 完整对象的构造函数将无条件调用虚基类的构造函数,并设定所有的 vptrs
2. 子对象的构造函数将不调用虚基类的构造函数,也可能不设定 vptrs
• 在一个类的构造函数中,经由构造中的对象来调用一个虚函数,其函数实例应该是在此类中有作用的那个
Eg:(假设上述类 Point, Point3d, Vertex, Vertex3d, Pvertex 中均定义了各自的虚函数 size(),且在各自的构造函数中均调用了该函数)
Pvertex pobj; //该定义中调用构造函数的顺序为:
Point(x,y); //调用的size()为: Point::size()
Point3d(x,y,z); //调用的size()为:Point3d::size()
Vertex(x,y,z);
Vertex3d(x,y,z);
Pvertex(x,y,z);
• 该问题的实现由三种解决办法:
将调用操作限制必须在构造函数中直接调用,此时将每一个调用操作以静态方式决议(但当该静态调用的函数中又调用了一个虚函数时,将无法处理)
在构造函数中设立一个标志,该标志说明此时是否要以静态方式来决议(与虚基类的构造函数是否调用采用相类似的方法,但该方法不够优雅与有效)
3. 在执行一个构造函数时,必须限制一组虚函数的候选名单
• 限制虚函数的候选名单,将 vptr 的初始化放置在基类构造函数的调用操作之后,程序提供的代码与成员初始化列表之前
Eg:(此时Pvertex的构造函数将被扩展)
Pvertex * Pvertex::Pvertex(Pvertex * this, bool __most_derived, int i, int j, int k)
{
//条件式调用虚基类的构造函数
if(__most_derived)
this->Point::Point(i, j);
//调用上层的基类的构造函数
this->Vertex3d::Vertex3d(i, j, k);
//初始化相关的 vptrs
this->__vptr_Pvertex = __vtbl_Pvertex;
this->__vptr_Point_Pvetex = __vtbl_Point_Pvertex; //虚基类由最底层类进行维护
//执行程序员定义的成员初始化列表与代码
return this;
}
• 缺陷:当出现下述情况时,限定虚函数候选名单的办法将存在缺陷
Eg:
Point3d::Point3d(int i, int j, int k) : Point(i, j),z(k){}
分析:在成员初始化列表执行之前,就设定了_vptr_Point3d,因此在成员初始化列表中调用构造函数时,Point 构造函数中的虚函数将不再是 Point::size()
• 完美的解决办法:把构造函数分裂为一个完整的对象实例与一个子对象实例
1. 完美对象实例中设定 vptr 的值,并调用虚基类的构造函数
2. 子对象实例中不设定vptr 的值,且不调用虚基类的构造函数
• 基于分裂法下关于虚函数调用是否安全的讨论
若在类的构造函数的初始化列表中调用类的一个虚函数
• 此时调用虚函数是安全的,因为 vptr 值的设定是在成员初始化列表执行之前。但从语意上说是不推荐的
若类的基类的构造函数的初始化列表中调用了一个虚函数
• 此时调用虚函数将是不安全的,因为根据分裂法而言,虚函数的构造函数调用的是子对象实例的版本,该版本中并不设定 vptr 的值,因此在该基类构造函数的成员初始化列表中调用虚函数将是一个不安全的操作