0

Un saludo.

Estoy haciendo un ejercicio corto y que en principio no debería dar más problemas, pero mis fieles amigos los segmentation fault ya están aquí. El programa trata de la gestión de un blog. Lo que a nosotros nos interesa es la relación de composición entre una entrada y sus comentarios, que son punteros dentro de un array, que a su vez es un atributo de la entrada:

#ifndef ENTRADA_H
#define ENTRADA_H
#include <string>
#include <stdexcept>
#include "Comentario.h"
class Entrada {
public:
    Entrada();
    Entrada(std::string titulo, std::string fecha, std::string texto);
    Entrada(const Entrada& orig);
    virtual ~Entrada();
    void addComentario(Comentario *comentario);
    void eliminarComentario(int cual);
    std::string getTexto();
    std::string getTitulo();
    std::string getFecha();
    int getNumComentarios();
    Entrada& setTexto(std::string &texto);
    Entrada& setTitulo(std::string &titulo);
    Entrada& setFecha(std::string &fecha);
    Comentario& getComentario (int cual);
    static const int MAX_COMENTARIOS = 30;
private:
    std::string _titulo = "";
    std::string _texto = "";
    std::string _fecha = "";
    int _numComentarios = 0;
    Comentario *_comentarios[MAX_COMENTARIOS];
};

#endif /* ENTRADA_H */

Como es lógico, si se elimina una entrada los comentarios mueren con ella, no siguen existiendo (es una relación de composición). Pues bien, el destructor de la entrada queda tal que así:

Entrada::~Entrada() {
    for (int i = 0; i < _numComentarios; i++){ //Con la entrada se destruyen los comentarios
        delete _comentarios[i];
    }
}

_comentarios[] se rellena en el propio main utilizando el método addCometnario de la clase Entrada:

void Entrada::addComentario(Comentario* comentario) {
    if (_numComentarios == MAX_COMENTARIOS)
        throw std::string ("Entrada::addComentario: No caben mas comentarios");

    _comentarios[_numComentarios] = comentario;
    _numComentarios++;
}

En el main queda tal que así:

Entrada entrada1 ("Bienvenida", "1-1-2010", "Bienvenidos al nuevo blog");
Comentario com1 ("AAA", "Hola!!!", "3-1-2010");
entrada1.addComentario(&com1);

Después de lo cual hay algunos cout y algunos try-catch que sí se comportan como se espera.

¿El problema? Al llegar al return 0 e ir a finalizar el programa, cuando se llama a los destructores de los objetos automáticos, es cuando se produce el segmentation fault, concretamente al ir a destruirse la entrada. El bucle for se ejecuta una vez (hay un comentario), y en esa ejecución se produce el segmentation fault al llamar al destructor de Comentario, que está por defecto, sin nada especial (en esa clase los atributos son objetos std::string sin arrays ni nada "especial").

Ahora me estoy planteando la hipótesis de que esto se producía porque se destruía antes el comentario que la entrada (o al revés), llamándose siempre a un destructor sin objeto que destruir, con lo cual he probado a crearlos manualmente con new y a borrarlos también manualmente con delete y, en efecto, ya no se produce el error (obviamente solo hago delete entrada1, pues ahí ya se elimina el comentario también):

    Entrada *entrada1 = new Entrada ("Bienvenida", "1-1-2010", "Bienvenidos al nuevo blog");
    Comentario *com1 = new Comentario ("AAA", "Hola!!!", "3-1-2010");
    entrada1->addComentario(com1);
    //Resto del main sin mayor importancia
    delete entrada1;
    //delete com1 no se pone, pues ya lo ha eliminado el destructor de la entrada
    return 0;

Pero claro, esto ya me lleva a plantearme: ¿es imposible hacerlo con objetos automáticos, al no poder controlar la llamada a los destructores?

Gracias. Saludos.

  • 2
    ¿ Puedes mostrar donde asignas o rellenas `_comentarios` ? Y, para futuras ocasiones, deberías consultar lo que es un [mcve]. – Trauma Mar 28 '20 at 13:11
  • Hecho, editado, también con una hipótesis/posible solución, que reduce la duda a algo mucho más simple. – Intento_de_programador Mar 28 '20 at 18:39
  • Y otra cosa, aunque no venga muy a cuento. Aunque tenga la carpeta del los proyectos en excepciones, el Antivirus ralentiza mucho el compilador, ¿sabéis si puede solucionarse esto y no tener que desactivarlo siempre que voy a programar? Es Bitdefender Free, por si importa. – Intento_de_programador Mar 28 '20 at 18:46
  • El código que muestras es correcto. Puesto que dices que al hacer estática las llamadas a `new` y `delete` de `Entrada` no se produce el error ... ¿ Como creas normalmente las entradas ? – Trauma Mar 28 '20 at 18:47
  • No creo entradas "normalmente", es la primera vez que pruebo el programa. La cuestión es esa, si puede hacerse (ese o programas similares con relaciones de composición) con objetos automáticos o debe hacerse en memoria dinámica para poder gestionarlo todo manualmente. – Intento_de_programador Mar 28 '20 at 18:49
  • Estamos perdiendo el tiempo. Por favor, proporciona un [mcve] que permita reproducir el problema. ¿ Como dices que es la primera vez que ejecutas el programa ? ¿ Te da error o no te da error ? Y existen los `std::auto_ptr` y los `std::vector` para automatizar formaciones variables en tamaño y destrucción automática de instancias. – Trauma Mar 28 '20 at 18:52
  • Vamos a ver, que no es un programa que utilice habitualmente ni nada de eso. Haciéndolo en memoria dinámica NO da error, haciéndolo con objetos automáticos SÍ. Y eso es lo que quiero, saber si habrá forma de hacerlo o no con objetos automáticos. En cuanto al ejemplo mínimo verificable, ¿para qué quieres más código, si ya tienes las clases, los destructores y el **main** que prueba todo? Y finalmente, no nos han explicado aún todo eso de los punteros inteligentes y esas cosas, así que ahí no llego. – Intento_de_programador Mar 28 '20 at 18:55
  • Perdona, creí que tenías un problema y querías solucionarlo. No te molesto mas. – Trauma Mar 28 '20 at 19:03

1 Answers1

3

Un segmentatoion fault puede darse por varios motivos:

  • Acceder a una zona de memoria que no pertenece a la memoria asignada por el programa.
    • Por ejemplo: Leer/Escribir más allá del límite de un búfer.
  • Acceder a una zona de memoria que ya fue liberada.
    • Por ejemplo: Borrar dos veces una zona de memoria.
  • Escribir en una zona de memoria marcada como sólo lectura.

Para evitar estos problemas se debe tener mucho cuidado con la memoria solicitada y su ciclo de vida; en el código que compartes aparentemente lo estás teniendo. Pero podrías ahorrarte todas las precauciones si siguieses ciertos principios de programación:

Define correctamente qué objetos son responsables de la memoria.

Tu objeto Entrada es conceptualmente incorrecto pues se hace cargo de memoria que no le pertenece: Se le facilita un puntero a datos cuyo origen desconoce, tendrás problemas si se le pasa un puntero a memoria que no es dinámica, un puntero nulo, un puntero incorrecto, un puntero a memoria ya liberada o un puntero a un tipo incorrecto:

Entrada e;
Comentario c("", "", "");
auto p = new Comentario("", "", "");
delete p;

e.addComentario(new Comentario ("AAA", "Hola!!!", "3-1-2010")); // Vale.
e.addComentario(&c);                     // Oh, oh...
e.addComentario(nullptr);                // ¡Repámpanos!
e.addComentario((Comentario *)0xfabada); // ¡Rayos y centellas!
e.addComentario(p);                      // ¡Maldición!
e.addComentario((Comentario *)&e);       // ¡Apocalipsis!

Para evitar semejantes disparates, si un objeto es responsable de liberar memoria, también debería ser responsable de crearla, por lo tanto la función Entrada::addComentario NO debería recibir un puntero a Comentario si no los datos necesarios para crear uno:

void Entrada::addComentario(const std::string &a, const std::string &b, const std::string &c) {
    if (_numComentarios == MAX_COMENTARIOS)
        throw std::string ("Entrada::addComentario: No caben mas comentarios");

    _comentarios[_numComentarios] = new Comentario(a, b, c);
    _numComentarios++;
}

Si además, no fuese necesario gestionar objetos Comentario fuera de Entrada, el primer objeto debería ser un tipo privado del segundo:

class Entrada {
public:
    // ...
    void addComentario(const std::string &a, const std::string &b, const std::string &c);
    // ...
private:
    struct Comentario { ... };
    // ...
    Comentario *_comentarios[MAX_COMENTARIOS];
};

Mejor aún, no gestiones memoria dinámica.

Tu código no parece un caso en que la memoria dinámica sea la solución, al fin y al cabo la cantidad de comentarios está prefijada ¿Para qué crearlos dinámicamente?

class Entrada {
public:
    // ...
    void addComentario(const std::string &a, const std::string &b, const std::string &c)
    {
        if (_numComentarios == MAX_COMENTARIOS)
            throw std::string ("Entrada::addComentario: No caben mas comentarios");

        _comentarios[_numComentarios].a = a;
        _comentarios[_numComentarios].b = b;
        _comentarios[_numComentarios].c = c;
        _numComentarios++;
    }
    // ...
private:
    struct Comentario { ... };
    // Colección de comentarios, NO DE PUNTEROS A comentario.
    Comentario _comentarios[MAX_COMENTARIOS];
};

Con esto desaparece la necesidad de destructor.

Otras cosas a tener en cuenta.

  • Evita identificadores que empiecen por guiones bajos (_), lee este hilo para saber por qué (aunque ya se te mencionó en otra pregunta tuya).
  • Los destructores virtuales son para llamar correctamente a los destructores de clases base cuando se deriva de ellas, si no vas a derivar nada de Entrada, no hay necesidad de que el destructor sea virtual (aunque ni siquiera tienes necesidad de destructor).
  • Las funciones miembro que leen datos son conceptualmente constantes, márcalas como tal:

    class Entrada {
    public:
        // ...
        std::string getTexto() const;  // Sólo lectura.
        std::string getTitulo() const; // Sólo lectura.
        std::string getFecha() const;  // Sólo lectura.
        int getNumComentarios();
        // ...
        Comentario& getComentario (int cual) const;   // Sólo lectura.
        // ...
    };
    
PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
  • Tus respuestas suelen ser un modelo para mi, y te considero un referente técnico, pero ... no puedo considerar esto una respuesta. Es un conjunto de opiniones/buenas prácticas. – Trauma Mar 28 '20 at 21:17
  • 1
    Es una solución al problema que el autor tiene. Vota negativo si no te gusta. – PaperBirdMaster Mar 28 '20 at 21:19
  • Claro. Y la respuesta `no uses punteros, usa auto_ptr` también podría serlo. Y es sensiblemente mas corta :-) – Trauma Mar 28 '20 at 21:21
  • Gracias por ser algo más comprensivo que Trauma con alguien que solo está dando sus primeros pasos :) Voy a revisar el código con algunos de los consejos que me das y cuando lo haya hecho te comento el resultado. Gracias de nuevo. – Intento_de_programador Mar 29 '20 at 14:16
  • En efecto, de hecho estaba violando una regla de la composición, que es que el objeto que "se compone de" es el que crea y destruye los objetos, a diferencia de la agregación. Lo dicho, muchas gracias y a cuidarse, sobre todo en las condiciones en las que estamos ;) – Intento_de_programador Mar 29 '20 at 15:07
  • @Trauma en este caso, los punteros (automáticos o en crudo) no eran la respuesta adecuada ¿Para qué usar memoria dinámica si la cantidad de `Comentario` en cada `Entrada` está prefijada estáticamente? – PaperBirdMaster Mar 29 '20 at 15:11
  • Eso no es lo que yo veo. Yo veo que está fijado **el máximo** de comentarios por cada entrada. ¿ Es lo que yo haría ? Por supuesto que no. ¿ Es una pregunta X Y ? Por supuesto que si. Pero eso no quita que siga siendo una pregunta que no se puede responder, puesto que no muestra el problema. De ahora en adelante, a cualquier pregunta sobre c/c++, le responderé con una lista de libros. Seguro que, si se los lee todos, encuentra la respuesta. – Trauma Mar 29 '20 at 18:16
  • El máximo no está prefijado, está prefijado el total. No habrán ni más ni menos `Comentario` que lo que está definido, así pues no hay necesidad alguna de memoria dinámica, ni con punteros en crudo ni con inteligentes. El problema no se puede resolver porque el código que muestra no tiene problema alguno, no porque sea un problema XY. – PaperBirdMaster Mar 29 '20 at 18:28
  • @Intento_de_programador yo no he sido más comprensivo que Trauma, de hecho soy mucho más crítico que él con tu trabajo, sigo pensando que te están enseñando mal. – PaperBirdMaster Mar 29 '20 at 18:30
  • @PaperBirdMaster sí has sido más comprensivo. Él creo que ni siquiera leyó el mensaje editado. En cuanto a lo de que me están enseñando mal, el otro día en una tutoría le solté al profesor lo de los punteros inteligentes, y me dijo que en efecto era lo que más pronto que tarde empezaríamos a utilizar, pero que querían que tuviésemos cierto dominio de los punteros "tradicionales". – Intento_de_programador Mar 29 '20 at 18:56
  • Pero @Trauma, ¿cómo me dices que no muestro el problema si tienes el vector y el destructor que daban problemas? ¿Quieres todo el proyecto? La próxima vez te lo subo a GitHub y ya está, me lo dices y sin problema. – Intento_de_programador Mar 29 '20 at 18:57
  • 1
    @Intento_de_programador Trauma dice (con razón) que no muestras el problema porque el problema no se puede reproducir con el código que compartes. – PaperBirdMaster Mar 29 '20 at 19:14
  • @PaperBirdMaster pero es que en este caso para reproducir el problema hace falta bastante código, y el post se haría demasiado largo, con lo que consideré que con poner lo que realmente daba errores era suficiente. – Intento_de_programador Mar 30 '20 at 09:33
  • 1
    @Intento_de_programador Todo problema se puede reducir a partes más simples... incluso cuando investigues cómo reducir tu problema para redactar una pregunta en el sitio podrías llegar a la solución por tu cuenta. Te podemos decir qué estás haciendo mal en base a imaginar cuál es el problema, pero para solucionar realmente el problema debemos poder reproducirlo. – PaperBirdMaster Mar 30 '20 at 09:56
  • Lo intentaré la próxima vez :) – Intento_de_programador Mar 30 '20 at 13:27