首先假设我们要设计一系列交通工具的类,一般来说我们会定义一个交通工具的基类,里面存放所有交通工具都有的成员和属性,比如这样:
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 parking_lot[1000];
仔细一想,这么写好像不对,为什么呢?因为 Vehicle 里面有纯虚函数,所以 Vehicle 是个抽象类,抽象类是不会有对象的,所以这么定义是肯定不行的。一般分析也就到这里为止了,但继续想一下,如果我把 Vehicle 中的所有纯虚函数去掉,那么这种定义好像就是OK的,语法上不会有问题,但是有另一个问题,比如下面这样的赋值:
Helicopter x = /* ...... */
parking_lot[num_vehicles++] = x;
这样的赋值会导致 Helicopter 对象被转换成一个Vehicle对象,它将丢失自己的 Helicopter 属性,这可不是我们想要的,这就好像把一个 double 数转换成整型放进×××数组里,丢失了自己的小数部分。
看到这里,马上有人会提出,那么在 parking_lot 中存储 Vehicle 的指针不就可以了吗?我们一起来看看:
Vehicle *parking_lot[1000]; // 指针数组
然后我们重复上面的赋值操作:
Helicopter x = /* ...... */
parking_lot[num_vehicles++] = &x;
看起来一切OK,但是有经验的程序员(比如说我,:))一眼就看出这里很危险,为什么危险呢?因为存储指针本身就是一件危险的事情,具体说来,这里的 x 看起来是一个局部变量,如果 x 被释放掉了,那么 parking_lot 数组里的指针立马成了悬垂指针,指向什么内容就不知道了。一个富有责任心的程序员是铁定不会这么干的。
那我们是不是就没折了呢?也不是,既然放指针不行,那么我复制一下这个对象算了,如下:
Helicopter x = /* ...... */
parking_lot[num_vehicles++] = new Helicopter(x);
虽然浪费了些时间和内存,但是这么做看起来确实可以,自己分配了内存当然要由自己来释放,所以我们继续规定在 delete 这个 parking_lot 的时候,我们也释放其中所指向的对象。如果这么干只有自己管理内存这么一个负担的话,我想我还能接受,但是这里有一个不那么明显的问题。就是我们放入 parking_lot 中的对象,必须要是已知类型的对象,一说到这里有的看官就立马明白了我的意思了,也就是说对于那些编译时类型未知的对象,这里就没办法保存了,举个例子,比如我需要在 parking_lot[p] 中放 parking_lot[q] 的对象,该怎么办呢?我们并不知道 parking_lot[q] 的对象类型,所以我们没办法复制这个对象,同时,我们不能让 parking_lot 中有两个指针指向同一个对象,因为我们在删除这个容器时会把里面的对象也删掉,如果有两个指针指向同一个对象那么就会删除两次。当然,你可以用别的方法来避免,但这还是让我无法忍受了。
对于编译时的未知对象,聪明的程序员已经想到办法解决了。为什么我们要知道它们是什么?只要它们自己知道自己是什么,然后告诉我们就OK了呗!good boy!说明白些,就是我们可以让继承自 Vehicle 的类来告诉别人他们到底是什么,一个简单的办法就是在 Vehicle 中定义的 copy 的纯虚函数,然后继承自 Vehicle 的类都设计自己的 copy 函数,用来把自己复制一份返回给调用者,这样调用者就不用知道这些乱七八糟的交通工具是什么了。我们来继续修改代码:
class Vehicle {
public:
virtual double weight() const = 0;
virtual void start() = 0;
virtual Vehicle *copy() const = 0;
// ......
};
然后我们修改 Helicopter 类,增加一个 copy 函数:
Vehicle *Helicopter::copy() const
{
return new Helicopter(*this);
}
这样我们就再也不需要知道x的类型或者是 parking_lot[q] 的类型了,直接调用 x.copy() 函数或者 parking_lot[q]->copy() 函数就OK了。
parking_lot[num_vehicles++] = x.copy();
parking_lot[p] = parking_lot[q]->copy();
我们完美的解决了上面提到的第二个问题,但程序员从来都是追求完美的,那么我们有办法解决这个显示处理内存分配的问题吗?这也是程序员幸福的地方,别的领域追求完美是极其困难的,但代码总能让我们欣喜。《C++ 沉思录》里提到了一个非常深刻的概念——“用类来表示概念”,到底是个什么意思呢?就是说我们设计类,不光可以是一个具体的事物,同样,也可以是一个概念,比如,你可以用类来表示人,男人,女人等等,同样你可以用类来表示家庭,人是具体的,而家庭只是一个概念,家庭里肯定有有人,所以把控了家庭这个概念,也就把控了人(不要跟我抬杠说有些人没有家庭,举个例子而已,亲!)。
具体表现在代码上就是我们通过定义一个代理类,来表达这些不同的交通工具,这个代理类应该可以代表不同的交通工具,同时它需要帮助我管理内存,而且需要能够实例化,因为这样我就不用再纠结上面那个 Vehicle 是抽象类没办法定义容器的问题,所以,这个代理类的作用是让我能够定义代理类的容器,同时不需要我来考虑内存的管理问题,而且要支持编译时类型未知的情况。
代理类只是一个管理交通工具的管理者,它不是一个具体的东西,就跟大明星的经纪人一样。那看来它必须保存一个明星,也就是得有一个指向交通工具的指针,同时它需要上台面,那么它需要真实的构造函数,同时它需要能够放进容器,所以它需要一个默认构造函数:
class VechicleProxy {
public:
VechicleProxy();
VechicleProxy(const Vehicle &);
~VechicleProxy();
VechicleProxy(const VechicleProxy &);
VechicleProxy &operator=(const VechicleProxy &);
private:
Vehicle *p;
};
上面多加了几个构造函数和赋值操作符,也不难理解,毕竟是一个真实的类嘛。其中以 const Vehicle& 为参数的复制构造函数就提供了为任意交通工具做代理的能力。一切看起来OK,但是在默认构造函数里我们能够为 p 指针赋值什么呢?好像只能赋为0了。这个零指针也就是说通常说的空代理。那么让我们来完成这个代理类的成员函数吧:
VechicleProxy::VechicleProxy(): p(0) { }
VechicleProxy::VechicleProxy(const Vehicle &BigStar): p(BigStar.copy()) {}
VechicleProxy::~VechicleProxy() { delete p; }
VechicleProxy::VechicleProxy(const VechicleProxy &v): p(v.p ? v.p->copy() : 0) {}
VechicleProxy::operator=(const VechicleProxy &v)
{
if (this != &v)
{
delete p;
p = (v.p ? v.p->copy() : 0);
}
return *this;
}
这里没有什么多余的秘密了,仔细点都OK。写到这里我们终于可以定义一个完美的 parking_lot 了。
VehicleProxy parking_lot[1000];
Helicopter x;
parking_lot[num_vehicles++] = x;
总结一下:
当我们使用继承和容器的时候,通常需要处理两个问题:内存的分配和编译时类型未知对象的绑定。使用一个被成为代理类的东西,我们把复杂的继承层次压缩到了一起,让这个类能够代表所有的子类型,用类来表示概念的武器果然犀利。