主题:读C++c沉思录-第5章 代理
简单的说:代理(Surrogate)运行起来和它所代表的对象基本对象相同,但是允许将整个派生层次压缩在一个对象类型中.surrogate是handle类中最简单的一种.
使用类的抽象,不仅仅是一种方法和技巧,不仅仅是解决问题时的一种下意识的行为,而应当成为一种思想,成为编程时时刻要遵循的一种思想,从而让这种技巧从实践的层面上升成为一种理论上的思考.
好了,让我们来看一下一个伟大的代理类是如何从作者笔下诞生的吧!(作者不愧是个教授C++的高手)
1,首先,是创建一个表示不同交通工具的类体系(类的派生层次),这个相信学过一个C++的人都能写出来.
当然有一个基类,命名为Vehicle,这个Vehicle是个虚基类(你不会还有别的想法吧),它包含了一些交通工具的共同操作和属性.代码象下面这样:
class Vehicle
{
virtual double weight() const = 0; // 交通工具有自重
virtual void start() = 0; // 交通工具当然也能跑起来
virtual void stop() = 0; // 交通工具当然也能停下来
...... // 还有许多的操作,当然包括构造和析构
}
好了,基类有了,其他的交通工具就从这里派生吧.于是,有了:
class RoadVehicle: public Vehicle{......} // 直接派生Car或者Truck等具体类显然不是好主意
class AutoVehicle: public RoadVehicle{......} // 这样更有层次
class Aircraft: public Vehicle{......} // 这个是空中的
class Helicopter: public Aircraft{......} // 终于到了具体的类
class WaterVehicle: public Vehicle{......} // 再来个水里的
class Boat: public WaterVehicle{......} // 小船
..... // 当然水路空都有了,具体的类可以有成千上万种.但基本都可以从上面的类来派生.
类体系有了,现在我们要保存和处理这个类体系中的任意一种的交通工具.我们很自然会用基类数组去保存,你肯定不会为每个类建立一个数组.于是有了:
Vehicle parking_lot[1000]; //先假设有一千个吧
但是这么做是有问题的,首先,Vehicle是个虚基类,它没有具体的对象,当然也就无法有对象的数组.而且当你把子类的对象赋值给基类的对象时,因为子类的对象通常比基类大,于是子类被裁减了.也就是说,子类被修剪成基类的大小.你不知道这会会给你程序带来什么.
既然用对象不行,实际中经常使用的是指针.OK,是这样,Vehicle* parking_lot[1000];编译的确是没有问题了.但是用起来是比较麻烦的.
如果这样用:
AutoMobile x = ...; // 具体代码省略
Parking_lot[num_Of_Vehicle++] = &x; // 这句没问题,问题是如果以后对象没有了,指针指到哪里去了?
好,我们再想办法,改成这样:
Parking_lot[num_Of_Vehicle++] = new Automobile(x); // 这个办法还行,但是以后这个数组归你自己管了,自己负责释放所有的指针,不能漏掉.
尤其还有一个麻烦,如果要让数组中的一个元素,如Parking_lot[p],等于数组中的另一个元素Parking_lot[q],怎么办呢?仿佛很容易,很多人提起笔就能写:
delete Parking_lot[p]; // 先删除原指针
Parking_lot[p] = Parking_lot[q]; // 赋值简单吧,有问题吗?
有~!!!
Parking_lot[p] 和 Parking_lot[q]指向了相同的对象,删除一个指针Parking_lot[p]的时候,另一个Parking_lot[q]指向了哪里呢?如果你还使用那个指针Parking_lot[q]的话,结果将无法预料.
有人还想改良一下,改成这样:
delete Parking_lot[p]; // 先删除原指针
Parking_lot[p] = new Vehicle(Parking_lot[q]); // 新创建一个指针,总可以吧?
不可以,别忘了Vehicle是虚基类.我们也不知道Parking_lot中的指针是指向的是什么对象.如果你要自己搞一套进行运行时类型识别的东西,那我就没辙了.(你的主意很酷,但是不是用在这里的)
好,怎么办?只要想一个办法,复制这个对象就可以了.在对象外面new不行,我们就在对象里面实现.不过既然要支持所有的Vehicle,那么最好就是在虚基类里声明这个接口了.于是就添加一个纯虚函数:
virtual Vehicle* copy() const = 0;
好的,在每个派生类里实现它吧.具体代码象这样:
Vehicle* Truck::copy() const
{
return new Truck(*this);
}
好了,问题解决了.现在我们提出一个更高的要求,能不能避免显式的处理内存分配,同时又保持运行时绑定的属性呢?
作者说,用类来表示概念.这是C++设计的基本原则.在复制对象的过程中运用这个设计原则,就是定义一个行为和Vehicle对象类似,而又潜在的表示所有继承自Vehicle类的对象的东西.我们称这种类的对象叫做代理.
这个代理有哪些性质呢?首先,每个代理类都代表某个继承自基类的对象.只要代理还关联着这个对象,该对象就一定存在.对代理的操作也就相应的对这个对象进行操作.
好了,我们知道了代理该做些什么事情,那么这个代理类就可以这么定义:
class VehicleSurrogate
{
public:
VehicleSurrogate(); // 缺省构造函数,这样就可以构建数组
VehicleSurrogate(const Vehicle&); // 从Vehicle对象构造一个代理,没有问题
~VehicleSurrogate();
VehicleSurrogate(const VehicleSurrogate&); // 拷贝构造函数,容易构建一个新对象
VehicleSurrogate& operator=(const VehicleSurrogate&); // 赋值操作符
private:
Vehicle* vp; // 真正的代理的对象
}
缺省构造函数会带来一个问题,就是如何规定VehicleSurrogate的缺省操作.缺省的情况下它所指向的对象类型是什么?不能是Vehicle对象.
于是引入类似于零指针的空代理(empty surrogate),空代理可以被创建,销毁和复制,不能进行其他操作.
根据这些要求,来写出成员函数的定义:
VehicleSurrogate::VehicleSurrogate():vp(0) {} // 缺省构造
VehicleSurrogate::VehicleSurrogate(const Vehicle& v)
: vp(v.copy()) { } // 是copy,不是v,因为参数是引用,所以copy函数是动态绑定的
VehicleSurrogate::VehicleSurrogate(const VehicleSurrogate& v)
:vp(v.vp?v.vp->copy():0) { }
// 这里进行了参数指针的非零检测,防止copy的调用失败.
VehicleSurrogate::~VehicleSurrogate()
{
delete vp;
}
VehicleSurrogaet& VehicleSurrogate::operator=(const VehicleSurrogate& v)
{
if(this != &v) // 防止赋值给自己
{
delete vp;
vp = (v.vp? v.vp->copy() : 0);
}
return *this;
}
然后,还要支持被代理的类的其他操作,如:
double weight() const;
void start();
void stop();
......
他们的定义也很简单,主要是将操作传递到具体的Vehicle对象,代码象下面这样:
double VehicleSurrogate::weight() const
{
if(vp == 0)
{
throw "empty VehicleSurrogate.weight()"; // 这里抛出异常了
}
return vp->weight(); // 把操作传递给具体的Vehicle指针
}
到现在,我们终于拥有了神奇的代理类.
我们有了代理类,那么就要重新定义一下我们的Parking_lot[]了,改成这样:
VehicleSurrogate Parking_lot[1000];
Automobile x;
Parking_lot[num_of_Vehicle++] = x;
OK,代码里面没有指针,你可以象使用基本数据类型一样使用他们.
使用类的抽象,不仅仅是一种方法和技巧,不仅仅是解决问题时的一种下意识的行为,而应当成为一种思想,成为编程时时刻要遵循的一种思想,从而让这种技巧从实践的层面上升成为一种理论上的思考.
好了,让我们来看一下一个伟大的代理类是如何从作者笔下诞生的吧!(作者不愧是个教授C++的高手)
1,首先,是创建一个表示不同交通工具的类体系(类的派生层次),这个相信学过一个C++的人都能写出来.
当然有一个基类,命名为Vehicle,这个Vehicle是个虚基类(你不会还有别的想法吧),它包含了一些交通工具的共同操作和属性.代码象下面这样:
class Vehicle
{
virtual double weight() const = 0; // 交通工具有自重
virtual void start() = 0; // 交通工具当然也能跑起来
virtual void stop() = 0; // 交通工具当然也能停下来
...... // 还有许多的操作,当然包括构造和析构
}
好了,基类有了,其他的交通工具就从这里派生吧.于是,有了:
class RoadVehicle: public Vehicle{......} // 直接派生Car或者Truck等具体类显然不是好主意
class AutoVehicle: public RoadVehicle{......} // 这样更有层次
class Aircraft: public Vehicle{......} // 这个是空中的
class Helicopter: public Aircraft{......} // 终于到了具体的类
class WaterVehicle: public Vehicle{......} // 再来个水里的
class Boat: public WaterVehicle{......} // 小船
..... // 当然水路空都有了,具体的类可以有成千上万种.但基本都可以从上面的类来派生.
类体系有了,现在我们要保存和处理这个类体系中的任意一种的交通工具.我们很自然会用基类数组去保存,你肯定不会为每个类建立一个数组.于是有了:
Vehicle parking_lot[1000]; //先假设有一千个吧
但是这么做是有问题的,首先,Vehicle是个虚基类,它没有具体的对象,当然也就无法有对象的数组.而且当你把子类的对象赋值给基类的对象时,因为子类的对象通常比基类大,于是子类被裁减了.也就是说,子类被修剪成基类的大小.你不知道这会会给你程序带来什么.
既然用对象不行,实际中经常使用的是指针.OK,是这样,Vehicle* parking_lot[1000];编译的确是没有问题了.但是用起来是比较麻烦的.
如果这样用:
AutoMobile x = ...; // 具体代码省略
Parking_lot[num_Of_Vehicle++] = &x; // 这句没问题,问题是如果以后对象没有了,指针指到哪里去了?
好,我们再想办法,改成这样:
Parking_lot[num_Of_Vehicle++] = new Automobile(x); // 这个办法还行,但是以后这个数组归你自己管了,自己负责释放所有的指针,不能漏掉.
尤其还有一个麻烦,如果要让数组中的一个元素,如Parking_lot[p],等于数组中的另一个元素Parking_lot[q],怎么办呢?仿佛很容易,很多人提起笔就能写:
delete Parking_lot[p]; // 先删除原指针
Parking_lot[p] = Parking_lot[q]; // 赋值简单吧,有问题吗?
有~!!!
Parking_lot[p] 和 Parking_lot[q]指向了相同的对象,删除一个指针Parking_lot[p]的时候,另一个Parking_lot[q]指向了哪里呢?如果你还使用那个指针Parking_lot[q]的话,结果将无法预料.
有人还想改良一下,改成这样:
delete Parking_lot[p]; // 先删除原指针
Parking_lot[p] = new Vehicle(Parking_lot[q]); // 新创建一个指针,总可以吧?
不可以,别忘了Vehicle是虚基类.我们也不知道Parking_lot中的指针是指向的是什么对象.如果你要自己搞一套进行运行时类型识别的东西,那我就没辙了.(你的主意很酷,但是不是用在这里的)
好,怎么办?只要想一个办法,复制这个对象就可以了.在对象外面new不行,我们就在对象里面实现.不过既然要支持所有的Vehicle,那么最好就是在虚基类里声明这个接口了.于是就添加一个纯虚函数:
virtual Vehicle* copy() const = 0;
好的,在每个派生类里实现它吧.具体代码象这样:
Vehicle* Truck::copy() const
{
return new Truck(*this);
}
好了,问题解决了.现在我们提出一个更高的要求,能不能避免显式的处理内存分配,同时又保持运行时绑定的属性呢?
作者说,用类来表示概念.这是C++设计的基本原则.在复制对象的过程中运用这个设计原则,就是定义一个行为和Vehicle对象类似,而又潜在的表示所有继承自Vehicle类的对象的东西.我们称这种类的对象叫做代理.
这个代理有哪些性质呢?首先,每个代理类都代表某个继承自基类的对象.只要代理还关联着这个对象,该对象就一定存在.对代理的操作也就相应的对这个对象进行操作.
好了,我们知道了代理该做些什么事情,那么这个代理类就可以这么定义:
class VehicleSurrogate
{
public:
VehicleSurrogate(); // 缺省构造函数,这样就可以构建数组
VehicleSurrogate(const Vehicle&); // 从Vehicle对象构造一个代理,没有问题
~VehicleSurrogate();
VehicleSurrogate(const VehicleSurrogate&); // 拷贝构造函数,容易构建一个新对象
VehicleSurrogate& operator=(const VehicleSurrogate&); // 赋值操作符
private:
Vehicle* vp; // 真正的代理的对象
}
缺省构造函数会带来一个问题,就是如何规定VehicleSurrogate的缺省操作.缺省的情况下它所指向的对象类型是什么?不能是Vehicle对象.
于是引入类似于零指针的空代理(empty surrogate),空代理可以被创建,销毁和复制,不能进行其他操作.
根据这些要求,来写出成员函数的定义:
VehicleSurrogate::VehicleSurrogate():vp(0) {} // 缺省构造
VehicleSurrogate::VehicleSurrogate(const Vehicle& v)
: vp(v.copy()) { } // 是copy,不是v,因为参数是引用,所以copy函数是动态绑定的
VehicleSurrogate::VehicleSurrogate(const VehicleSurrogate& v)
:vp(v.vp?v.vp->copy():0) { }
// 这里进行了参数指针的非零检测,防止copy的调用失败.
VehicleSurrogate::~VehicleSurrogate()
{
delete vp;
}
VehicleSurrogaet& VehicleSurrogate::operator=(const VehicleSurrogate& v)
{
if(this != &v) // 防止赋值给自己
{
delete vp;
vp = (v.vp? v.vp->copy() : 0);
}
return *this;
}
然后,还要支持被代理的类的其他操作,如:
double weight() const;
void start();
void stop();
......
他们的定义也很简单,主要是将操作传递到具体的Vehicle对象,代码象下面这样:
double VehicleSurrogate::weight() const
{
if(vp == 0)
{
throw "empty VehicleSurrogate.weight()"; // 这里抛出异常了
}
return vp->weight(); // 把操作传递给具体的Vehicle指针
}
到现在,我们终于拥有了神奇的代理类.
我们有了代理类,那么就要重新定义一下我们的Parking_lot[]了,改成这样:
VehicleSurrogate Parking_lot[1000];
Automobile x;
Parking_lot[num_of_Vehicle++] = x;
OK,代码里面没有指针,你可以象使用基本数据类型一样使用他们.