zoukankan      html  css  js  c++  java
  • 《C++ 沉思录》阅读笔记——代理类

    Andrew Koenig 和 Barbara Moo 堪称C++研究领域的”第一神仙眷侣”,看他们的书非常有条理性。这次要解释的是C++中的另一个常见问题。
    找出一种优美的控制内存分配的方法来绑定不同子类对象到容器中。多么复杂的一句话,莫慌,其实很简单,跟着步伐来看。

    首先假设我们要设计一系列交通工具的类,一般来说我们会定义一个交通工具的基类,里面存放所有交通工具都有的成员和属性,比如这样:

    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;
     

    总结一下:

    当我们使用继承和容器的时候,通常需要处理两个问题:内存的分配编译时类型未知对象的绑定使用一个被成为代理类的东西,我们把复杂的继承层次压缩到了一起,让这个类能够代表所有的子类型,用类来表示概念的武器果然犀利。

  • 相关阅读:
    Git 自救指南:这些坑你都跳得出吗?
    敢不敢模拟超过 5 万的并发用户?
    一条简单的 SQL 执行超过 1000ms,纳尼?
    JVM 最多支持多少个线程?
    19 条效率至少提高 3 倍的 MySQL 技巧
    LeetCode 剑指offer 面试题04. 二维数组中的查找
    LeetCode 剑指offer 面试题03 数组中重复的数字
    东华大学计算机软件工程 复试最后一百题
    东华大学计算机软件工程复试 挑战练习
    东华大学计算机软件工程复试 进阶练习
  • 原文地址:https://www.cnblogs.com/xiangcunjiaoshi/p/12541698.html
Copyright © 2011-2022 走看看