Конструктор Копирования C++ + Объект Указателя

Я пытаюсь выучить "большую тройку" на C++.. Мне удалось сделать очень простую программу для "большой тройки".. но я не уверен, как использовать указатель на объект.. Ниже приводится моя первая попытка.

у меня есть сомнения, когда я писал это...

вопросы

  1. это правильный способ реализации конструктора по умолчанию? Я не уверен, нужно мне это или нет. Но то, что я нашел в другом потоке о конструкторе копирования с указатель - это то, что мне нужно выделить пространство для этого указателя перед копированием адреса в конструкторе копирования..
  2. как назначить переменную указателя в конструкторе копирования? Способ, который я написал в конструкторе копирования, может быть неправильным.
  3. нужно ли реализовывать один и тот же код (кроме return ) для конструктора копирования и operatior=?
  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;
}

спасибо заранее.

5 ответов


правильно ли я говорю, что мне нужно удалить указатель в деструкторе?

всякий раз, когда вы проектируете такой объект, вам сначала нужно ответить на вопрос: владеет ли объект памятью, на которую указывает этот указатель? Если да, то очевидно, что деструктор объекта должен очистить эту память, поэтому да, он должен вызвать delete. И это, кажется, ваше намерение с данным кодом.

однако в некоторых случаях вы можете захотеть иметь указатели, которые ссылаются на другие объекты, чье время жизни должно управляться чем-то другим. В этом случае вы не хотите вызывать delete, потому что это обязанность какой-то другой части программы. Кроме того, это изменяет весь последующий дизайн, который входит в конструктор копирования и оператор присваивания.

я продолжу отвечать на остальные вопросы в предположении, что вы хотите, чтобы каждый объект TreeNode имел право собственности на левый и правый объекты.

это правильный способ реализации конструктора по умолчанию?

нет. Вам нужно инициализировать left и right указатели на NULL (или 0, если хотите). Это необходимо, потому что неинициализированные указатели могут иметь любое произвольное значение. Если ваш код когда-либо по умолчанию создает TreeNode, а затем уничтожает его, не назначая ничего этим указателям, то delete будет вызван независимо от того, что это начальное значение. Так что в этом дизайн, если эти указатели не указывают ни на что, то вы должны гарантировать, что они установлены в NULL.

как назначить переменную указателя в конструкторе копирования? Способ, который я написал в конструкторе копирования, может быть неправильным.

строку 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);
}

нужно ли реализовывать один и тот же код (кроме return ) для конструктора копирования и operatior=?

почти наверняка. Или, по крайней мере, код в каждом из них должен иметь одинаковый конечный результат. Он было бы очень запутанно, если бы конструкция копирования и назначение имели разные последствия.

EDIT-приведенный выше абзац должен быть: код в каждом из них должен иметь одинаковый конечный результат в том, что данные копируются из другого объекта. Это часто включает в себя очень похожий код. Однако оператор присваивания, вероятно, должен проверить, было ли что-либо уже назначено left и right и поэтому очистите их. Как следствие, может также потребоваться следить за самоназначение или же быть написанным таким образом, чтобы ничего плохого не произошло во время самоназначения.

на самом деле, есть способы реализовать один, используя другой, так что фактический код, который манипулирует переменными-членами, записывается только в одном месте. Другие вопросы на SO обсудили, что, например этот.


еще лучше, я думаю

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

плюс вы должны удалить указатели "влево" и "вправо" в операции назначения, или у вас будет утечка памяти


вот как я бы это сделал:
Поскольку вы управляете двумя ресурсами в одном объекте, это становится немного сложнее (поэтому я рекомендую никогда не управлять более чем одним ресурсом в объекте). Если вы используете идиому copy/swap, то сложность ограничена конструктором copy (который нетривиален, чтобы получить правильный для сильная гарантия исключения).

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

это правильный способ реализации конструктора по умолчанию?

нет, называя delete на что-то, что не было выделено с помощью new вызывает неопределенное поведение (в большинстве случаев это приводит к краху приложения)

установить указатели NULL в конструкторе по умолчанию.

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

Я не вижу таких проблем с остальной частью вашего кода. Ваш оператор присваивания shallow копирует узлы, тогда как ваша копия конструктор глубоко копирует их.

следовать соответствующим подходом согласно вашему требованию. :-)

редактировать:

вместо назначения указателей в конструкторе по умолчанию используйте список инициализации


могу ли я также предложить boost:: shared_ptr из библиотеки boost (если вы можете использовать его) вместо простых указателей? Это решит множество проблем, которые могут возникнуть с недопустимыми указателями, глубокими копиями e.т. c.