Las siguientes técnicas sencillas de depuración están explicadas en el Volumen 1:
1. Para comprobar los límites de un array, usa la plantilla Array en C16:Array3.cpp del Volumen 1 para todos los arrays. Puede desactivar la comprobación e incrementar la eficiencia cuando esté listo para enviar. (Aunque esto no trata con el caso de coger un puntero a un array.)
2. Comprobar destructores no virtuales en clases base. Seguirle la pista a new/delete y malloc/free
Los problemas comunes con la asignación de memoria incluyen llamadas por error a delete para memoria que no está libre, borrar el espacio libre más de una vez, y más a menudo, olvidando borrar un puntero. Esta sección discute un sistema que puede ayudarle a localizar estos tipos de problemas.
Como cláusula adicional de exención de responsabilidad más allá de la sección precedente: por el modo que sobrecargamos new, la siguiente técnica puede no funcionar en todas las plataformas, y funcionará sólo para programas que no llaman explicitamente al operador de función new( ). Hemos sido bastante cuidadosos en este libro para presentar sólo código que se ajuste completamente al Estándar C++, pero en este ejemplo estamos haciendo una excepción por las siguientes razones:
1. A pesar de que es técnicamente ilegal, funciona en muchos compiladores.[29]
2. Ilustramos algunos pensamientos útiles en el trascurso del camino.
Para usar el sistema de comprobación de memoria, simplemente incluya el fichero de cabecera MemCheck.h, conecte el fichero MemCheck.obj a su aplicación para interceptar todas las llamadas a new y delete, y llame a la macro MEM_ON( ) (se explica más tarde en esta sección) para iniciar el seguimiento de la memoria. Un seguimiento de todas las asignaciones y desasignaciones es impreso en la salida estándar (mediante stdout). Cuando use este sistema, todas las llamadas a new almacenan información sobre el fichero y la línea donde fueron llamados. Esto está dotado usando la sintaxis de colocación para el operador new.[30] Aunque normalmente use la sintaxis de colocación cuando necesite colocar objetos en un punto de memoria específico, puede también crear un operador new( ) con cualquier número de argumentos. Esto se usa en el siguiente ejemplo para almacenar los resultados de las macros __FILE__ y __LINE__ cuando se llama a new:
//: C02:MemCheck.h #ifndef MEMCHECK_H #define MEMCHECK_H #include <cstddef> // For size_t // Usurp the new operator (both scalar and array versions) void* operator new(std::size_t, const char*, long); void* operator new[](std::size_t, const char*, long); #define new new (__FILE__, __LINE__) extern bool traceFlag; #define TRACE_ON() traceFlag = true #define TRACE_OFF() traceFlag = false extern bool activeFlag; #define MEM_ON() activeFlag = true #define MEM_OFF() activeFlag = false #endif // MEMCHECK_H ///:~
Listado 3.14. C02/MemCheck.h
Es importante incluir este fichero en cualquier fichero fuente en el que quiera seguir la actividad de la memoria libre, pero inclúyalo al final (después de sus otras directivas #include). La mayoría de las cabeceras en la biblioteca estándar son plantillas, y puesto que la mayoría de los compiladores usan el modelo de inclusión de compilación de plantilla (significa que todo el código fuente está en las cabeceras), la macro que reemplaza new en MemCheck.h usurpará todas las instancias del operador new en el código fuente de la biblioteca (y casi resultaría en errores de compilación). Además, está sólo interesado en seguir sus propios errores de memoria, no los de la biblioteca.
En el siguiente fichero, que contiene la implementación del seguimiento de memoria, todo está hecho con C estándar I/O más que con iostreams C++. No debería influir, puesto que no estamos interfiriendo con el uso de iostream en la memoria libre, pero cuando lo intentamos, algunos compiladores se quejaron. Todos los compiladores estaban felices con la versión <cstdio>.
//: C02:MemCheck.cpp {O} #include <cstdio> #include <cstdlib> #include <cassert> #include <cstddef> using namespace std; #undef new // Global flags set by macros in MemCheck.h bool traceFlag = true; bool activeFlag = false; namespace { // Memory map entry type struct Info { void* ptr; const char* file; long line; }; // Memory map data const size_t MAXPTRS = 10000u; Info memMap[MAXPTRS]; size_t nptrs = 0; // Searches the map for an address int findPtr(void* p) { for(size_t i = 0; i < nptrs; ++i) if(memMap[i].ptr == p) return i; return -1; } void delPtr(void* p) { int pos = findPtr(p); assert(pos >= 0); // Remove pointer from map for(size_t i = pos; i < nptrs-1; ++i) memMap[i] = memMap[i+1]; --nptrs; } // Dummy type for static destructor struct Sentinel { ~Sentinel() { if(nptrs > 0) { printf("Leaked memory at:\n"); for(size_t i = 0; i < nptrs; ++i) printf("\t%p (file: %s, line %ld)\n", memMap[i].ptr, memMap[i].file, memMap[i].line); } else printf("No user memory leaks!\n"); } }; // Static dummy object Sentinel s; } // End anonymous namespace // Overload scalar new void* operator new(size_t siz, const char* file, long line) { void* p = malloc(siz); if(activeFlag) { if(nptrs == MAXPTRS) { printf("memory map too small (increase MAXPTRS)\n"); exit(1); } memMap[nptrs].ptr = p; memMap[nptrs].file = file; memMap[nptrs].line = line; ++nptrs; } if(traceFlag) { printf("Allocated %u bytes at address %p ", siz, p); printf("(file: %s, line: %ld)\n", file, line); } return p; } // Overload array new void* operator new[](size_t siz, const char* file, long line) { return operator new(siz, file, line); } // Override scalar delete void operator delete(void* p) { if(findPtr(p) >= 0) { free(p); assert(nptrs > 0); delPtr(p); if(traceFlag) printf("Deleted memory at address %p\n", p); } else if(!p && activeFlag) printf("Attempt to delete unknown pointer: %p\n", p); } // Override array delete void operator delete[](void* p) { operator delete(p); } ///:~
Listado 3.15. C02/MemCheck.cpp
Las banderas booleanas de traceFalg y activeFlag son globales, por lo que pueden ser modificados en su código por las macros TRACE_ON( ), TRACE_OFF( ), MEM_ON( ), y MEM_OFF( ). En general, encierre todo el código en su main( ) dentro una pareja MEM_ON( )-MEM_OFF( ) de modo que la memoria sea siempre trazada. Trazar, que repite la actividad de las funciones de sustitución por el operador new( ) y el operador delete( ), es por defecto, pero puede desactivarlo con TRACE_OFF( ). En cualquier caso, los resultados finales son siempre impresos (vea la prueba que se ejecuta más tarde en este capítulo).
La facilidad MemCheck rastrea la memoria guardando todas las direcciones asignadas por el operador new( ) en un array de estructuras Info, que también tiene el nombre del fichero y el número de línea donde la llamada new se encuentra. Para prevenir la colisión con cualquier nombre que haya colocado en el espacio de nombres global, tanta información como sea posible se guarda dentro del espacio de nombre anónimo. La clase Sentinel existe únicamente para llamar a un destructor de objetos con estático cuando el programa termina. Este destructor inspecciona memMap para ver si algún puntero está esperando a ser borrado (indicando una perdida de memoria).
Nuestro operador new( ) usa malloc( ) para conseguir memoria, y luego añade el puntero y su información de fichero asociado a memMap. La función de operador delete( ) deshace todo el trabajo llamando a free( ) y decrementando nptrs, pero primero se comprueba para ver si el puntero en cuestión está en el mapa en el primer lugar. Si no es así, o reintenta borrar una dirección que no está en el almacén libre, o re intenta borrar la que ya ha sido borrada y eliminada del mapa. La variable activeFlag es importante aquí porque no queremos procesar ninguna desasignación de alguna actividad del cierre del sistema. Llamando a MEM_OFF( ) al final de su código, activeFlag será puesta a falso, y posteriores llamadas para borrar serán ignoradas. (Está mal en un programa real, pero nuestra intención aquí es encontrar agujeros, no está depurando la biblioteca.) Por simplicidad, enviamos todo el trabajo por array new y delete a sus homólogos escalares.
Lo siguiente es un test sencillo usando la facilidad MemCheck:
//: C02:MemTest.cpp //{L} MemCheck // Test of MemCheck system. #include <iostream> #include <vector> #include <cstring> #include "MemCheck.h" // Must appear last! using namespace std; class Foo { char* s; public: Foo(const char*s ) { this->s = new char[strlen(s) + 1]; strcpy(this->s, s); } ~Foo() { delete [] s; } }; int main() { MEM_ON(); cout << "hello" << endl; int* p = new int; delete p; int* q = new int[3]; delete [] q; int* r; delete r; vector<int> v; v.push_back(1); Foo s("goodbye"); MEM_OFF(); } ///:~
Listado 3.16. C02/MemTest.cpp
Este ejemplo verifica que puede usar MemCheck en presencia de streams, contenedores estándar, y clases que asignan memoria en constructores. Los punteros p y q son asignados y desasignados sin ningún problema, pero r no es un puntero de pila válido, así que la salida indica el error como un intento de borrar un puntero desconocido:
hola Asignados 4 bytes en la dirección 0xa010778 (fichero: memtest.cpp, línea: 25) Deleted memory at address 0xa010778 Asignados 12 bytes en la dirección 0xa010778 (fichero: memtest.cpp, línea: 27) Memoria borrada en la dirección 0xa010778 Intento de borrar puntero desconocido: 0x1 Asignados 8 bytes en la dirección 0xa0108c0 (fichero: memtest.cpp, línea: 14) Memoria borrada en la dirección 0xa0108c0 ¡No hay agujeros de memoria de usuario!
A causa de la llamada a MEM_OFF( ), no se procesan posteriores llamadas al operador delete( ) por vector o ostream. Todavía podría conseguir algunas llamadas a delete realizadas dsede reasignaciones por los contenedores.
Si llama a TRACE_OFF( ) al principio del programa, la salida es
Hola Intento de borrar puntero desconocido: 0x1 ¡No hay agujeros de memoria de usuario!