¿Por qué hay una pérdida de memoria en este programa y cómo puedo solucionarlo, dadas las restricciones (usando malloc y free para objetos que contienen std::string)? [duplicate]

9 minutos de lectura

Avatar de usuario de Anurag Vohra
Anurag Vohra

Este es un ejemplo de trabajo mínimo para el problema al que me enfrento en mi código real.

#include <iostream>

namespace Test1 {
    static const std::string MSG1="Something really big message";
}

struct Person{
    std::string name;
};

int main() {
    auto p = (Person*)malloc(sizeof(Person));
    p = new(p)Person();
    p->name=Test1::MSG1;

    std::cout << "name: "<< p->name << std::endl;

    free(p);

    std::cout << "done" << std::endl;

    return 0;
}

Cuando lo compilo y lo ejecuto a través de Valgrindme da este error:

definitivamente perdido: 31 bytes en 1 bloque


Restricciones

  1. estoy obligado a usar malloc en el ejemplo anterior, como en mi código real, uso una biblioteca C en mi proyecto C++, que usa esto malloc internamente. Así que no puedo alejarme de malloc uso, ya que no lo hago explícitamente en ninguna parte de mi código.
  2. necesito reasignar std::string name de Person una y otra vez en mi código.

  • Debes llamar al destructor antes free.

    – Santo Gato Negro

    1 de marzo a las 7:16

  • Cuando realiza una ubicación nueva, debe llamar explícitamente al destructor de objetos. Al igual que malloc no construye objetos, free no destruye objetos.

    – Un tipo programador

    1 de marzo a las 7:16

  • Este es un ejemplo de trabajo mínimo — Te olvidaste #include <string> y #include <cstdlib>

    – Paul McKenzie

    1 de marzo a las 7:57

  • @PaulMcKenzie Sin embargo, es un descuido comprensible: algunos (aunque no todos) los compiladores/bibliotecas del mundo real tienen <string> y <cstdlib> están incluidos por <iostream> (la norma no lo exige ni lo impide).

    – Pedro

    1 de marzo a las 9:29

  • no veo ninguna necesidad de alignas@PaulSanders, dado que std::malloc() devuelve la memoria convenientemente alineado para que pueda ser asignado a un puntero a cualquier tipo de objeto con un requisito de alineación fundamental (o un puntero nulo, por supuesto).

    –Toby Speight

    2 de marzo a las 8:20

Avatar de usuario de 463035818_is_not_a_number
463035818_no_es_un_número

Las piezas importantes de su código línea por línea…

Asigne memoria para un objeto Person:

auto p = (Person*)malloc(sizeof(Person));

Construya un objeto Person en esa memoria ya asignada llamando a su constructor:

p = new(p)Person();

Libera la memoria asignada a través de malloc:

free(p);

Llamar al constructor a través de la colocación new crea un std::string. Esa cadena se destruiría en el destructor, pero nunca se llama al destructor. free no llama a los destructores (al igual que malloc no llama a un constructor).

malloc sólo asigna la memoria. La ubicación nueva solo construye el objeto en la memoria ya asignada. Por lo tanto, debe llamar al destructor antes de llamar free. Este es el único caso que conozco donde es correcto y necesario llamar explícitamente a un destructor:

auto p = (Person*)malloc(sizeof(Person));
p = new(p)Person();
p->~Person();
free(p);

  • La discusión sobre los comentarios en el código siempre es un poco complicada, pero para mí, el único comentario en el código que esperaría es la razón para usar la ubicación nueva con malloc (asignador, ejercicio…)

    – stefaanv

    1 de marzo a las 10:36

  • Para hacerlo en realidad claro lo que se filtra: porque el Person destructor no está siendo llamado, Person::name no se destruye, por lo que la memoria asignada por std::string no está siendo desasignado. Eso es: Person se está liberando, pero cualquier recuerdo al que apunte no es.

    –Roger Lipscombe

    1 de marzo a las 16:04

  • @AnsonSavage sin ninguna restricción, probablemente no usarían la asignación dinámica para algo que es básicamente un std::string que ya administra la memoria asignada dinámicamente

    – 463035818_no_es_un_número

    2 de marzo a las 8:23


  • @AnsonSavage Creo que tanto valgrind como el desinfectante de direcciones darían un error con respecto a la falta de coincidencia entre malloc y operator delete si intentaste hacer eso.

    – Daniel Schepler

    2 de marzo a las 16:27

  • @AnsonSavage: new puede agregar metadatos específicos antes del elemento asignado, diferente (a menudo una expansión o reemplazo) de los metadatos malloc es contrabando. delete se basa en esos metadatos para realizar la limpieza correctamente. Si new y malloc no estar de acuerdo en el exacto estructura de metadatos, entonces delete y free también, y sucederán cosas terribles cuando los mezcles.

    – ShadowRanger

    3 de marzo a las 0:51

Debe llamar manualmente al destructor antes free(p);:

p->~Person();

O std::destroy_at(p)que es lo mismo.

Avatar de usuario de Matthieu M.
Matthieu M.

Señalando el problema

En primer lugar, aclaremos cuál es exactamente el problema ilustrando el estado de la memoria después de cada declaración.

int main() {
    auto p = (Person*)malloc(sizeof(Person));

    //  +---+    +-------+
    //  | p | -> | ~~~~~ |
    //  +---+    +-------+

    p = new(p)Person();

    //  +---+    +-------+
    //  | p | -> | name  |
    //  +---+    +-------+

    p->name=Test1::MSG1;

    //  +---+    +-------+    +---...
    //  | p | -> | name  | -> |Something...
    //  +---+    +-------+    +---...

    free(p);

    //  +---+                 +---...
    //  | p |                 |Something...
    //  +---+                 +---...

    return 0;
}

Como puedes ver, llamando free(p) liberó la memoria asignada originalmente por mallocpero no liberó la memoria asignada por p->name cuando se le asignó.

Este es tu fuga.

Resolviendo el problema

Hay dos aspectos para tener una Person objeto en el montón:

  • Una asignación de memoria, manejada por malloc/free aquí.
  • Inicializando y finalizando esa memoria, manejada por llamadas a constructores y destructores.

Le falta la llamada al destructor, por lo tanto, los recursos en poder de Person se filtran Aquí es la memoria, pero si Person mantuvo un bloqueo, podría tener un mutex bloqueado para siempre, etc., por lo tanto, es necesario ejecutar el destructor.

El enfoque de estilo C es llamar al destructor usted mismo:

int main() {
    auto p = (Person*)malloc(sizeof(Person));
    p = new(p) Person();
    p->name = Test1::MSG1;

    std::cout << "name: "<< p->name << "\n";

    //  Problem "fixed".
    p->~Person();

    free(p);

    std::cout << "done" << "\n";

    return 0;
}

Sin embargo, esto no es C ++ idiomático: es propenso a errores, etc.

El enfoque de C++ es usar RAI para asegurarse de que cuando p sale fuera de alcance, todos sus recursos están debidamente dispuestos: el destructor de Person es ejecutado y la memoria asignada para Person mismo se libera.

En primer lugar, vamos a crear algunos ayudantes. usé el c namespace ya que no se el nombre de la biblioteca de C que usas, pero te invito a ser mas especifico:

namespace c {
struct Disposer<T> {
    void operator()(T* p) {
        p->~T();
        free(p);
    }
};

template <typename T>
using UniquePointer<T> = std::unique_ptr<T, Disposer<T>>;

template <typename T, typename... Args>
UniquePointer<T> make_unique(T* t, Args&&... args) {
    try {
        new 
    } catch(...) {
        free
        throw;
    }

    return UniquePointer{t};
}
} // namespace c

Y con eso, podemos mejorar el ejemplo original:

int main() {
    auto raw = (Person*) malloc(sizeof(Person));

    auto p = c::make_unique(raw);

    p->name = Test1::MSG1;

    std::cout << "name: "<< p->name << "\n";

    //  No need to call the destructor or free ourselves, welcome to RAII.

    std::cout << "done" << "\n";

    return 0;
}

Nota: No utilice std::endlusar '\n' o "\n" en cambio. std::endl llamadas .flush() además de poner un final de línea, que rara vez es lo que quieres, ralentiza las cosas.

avatar de usuario de jxh
jxh

Como se mencionó en otras respuestas, la fuente de la fuga es que el destructor del name miembro de Person no se llama. Normalmente se llamaría implícitamente cuando el destructor para Person se llama. Sin embargo, Person nunca se destruye. El recuerdo por el Person la instancia simplemente se libera con free.

Entonces, tal como tenía que invocar explícitamente al constructor con la ubicación new después malloctambién necesita invocar explícitamente el destructor antes free.

También puede considerar sobrecargar el new y delete operadores.

struct Person {
    std::string name;
    void * operator new (std::size_t sz) { return std::malloc(sz); }
    void operator delete (void *p) { std::free(p); }
};

De esta manera, puedes usar new y delete normalmente, cuando debajo usarán malloc y free.

int main (void) {
    auto p = new Person;
    //... 
    delete p;
}

Y de esta manera, puede usar un puntero inteligente de forma más natural.

int main (void) {
    auto p = std:make_unique<Person>();
    //... unique pointer will delete automatically
}

Por supuesto, podrías haber usado unique_ptr con un eliminador personalizado con sus llamadas explícitas a malloc y freepero habría sido mucho más engorroso, y su eliminador aún necesitaría saber para invocar explícitamente al destructor también.

Avatar de usuario de Davislor
Davislor

Como han mencionado otros, la memoria dinámica asignada por los miembros de Person solo es liberado por el destructor ~Personcual free() no llama

Si tiene que usar esta función con una biblioteca que requiere alguna inicialización y limpieza que no sea la predeterminada, como aquí, un enfoque es definir un nuevo eliminador, para que lo usen los punteros inteligentes de la biblioteca estándar: Esto funcionará incluso con un bloque de memoria que no asignó usted mismo.

#include <memory>
#include <new> // std::bad_alloc
#include <stdlib.h>
#include <string>

struct Person{
    std::string name;
};

struct PersonDeleterForSomeLib {
  constexpr void operator()(Person* ptr) const noexcept {
    ptr->~Person();
    free(ptr);
  }
};


Person* Person_factory() // Dummy for the foreign code.
{
  Person* const p = static_cast<Person*>(malloc(sizeof(Person)));
  if (!p) {
    throw std::bad_alloc();
  }
  new(p) Person();
  return p;
}

Esto le permite usar con seguridad:

const auto p =
  std::unique_ptr<Person, PersonDeleterForSomeLib>(Person_factory());

con gestión automática de memoria. Puede devolver el puntero inteligente de la función, y tanto el destructor como free() será llamado cuando termine su vida útil. También puede crear un std::shared_ptr Por aquí. Si por alguna razón necesita destruir el objeto mientras el puntero inteligente todavía está activo, puede reset o release él.

  • ¿”STL” es una especie de sinónimo? no el literal STL?

    -Peter Mortensen

    2 de marzo a las 17:33

  • @PeterMortensen Limpié ligeramente la redacción de eso (y qué memoria se está filtrando).

    – Davislor

    2 mar a las 18:00

  • ¿”STL” es una especie de sinónimo? no el literal STL?

    -Peter Mortensen

    2 de marzo a las 17:33

  • @PeterMortensen Limpié ligeramente la redacción de eso (y qué memoria se está filtrando).

    – Davislor

    2 mar a las 18:00

¿Ha sido útil esta solución?