10

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

我在写这篇文章的时候有一个疑问...

问题

  1. 这是实现默认构造函数的正确方法吗?我不确定我是否需要拥有它。但是我在另一个关于带有指针的复制构造函数的线程中发现的是,在复制构造函数中的地址之前,我需要为该指针分配空间。
  2. 如何在复制构造函数中分配指针变量?我在 Copy Constructor 中写的方式可能是错误的。
  3. 我是否需要为复制构造函数和 operator= 实现相同的代码(除了 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;
}

提前致谢。

4

5 回答 5

22

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

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

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

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

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

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

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

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

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

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

由于每个 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);
}

我是否需要为复制构造函数和 operator= 实现相同的代码(除了 return )?

几乎可以确定。或者至少,每个中的代码应该具有相同的最终结果。如果复制构造和分配具有不同的效果,那将是非常混乱的。

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

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

于 2010-09-18T06:08:03.487 回答
8

更好的是,我认为

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

另外你必须在赋值操作中删除指针'left'和'right',否则你会有内存泄漏

于 2010-09-18T05:29:59.193 回答
4

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

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();
}
于 2010-09-18T16:23:33.333 回答
1

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

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

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

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

我没有看到您的其余代码存在此类问题。您的赋值运算符浅复制节点,而您的复制构造函数深复制它们。

根据您的要求采用合适的方法。:-)

编辑

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

于 2010-09-18T05:14:40.963 回答
1

我还可以从库 boost 中建议 boost::shared_ptr (如果可以使用的话),而不是简单的指针?它将解决您在无效指针、深拷贝等方面可能遇到的许多问题

于 2010-09-18T06:11:14.117 回答