Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


1.4.5  Depuración

"Software is a conversation, between the software developer and the user. But for that conversation to happen requires a lot of work beyond the software development. It takes marketing, yes, but also sales, and public relations, and an office, and a network, and infrastructure, and air conditioning in the office, and customer service, and accounting, and a bunch of other support tasks". Joel Spolsky. "The Development Abstraction Layer". www.joelonsoftware.com.

§1  Generalidades

La depuración está estrechamente relacionada con el concepto de calidad. En un sentido amplio, depurar un programa significa librarlo de errores e inconvenientes más o menos graves, que con frecuencia es un proceso mucho mas costoso y arduo de lo que pudiera parecer a primera vista, en especial en programas grandes y complejos. Sin embargo, es imprescindible si queremos ofrecer al público un producto con un mínimo de calidad.

Es interesante comprobar como la posición comercial de las grandes empresas de software ha evolucionado hasta una posición que podría considerarse envidiable y casi única, en relación con los productos producidos por el resto de la industria. Los que manejamos habitualmente software de cualquier tipo, desde la más humilde utilidad shareware [1] hasta los propios sistemas operativos, estamos acostumbrados a ver durante el proceso de instalación cláusulas en las que se anuncia que el producto se vende "Tal cual" ("As is");  sin ningún tipo de garantía o de responsabilidad que pudiera derivarse de su posible mal funcionamiento. Es cierto que el software tiene unas características muy especiales y que se podría decir mucho al respecto, pero no es menos cierto que habitualmente aceptamos condiciones que no le permitiríamos al fabricante que nos vende un automóvil, un par de zapatos o una lata de conserva por ejemplo.

Podemos observar como incluso productos software muy conocidos y de gran consumo, producidos por compañías muy poderosas [4], son lanzados a veces sin un proceso de depuración adecuado;  motivado la mayoría de las veces por la excesiva presión de su propia maquinaria de márketing, que las fuerza a lanzar nuevos productos y versiones a un ritmo desenfrenado.

En Junio de 2002, el NIST ("National Institute of Standards and Technology"), organismo dependiente del Gobierno USA publicaba un informe titulado "The Economic Impacts of Inadequate Infrastructure for Software Testing", en el que se analiza la repercusión económica debida a los defectos del software en dos sectores económicos del país:  Servicios financieros, e industria de fabricación de medios de transporte. El resultado es que solo en estos sectores, el coste de los errores (bugs) derivados de la incorrecta depuración del software asciende a unos 60.000 millones de dólares anuales [5].


§2  Depuración

En general la depuración de un programa se completa en tres fases; es un proceso en el que el producto se va acercando a la perfección (estar libre de errores e inconvenientes graves) mediante una serie de transformaciones sucesivas que responden al esquema de la figura.


En una primera fase, cuando ya el programa está prácticamente terminado, se somete a pruebas que podríamos llamar "de laboratorio" para comprobar que todo marcha según lo esperado y sin errores [2].  Son pruebas que realiza el propio programador antes de dar a conocer el producto. Estas versiones se suelen denominar alfa y corresponden a un punto en que el programa todavía está en fase de gestación. En una versión alfa son de esperar todo tipo de errores y "cuelgues".

En una segunda fase, cuando el programador cree que su producto ya está suficientemente presentable y él no consigue encontrar más errores aparentes (o los ya conocidos están en proceso de depuración), se procede a la distribución del producto a una serie de probadores seleccionados ("beta testers"). Son las denominadas versiones beta, que aunque con errores esporádicos, pueden tener un comportamiento más o menos aceptable.

Finalmente, en una tercera fase, con la información, opiniones y sugerencias de los "beta testers", se procede a lanzar la primera versión pública v 1.0 del producto ("Release"),  también denominadas versiones gamma. A partir de aquí, lo normal es que se vayan recogiendo las sugerencias, cuestiones y posibles errores que hayan pasado inadvertidos en las pruebas beta y sean reportados por los usuarios. Las soluciones a estos problemas, junto con las mejoras que se vayan incorporando, son implementados en versiones sucesivas.  Generalmente a partir de la versión 2.0 el producto se considera estable.

§3  El cuaderno de bitácora

Es frecuente que los programadores cuidadosos mantengan una especie de registro histórico o cuaderno de bitácora donde se recoge la evolución del programa. Puede tratarse de un simple fichero de texto plano con cualquier nombre alusivo:  Cambios.txt;  Historia.txt;  Versiones.txt [6], Etc. En él se recoge la información que se estima pertinente. Por ejemplo, las fechas de publicación de las diversas versiones; las mejoras introducidas respecto a las anteriores, y sobre todo los errores ("bugs") corregidos. Este fichero suele acompañar a cada nueva versión, pero lo mejor es darles la máxima publicidad entre los usuarios potenciales y actuales. Los primeros, porque se pueden hacer una idea de la vitalidad del producto (una historia larga de actualizaciones regulares y frecuentes es buena señal). A los usuarios actuales porque les permite evaluar si les merece la pena o no el proceso de actualización a las nuevas versiones.

En lo que respecta a este capítulo, nos referiremos exclusivamente a la depuración de versiones alfa, en las que los procesos de construcción del ejecutable suelen incluir los mecanismos de depuración, que posteriormente se omiten en las versiones de campo.

Nota:   Llegados a este punto, aconsejamos al lector novel saltarse por ahora el resto del capítulo y seguir con las siguientes secciones. Para lo que sigue es necesario conocer previamente los capítulos dedicados a: Tratamiento de Excepciones ( 1.6) y los Operadores de Preproceso ( 4.9.10).

§4  El depurador

Como se ha señalado anteriormente ( 1.4), los compiladores tienen una opción que permite incluir o no información adicional de depuración en el ejecutable. Esta información adicional consiste básicamente en la inclusión del número de línea (del código fuente) de cada sentencia, aunque también se pueden tomar otras medidas. Por ejemplo, tratar todas las funciones como si fuesen normales, no realizándose en estos casos sustituciones inline ( 4.4.6b).

Nota:  en el caso del compilador Borland C++ 5.5, la opción correspondiente es el comando -v, que está conectado (ON) por defecto ( 4.4.6b). El compilador GNU gcc utiliza la opción -g para este propósito.  No olvide que la presencia de la información de depuración produce un gran aumento de tamaño del ejecutable, por lo que solo debe utilizarse cuando verdaderamente sea necesaria.


Los entornos de desarrollo C++ actuales incluyen potentes depuradores con los que es posible controlar prácticamente todos los aspectos de ejecución de versiones alfa. Su manejo depende naturalmente de cada caso concreto, pero en general permiten inspeccionar el estado de llamadas de la pila, lo que significa controlar la "traza" de la ejecución. De esta forma es posible conocer el camino que ha seguido la ejecución hasta llegar a un punto concreto (que funciones han sido invocadas y cual es el valor de las variables). Por ejemplo, cuantas veces se ha invocado a sí misma una función recursiva y que valores tienen sus variables en cada una de las instancias; el valor de las variables globales, locales, automáticas y estáticas.

Las opciones anteriores son las que podríamos llamar mínimas. Por supuesto las versiones más avanzadas de los productos punteros permiten hurgar más cómoda y profundamente en las entrañas del ejecutable. Por ejemplo, depurar funciones miembro y aplicaciones multihebra; controlar la ejecución paso a paso; a nivel de instrucciones máquina (ensamblador) o a nivel de sentencia de nuestro código fuente, así como instalar puntos de control ("break points") en el ejecutable. Se trata de instrucciones específicas instaladas a la entrada de las zonas de código donde sospechamos que se presentan los problemas. Esto nos permite correr el programa a velocidad normal, pero al llegar a dichos puntos, es invocado automáticamente el depurador que nos muestra todas sus herramientas [3];  entonces se pueden realizar las comprobaciones oportunas, y si todo está correcto, volver a modo ejecución normal hasta que se alcanza el próximo punto de control (en los ejemplos que siguen aprenderemos a instalar en nuestro código puntos de control rudimentarios sin necesidad de utilizar ningún depurador).

Para cuando estas posibilidades no bastan, o sencillamente no se dispone de ellas, existen una serie de trucos y técnicas generales que facilitan la depuración. El proceso más general y socorrido consiste en incluir en el código una serie de puntos testigo ("flags") con salidas provisionales que nos informan que el programa ha pasado por el punto sin novedad y los valores de las variables sospechosas de mal funcionamiento.

Ejemplo 1:

Una forma sencilla de implementar estos semáforos puede ser utilizar un define ( 4.9.10b). Como ejemplo construimos un programa que muestre el paso por diversos puntos:

#include <iostream.h>
#define testigo(punto) cout << "Alcanzado punto " #punto << endl;

int main() {            // ==============
  char* ptr = "El valor actual de X: ";
  int x = 0;
  testigo(1);
  cout << ptr << x++ << endl;
  testigo(2);
  cout << ptr << x++ << endl;
  testigo(3);
  cout << ptr << x++ << endl;
  testigo(4); 
}

Salida:

Alcanzado punto 1
El valor actual de X: 0
Alcanzado punto 2
El valor actual de X: 1
Alcanzado punto 3
El valor actual de X: 2
Alcanzado punto 4

Ejemplo-2

Es posible que nos interese detener el proceso en cada punto con objeto de ver con comodidad la evolución. En este caso, unas pequeñas modificaciones en el las directivas del ejemplo cumplen perfectamente el cometido:

#include <iostream>
#include <conio.h>      // para getch() 
#define PAUSA for( ; ; ) if(getch()!=0) break ;
#define testigo(punto) cout << "Alcanzado punto " #punto << endl; PAUSA

using namespace std;

int main() {            // ============
  char* ptr = "El valor actual de X: ";
  int x = 0;
  testigo(1);
  cout << ptr << x++ << endl;
  testigo(2);
  cout << ptr << x++ << endl;
  testigo(3);
  cout << ptr << x++ << endl;
  testigo(4); 
}

En este caso, el cuerpo de main y la salida son idénticas al anterior, pero después de informar del paso por cada punto, el programa espera la pulsación de una tecla antes de continuar.

Ejemplo 3

Si el programa es complejo puede interesar no tener que estar pendientes de la situación de los semáforos que vamos colocando ni de su numeración. Una opción puede ser utilizar las constantes simbólicas ( 1.4.1a) del compilador. En la nueva versión que presentamos, el testigo nos informa automáticamente del fichero y número de linea del código fuente a que corresponde el punto que se está ejecutando.

#include <iostream.h>
#include <conio.h>             // para getch()
#define PAUSA for( ; ; ) if(getch()!=0) break ;
#define testigo cout << "Alcanzada linea " << __LINE__ << " modulo: " << __FILE__ << endl; PAUSA

int main() {                   // ================
   char* ptr = "El valor actual de X: ";
   int x = 0;
   testigo;
   cout << ptr << x++ << endl;
   testigo;
   cout << ptr << x++ << endl;
   testigo;
   cout << ptr << x++ << endl;
   testigo; 
}

Salida (el fichero fuente utilizado se denomina p3.c ):

Alcanzada linea 9 modulo: p3.c
El valor actual de X: 0
Alcanzada linea 11 modulo: p3.c
El valor actual de X: 1
Alcanzada linea 13 modulo: p3.c
El valor actual de X: 2
Alcanzada linea 15 modulo: p3.c

Ejemplo 4

En ocasiones puede resultar molesto tener que repasar la totalidad del código para eliminar las sentencias testigo provisionales de depuración. Este trabajo extra puede eliminarse fácilmente utilizando una directiva adecuada que ponemos y quitamos alternativamente según que la versión a construir sea o no de depuración:

#include <iostream.h>
#include <conio.h> 
#define PAUSA for( ; ; ) if(getch()!=0) break ;
#define DEBUG         // L.4: Comentar para versiones definitivas
#ifdef DEBUG
  #define testigo cout << "Alcanzada linea " << __LINE__ << " modulo: " << __FILE__ << endl; PAUSA
#else
  #define testigo
#endif 
int main() {          // ===============
  char* ptr = "El valor actual de X: ";
  int x = 0;
  testigo;
  cout << ptr << x++ << endl;
  testigo;
  cout << ptr << x++ << endl;
  testigo;
  cout << ptr << x++ << endl;
  testigo; 
}

La salida es análoga a la del ejemplo anterior, pero con esta disposición de las directivas de cabecera, solo hay que comentar L.4 para pasar a la versión definitiva; automáticamente quedarán eliminadas del código las sentencias de depuración.

En la práctica, este tipo de disposiciones se realizan en un fichero de cabecera particular que almacenamos aparte y que incluimos en todos los módulos que componen nuestro programa. En nuestro caso, podríamos definir un fichero  MiCabecera.h con el siguiente contenido:

#include <iostream.h>
#include <conio.h> 
#define PAUSA for( ; ; ) if(getch()!=0) break ;
#define DEBUG         // L.4: Comentar para versiones definitivas
#ifdef DEBUG
  #define testigo cout << "Alcanzada linea " << __LINE__ << " modulo: " << __FILE__ << endl; PAUSA
#else
  #define testigo
#endif

Posteriormente, solo necesitamos incluir una directiva de preproceso en el fichero fuente p3.c (y en cualquier otro que incluyera nuestra aplicación). Por ejemplo:

#include <MiCabecera.h>
int main() {          // ===============
  char* ptr = "El valor actual de X: ";
  int x = 0;
  testigo;
  cout << ptr << x++ << endl;
  testigo;
  cout << ptr << x++ << endl;
  testigo;
  cout << ptr << x++ << endl;
  testigo; 
}

§5  La función assert()

La librería Estándar C++ contiene una función particular que entre otros usos, puede servir muy bien para ayudar en la depuración. Se trata de la función assert, cuya definición es:

void assert(int test);

La función no devuelve nada y espera un entero como argumento. En realidad se trata de una macro ( 4.9.10b) contenida en el fichero de cabecera <assert.h> que sustituye la función por un trozo de código del siguiente aspecto:

#ifdef NDEBUG
  #define assert(p) ((void)0)
#else
  #define assert(p) ((p) ? (void)0 : _assert(#p, __FILE__, __LINE__))
#endif

Como puede comprobarse, el proceso de compilación sustituye la función por una expresión condicional ( 4.9.6) siempre que no esté definida la etiqueta NDEBUG (sin depuración); en caso contrario, la función es sencillamente eliminada del código (más propiamente podríamos decir que es sustituida directamente por el valor que devuelve: void 2.2.1 sin que se realice ninguna acción).

La función acepta como argumento cualquier valor que pueda ser promovido a entero (incluyendo punteros y expresiones relacionales). Posteriormente, el entero es considerado el resultado de una expresión relacional ( 4.9.12); si el valor es cierto (un valor distinto de cero), el programa sigue su ejecución normal, pero si es falso (valor igual a cero), se invoca la función _assert con tres parámetros: una cadena alfanumérica, copia del argumento test utilizado, y las constantes simbólicas ( 1.4.1a) __FILE__ y __LINE__.

Lo que hace a su vez la función interna _assert, depende del tipo de aplicación; de que se haya compilado, o no, con las opciones de depuración y de la plataforma.  En las aplicaciones de consola (no gráficas) se lanza un mensaje por el dispositivo estándar de salida de errores stderr ( 5.3): Assertion failed: test, file nombre-de-fichero, line numero-de-línea; a continuación se invoca la función abort ( 1.5.1) que termina la ejecución.

Como ejemplo vamos a construir un programa que construye una tabla de inversos de números enteros, en el que incluimos una comprobación de que en ningún caso el valor de la variable x sea igual a cero, ya que este valor supondría un error numérico en el programa (división por cero):

#include <iostream.h>
#include <assert.h>
#define NDEBUG

void func(int i);        // prototipo

int main() {             // ===============
  for (int x = 5; x <= 5; x--) {
    assert(x != 0);
    func(x);
  }
}

void func(int i) {       // definición
  float f1 = float(1)/float(i);
  cout << "X = " << i << " 1/x = " << f1 << endl;
}

Salida:

X = 5 1/x = 0.2
X = 4 1/x = 0.25
X = 3 1/x = 0.333333
X = 2 1/x = 0.5
X = 1 1/x = 1

Assertion failed: x != 0, file p3.c, line 9

Abnormal program termination


  Como se ha visto, cada aparición de la función assert en el código supone la verificación de una hipótesis. Durante la depuración de las versiones alfa, es buena práctica la colocación sistemática de comprobadores de este tipo en todos los puntos que sea posible, a fin de garantizar que las condiciones de contorno se mantienen dentro de lo esperado.

  Inicio.


[1]  Shareware: Modalidad de venta de software basada en el principio de probar antes de comprar. Ha experimentado un tremendo auge gracias a Internet. El software se ofrece de forma gratuita, totalmente operacional o con alguna limitación funcional, durante un periodo de tiempo de prueba (unos 30 días); pasado este tiempo el usuario debe abonar su importe o borrarlo del sistema.

[2]  Las teorías actuales sobre desarrollo de software señalan que debe conseguirse un producto mínimamente funcional lo antes posible, para ir perfeccionándolo en una serie de actualizaciones sucesivas de ciclo muy corto. Una posición límite preconiza que se deben escribir los test antes que el propio código, a fin de escribir este de forma que satisfaga los test previamente definidos.

[3]  En los depuradores más avanzados estos puntos de control pueden ser de dos clases: Simples y condicionales; los primeros invocan siempre al depurador al llegar la ejecución a dicho punto; los segundos solo actúan si al llegar a ellos se cumplen determinadas condiciones que pueden establecerse de antemano. En algunos casos es posible incluso establecer puntos de control aún después de haberse iniciado la ejecución del programa.

[4]  Como botón de muestra incluimos un comentario tomado del boletín IBLNEWS.com de 7-08-2001 refiriéndose a un software de Oracle: "Estas firmas han aparecido en el escenario, requeridas por Wall Street Journal, señalando que apenas utilizan 11i. Además, el diario neoyorquino ha indagado hasta concluir que el nuevo software de Oracle, que se esperaba supondría el futuro de la segunda compañía mundial del sector (sólo superada por Microsoft), está plagado de bugs, o errores informáticos, contradiciendo así al gran Ellison. La crítica, difundida en la edición del lunes 4, pone en solfa no tanto los fallos de un programa nuevo, sin apenas testar, algo que es común en la industria, sino el concepto en sí de la publicidad engañosa,".

[5]  El documento, de 1389 KB, está disponible en formato .pdf: report02-3.pdf

[6]  Este fichero suele denominarse "Changelog" en la literatura Aglosajona.