抽象基类:如何为包含指向(抽象)基类指针的类定义复制构造函数或赋值操作符

Abstract Base Classes: How do you define a copy constructor or assignment operator for a class that contains a pointer to a (abstract) base class?

本文关键字:抽象 基类 定义 构造函数 赋值操作符 指针 复制 包含指      更新时间:2023-10-16

我刚刚在parashift.com上遇到了一个关于c++抽象基类的问题。

作者提出了在抽象基类中创建纯虚成员函数Clone()的解决方案。这个函数的目的是创建并返回ABC的克隆对象所指向的地址。这里我有点困惑,如果我们不这样做而实现相同的目的,那么创建这个虚函数并重写赋值操作符和复制构造函数有什么用呢?
class Shape {
    public:
      // ...
      virtual Shape* clone() const = 0;   // The Virtual (Copy) Constructor
      // ...
};

然后在每个派生类中实现这个clone()方法。下面是派生类Circle的代码:

class Circle : public Shape {
public:
  // ...
  virtual Circle* clone() const;
  // ...
};
Circle* Circle::clone() const
{
  return new Circle(*this);
}

现在假设每个Fred对象"有一个"Shape对象。Fred对象自然不知道Shape是Circle还是Square还是。Fred的复制构造函数和赋值运算符将调用Shape的clone()方法来复制对象:

class Fred {
public:
  // p must be a pointer returned by new; it must not be NULL
  Fred(Shape* p)
    : p_(p) { assert(p != NULL); }
 ~Fred()
    { delete p_; }
  Fred(const Fred& f)
    : p_(f.p_->clone()) { }
  Fred& operator= (const Fred& f)
    {
      if (this != &f) {              // Check for self-assignment
        Shape* p2 = f.p_->clone();   // Create the new one FIRST...
        delete p_;                   // ...THEN delete the old one
        p_ = p2;
      }
      return *this;
    }
  // ...
private:
  Shape* p_;
};

我认为我们可以在不重写赋值操作符或复制构造函数的情况下实现上述行为。如果我们有两个Fred类型的对象f1 (P_指向Circle)和f2 (P_指向Square)。

f1=f2;  // This line exhibits the same behavior what  above code is doing. 

在默认情况下,f2P_ (Address of Square)将被复制到P_f1。现在f1将指向Square。我们唯一需要注意的是删除Circle的对象,否则它将处于悬空状态。

为什么作者提到上述技术来解决这个问题?请建议。

你确实可以做到

delete f1.p;
f1 = f2;

但是这意味着Fred类的用户——不一定是它的作者——必须知道他必须首先调用delete f1.p。这对您来说可能是显而易见的,但其他人可能会非常惊讶,一个简单的赋值会导致内存泄漏。此外,如果您在很长一段时间后返回代码,可能您自己忘记了这条小规则并犯了错误。

由于总是必须在赋值Fred之前删除形状,因此在重写的等于操作符中编写此操作绝对是明智的。因此,删除是自动发生的,用户无需担心。

编辑回答评论中的问题:

基类中的virtual Shape *clone()函数强制每个派生类实现clone()函数。如果您从Shape派生而忘记实现clone(),那么您的代码将无法编译。这很好,因为Fred的重写赋值操作符依赖于它。

在这种情况下,您想要对Fred对象进行深度复制。由于析构函数执行delete p_;,如果有两个Fred对象指向同一个Shape,则会得到双重错误。clone()接口的原因是Fred不知道p_指向什么类型的对象,所以它不能直接调用正确的复制构造函数。相反,它依赖于Shape的子类来创建自己的副本,并使用虚拟方法调度来创建正确类型的对象。

示例并没有试图将f2分配给f1,因此OP认为f1 = f2;表现出相同的行为是不正确的。该示例将f2的副本赋值给f1,因此行为更接近f1 = new Whatever_f2_Is(*f2)

由于f2是指向基类的指针,因此此时没有足够的信息来知道使用哪个复制构造函数(这是一个小谎言,但clone方法仍然更容易使用)。即使Shape不是纯虚拟的,也不能调用new Shape(),因为Shape不知道子类的额外信息。你会有一个形状,但失去了圆形或方形的所有额外方面。

幸运的是,虽然我们不知道f2上的对象是什么,但f2上的对象仍然知道它是什么,我们可以将副本的创建委托给它。这就是克隆方法的作用。

另一种选择是玩is-a游戏或使用ID码和工厂。