Gestión Memoria Memory Leaks

Tuberías, fugas de memoria

Como bien sabemos los que programamos en C hay que ser muy cuidadosos con la gestión de memoria. Si creamos un espacio de memoria después debemos liberarnos.

En el IDE de VisualEstudio cuando finalizamos un programa en la ventana de la consola nos aparece el aviso de memory leaks cuando hemos creado memoria que no hemos liberado. Esto con el gdb no sucede, al menos yo no he sido capaz de que al finalizar un programa con gdb, me muestre algún tipo de aviso indicando que se ha quedado memoria sin liberar.

He creado la clase CMemoryLeaks, para detectar estos casos. Esta clase se implementa utilizando el patrón singleton.

Patrón singlenton

El patrón singleton, se define porque sólo se permite la creación de una única instancia (objeto) de una clase.

Este concepto se aplica, para que varios procesos (hilos, procesos) accedan a una única entidad. Por ejemplo a los datos/propiedades de un usuario, un dispositivo, como puede ser puerto serie, paralelo, usb, un disco duro, o como no la memoria. Por lo que cada vez que varios procesos instancien un objeto singleton, dichos procesos acceden a los mismos datos del objeto.

La característica principal de una clase con patrón singleton es que NUNCA debemos de instanciar o borrar directamente el objeto. Prohibido usar directamente new y delete.

Implementación de la clase CMemoryLeaks

Básicamente nuestra clase tiene un contenedor (hasmap) (std::map <UINT_PTR,std::string> mMap), que contiene tuplas de clave (dirección de memoria creada), dato (texto libre).

Cada vez que creemos memoria, le pasaremos la dirección de memoria y un texto, que insertará en el contenedor (hasmap).

Cuando liberemos memoria, le pasaremos la dirección de memoria que hemos liberado la buscará en el contenedor y borrará la tupla.

Primero definimos en la parte privada (prívate/protected) de la clase, la variable que contendrá la instancia de la clase CMemoryLeaks y el mutex.

CMemoryLeaks.h

static CMemoryLeaks *mspInstancia;
static std::mutex msMutex;

En la implementación de la clase debemos de iniciar la instancia a null.

CMemoryLeaks.cpp

CMemoryLeaks* CMemoryLeaks::mspInstancia = nullptr;
std::mutex CMemoryLeaks::msMutex;

Ahora definimos la función que creara una única instancia.

En la definición pública de la clase definimos la función getInstance como static.

CMemoryLeaks.h

public:
  static CMemoryLeaks *getInstance(void);

La implementación de la función

CMemoryLeaks.cpp

CMemoryLeaks *CMemoryLeaks::getInstance(void)
{
 //bloqueamos el acceso a la parte de código critica.
 CMemoryLeaks::msMutex.lock();
 if (CMemoryLeaks::mspInstancia== nullptr)
 {
  //no hay instancia, la creamos
  CMemoryLeaks::mspInstancia=new CMemoryLeaks();
 }
 //desbloquemoas la parte del código crítica
 CMemoryLeaks::msMutex.unlock();

 //devolvemos la instancia
 return CMemoryLeaks::mspInstancia;
}

Ahora definimos la función que destruirá la instancia.

En la definición pública de la clase definimos la función finalizar como static. Esta función devolverá recorrerá el contenedor devolviendo los registros que no se hayan borrado, en un puntero de un string.

CMemoryLeaks.h

static std::string *finalizar(void);

La implementación de la función

CMemoryLeaks.cpp

std::string *CMemoryLeaks::finalizar(void)
{
 std::map <UINT_PTR,std::string> lMap;
 std::map <UINT_PTR,std::string>::const_iterator lIter;
 //string donde dejaremos los registros
 std::string *lpstrAviso;

 lpstrAviso=NULL;

 if (CMemoryLeaks::mspInstancia!= nullptr)
 {
  lMap=CMemoryLeaks::mspInstancia->getMap();
  if (lMap.size()!=0)
  {//tengo registros en el mapa
   for (lIter=lMap.begin();lIter!=lMap.end();lIter++ )
   {//recorro todos los registros y los voy concatenando en el std::string
    if (lpstrAviso==NULL)
     lpstrAviso=new std::string(lIter->second);
    else
       lpstrAviso->append(lIter->second);
    //añado el retorno de carro
    lpstrAviso->append("\n");
   }//fin for recorre los registros del mapa

   lMap.clear();
   
   //borramos la instancia
   delete CMemoryLeaks::mspInstancia;
   CMemoryLeaks::mspInstancia= nullptr;
  }//fin tengo registros en el mapa
 }

 return lpstrAviso;
}

Indicar la creación de memoria

Cuando creemos memoria deberemos de llamar a la función crear que se le pasarán 5 parámetros. Esta función no está definida como static.

Primero buscara por la dirección de memoria (clave) en el contenedor (mMap).

Si no lo encuentra inserta la tupla en el contenedor.

CMemoryLeaks.h

void crear(UINT_PTR pDirMem,std::string pstrFile,
	   std::string pstrfuncion, unsigned int puiLine,
	   std::string pstrInf);

La implementación de la función

CMemoryLeaks.cpp

void CMemoryLeaks::crear(UINT_PTR pDirMem,
                             std::string pstrFile,
                             std::string pstrfuncion, 
                             unsigned int puiLine,
                             std::string pstrInf)
{
 std::map <UINT_PTR,std::string>::const_iterator lIter;
 std::string lstr;

 //busco si existe un registro para la dirección de memoria (pDirMen)
 lIter=mMap.find(pDirMem);
 if (lIter==mMap.end())
 {//no hay registro inserto un registro en el mapa
  //bloqueo el acceso al mapa.
  CMemoryLeaks::msMutex.lock();
  lstr=pstrFile+":"+pstrfuncion+":"+std::to_string(puiLine)+": "+pstrInf;
  //inserto la tupla (dirección memoria(clave), texto(dato))
  mMap.insert(std::map <UINT_PTR,std::string>::value_type(pDirMem,lstr));
  //desbloqueo el acceso al mapa
  CMemoryLeaks::msMutex.unlock();
 }
}

Indicar liberación de memoria

Cuando liberemos la memoria deberemos de llamar a la función liberar al igual que crear no está definida como static, se le pasará la dirección de memoria que se libera.

Primero buscara por la dirección de memoria (clave) en el contenedor (mMap).

Si la encuentra borra la tupla del contenedor.

CMemoryLeaks.h

void liberar(UINT_PTR pDirMem);

La implementación de la función

CMemoryLeaks.cpp

void CMemoryLeaks::liberar(UINT_PTR pDirMem)
{
 std::map <UINT_PTR,std::string>::const_iterator lIter;

 //busco si existe un registro para la dirección de memoria (pDirMen)
 lIter=mMap.find(pDirMem);
 if (lIter!=mMap.end())
 {//hay registro inserto un registro en el mapa
  //bloqueo el acceso al mapa.
  CMemoryLeaks::sMutex.lock();
  //borro la tupla encontrada
  mMap.erase(lIter);
  //desbloqueo el acceso al mapa
  CMemoryLeaks::sMutex.unlock();
 }
}

Es importante remarcar que el constructor, constructor de copia y el destructor de la clase están definidos dentro de la sección private/protected de la clase al igual que el operador de asignación (=).

De esta manera evitamos que podamos utilizar new, delete o = directamente sobre un objeto de clase CMemoryLeaks.

CMemoryLeaks.h

private:
  //constructor y destructor lo definimos en la parte privada,
  //para que no puedan ser utilizados
  CMemoryLeaks(void);
  ~CMemoryLeaks(void);
  //prohibimos el uso de los operadores constructor de copia y asignación
  CMemoryLeaks(CMemoryLeaks const&)=delete;
  CMemoryLeaks& operator=(CMemoryLeaks const&)=delete;

Como usar clase CMemoryLeaks

Vamos a hacer una aplicación en modo consola, que lanzará un hilo. Tanto el programa principal como el hilo utilizarán la clase CMemoryLeaks.

También habrá una función que crea memoria y devolverá un puntero del espacio de memoria creado.

Al final de la aplicación se llamará al método finalizar. Si dicho método devuelve un string, es que no hemos liberado correctamente la memoria.

Es un proyecto en eclipse, en un principio lo único que habrá que configurar es donde está el compilador de C/C++.

Menú -> Project -> C/C++ Build -> Environment.

En la variable MINGW_HOME

Editar variable MINGW_HOME
Editar variable MINGW_HOME

Se debe de indicar donde está el compilador de C/C++.

Indicar ruta del compilador
Ruta del compilador

En un principio con esto ya se puede compilar el proyecto.

memoryLeaks.cpp
hilo (El hilo)

La función hilo, obtendrá una instancia de CMemoryLeaks.

lpContMem=CMemoryLeaks::getInstance();

Recordar que no podemos utilizar new, para crear una instancia de la clase CMemoryLeaks. Por eso utilizamos getInstance. Como hemos visto anteriormente, si ya existe una instancia de CMemoryLeaks devolverá la instancia, en caso contrario creará una instancia (objeto) y devolverá dicha instancia.

Creará memoria, en este caso un array de char de 6 elementos

lpchrPru=new char[6];

Indicará que se ha creado memoria.

lpContMem->crear((UINT_PTR)lpchrPru, __FILE__, __func__, __LINE__-1, "pchrPru=new char[6];");

Utilizará los identificadores del preprocesador ,__FILE__,__func__,__LINE__, para indicar en que fichero se ha creado la memoria, que función y en que línea se llama a la función crear, el quinto parámetro es un texto libre.

Esperará unos 5 segundos

sleep(5);

Función creaMemoryChar

Esta función simplemente creara un array de chars, con la longitud que se le pase como parámetro.

main (Programa principal)

La función main, lanzará el hilo

pthread_create(&lpthreadHilo,NULL,hilo,NULL);

Obtendrá una instancia de CMemoryLeaks.

lpContMemMain=CMemoryLeaks::getInstance();

Si ya existe una instancia de CMemoryLeaks devolverá la instancia, en caso contrario creará una instancia (objeto) y la devolverá.

Una vez lanzado el hilo. Se creará memoria para un integer

lpiPru=new int;

Se indicará que hemos creado memoria.

lpContMem->crear((UINT_PTR) lpiPru, __FILE__, __func__, __LINE__-1, "lpiPru=new int;");

Se llamara a la función creaMemoryChar para que cree un array de 10 elementos de chars;

Se indicará que la función crearMemoryChar ha creado memoria

lpContMemMain->crear((UINT_PTR)lpchrPru,__FILE__,__func__,__LINE__-1, "creaMemoryChar(10);");
lpstrError=CMemoryLeaks::finalizar();

Si lpstrError es diferente de NULL se mostrará los registros de la memoria que no se han liberado correctamente.

if (lpstrError!=NULL)
{
 puts("Main-> no se ha gestionado la memoria correctamente");
 puts(lpstrError->c_str());
 delete lpstrError;
}
Mensajes de fuga de memoria
Mensajes de fuga de memoria

Podemos ver que no hemos liberado correctamente la memoria.

Indicando que fichero, función y línea donde se ha creado memoria, pero que dicha memoria no ha sido liberada.

Main-> no se ha gestionado la memoria correctamente
..\memoryLeaks.cpp:main:87: creaMemoryChar(10);
..\memoryLeaks.cpp:main:75: lpiPru=new int;
..\memoryLeaks.cpp:hilo:32: pchrPru=new char[6];

Si por ejemplo liberamos memoria correctamente en el hilo.

memoryLeaks.cpp
hilo (El hilo)

Descomentando las siguientes líneas.

lpContMem->liberar((UINT_PTR)lpchrPru);
delete []lpchrPru;
lpchrPru=NULL;
Salida sin fugas de memoria
Salida sin fugas de memoria

Podemos ver como ya no aparece el registro del hilo (..\memoryLeaks.cpp:hilo:32: pchrPru=new char[6]; ) y sólo aparecen los registros de la creación de la memoria del main y de la función crearMemoryChar.

Main-> no se ha gestionado la memoria correctamente
..\memoryLeaks.cpp:main:87: creaMemoryChar(10);
..\memoryLeaks.cpp:main:75: lpiPru=new int;

Evidentemente si cuando creamos o cuando liberamos memoria no se lo indicamos a la clase, puede darnos información incorrecta.

Código

Nombre Fichero Fecha Tamaño Contador Descarga
memoryLeaks memoryleaks.zip 2024-07-14 8170 bytes 65