C++ 复制构造函数 + 指针对象

C++ Copy Constructor + Pointer Object

提问人:Michael Sync 提问时间:9/18/2010 最后编辑:N.A.Michael Sync 更新时间:8/11/2016 访问量:17590

问:

我正在尝试学习C++中的“三巨头”。我设法为“三巨头”做了非常简单的程序。但我不确定如何使用对象指针。以下是我的第一次尝试。

当我写这篇文章时,我有一个疑问......

问题

  1. 这是实现默认构造函数的正确方法吗?我不确定我是否需要拥有它。但是我在另一个关于带有指针的复制构造函数的线程中发现的是,在复制构造函数中的地址之前,我需要为该指针分配空间。
  2. 如何在复制构造函数中分配指针变量?我在 Copy Constructor 中编写的方式可能是错误的。
  3. 我是否需要为 copy constructor 和 operatior= 实现相同的代码(return 除外)?
  4. 我说我需要删除析构函数中的指针是否正确?

    class TreeNode
    {
    public:  
       TreeNode(); 
       TreeNode(const TreeNode& node);
       TreeNode& operator= (const TreeNode& node);
       ~TreeNode();
    private:
       string data;
       TreeNode* left;
       TreeNode* right;
       friend class MyAnotherClass;
    };
    

实现

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;
}

提前致谢。

C++ 三法则

评论

2赞 Josh Townzen 9/18/2010
没有必要在构造函数中分配空字符串,因为默认构造函数 会自行执行此操作。dataTreeNodestd::string
3赞 sbi 9/18/2010
复制构造函数和赋值运算符的复制语义略有不同。这对您班级的用户来说非常令人困惑。
0赞 Michael Sync 9/18/2010
嗨 sbi,您的意思是,我应该为复制构造函数和运算符使用相同代码的精确副本?
0赞 Martin York 9/19/2010
见下文。对于提供 .上面的当前版本在复制构造和分配过程中都会泄漏(左和右)。Strong Exception Guarantee
0赞 sbi 9/26/2010
(您需要在评论回复中正确@address用户,否则您的回复将不会显示在他们的回复选项卡中。不,你不应该。复制构造函数通过调用成员的构造函数来初始化成员,而赋值运算符则为已构造的成员分配新值。除此之外,您可以执行深度复制或浅层复制,而您的代码是这些的不健康组合。

答:

1赞 Prasoon Saurav 9/18/2010 #1

这是实现默认构造函数的正确方法吗?

否,调用未分配的内容 invokes Undefined Behavior(在大多数情况下,这会导致应用程序崩溃)deletenew

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

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

我没有看到你的其余代码有这样的问题。赋值运算符浅层复制节点,而复制构造函数深层复制节点。

根据您的要求遵循合适的方法。:-)

编辑

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

评论

0赞 Michael Sync 9/18/2010
谢谢。其他问题呢?
0赞 Prasoon Saurav 9/18/2010
查看编辑,我已尝试简要回答您的问题。:-)
0赞 Michael Sync 9/18/2010
谢谢。。我必须将“left = new TreeNode();”放在复制构造函数中吗?有必要吗?否则,我会删除它......
1赞 Prasoon Saurav 9/18/2010
@Michael:取决于您是要执行深拷贝还是浅拷贝
0赞 Michael Sync 9/18/2010
AFAIK,使用复制构造函数是用于深度复制的,对吧?所以,我会说我想要深度复制。
7赞 frag 9/18/2010 #2

我认为更好

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

另外,您必须在分配操作中删除指针“左”和“右”,否则会出现内存泄漏

21赞 TheUndeadFish 9/18/2010 #3

我说我需要删除析构函数中的指针是否正确?

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

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

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

这是实现默认构造函数的正确方法吗?

不。您需要初始化指向 NULL 的 and 指针(如果您愿意,也可以初始化为 0)。这是必需的,因为未初始化的指针可以具有任何任意值。如果您的代码默认构造一个 TreeNode,然后销毁它而不为这些指针分配任何内容,那么无论初始值是什么,都将调用 delete。因此,在此设计中,如果这些指针不指向任何内容,则必须保证它们设置为 NULL。leftright

如何在复制构造函数中分配指针变量?我在 Copy Constructor 中编写的方式可能是错误的。

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

问题 1:现在没有任何东西指向那个新的 TreeNode。它丢失了,变成了内存泄漏,因为没有什么可以破坏它。

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

由于每个 TreeNode 都拥有其左右节点,因此最合理的做法可能是制作副本。所以你会写类似的东西:

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);
}

我是否需要为 copy constructor 和 operatior= 实现相同的代码(return 除外)?

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

编辑 - 上面的段落应该是:每个对象中的代码都应该具有相同的最终结果,因为数据是从另一个对象复制的。这通常涉及非常相似的代码。但是,赋值运算符可能需要检查是否已经分配了任何内容,以便清理它们。因此,它可能还需要注意自我分配,或者以不会导致自我分配期间发生任何不好的事情的方式编写。leftright

事实上,有一些方法可以使用另一个来实现一个,以便操作成员变量的实际代码只写在一个地方。关于 SO 的其他问题已经讨论过这个问题,例如这个问题

评论

1赞 Evan 9/18/2010
你是唯一一个指出内存泄漏的人吗?new TreeNode();
1赞 Prasoon Saurav 9/18/2010
+1 用于理解问题并给出良好的解决方案。这个答案肯定比我的好。:)
0赞 Martin York 9/19/2010
在分配给您之前,您可能应该删除旧值(dito forleftright)
0赞 TheUndeadFish 9/19/2010
哦,对了,我完全忘记了赋值运算符需要注意并正确清理以前分配给 和 的任何内容。leftright
1赞 frag 9/18/2010 #4

我还可以建议从库 boost 中推荐 boost::shared_ptr(如果您可以使用它),而不是简单的指针吗?它将解决您可能遇到的许多问题,例如无效指针,深度副本等。

评论

0赞 Andre Holzner 9/18/2010
事实上,当你从另一个树节点分配一个树节点,然后两个树节点都被删除时,你会遇到代码问题:子节点将被删除两次,但只分配了一次。所以我只能支持这个使用智能指针的建议。
4赞 Martin York 9/19/2010 #5

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

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;
}

现在最困难的部分:

/*
 * 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();
}

评论

0赞 TheUndeadFish 9/19/2010
+1 即使对于刚刚学习管理指针和实现三大运算符的基础知识的人来说,这可能有点高级,但这仍然是一个正确的“工业级”解决方案。
0赞 Martin York 9/19/2010
@TheUndeadFish:它之所以复杂,是因为对象中有两个资源需要管理。如果它是单个资源,则基本上不需要任何代码(这就是为什么恕我直言,对象一次只能管理一个资源)。
0赞 Michael Sync 9/19/2010
非常感谢,马丁。我会尝试在我这边编译它..我正在使用 MinGW....我以前没有使用过 auto_ptr 和 release()..我认为我需要两个对象的原因是我需要有左节点和右节点....
0赞 Martin York 9/20/2010
@Michael 同步:稍微简化了复制构造函数。