代理类-摘自《C++沉思录》Andrew Koenig

       我们怎样才能设计一个C++容器,使它有能力包含类型不同而彼此相关的对象呢?容器通常只能包含一种类型的对象,所以很难在容器中存储对象本身。存储指向对象的指针,虽然允许通过继承来处理类型不同的问题,但是也增加了内存分配的额外负担。

       这里,我们将讨论一种方法,通过定义名为代理(surrogate)的对象来解决该问题。代理运行起来和它所代表的对象基本相同,但是允许将整个派生类层次压缩在一个对象类型中。正如对本部分的介绍中所说的那样,surrogate是handle类中最简单的一种。

1.    问题   ​  

       假如有一个表示不同种类的交通工具的类派生层次:

class Vehicle{
public:
	virtual double weight() const = 0;
    	virtual void start() = 0;
    	// ...
};

class RoadVehicle: public Vehicle { /*...*/ };
class AutoVehicle: public RoadVehicle { /*...*/ };
class Aircraft: public Vehicle { /*...*/ };
class Helicopter: public Aircraft { /*...*/ };

    ​   所有Vehicle都有一些类Vehicle中成员声明的共有属性。但是,有的Vehicle具有一些其他Vehicle所没有的属性。例如,只有Aircraft能飞,也只有Helicopter能盘旋。

       下面假设我们要跟踪处理一系列不同种类的Vehicle。在实际中,我们可能会使用某种容器类;然而,为了使表述更简洁,我们使用数组来实现。这样的话,就试一试

       Vehicle parking_lot[1000];

没有产生预期的效果,为什么?

       表面上看是出于Vehicle是一个抽象基类,因为成员函数weight和start都是纯虚函数。通过在声明中写=0,明确声明了这些函数可以空着不定义。因此,只有从类Vehicle派生出来的类才能够实例化,类Vehicle本身不会有对象。既然Vehicle对象不存在,当然也就不可能有其对象数组了。

       我们的失败还有更深层次的原因。如果我们思考一下,假设存在类Vehicle的对象会出现什么样的情况,原因就会更加明显。譬如,假设我们剔除了类Vehicle中的所有纯虚数。如果我们写类似于下面的语句,会有什么结果呢?

Automobile x = /*...*/
Partking_log[num_vehicles++] = x;

       答案是:把x赋给parking_lot的元素,会把x转换成一个Vehicle对象,同时会丢失所有在Vehicle类中没有的成员。该赋值语句还会把这个被剪裁了的对象复制到parking_lot数组中去。

      这样,我们就只能说parking_lot是Vehicles的集合,而不是所有继承自Vehicle的对象的集合。

2.    经典解决方案

       这种情况,实现灵活性的常见做法是提供一个间接层(indirection)最早的合适的间接层形式就是存储指针,而不是对象本身:

Vehicle* parking_lot[1000];			//指针数组

       然后,输入类似

Automobile x = /*...*/;
Partking_log[num_vehicles++] = &x;

的语句。这种方法解决了迫切的问题,但也带来了两个新问题。

       首先,我们存储在parking_lot中的是指向x的指针,在本例中是一个局部变量。这样,一旦变量x没有了,parking_lot就不知道指向什么东西了。  

       我们可以这么变通一下,放入parking_lot中的值,不是指向原对象的指针,而是指向它们的副本的指针。然后,可以采用一个约定,就是当我们释放parking_lot时,也释放其中所指向的全部对象。因此,可以把前面的例子改为:

Automobile x = /*...*/;
Partking_log[num_vehicles++] = new Automobile(x);

       尽管这样修改可以不用存储指向本地对象的指针,它也带来了动态内存管理的负担。另外,只有当我们知道要放到parking_lot中的对象是静态类型后,这种方法才能起作用。如果不知道又会怎样?例如,假设我们想让parking_lot[p]指向一个新建的Vehicle,这个Vehicle的类型和值与由parking_lot[q]指向的对象相同,情况会是怎样?我们不能使用这样的语句

if(p != q){
	delete parking_lot[p];
	parking_lot[p] = parking_lot[q];
}

       因为接下来parking_lot[p]和parking_lot[q]将指向相同的对象。我们也不能使用这样的语句

if(p != q){
	delete parking_lot[p];
	parking_lot[p] = new Vehicle(parking_lot[q]);
}

       因为这样我们又会回到前面的问题:没有Vehicle类型的对象,即使有,也不是我们想要的! 

    
2.    虚复制函数

       让我们想一个办法来复制编译时类型未知的对象。我们知道,C++中处理未知类型的对象的方法就是使用虚函数。这种函数的一个显而易见的名字就是copy,clone也可以,不过稍微有点似是而非。

       由于我们是想能够复制任何类型的Vehicle,所以应该在Vehicle类中增加一个合适的纯虚函数:

class Vehicle{
public:
	virtual double weight() const = 0;
	virtual void start() = 0;
	virtual Vehicle* copy() const = 0;
	//...
};

       接下来,在每个派生自Vehicle的类中添加一个新的成员函数copy。指导思想就是,如果vp指向某个继承自Vehicle的不确定类的对象,则vp->copy()会获得一个指针,该指针指向该对象的一个新建副本。例如,如果Truck继承自(直接或者间接地)类Vehicle,则它的copy函数就类似于:

Vehicle* Truck::copy() const
{
	return new Truck(*this);
}

       当然,处理完一个对象后,需要清除该对象。要做到这一点,就必须确保类Vehicle有一个虚析构函数:

class Vehicle{
public:
	virtual double weight() const = 0;
	virtual void start() = 0;
	virtual Vehicle* copy() const = 0;
	virtual ~Vehicle() {}
	//...
};

4.    定义代理类

       我们已经理解了根据需要复制对象的方法。现在,来看看内存分配。有没有一种方法既能使我们避免显式地处理内存分配,又能保持类Vehicle在运行时绑定的属性呢?

       解决这个问题的关键是要用类来表示概念,这在C++中是很常见的。我总把这一点当作最基本的C++设计原则。在复制对象的过程中运用这个设计原则,就是定义一个行为和Vehicle对象相似、而又潜在地表示例所有继承自Vehicle类的对象的东西。我们把这种类的对象叫做代理(surrogate)。

       每个Vehicle代理都代表某个继承自Vehicle类的对象。只要该代理关联着这个对象,该对象就肯定存在。因此,复制代理就会复制相应的对象,而给代理赋新值也会先删除旧对象、再复制新对象(Dag Bruck指出,这种处理方式与我们所预期的赋值行为稍微有所不同,因为这种方式改变了左侧的代理类实际关联的那个对象的类型)。所幸的是,我们在类Vehicle中已经有了虚函数copy来完成这些复制工作。所以,我们可以开始定义自己的代理了:

class VehicleSurrogate{
public:
	VehicleSurrogate();
	VehicleSurrogate(const Vehicle&);
	~VehicleSurrogate();
	VehicleSurrogate(const VehicleSurrogate&);
	VehicleSurrogate& operator=(const VehicleSurrogate&);

private:
	Vehicle* vp;
};

       上述代理类有一个以const Vehicle&为参数的构造函数,这样就能为任意继承自Vehicle的类的对象创建代理类。同时,代理类还有一个缺省构造函数,所以我们能够创建VehicleSurrogate对象的数组。

       然而,缺省构造函数也会给我们带来了问题:如果Vehicle是个抽象类,我们应该如何规定VehicleSurrogate的缺省操作?它所指向的对象的类型是什么?不可能是Vehicle,因为根本就没有Vehicle对象。

       为了得到一个更好的方法,我们要引入行为类似于零指针的空代理(empty surrogate)的概念。能够创建、销毁、和复制这样的代理,但是进行其他操作就视为出错。

       到目前为止,不再需要任何其他的操作了,这就使得我们能很容易地写出成员函数的定义:

VehicleSurrogate::VehicleSurrogate(): vp(0) {}
VehicleSurrogate::VehicleSurrogate(const Vehicle& v): vp(v.copy()) {}
VehicleSurrogate::~VehicleSurrogate()
{
	delete vp;
}

VehicleSurrogate::VehicleSurrogate(const VehicleSurrogate& v): vp(v.vp? v.vp->copy(): 0) {}
VehicleSurrogate& VehicleSurrogate::operate=(const VehicleSurrogate& v)
{
	if(this != &v)
	{
		delete vp;
		vp = (v.vp ? v.vp->copy() : 0);
	}
	return *this;
}

       这里有3个技巧值得我们注意。

       首先,注意每次对copy的调用都是一个虚拟调用。这些调用只能是虚拟的,别无选择,因为类Vehicle的对象不存在。即使是在那个只接收一个const Vehicle&参数的复制构造函数中,它所进行的v.copy调用也是一次虚拟调用,因为v是一个引用而不是一个普通对象。

       其次,注意关于复制构造函数和赋值操作符中的v.vp非零的检测。这个检测是必需的,否则调用v.vp->copy时就会出错。

       再次,注意对赋值操作符进行检测,确保没有将代理赋值给它自身。

       下面剩下的工作只是另该代理类支持Vehicle所能支持的其他操作了。在前面的例子中,有weight和start,所以要把它们加入到类VehicleSurrogate中:

class VehicleSurrogate{
public:
	VehicleSurrogate();
	VehicleSurrogate(const Vehicle&);
	~VehicleSurrogate();
	VehicleSurrogate(const VehicleSurrogate&);
	VehicleSurrogate& operate=(const VehicleSurrogate&);
	// 来自类Vehicle的操作
	double weight() const;
	void start();
	//...
private:
	Vehicle* vp;
};

       注意这些函数都不是虚拟的:我们这里所使用的对象都是类VehicleSurrogate的对象;没有继承自该类的对象。当然,函数本身可以调用相应Vehicle对象中的虚函数。它们也应该检查确保vp不为零:

double VehicleSurrogate::weight() const
{
	if(vp == 0)
		throw "empty VehicleSurrogate.weight()";
	vp->weight();
}
double VehicleSurrogate::start() const
{
	if(vp == 0)
		throw "empty VehicleSurrogate.start()";
	vp->start();
}

       一旦我们完成了所有这些工作,就很容易定义我们的停车场(parking_lot)了:

VehicleSurrogate parking_lot[1000];
Automobile x;
Parking_lot[num_vehicles++] = x;

       最后一条语句等价于

parking_lot[num_vehicles++] = VehicleSurrogate(x);

       这个语句创建了一个关于对象x的副本,并将VehicleSurrogate对象绑定到该副本,然后将这个对象赋值给parking_lot一个元素。当最后销毁parking_lot数组时,所有这些副本也将被销毁。

 

总结:

       代理类通过最顶层基类(抽象类)指针获得处理未知类型对象的能力,然后所有的操作函数都在代理类定义,用户直接使用代理类而不是抽象类。此功能最大好处是,能在同一个容器中,通过统一接口(即代理类)操作来自于同一抽象类的派生类,从而使得数据构建简便而无数据损失。

       另一种理解:希望把继承于同一基类的不同派生类放到同一容器中时,可以使用基类指针的办法,但是会出现不同指针指向同一对象的问题,导致指针悬挂风险。为了避免此问题,可以把基类指针嵌套在一个代理类里面,代理类的复制和赋值操作都是通过对被操作对象的拷贝实现,而不是对指针的拷贝,这也就避免了出现多指针指向同一对象的情况。用户使用的是代理类而不是基类指针,操作代理类与操作其代理的对象效果一样。

       这样做的弊端是:类的复制次数无法控制,提高内存空间成本。解决这个问题的一种方法是使用智能指针,通过对指向对象的指针进行计数,来决定创建和销毁对象的时机。当然,如果C++有类似于JAVA的垃圾回收机制,就不用考虑这些问题了,值得注意的是,JAVA垃圾回收机制的早期方法用的正是智能指针(即对对象被引用情况进行计数,以决定是否可以将其回收)。但是此方法在存在类循环引用时无效,因此后期JAVA使用另一种方法,即对非活跃对象通过二叉树进行遍历,遍历不到的即可回收。
 




 

发布了12 篇原创文章 · 获赞 1 · 访问量 1万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章