Saltar al contenido
Topos Uranos

Resumen

Un programa que da un resultado equivocado no se corrige cambiando líneas al azar, sino investigando. Esta clase presenta la depuración como un método experimental: reproducir la falla, formular una hipótesis sobre su causa, diseñar un experimento que la confirme o la descarte, y acotar el lugar del error; la bisección lo encuentra en un número de pasos que crece como el logaritmo del tamaño del programa. Desarrolla el instrumento (la compilación con -g y sin optimización, la configuración de Visual Studio Code en los tres sistemas y una sesión real de GDB que muestra lo que el editor hace por debajo), los puntos de interrupción simples y condicionales, los pasos, la pila de llamadas y la inspección del estado, con un ejemplo central: la mediana que falla solo con un número par de datos. Cierra con las estrategias que valen más que la herramienta (reducir el caso, buscar el último punto correcto, comprobar invariantes con assert, leer el informe del detector) y con tres casos: un error por uno, una recursión que no termina y una referencia invalidada.

Objetivos de aprendizaje

  1. Configurar la compilación con información de depuración (-g, sin optimización) y lanzar el depurador desde Visual Studio Code en el sistema propio.
  2. Usar puntos de interrupción simples y condicionales, la ejecución paso a paso (entrar, saltar, salir) y la inspección de variables, de expresiones y de la pila de llamadas.
  3. Aplicar una estrategia sistemática (reproducir, reducir, bisecar, formular y poner a prueba hipótesis) para localizar un error dado, documentando cada hipótesis.
  4. Combinar el depurador con los detectores y con las pruebas para localizar un error de memoria a partir de su informe.

Depurar es investigar

El error y la falla

La falla es lo que se observa: un resultado equivocado, un programa que no termina, un informe del detector. El error (o defecto) es la causa: una línea, una condición o una decisión de diseño que no corresponde a la especificación. La falla se ve; el error hay que encontrarlo, y puede estar lejos de ella. De ello se sigue que corregir donde se ve la falla, por ejemplo restando 2 al resultado porque «siempre sale 2 de más», corrige el síntoma y deja la causa, que volverá con otros datos.

Si esto es así, depurar es una investigación sobre la causa, y su método es el de las ciencias experimentales:

  1. Observar y reproducir. Describir con precisión la falla (con qué entrada, qué se esperaba, qué se obtuvo) y conseguir que ocurra siempre que se quiera. Un error que no se reproduce no puede investigarse, ni puede comprobarse que se corrigió.
  2. Formular una hipótesis. Una afirmación sobre la causa que pueda resultar falsa: «el ordenamiento no ordena», «el índice del segundo dato central es uno de más».
  3. Diseñar un experimento. Elegir un punto del programa y predecir qué estado debería verse allí si la hipótesis es verdadera y qué si es falsa.
  4. Concluir y acotar. Descartar la hipótesis o confirmarla; en ambos casos, el tramo sospechoso se reduce. Se repite hasta que el tramo es una línea.

Ahora bien, el método exige disciplina: cambiar una sola cosa a la vez, anotar cada hipótesis con su resultado, y no tocar el código mientras no se sepa qué se busca. Cambiar líneas al azar hasta que la salida parezca buena puede ocultar la falla sin quitar el error.

EjemploLa mediana con un número par de datos

La mediana de una lista de datos es el dato central de la lista ordenada si la cantidad es impar, y el promedio de los dos centrales si es par. La clase Probar programas mostró que las pruebas descubren cuándo falla una función, pero no dónde está su error. La función siguiente pasa la prueba con cinco datos y no con cuatro.

Programa en C++
#include <algorithm>#include <print>#include <vector>​// Mediana de una lista de datos.// Precondición: values no está vacío.// Poscondición: devuelve el dato central de la lista ordenada, o el promedio de los dos centrales.double median(std::vector<double> values){    std::ranges::sort(values);    const std::size_t n{values.size()};    const std::size_t middle{n / 2};    if (n % 2 == 1) {        return values[middle];    }    return (values[middle] + values[middle + 1]) / 2.0;}​int main(){    const std::vector<double> odd{7.0, 1.0, 5.0, 3.0, 9.0};    const std::vector<double> even{7.0, 1.0, 5.0, 3.0};    std::println("Mediana de {}: {}", odd, median(odd));    std::println("Mediana de {}: {}", even, median(even));}
Salida
Mediana de [7, 1, 5, 3, 9]: 5Mediana de [7, 1, 5, 3]: 6

La observación es precisa: con {7,1,5,3}\{7, 1, 5, 3\}, ordenados {1,3,5,7}\{1, 3, 5, 7\}, se esperaba (3+5)/2=4(3 + 5)/2 = 4 y se obtuvo 6; con cinco datos, el resultado es correcto. Caben al menos tres hipótesis: (H1) el ordenamiento no ordena; (H2) los índices de los dos datos centrales están mal; (H3) la suma o la división están mal escritas. Cada una predice algo distinto en la línea 16, antes de ejecutarla: si H1 es verdadera, values no está ordenado; si H2 lo es, values[middle] y values[middle + 1] no son 3 y 5; si H3 lo es, los dos datos son 3 y 5 y aun así la cuenta da 6. Que el caso impar funcione hace improbable H1, pero «improbable» no es «descartada»; el experimento, con el depurador, es detener el programa en la línea 16 y mirar.

La bisección

Cuando no hay una hipótesis clara, queda un método que no falla: la bisección. Se supone que el estado del programa es correcto al comienzo y equivocado al final, y que en cada punto intermedio puede decidirse si es correcto (comparándolo con un valor calculado a mano, o comprobando un invariante). Se examina el estado en la mitad del tramo sospechoso: si es correcto, el error está en la segunda mitad; si no, en la primera. Es la búsqueda binaria de la clase Buscar y ordenar, aplicada a las líneas de un programa (o a las vueltas de un bucle, o a las versiones de un archivo).

La figura sigue la bisección de dieciséis líneas con el error en la 11; cada raya es un experimento sobre el estado después de esa línea: correcto (✓) o equivocado (✗).

Demostración

  1. Al comienzo, las dieciséis líneas son sospechosas: se sabe que el estado es correcto antes de la línea 1 y equivocado después de la 16.

  2. Después de la línea 8, el estado es correcto: las líneas 1 a 8 quedan exoneradas.

  3. Después de la 12, el estado ya es incorrecto: el error está entre la 9 y la 12.

  4. Después de la 10, correcto: quedan la 11 y la 12.

  5. Después de la 11, incorrecto: el último punto correcto y el primero incorrecto son vecinos, y el error está en la línea 11.

El conteo es el de la búsqueda binaria: cada experimento divide el tramo por dos, de modo que kk experimentos bastan para 2k2^k líneas. Para un programa de mil líneas bastan diez, porque 210=10242^{10} = 1024. De donde la ventaja del método: su costo crece como el logaritmo del tamaño, y no exige comprender todo el programa, sino saber decidir, en un punto, si el estado es el debido. Ahora bien, esa decisión es la parte difícil: las especificaciones y los invariantes son el criterio con que se juzga cada estado intermedio.