[C con Clase] Tengo un error lógico creo...

Salvador Pozo salvador en conclase.net
Lun Dic 4 19:21:49 CET 2006


El pasado 2006-12-04 12:51:23, Rey escribió:
 
Hola:

Efectivamente, en tu programa hay algunos errores. Sobre todo en los constructores.

Declaras la clase como:

R> class DatosEstudiantes{
R>   private:
R>     char *mNombre;
R>     char *mGrupo;
R>     int mEdad;
R>     int mAnno;
R>  

Según esta declaración, los constructores están mal:
R>     DatosEstudiantes()
R>     {
R>       mNombre = '\000';
R>       mGrupo = '\000';
R>       mEdad = 1;
R>       mAnno = 1990;
R>      }


mNombre y mGrupo son punteros a char, por lo tanto no puedes inicializarlos con un valor char, sino que deben ser iniciados con valores de tipo puntero, por ejemplo con NULL.

Además, por coherencia, es recomendable usar los constructores para cada objeto, incluso aunque se trate de objetos de tipos fundamentales o como en este caso de punteros.

Yo escribiría este constructor como:

    DatosEstudiantes() :mNombre(0), mGrupo(0), mEdad(1), mAnno(1990) {}

En cuanto al segundo contructor, pasa tres cuartas partes de lo mismo:

R>     DatosEstudiantes(char *pNombre, char *pGrupo, int pEdad, int pAnno);

Aunque en este caso no existe una definición del constructor, para los miembros mEdad y mAnno se procedería igual, y para los datos miembro de tipo puntero, se procedería igual que en los casos siguientes.

R> //  *** METODOS SET DE MI CLASE *** //
R>     void Set_Nombre(char *pNombre){ mNombre = pNombre;}
R>     void Set_Grupo(char *pGrupo){ mGrupo = pGrupo;}

Esto también es incorrecto.

Ten en cuenta que lo que estás copiando es el valor de un puntero, y no el contenido (en este caso cadenas) apuntadas por esos punteros.

Esto quiere decir que si llamas a una de estas funciones con un puntero que señale a una cadena de caracteres, y posteriormente modificas esa cadena de caracteres, el valor del puntero almacenado en el objeto no cambia, y por lo tanto, ahora apuntará a la misma dirección de memoria. Es decir, que cada vez que modifiques la cadena, modificarás todos los valores en los objetos que tengan almacenada esa dirección.

Por ejemplo, en tu programa usas una variable:
  char aNombre[100];

Después lees una cadena desde el teclado usando:
gets(aNombre);

Y posteriormente, asignas ese nombre a un objeto usando:
GrupoX[i].Set_Nombre(aNombre);

Si aNombre toma, por ejemplo, el valor de dirección de memoria 0xa000, el objeto "GrupoX[i]" almacenará ese valor (0xa000) para el puntero mNombre.

Si posteriormente, cuando lees el siguiente valor, modificas el contenido de la cadena aNombre, el valor de puntero no cambia, sigue siendo 0xa000, y por lo tanto, asignas el mismo valor a "GrupoX[i+1]", por lo que ahora, ambos objetos tienen un miembro mNombre apuntando a la misma dirección de memoria, y por lo tanto, ambos parecen tener el mismo nombre.

Si quieres que cada objeto tenga sus propios valores de mNombre y mGrupo, deberás crear memoria dinámica para cada dato, y modificar los valores de los punteros miembros.

Esto además te obliga a crear un destructor para que cuando el objeto sea destruido, también se libere la memoria correspondiente a cada puntero miembro.

Por ejemplo:

void Set_Nombre(char *pNombre) { 
   mNombre = new char[strlen(pNombre)+1]; // Un espacio extra para el nulo
   strcpy(mNombre, pNombre);
}

Pero esto tampoco es correcto (hay que tener muchas precauciones con los punteros) :-)

¿Qué pasaría si mNombre ya estaba inicializado anteriormente a la llamada a Set_Nombre?

Piensa que en realidad no hay nada que impida asignar un nombre varias veces, y en ese caso el programa debe funcionar correctamente.

Debemos prevenir todos los casos posibles, y este es, evidentemente, probable.

Por lo tanto, antes de asignar un nuevo valor debemos eliminar el anterior, si es que existe:

void Set_Nombre(char *pNombre) { 
   if(mNombre) delete[] mNombre; // Liberar memoria, si existe
   if(pNombre) {
      mNombre = new char[strlen(pNombre)+1]; // Un espacio extra para el nulo
      strcpy(mNombre, pNombre);
   }
}

He aprovechado para tener en cuenta otro posible caso: que se llame a la función Set_Nombre con un puntero nulo. En ese caso, se libera la memoria actual, si existe, y se sale sin más.


Pero sigamos, porque aún se puede afinar un poco más.

Tenemos ahora las funciones para leer los valores de Nombre o Grupo.

R>     char *Get_Nombre();
R> ...
R>   char *DatosEstudiantes::Get_Nombre()
R>   {
R>     return mNombre;
R>   }

Cuando se diseñan clases hay que tener en cuenta que una de las características que deben cumplir es la protección de los datos miembro.

En este caso, has declarado (acertadamente) mNombre y mGrupo como privados. Sin embargo, cuando lees esos valores para usarlos en el programa, devuelves los valores de esos punteros.

Generalmente, devolver valores es un método seguro, y mantiene la integridad de los datos. Eso pasa en este caso con edad o anno. Pero no pasa lo mismo si se trata de punteros (o al menos no exactamente lo mismo).

Los punteros en sí se devuelven de forma segura, ya que no podemos modificar el valor de los datos miembros mNombre y mGrupo. Pero sí podemos modificar los valores apuntados por esos punteros.

Es decir, al devolver un puntero, estamos dejando desprotegidos los valores a los que ese puntero apunta.

Cuando haces:

      printf("Nombre alumno [%2d]: %s\n", i + 1, GrupoX[i].Get_Nombre());

No hay peligro, ya que la función printf no modifica el valor de los parámetros, pero imagina que haces esto:

    strcpy(GrupoX[i].Get_Nombre(), "Carpanta");

Ahora, el nombre del GrupoX[i] ha perdido el valor previo, y pasa a tener "Carpanta".

Seguro que no querías esto cuando declaraste mNombre como privado.

Para evitar esto existen varias opciones.

Una consiste en hacer una copia del valor apuntado por el puntero, y devolver esa copia.

Esta solución es un tanto rebuscada, y además, puede crear la falsa impresión de que el dato sigue desprotegido.

Otra solución es declarar el valor de retorno como constante:

   const char *Get_Nombre();

Y de momento, creo que eso es todo.

Hasta pronto.

-- 
Salvador Pozo (Administrador)
mailto:salvador en conclase.net


Más información sobre la lista de distribución Cconclase