【问题标题】:Copy constructor with pointers to own class [closed]使用指向自己的类的指针复制构造函数[关闭]
【发布时间】:2015-05-20 14:18:07
【问题描述】:

假设我们有这样一个类:

class C
{
public:
  C() {}
  virtual ~C() noexcept { if (c) { delete c; } }

protected:
  int a;
  float b;
  C* c;
}

您将如何正确实现复制和移动构造函数?通常你只会调用需要复制的对象的复制构造函数,但由于它是同一个类,你将如何正确处理呢?

【问题讨论】:

  • c 在哪里初始化?
  • 既然有this,为什么还要有c?无论如何,两个运算符都应该设置 c 指向新对象。
  • 因不清楚而投票关闭。我可以回答例如一个单链表,这可能是,但它可能还有无数其他的东西。
  • 如果你要处理原始指针和内存管理,我建议你使用copy and swap idiom
  • 这取决于 C 的新实例应该指向哪个 C 实例。你需要告诉我们更多关于你试图用这个结构解决的问题。

标签: c++ c++11 c++14


【解决方案1】:

您将如何正确实现复制和移动构造函数?

移动很容易:将受害者的指针复制到目标,然后将受害者的指针设为空,以表明它不再拥有任何东西。当然,还要复制其他值。

对于复制,您需要选择所需的语义:唯一所有权(在这种情况下不允许复制)、共享所有权(在这种情况下增加引用计数,并更改析构函数以减少它)、深度复制(在这种情况下分配一个新对象,复制旧对象),或其他东西。

通常你只会调用需要复制的对象的复制构造函数,但由于它是同一个类,你将如何正确处理呢?

这不一定是个问题。如果指针链在某处结束,您最终只会递归地复制该链上的所有内容;尽管最好编写一个循环以避免无限递归。如果它没有结束,那么它一定是循环的,所以你需要检查你是否回到了你开始的对象。

【讨论】:

    【解决方案2】:

    通常你只会调用需要复制的对象的复制构造函数,但由于它是同一个类,你将如何正确处理呢?

    以与调用同一类的析构函数相同的递归方式。由于您的析构函数承担了指向对象的所有权,因此您必须进行深层复制。

    移动很简单,只需清除指针,这样当移动的对象被销毁时它就不会被删除。就像任何其他拥有的原始指针一样。

    C(const C& other): a(other.a), b(other.b), c(other.c) {
        if(other.c)
            this->c = new C(*other.c);
    }
    C(C&& other): a(other.a), b(other.b), c(other.c) {
        other.c = nullptr;
    }
    

    请记住,堆栈大小会限制数据结构的递归。如果您将内存管理留在类之外,那么您可以使用循环而不是递归来迭代链接的对象。

    【讨论】:

    • 对此有评论,但如果您采用此解决方案请做在所有非复制构造函数中将c 初始化为nullptr
    • @Sh3ljohn 好点。我假设初始化在那里,但为了简单起见,省略了示例代码。
    【解决方案3】:

    好的,这个问题的目的还不清楚,你编写复制和移动运算符的方式取决于你想要的行为:

    • 是否要将下属 C 的所有权转移到正在移动的新类?

    • 在复制的情况下要复制下属C吗? (深拷贝)

    • 在复制时,目标 C 是否应该与源 C 共享从属 C?

    我们可以猜测并假设答案是:是,是,否:

    class C
    {
    public:
    
        // custom destructor == rule of 5
    
        C() {}
    
        C(C&& r) noexcept
        : _child { r._child }
        {
            r._child = nullptr;
        }
    
        C& operator=(C&& r) noexcept {
            swap(r);
            return *this;
        }
    
        C(const C& r)
        : _child { r._child ? r._child->clone() : nullptr }
        {
        }
    
        C& operator=(const C& r) {
            C tmp { r };
            swap(tmp);
            return *this;
        }
    
        // test for null is not necessary
        virtual ~C() noexcept { delete _child; }
    
    public:
        void swap(C& r) noexcept {
            using std::swap;
            swap(_child, r._child);
        }
    
        // in case C is a polymorphic base class
        virtual C* clone() const {
            return new C { *this };
        }
    
    private:
        C* _child = nullptr;
    };
    

    【讨论】:

    • 嗯,在循环列表的情况下会发生什么,这里?这也是问题缺乏明确性的一个方面。 ;-)
    • @Cheersandhth.-Alf 是众多此类维度之一 :)
    【解决方案4】:

    这是发明智能指针的原因之一。它们是自我管理的,因此您唯一需要做的就是在需要时设置它们,并且它们会在不再使用时释放内存。

    #include <memory>
    
    class C
    {
    public:
      C() {}
    
    protected:
      int a;
      float b;
      std::shared_ptr<C> c;
    }
    

    PS:我没有问,但是需要指向 self 的指针的情况并不常见;您确定您的情况没有解决方法吗?

    【讨论】:

    • 智能指针对于链表来说非常不利,这可能是(并且似乎很可能是)。特别是当列表可能是循环时。我认为这是不好的建议。
    • 你说得对,这就是我写 PS 的原因。 OP 没有说明该应用程序的任何内容,因此如果没有更多信息,这是一个完全有效的解决方案。我不确定我对投票的感受,因为它可以如何使用。刀可以用来杀人,它们仍然是剪纸之类的完美选择,尽管剪刀可能会更好。
    • 对于一揽子不合格的建议,重要的是它会在最可能的情况下失败或增加不合理的开销。当我写“坏”建议时,我的意思是“最有可能直接有害”的建议,并不是说它不完美。
    • @Cheersandhth.-Alf 不用担心,无论如何你说 OP 不清楚是对的 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-04-08
    • 2010-10-21
    • 1970-01-01
    • 2018-06-08
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多