c++ 沉思录——代理类

代理类

引言

怎么设计一个c++容器,使它有能力包含类型不同而彼此相关的对象呢?
容器通常只能包含一种类型的对象,所以很难在容器中存储对象本身。
存储指向对象的指针,虽然允许通过继承来处理类型不同的问题,但是也增加了内存分配的额外负担。
这里讨论一种方法,通过定义名为 代理(surrogate) 的对象来解决该问题。代理运行起来和它所代表的对象基本相同,
但是允许将整个派生层次压缩在一个对象类型中。

5.1 问题

假设有不同种类的交通工具的类派生层次,不同的派生类都有一些基类的共有属性,但也有一些自己独有的属性(飞翔、盘旋等)。

// 基类:Vehicle  
// 派生类:RoadVehicle AutoVehicle Aircraft ...   
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 Hilicopter : public Aircraft { /*...*/ }; // 直升飞机

下面假设我们要处理一系列不同种类的Vehicle
在实际开发中我们可能会使用某种容器类vector<Vehicle> parking_lot;
但是为了表述更简洁,使用数组来实现Vehicle parking_lot[1000]; //parking lot 停车场
但有一个问题,由于 Vehicle 是纯虚函数,所以不可能实例化出来对象,当然不可能存在 Vehicle 对象数组了!

5.2 经典解决方案

常见做法:提供一个间接层(indirection)。
最早的合适的间接层形式就是存储指针,而不是对象本身:Vehicle* parking_lot[1000]; //指针数组
然后输入以下代码:

Vehicle* parking_lot[1000];
int num_vehicles = 0;
Automobile auto_mobile = /*...*/;    
Parking_lot[num_vehicles++] = &auto_mobile;  

带来了两个新问题:
问题1: 首先 auto_mobile 是局部变量,一旦 auto_mobile 没有了,parking_lot 就不知道指向哪里了。
变通:放入parking_lot中的值,不是指向原对象的指针,而是指向原对象的副本的指针。
然后,采用一个约定,就是当我们释放parking_lot时,也释放其中所指向的全部对象。因此,把前面例子改为:

int num_vehicles = 0;
Automobile auto_mobile = /*...*/;    
Parking_lot[num_vehicles++] = new Automobile(auto_mobile); 

尽管这样修改可以不用存储指向本地对象的指针,但是它也带来了动态内存管理的负担。
另外,只有当我们知道要放到 parking_lot 中的对象是静态类型后,这种方法才能起作用。(这是为什么?)

//静态类型:一个对象(变量)的静态类型就是其声明类型,如表达式int a中的int就是对象a的声明类型,即静态类型;
//动态类型:一个对象(变量)的动态类型就是指程序执行过程中对象(指针或引用)实际所指对象的类型
Base* pB = new Drived;
// pB的静态类型就是 Base,动态类型就是 Drived

如果不知道会怎么样?例如,假设我们想让parking_lot[p]指向一个新建的 Vehicle,这个 Vehicle 的类型和值与parking_lot[q]指向的
对象相同,情况会怎么样?那么,我们将不可以使用以下的语句:

#include <iostream>

using namespace std;

class Vehicle {
public:
    virtual double weight() const = 0;
    virtual void start() = 0;
    virtual ~Vehicle() {
        cout << "virtual ~Vehicle()" << endl;
    }
};

// 陆地车
class RoadVehicle : public Vehicle {
    double weight() const override {
        return 100;
    }
    void start() override {
        cout << "RoadVehicle start()" << endl;
    }
};

// 汽车
class Automobile : public RoadVehicle {
    double weight() const override {
        return 100;
    }
    void start() override {
        cout << "Automobile start()" << endl;
    }
};

int main () {
    Vehicle* parking_lot[1000];
    int p = 0, q = 1;
    Automobile auto_mobile_p;
    parking_lot[p] = new Automobile(auto_mobile_p);
    Automobile auto_mobile_q;
    parking_lot[q] = new Automobile(auto_mobile_q);

    if(p != q) { // 不相等
        delete parking_lot[p];
        parking_lot[p] = parking_lot[q]; // 现在 parking_lot[p] 指向了 parking_lot[q]
    }
    cout << "parking_lot[p] = " << parking_lot[p] << endl; // 0x1d1790
    cout << "parking_lot[q] = " << parking_lot[q] << endl; // 0x1d1790
}

因为接下来parking_lot[p] 和 parking_lot[q]将指向相同的对象;我们也不可以使用以下的语句:

if(p != q) {
    delete parking_lot[p];
    parking_lot[p] = new Vehicle(parking_lot[q]); // 没有Vehicle类型
}  

5.3 虚复制函数

想一个办法:来复制编译时类型未知的对象
我们知道,c++中处理未知类型的对象的方法就是使用虚函数。这种函数的一个显而易见的名字就是copy or clone。
由于我们是想能够复制任何类型的Vehicle,所以应该在Vehicle种增加一个合适的纯虚函数:

class Vehicle {
public:
    virtual double weight() const = 0; // 函数后加const,这些成员函数不改变类的数据成员,也就是说,这些函数是"只读"函数
    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); // *this 对象的值
}  

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

#include <iostream>
#include <memory>

using namespace std;

class Vehicle {
public:
    Vehicle() {
        cout << "Vehicle 构造函数" << endl;
    }
    virtual ~Vehicle() {
        cout << "Vehicle 析构函数" << endl;
    }
public:
    virtual Vehicle* copy() const = 0;
    virtual void say() const = 0;
};

class Truck : public Vehicle {
public:
    Truck() {
        cout << "Truck 构造函数" << endl;
    }
    ~Truck() override {
        cout << "Truck 析构函数" << endl;
    }
public:
    Vehicle* copy() const override {
        cout << "Truck copy()" << endl;
        return new Truck(*this);
    }
    void say() const override {
        cout << "Truck say" << endl;
    }

};

int main() {
    auto vp = make_shared<Truck>();
    auto vehicle = vp->copy();
    vehicle->say();
}

5.4 定义代理类

有没有一种方法:既能使我们避免显示地处理内存分配,又能保持Vehicle类在运行时绑定的属性呢?

答案:定义一个行为和Vehicle对象相似、而又潜在地表示了所有继承自Vehicle类的对象的东西。我们把这种类的对象叫做 代理(surrogate)。

每个Vehicle代理都代表某个继承自Vehicle类的对象。只要该代理关联着这个对象,该对象肯定存在。因此,复制代理就会复制相对应的对象,而给代理赋新值也会先删除旧对象,在复制新对象。

class VehicleSurrogate {
public:
    VehicleSurrogate() = default;
    VehicleSurrogate(const Vehicle&);
    ~VehicleSurrogate() = default;
    VehicleSurrogate(const VehicleSurrogate&);
    VehicleSurrogate& operator=(const VehicleSurrogate&);
    
private:
    Vehicle *vp;
}
  • 上述代理类有一个以 const Vehicle& 为参数的构造函数,这样就可以为任何继承自 Vehicle类的对象创建代理了。
  • 同时,代理类还有一个默认的构造函数,所以可以创建 VehicleSurrogate 对象的数组了。

默认的构造函数带来了新问题:
如果 Vehicle 是抽象基类,我们应该如何规定 VehicleSurrogate 的默认操作呢?
它所指向的对象的类型是什么呢?不可能是 Vehicle,因为根本没有 Vehicle对象。

引入空代理(empty surrogate)
为了有更好的办法,我们要引入行为类似零指针的 空代理(empty surrogate) 的概念。能够创建、销毁和复制这样的代理,
但是进行其他操作就视为出错。
ok,下面是成员函数的定义:

class VehicleSurrogate {
public:
    VehicleSurrogate() : vp(nullptr) {
        cout << "VehicleSurrogate 默认构造函数" << endl;
    }
    explicit VehicleSurrogate(const Vehicle& v) : vp(v.copy()) {
        cout << "VehicleSurrogate(const Vehicle& v) " << endl;
    }
    ~VehicleSurrogate() {
        cout << "VehicleSurrogate 析构函数" << endl;
        delete vp;
    }
    VehicleSurrogate(const VehicleSurrogate& v) : vp(v.vp ? v.vp->copy() : nullptr) {
        cout << "VehicleSurrogate 复制构造函数" << endl;

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

private:
    Vehicle *vp;
};

3个技巧要注意:

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

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

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

下面令该代理类支持 Vehicle类所能支持的其他操作。我们把start() 和 weight() 加入类VehicleSurrogate 中。

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

class VehicleSurrogate {
public:
    VehicleSurrogate() = default;
    VehicleSurrogate(const Vehicle&);
    ~VehicleSurrogate() = default;
    VehicleSurrogate(const VehicleSurrogate&);
    VehicleSurrogate& operator=(const VehicleSurrogate&);
    
public: // 类Vehicle支持的操作
    double weight() const {
        if (!vp)
            throw "empty VehicleSurrogate.weight()";
        return vp->weight();
    }
    void start() const {
        if (!vp)
            throw "empty VehicleSurrogate.start()";
        return vp->start();
    }
    // ...

private:
    Vehicle *vp;
}

接下来,我们就很容易定义我们的停车场了(parking_lot)

VehicleSurrogate parking_lot[5];
Truck x;
parking_lot[2] = VehicleSurrogate(x);

最后这个语句创建了一个关于对象x的副本,并将 VehicleSurrogate 对象绑定到该副本。

5.5 小结

将继承和容器共用,我们需要解决两个问题:

  • 控制内存分配
  • 把不同类型的对象放入同一个容器中

提出代理类:这个类的每个对象都代表另一个对象,该对象可以是位于一个完整继承层次中的
任何类的对象。

5.6 完整代码

#include <iostream>
#include <memory>

using namespace std;

class Vehicle {
public:
    Vehicle() {
        cout << "Vehicle 构造函数" << endl;
    }
    virtual ~Vehicle() {
        cout << "Vehicle 析构函数" << endl;
    }

public:
    virtual Vehicle* copy() const = 0;
    virtual void say() const = 0;
    virtual double weight() const = 0;
    virtual void start() const = 0;
};

class Truck : public Vehicle {
public:
    Truck() {
        cout << "Truck 构造函数" << endl;
    }
    ~Truck() override {
        cout << "Truck 析构函数" << endl;
    }
public:
    Vehicle* copy() const override {
        cout << "Truck copy()" << endl;
        return new Truck(*this);
    }
    void say() const override {
        cout << "Truck say" << endl;
    }
    double weight() const override {
        cout << "Truck weight" << endl;
    }
    void start() const override {
        cout << "Truck start" << endl;
    }

};

class VehicleSurrogate {
public:
    VehicleSurrogate() : vp(nullptr) {
        cout << "VehicleSurrogate 默认构造函数" << endl;
    }
    explicit VehicleSurrogate(const Vehicle& v) : vp(v.copy()) {
        cout << "VehicleSurrogate(const Vehicle& v) " << endl;
    }
    ~VehicleSurrogate() {
        cout << "VehicleSurrogate 析构函数" << endl;
        delete vp;
    }
    VehicleSurrogate(const VehicleSurrogate& v) : vp(v.vp ? v.vp->copy() : nullptr) {
        cout << "VehicleSurrogate 复制构造函数" << endl;

    }
    VehicleSurrogate& operator=(const VehicleSurrogate& v){
        cout << "VehicleSurrogate 赋值构造函数" << endl;
        if (this != &v) {
            delete vp;
            vp = (v.vp ? v.vp->copy() : nullptr);
        }
        return *this;
    }
    double weight() const {
        if (!vp)
            throw "empty VehicleSurrogate.weight()";
        return vp->weight();
    }
    void start() const {
        if (!vp)
            throw "empty VehicleSurrogate.start()";
        return vp->start();
    }
    // ...
private:
    Vehicle *vp;
};

int main() {
    VehicleSurrogate parking_lot[5];
    Truck x;
    parking_lot[2] = VehicleSurrogate(x);
    return 0;
}
// 输出
VehicleSurrogate 默认构造函数
VehicleSurrogate 默认构造函数
VehicleSurrogate 默认构造函数
VehicleSurrogate 默认构造函数
VehicleSurrogate 默认构造函数
Vehicle 构造函数
Truck 构造函数
Truck copy()
VehicleSurrogate(const Vehicle& v) 
VehicleSurrogate 赋值构造函数
Truck copy()
VehicleSurrogate 析构函数
Truck 析构函数
Vehicle 析构函数
Truck 析构函数
Vehicle 析构函数
VehicleSurrogate 析构函数
VehicleSurrogate 析构函数
VehicleSurrogate 析构函数
Truck 析构函数
Vehicle 析构函数
VehicleSurrogate 析构函数
VehicleSurrogate 析构函数
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在面向对象的编程中,C语言并不直接支持和抽象的概念。引用中提到,final关键字用来修饰方法,表示该方法不能在子中被覆盖。而abstract关键字用来修饰抽象方法,表示该方法必须在子中被实现。然而,在C语言中,没有对应的关键字来实现和抽象的概念。 相反,C语言通过结构体来模拟的概念。结构体是一种用户自定义的数据型,可以包含多个不同型的数据成员。通过结构体,我们可以将相关的数据和功能组合在一起。然而,C语言中的结构体不支持继承和多态等面向对象的特性。 在C语言中,我们可以使用函数指针来模拟抽象和接口的概念。函数指针可以指向不同的函数,通过使用函数指针,我们可以实现多态性,即在运行时根据函数指针指向的具体函数来执行不同的操作。 综上所述,C语言并不直接支持面向对象中的和抽象的概念,但可以使用结构体和函数指针来实现似的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [面向对象——和对象](https://blog.csdn.net/shouyeren_st/article/details/126210622)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [面向对象编程原则(06)——依赖倒转原则](https://blog.csdn.net/lfdfhl/article/details/126673771)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值