关于规则三:C ++ Copy Constructor + Pointer Object

C++ Copy Constructor + Pointer Object

我正在努力学习C++中的"大三"。我为"三巨头"做了非常简单的程序。但我不知道如何使用对象指针。以下是我的第一次尝试。

我在写这篇文章的时候有点怀疑…

问题

  • 这是实现默认构造函数的正确方法吗?我不知道我是否需要它。但是我在另一个关于带指针的复制构造函数的线程中发现,在复制复制构造函数中的地址之前,我需要为该指针分配空间。
  • 如何在复制构造函数中分配指针变量?我在复制构造函数中编写的方法可能是错误的。
  • 我是否需要为复制构造函数和operator=实现相同的代码(返回代码除外)?
  • 我说我需要删除析构函数中的指针是正确的吗?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class TreeNode
    {
    public:  
       TreeNode();
       TreeNode(const TreeNode& node);
       TreeNode& operator= (const TreeNode& node);
       ~TreeNode();
    private:
       string data;
       TreeNode* left;
       TreeNode* right;
       friend class MyAnotherClass;
    };
  • 实施

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    TreeNode::TreeNode(){

        data ="";  

    }

    TreeNode::TreeNode(const TreeNode& node){
         data = node.data;

         left = new TreeNode();
         right = new TreeNode();

         left = node.left;
         right = node.right;
    }

    TreeNode& TreeNode::operator= (const TreeNode& node){
         data = node.data;
         left = node.left;
         right = node.right;
         return *this;
    }

    TreeNode::~TreeNode(){
         delete left;
         delete right;
    }

    事先谢谢。


    Am I correct in saying that I need to delete the pointer in destructor?

    每当设计这样的对象时,首先需要回答一个问题:对象是否拥有指针指向的内存?如果是,那么显然对象的析构函数需要清除内存,所以是的,它需要调用delete。这似乎是您对给定代码的意图。

    但是,在某些情况下,您可能希望有指针指向其他对象,这些对象的生存期应该由其他对象管理。在这种情况下,您不想调用delete,因为这样做是程序其他部分的职责。此外,这将更改进入复制构造函数和赋值运算符的所有后续设计。

    我将继续回答剩下的问题,假设您确实希望每个Treenode对象都拥有左对象和右对象的所有权。

    Is this the correct way to implement the default constructor?

    不需要。您需要将leftright指针初始化为空(如果您愿意,可以是0)。这是必需的,因为未初始化的指针可以有任意值。如果您的代码曾经默认地构造一个TreeNode,然后在不向这些指针分配任何内容的情况下销毁它,那么无论初始值是什么,都会调用Delete。所以在这个设计中,如果那些指针没有指向任何东西,那么您必须保证它们被设置为空。

    How to assign the pointer variable in the copy constructor? The way that I wrote in Copy Constructor might be wrong.

    left = new TreeNode();创建一个新的treenode对象,并设置left指向它。行left = node.left;重新分配该指针,指向treenode对象node.left指向的任何对象。这有两个问题。

    问题1:现在没有什么能指向新的树烯酮。它会丢失并成为内存泄漏,因为没有任何东西可以破坏它。

    问题2:现在,leftnode.left都指向同一个treenode。这意味着被复制构造的对象和它从中获取值的对象都会认为它们拥有相同的treenode,并且都会在析构函数中对其调用delete。在同一个对象上调用两次delete始终是一个错误,并将导致问题(包括可能的崩溃或内存损坏)。

    因为每个树节点都有自己的左节点和右节点,所以最合理的做法可能是复制。所以你可以写一些类似的东西:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    TreeNode::TreeNode(const TreeNode& node)
        : left(NULL), right(NULL)
    {
        data = node.data;

        if(node.left)
            left = new TreeNode(*node.left);
        if(node.right)
            right = new TreeNode(*node.right);
    }

    Do I need to implement the same code (except return ) for both copy constructor and operatior=?

    几乎可以肯定。或者至少,每个代码都应该有相同的最终结果。如果复制构造和分配有不同的效果,这将非常令人困惑。

    编辑-上面的段落应该是:每个代码的最终结果应该与从另一个对象复制数据的结果相同。这通常涉及非常相似的代码。但是,分配操作员可能需要检查是否有任何东西已经分配给了leftright,因此需要对它们进行清理。因此,它可能还需要注意自我分配,或者以不会导致自我分配期间发生任何不好的事情的方式书写。

    事实上,有一些方法可以使用另一种方法实现其中一个,这样操作成员变量的实际代码就只写在一个地方。其他问题已经讨论过了,比如这个问题。


    我觉得更好

    1
    2
    3
    4
     TreeNode::TreeNode():left(NULL), right(NULL)
     {
       // data is already set to"" if it is std::string
     }

    另外,您必须删除赋值操作中的指针"左"和"右",否则会出现内存泄漏。


    我就是这样做的:因为您在同一个对象中管理两个资源,所以正确地执行此操作会变得更加复杂(这就是为什么我建议不要在一个对象中管理多个资源的原因)。如果您使用复制/交换习惯用法,那么复杂性仅限于复制构造函数(这对于获得强异常保证的正确性是非常重要的)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    TreeNode::TreeNode()
        :left(NULL)
        ,right(NULL)
    {}

    /*
     * Use the copy and swap idium
     * Note: The parameter is by value to auto generate the copy.
     *       The copy uses the copy constructor above where the complex code is.
     *       Then the swap means that we release this tree correctly.
     */

    TreeNode& TreeNode::operator= (const TreeNode node)
    {
         std::swap(data,  node.data);
         std::swap(left,  node.left);
         std::swap(right, node.right);
         return *this;
    }

    TreeNode::~TreeNode()
    {
         delete left;
         delete right;
    }

    现在最难的部分是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
     * The copy constructor is a bit harder than normal.
     * This is because you want to provide the `Strong Exception Guarantee`
     * If something goes wrong you definitely don't want the object to be
     * in some indeterminate state.
     *
     * Simplified this a bit. Do the work that can generate an exception first.
     * Once all this has been completed we can do the work that will not throw.
     */
     
    TreeNode::TreeNode(const TreeNode& node)
    {
        // Do throwable work.
        std::auto_ptr<TreeNode>  nL(node.left  == null ? null : new TreeNode(*node.left));
        std::auto_ptr<TreeNode>  nR(node.right == null ? null : new TreeNode(*node.right));

        // All work that can throw has been completed.
        // So now set the current node with the correct values.
        data  = node.data;
        left  = nL.release();
        right = nR.release();
    }


    我也可以建议从库boost(如果您可以使用它)共享ptr,而不是简单的指针吗?它可以解决很多您可能遇到的问题,比如指针无效、E.T.C.的深度复制。


    Is this the correct way to implement the default constructor?

    不,对未使用new分配的对象调用delete会调用未定义的行为(在大多数情况下,这会导致应用程序崩溃)。

    在默认构造函数中设置指向NULL的指针。

    1
    2
    3
    4
    5
    TreeNode::TreeNode(){
      data ="";   //not required since data being a std::string is default initialized.
      left = NULL;
      right = NULL;  
    }

    我看不出你的其他代码有这样的问题。赋值操作符shallow复制节点,而复制构造函数deep复制节点。

    根据您的要求采取适当的方法。-)

    编辑:

    不要在默认构造函数中分配指针,而是使用初始化列表