抽象基 类:如何为包含指向(抽象)基 class 的指针的 class 定义复制构造函数或赋值运算符?
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?
我刚刚在 parashift.com 上遇到了关于抽象基础 classes in c++ 的问题。
作者提供了在Abstract Base中创建纯虚成员函数Clone()
的解决方案Class.The该函数的目的是创建和return克隆对象的地址ABC 指向的。在这里我有点困惑,如果我们不这样做就实现了同样的事情,那么创建这个虚函数并覆盖赋值运算符和复制构造函数有什么用。
class Shape {
public:
// ...
virtual Shape* clone() const = 0; // The Virtual (Copy) Constructor
// ...
};
然后我们在每个派生的class中实现这个clone()
方法。这是导出 class 圆的代码:
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.
在默认情况下,f2
的P_
(广场地址)将被复制到 P_
到 f1
。现在 f1
将指向 Square。唯一需要注意的是删除Circle的对象,否则会变成悬垂状态。
为什么作者提到了上面的技术来解决这个问题?
请指教。
你确实可以做到
delete f1.p;
f1 = f2;
但这意味着 Fred class 的用户——不一定是它的作者——需要知道他必须先调用 delete f1.p
。现在对你来说可能很明显,但其他人会非常惊讶一个简单的赋值会导致内存泄漏。另外,如果你 return 在很长一段时间后对你的代码,也许你自己忘记了这条小规则并犯了错误。
由于您总是必须在分配 Fred 之前删除形状,因此将其写在覆盖的等号运算符中是绝对明智的。因此删除会自动发生,用户无需担心。
编辑以回答评论中的问题:
基础 class 中的 virtual Shape *clone()
函数强制每个派生 class 实现 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是指向基class的指针,目前没有足够的信息知道使用哪个复制构造函数(有点谎,但clone方法还是更容易使用) .您不能调用 new Shape(),即使 shape 不是纯虚拟的,因此无法实例化,因为 shape 不知道 sublclass 的额外信息。您将拥有一个 Shape,但会失去 Circle-ness 或 Square-ness 的所有额外方面。
幸运的是,虽然我们对 f2 的真实情况一无所知,但 f2 的对象仍然知道它是什么,我们可以将副本的创建委托给它。这就是克隆方法为您所做的。
替代方案是玩 is-a 游戏或使用 ID 代码和工厂。
我刚刚在 parashift.com 上遇到了关于抽象基础 classes in c++ 的问题。
作者提供了在Abstract Base中创建纯虚成员函数Clone()
的解决方案Class.The该函数的目的是创建和return克隆对象的地址ABC 指向的。在这里我有点困惑,如果我们不这样做就实现了同样的事情,那么创建这个虚函数并覆盖赋值运算符和复制构造函数有什么用。
class Shape {
public:
// ...
virtual Shape* clone() const = 0; // The Virtual (Copy) Constructor
// ...
};
然后我们在每个派生的class中实现这个clone()
方法。这是导出 class 圆的代码:
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.
在默认情况下,f2
的P_
(广场地址)将被复制到 P_
到 f1
。现在 f1
将指向 Square。唯一需要注意的是删除Circle的对象,否则会变成悬垂状态。
为什么作者提到了上面的技术来解决这个问题? 请指教。
你确实可以做到
delete f1.p;
f1 = f2;
但这意味着 Fred class 的用户——不一定是它的作者——需要知道他必须先调用 delete f1.p
。现在对你来说可能很明显,但其他人会非常惊讶一个简单的赋值会导致内存泄漏。另外,如果你 return 在很长一段时间后对你的代码,也许你自己忘记了这条小规则并犯了错误。
由于您总是必须在分配 Fred 之前删除形状,因此将其写在覆盖的等号运算符中是绝对明智的。因此删除会自动发生,用户无需担心。
编辑以回答评论中的问题:
基础 class 中的 virtual Shape *clone()
函数强制每个派生 class 实现 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是指向基class的指针,目前没有足够的信息知道使用哪个复制构造函数(有点谎,但clone方法还是更容易使用) .您不能调用 new Shape(),即使 shape 不是纯虚拟的,因此无法实例化,因为 shape 不知道 sublclass 的额外信息。您将拥有一个 Shape,但会失去 Circle-ness 或 Square-ness 的所有额外方面。
幸运的是,虽然我们对 f2 的真实情况一无所知,但 f2 的对象仍然知道它是什么,我们可以将副本的创建委托给它。这就是克隆方法为您所做的。
替代方案是玩 is-a 游戏或使用 ID 代码和工厂。