2010-09-18 27 views
8

Estoy tratando de aprender "tres grandes" en C++ .. Logré hacer un programa muy simple para "tres grandes" ... pero no estoy seguro de cómo usar el puntero del objeto. . El siguiente es mi primer intento.C++ Copiar Constructor + Objeto Puntero

tengo una duda cuando estaba escribiendo esto ...

Preguntas

  1. Es esta la forma correcta para implementar el constructor por defecto? No estoy seguro si necesito tenerlo o no. Pero lo que encontré en otro hilo sobre copy constructor con puntero es que necesito asignar el espacio para ese puntero antes de copiar la dirección en el constructor de copia ...
  2. ¿Cómo asignar la variable del puntero en el constructor de copia? La forma en que escribí en Copy Constructor puede ser incorrecta.
  3. ¿Debo implementar el mismo código (excepto el retorno) para copy constructor y operatior =?
  4. ¿Estoy en lo correcto al decir que necesito eliminar el puntero en destructor?

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

Implementación

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

Gracias de antemano.

+2

Es necesario asignar una cadena vacía a '' data' en su constructor TreeNode', ya que el constructor por defecto de 'std :: string' va a hacer eso sí mismo. –

+3

El constructor de copias y el operador de asignación tienen una semántica de copia ligeramente diferente. Eso es muy confuso para los usuarios de tu clase. – sbi

+0

hola sbi, ¿Quiere decir que debería usar la copia exacta del mismo código tanto para el constructor de copias como para el operador? –

Respuesta

21

¿Estoy en lo correcto al decir que necesito eliminar el puntero en destructor?

Al diseñar un objeto como este, primero debe responder la pregunta: ¿El objeto posee la memoria apuntada por ese puntero? Si es así, entonces obviamente el destructor del objeto necesita limpiar esa memoria, entonces sí necesita llamar a delete. Y esta parece ser tu intención con el código dado.

Sin embargo, en algunos casos, es posible que desee tener punteros que se refieren a otros objetos cuya vida se supone que debe ser administrada por otra cosa. En ese caso, no desea llamar a eliminar porque es deber de alguna otra parte del programa hacerlo. Además, eso cambia todo el diseño posterior que va al constructor de copias y al operador de asignación.

Voy a proceder a responder el resto de las preguntas bajo el supuesto de que usted quiere que cada objeto TreeNode tenga la propiedad de los objetos izquierdo y derecho.

¿Es esta la forma correcta de implementar el constructor predeterminado?

No. Usted necesita para inicializar los left y right punteros a NULL (0 o si lo prefiere). Esto es necesario porque los punteros no identificados podrían tener cualquier valor arbitrario. Si su código alguna vez construye un TreeNode por defecto y luego lo destruye sin asignarle nada a esos punteros, se invocará delete en cualquier valor inicial. Entonces, en este diseño, si esos punteros no apuntan a nada, entonces debe garantizar que estén configurados en NULL.

¿Cómo asignar la variable de puntero en el constructor de copia? La forma en que escribí en Copy Constructor puede ser incorrecta.

La línea left = new TreeNode(); crea un nuevo objeto TreeNode y establece left para que apunte a él. La línea left = node.left; reasigna ese puntero para apuntar a cualquier objeto TreeNode al que apunta node.left. Hay dos problemas con eso.

Problema 1: Ahora nada apunta a ese nuevo TreeNode. Se pierde y se convierte en una pérdida de memoria porque nada puede destruirlo.

Problema 2: Ahora tanto left como node.left terminan apuntando al mismo TreeNode. Eso significa que el objeto que se está copiando y el objeto desde el que toma los valores creerán que poseen ese mismo TreeNode y ambos invocarán eliminar en sus destructores. Llamar a eliminar dos veces en el mismo objeto es siempre un error y causará problemas (incluso posibles bloqueos o daños en la memoria).

Dado que cada TreeNode posee sus nodos izquierdo y derecho, entonces probablemente lo más razonable es hacer copias. Por lo que iba a escribir algo similar a:

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

¿Es necesario aplicar el mismo código (a excepción de retorno) para ambos constructor de copia y operatior =?

Casi seguro. O al menos, el código en cada uno debería tener el mismo resultado final. Sería muy confuso si la construcción y la asignación de copias tuvieran efectos diferentes.

EDITAR - El párrafo anterior debe ser: El código en cada uno debe tener el mismo resultado final en que los datos se copian del otro objeto. Esto a menudo implicará un código muy similar. Sin embargo, el operador de asignación probablemente deba verificar si ya se ha asignado algo al left y right, así que límpielos. Como consecuencia, es posible que también tenga que tener cuidado con la autoasignación o escribir de una manera que no cause que nada malo suceda durante la autoasignación.

De hecho, hay formas de implementar una utilizando la otra para que el código real que manipula las variables miembro solo se escriba en un lugar. Otras preguntas sobre SO han discutido eso, como this one.

+1

¿Cómo fuiste el único en señalar la 'nueva fuga de memoria TreeNode();'? –

+1

+1 para comprender el problema y dar una buena solución. Esta respuesta es definitivamente mejor que la mía. :) –

+0

Muchas gracias por una respuesta muy detallada .. –

1

¿Es esta la forma correcta de implementar el constructor predeterminado?

No, llamando delete en algo que no se ha asignado usando new invoca un comportamiento indefinido (en la mayoría de los casos da lugar a la caída de la aplicación)

Establecer sus punteros a NULL en su constructor por defecto.

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

No veo tales problemas con el resto de su código. Su operador de asignación copia poco los nodos mientras que su constructor de copias los copia profundamente.

Siga un enfoque adecuado según su requisito. :-)

EDITAR:

En lugar de asignar punteros en su constructor por defecto utilizar un initialization list

+0

Gracias. ¿Qué hay de otras preguntas? –

+0

Ver las ediciones, intenté responder a sus preguntas brevemente. :-) –

+0

Gracias .. ¿Tengo que poner "left = new TreeNode();" en el constructor de copia? ¿Es necesario? De lo contrario, lo eliminaré ... –

8

Aún mejor, creo

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

además de que tenga que eliminar los punteros ' left 'y' right 'en la operación de asignación, o tendrá una pérdida de memoria

+4

+1 de mí por mencionar la lista de inicialización. –

1

¿Puedo sugerir boost :: shared_ptr desde el impulso de biblioteca (si puede usarlo), en lugar de simples punteros? Solucionará una gran cantidad de problemas que podría tener con punteros no válidos, copias profundas e.t.c.

+0

de hecho, tendrá problemas con el código tal como está cuando asigne un nodo de árbol de otro y luego ambos se eliminen: los hijos se eliminarán dos veces, pero solo se asignaron una vez. Por lo tanto, solo puedo respaldar esta sugerencia para usar punteros inteligentes. –

3

Esta es la forma en que lo haría:
Debido a que usted está administrando dos recursos en el mismo objeto de hacer esto se hace correctamente un poco más complejo (que es por eso que recomendamos Nunca administrar más de un recurso en un objeto) . Si utiliza el modificador copiar/intercambiar, entonces la complejidad se limita al constructor de copia (lo cual no es trivial para obtener la correcta excepción de fuerte excepción).

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

La parte difícil ahora:

/* 
* 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

+1 Incluso si esto puede ser un poco avanzado para alguien que acaba de aprender los conceptos básicos de la administración de punteros y la implementación de los tres grandes operadores, esta sigue siendo una solución correcta de "fortaleza industrial". – TheUndeadFish

+0

@TheUndeadFish: es solo complejo porque hay dos recursos en el objeto para administrar. Si se tratara de un único recurso, básicamente no se necesitaría código (por lo que los objetos en mi humilde opinión solo deberían gestionar un recurso a la vez). –

+0

Muchas gracias, Martin ... Trataré de compilarlo al final ... Estoy usando MinGW .... No usé auto_ptr y release() antes .. Creo que la razón por la que necesito dos objetos es que necesito tener el nodo izquierdo y el nodo derecho .... –

Cuestiones relacionadas