【问题标题】:Recursive copy constructor?递归复制构造函数?
【发布时间】:2011-03-17 20:21:14
【问题描述】:

我正在尝试编写一个复制构造函数,它接受四叉树的一个节点,并复制该节点及其所有子节点及其子节点,等等。这是我所拥有的:

 Node(const Node & n) {

    nChild=new Node(*(n.nwChild));
    neChild=new Node(*(n.nChild));
    eChild=new Node(*(n.seChild));
    wChild=new Node(*(n.sChild));   
}

我觉得最后 4 行有点时髦。我所做的有意义吗?

【问题讨论】:

  • 如果n.??Child 之一是NULL methinks,那么该代码将会崩溃。
  • 但它不检查吗?
  • 只有当??Child成员中的所有个成员都是NULL时,如果你想返回其中任何一个是NULL你,你的复制构造函数才会过早返回需要使用|| 运算符。
  • -1 你昨天问了一个问题:stackoverflow.com/questions/5332811/…。您可以(并且应该)在此处添加示例代码,而不是将其作为新问题发布在此处。

标签: c++ constructor


【解决方案1】:

您的构造函数不是异常安全的 - 假设第二个 new 抛出 bad_alloc,那么如何清理第一个?

您可以通过将Child 字段更改为您最喜欢的智能指针,让您的生活更轻松。如果您没有最喜欢的智能指针,请向自己介绍一些 ;-)

[编辑:刚刚注意到您没有提供数据成员定义,我只是假设它们是原始指针。如果它们已经是智能指针,请道歉。]

early-out 看起来有点奇怪 - 如果 rhs 上的所有四个孩子都为 NULL,那么 x/y/height/width 没有被复制怎么办?通常,这些成员将由初始化列表处理。如果 any 的孩子是 NULL,而不是 all 他们,你应该做一些特别的事情吗?目前,您不处理具有一个方向但不是全部 4 个子节点的节点。如果每个节点都有 4 个子节点或没有子节点是树的属性,那么您不需要检查所有 4 个子节点,但您 复制叶节点时是否需要初始化各个字段。如果一个节点可能正好有 1 个子节点,那么您就没有正确复制该节点。

除此之外,基本逻辑对我来说似乎还可以,从某种意义上说,只要没有出错,它就会递归复制。

【讨论】:

    【解决方案2】:

    对我来说很有意义。这是一个递归深拷贝,应该可以正常工作。但是不要忘记在析构函数中释放所有这些内存。并且在编码时要注意Quadtree 的临时副本,如果树很大,它们可能会很残酷。

    这段代码唯一真正的问题是它距离异常安全还有很长的路要走。一方面,它应该使用initializer list 编写,这样:

    Quadtree::QuadtreeNode::QuadtreeNode(const QuadtreeNode & n) 
     : x(n.x), y(n.y), height(n.height), width(n.width),
       nwChild(new QuadtreeNode(*(n.nwChild)), // ...
    

    构造函数主体可以保持为空,除非您想做的不仅仅是初始化成员。完成此操作后,您可能需要考虑将裸指针换成某种智能指针。这将为使类异常安全打下良好的基础。

    【讨论】:

    • 我在最后 4 行得到了很多“使用未初始化的值”和“无效的读取大小”以及“条件跳转或移动取决于未初始化的值”第一行。这意味着什么?
    • 您可能希望在友元工厂函数中执行此操作,以维护一组已复制的节点并防止导致堆栈溢出的循环。
    • 但是不要忘记三法则。您需要一个有用的析构函数和一个深拷贝赋值运算符(可能使用复制和交换)来配合您的复制构造函数。
    • @bitmoe valgrind 警告意味着您传入的 n 未正确初始化。是否有可能您的默认构造函数没有正确设置nwChild 等?
    • 我的默认构造函数只是将每个 child=NULL。所以构造函数中有 4 行这样的行。这样可以吗?
    【解决方案3】:

    除了这里的好答案(我赞成 Jon 的),我想介绍一种很酷的新方法来获得 C++0x 中的异常安全性。你的编译器可能还没有实现它。但我寄予厚望,一年后情况不会如此。

    我从 Jon 的复制构造函数开始:

    Quadtree::QuadtreeNode::QuadtreeNode(const QuadtreeNode & n) 
     : x(n.x), y(n.y), height(n.height), width(n.width),
       nwChild(new QuadtreeNode(*(n.nwChild)), // ...
    

    现在假设您有一个默认构造函数,如您在其中一个 cmets 中所述:

    Quadtree::QuadtreeNode::QuadtreeNode() 
     : x(0), y(0), height(0), width(0),
       nwChild(nullptr), // ...
    

    现在您可以通过首先调用默认构造函数将 Jon 的复制构造函数转换为异常安全的构造函数! :-) 具有讽刺意味的是,它看起来很像您的原始代码:

    Quadtree::QuadtreeNode::QuadtreeNode(const QuadtreeNode & n)
         : QuadtreeNode()
    {
        if(!(n.nwChild==NULL && n.neChild==NULL && n.seChild==NULL && n.swChild==NULL))
        {
            x=n.x;
            y=n.y;
            height=n.height;
            width=n.width;
            nwChild=new QuadtreeNode(*(n.nwChild));
            neChild=new QuadtreeNode(*(n.neChild));
            seChild=new QuadtreeNode(*(n.seChild));
            swChild=new QuadtreeNode(*(n.swChild)); 
        }
    }
    

    说明:这称为委托构造函数。并且是 C++0x 的一个新特性。您可以从字面上调用另一个构造函数。并且当任何构造函数完成时,该对象被视为已构造。从那时起,任何被抛出的异常都会激活析构函数进行清理。所以默认构造函数通常是一种以noexcept方式构造对象的便捷方式。然后你可以以自然的方式构建你的对象,知道如果抛出异常就会调用析构函数。

    【讨论】:

    • 如果初始化 swChild 的新 QuadtreeNode 以异常终止会发生什么?除非您进入 try/catch 块,否则每个指针都必须位于单独的类类型对象中,以便在失败时自动清理所有成功创建的对象
    • 我担心你错过了我的帖子的重点:新功能;委托构造函数;容易异常安全。参见open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf,15.3 [except.handle],第 11 段。
    • 是和不是。委托构造函数确实允许 QuadtreeNode 析构函数处理清理,因为一旦任何构造函数完成,对象就被认为已构造,并且将调用其析构函数。但在这种情况下,我仍然认为使用单独的类来包装每个指针(例如 copy_ptr)是一种更清洁的解决方案。它适用于当前的编译器:-)。
    【解决方案4】:

    节点是如何分配的?如果这种情况,我看到一个问题

     1   3
       5 
     7   9
    

    如果我尝试复制5,而1 有一个指向5 的se 指针,它将重新制作5,进而重新制作1 等等。

    【讨论】:

    • 如果是这样,那么它就不是四叉树树,它是一个在任意一个顶点最多有4条边的图。
    【解决方案5】:

    首先,我在理解响应时遇到了一些困难, 因为他们指的是我没有看到的条件测试 你的原始代码。不过……

    IIUC,你想做一个深拷贝。有两个问题 必须考虑:如果其中一个(或多个) 指针为空,以及如何使代码异常安全。这 最明显的解决方案是使用智能指针 对于每个指针,它都会进行深层复制并生成所有 必要的检查。设计一个好的通用 copy_ptr 不是 微不足道,因为它也应该处理多态指针。 但是对于任何一种情况,或给定的约定,它都非常简单; 如果 Node 不是多态的,那么 shared_ptr 的复制 ctor 可以 就这么简单:

    copy_ptr( T const& other )
        : m_ptr( other.m_ptr == NULL ? NULL : new T( *other.m_ptr ) )
    {
    }
    ~copy_ptr()
    {
        delete m_ptr;
    }
    

    (使用交换习语进行赋值,并提供其他常用的 智能指针函数。)

    这样做,您可以将初始化程序直接放在 初始化列表:

    Node( Node const& other )
        : nChild( other.nChild )
        , neChild( other.neChild )
        //  ...
    

    (没有智能指针,或者至少一个单独的类类型 封装每个指针,你必须初始化 在初始化列表中指向 null 的指针,然后分配 在 try-catch 块中新的指向它们的指针,以便删除 发生异常时已经更新的对象。)

    【讨论】:

      猜你喜欢
      • 2014-03-03
      • 2018-06-24
      • 1970-01-01
      • 2013-10-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-03-06
      相关资源
      最近更新 更多