Resumo
Um programa que dá um resultado errado não se corrige mudando linhas ao acaso, mas investigando. Esta aula apresenta a depuração como um método experimental: reproduzir a falha, formular uma hipótese sobre sua causa, projetar um experimento que a confirme ou a descarte, e delimitar o lugar do defeito; a bissecção o encontra em um número de passos que cresce como o logaritmo do tamanho do programa. Desenvolve o instrumento (a compilação com -g e sem otimização, a configuração do Visual Studio Code nos três sistemas e uma sessão real do GDB que mostra o que o editor faz por baixo), os pontos de interrupção simples e condicionais, os passos, a pilha de chamadas e a inspeção do estado, com um exemplo central: a mediana que falha apenas com um número par de dados. Fecha com as estratégias que valem mais que a ferramenta (reduzir o caso, buscar o último ponto correto, verificar invariantes com assert, ler o relatório do detector) e com três casos: um erro de um a mais, uma recursão que não termina e uma referência invalidada.
Objetivos de aprendizagem
- Configurar a compilação com informação de depuração (
-g, sem otimização) e iniciar o depurador a partir do Visual Studio Code no próprio sistema. - Usar pontos de interrupção simples e condicionais, a execução passo a passo (intervir, contornar, sair) e a inspeção de variáveis, de expressões e da pilha de chamadas.
- Aplicar uma estratégia sistemática (reproduzir, reduzir, bissectar, formular e pôr à prova hipóteses) para localizar um defeito dado, documentando cada hipótese.
- Combinar o depurador com os detectores e com os testes para localizar um erro de memória a partir de seu relatório.
Depurar é investigar
O defeito e a falha
A falha é o que se observa: um resultado errado, um programa que não termina, um relatório do detector (sanitizer). O defeito (ou erro) é a causa: uma linha, uma condição ou uma decisão de projeto que não corresponde à especificação. A falha se vê; o defeito é preciso encontrá-lo, e pode estar longe dela. Segue-se daí que corrigir onde se vê a falha, por exemplo subtraindo 2 do resultado porque “sempre sai 2 a mais”, corrige o sintoma e deixa a causa, que voltará com outros dados.
Se isso é assim, depurar é uma investigação sobre a causa, e seu método é o das ciências experimentais:
- Observar e reproduzir. Descrever com precisão a falha (com que entrada, o que se esperava, o que se obteve) e conseguir que ocorra sempre que se queira. Um defeito que não se reproduz não pode ser investigado, nem se pode verificar que foi corrigido.
- Formular uma hipótese. Uma afirmação sobre a causa que possa resultar falsa: “a ordenação não ordena”, “o índice do segundo dado central tem um a mais”.
- Projetar um experimento. Escolher um ponto do programa e prever que estado deveria ser visto ali se a hipótese for verdadeira e qual se for falsa.
- Concluir e delimitar. Descartar a hipótese ou confirmá-la; em ambos os casos, o intervalo suspeito se reduz. Repete-se até que o intervalo seja uma linha.
Ora, o método exige disciplina: mudar uma só coisa de cada vez, anotar cada hipótese com seu resultado, e não tocar no código enquanto não se souber o que se procura. Mudar linhas ao acaso até que a saída pareça boa pode ocultar a falha sem remover o defeito.
A mediana de uma lista de dados é o dado central da lista ordenada se a quantidade é ímpar, e a média dos dois centrais se é par. A aula Testar mostrou que os testes descobrem quando uma função falha, mas não onde está seu defeito. A função seguinte passa no teste com cinco dados e não com quatro.
#include <algorithm>#include <cstddef>#include <print>#include <vector>// Mediana de uma lista de dados.// Pré-condição: values não está vazio.// Pós-condição: devolve o dado central da lista ordenada, ou a média dos dois centrais.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));}Mediana de [7, 1, 5, 3, 9]: 5Mediana de [7, 1, 5, 3]: 6A observação é precisa: com , ordenados , esperava-se e obteve-se 6; com cinco dados, o resultado é correto. Cabem pelo menos três hipóteses: (H1) a ordenação não ordena; (H2) os índices dos dois dados centrais estão errados; (H3) a soma ou a divisão estão mal escritas. Cada uma prevê algo distinto na linha 17, antes de executá-la: se H1 é verdadeira, values não está ordenado; se H2 o é, values[middle] e values[middle + 1] não são 3 e 5; se H3 o é, os dois dados são 3 e 5 e ainda assim a conta dá 6. Que o caso ímpar funcione torna H1 improvável, mas “improvável” não é “descartada”; o experimento, com o depurador, é deter o programa na linha 17 e olhar.
A bissecção
Quando não há uma hipótese clara, resta um método que não falha: a bissecção. Supõe-se que o estado do programa é correto no começo e errado no final, e que em cada ponto intermediário se pode decidir se é correto (comparando-o com um valor calculado à mão, ou verificando um invariante). Examina-se o estado na metade do intervalo suspeito: se é correto, o defeito está na segunda metade; se não, na primeira. É a busca binária da aula Buscar e ordenar, aplicada às linhas de um programa (ou às iterações de um laço, ou às versões de um arquivo).
A figura acompanha a bissecção de dezesseis linhas com o defeito na 11; cada traço é um experimento sobre o estado depois dessa linha: correto (✓) ou errado (✗).
Demonstração
No começo, as dezesseis linhas são suspeitas: sabe-se que o estado é correto antes da linha 1 e errado depois da 16.
Depois da linha 8, o estado é correto: as linhas 1 a 8 ficam inocentadas.
Depois da 12, o estado já é incorreto: o defeito está entre a 9 e a 12.
Depois da 10, correto: restam a 11 e a 12.
Depois da 11, incorreto: o último ponto correto e o primeiro incorreto são vizinhos, e o defeito está na linha 11.
A contagem é a da busca binária: cada experimento divide o intervalo por dois, de modo que experimentos bastam para linhas. Para um programa de mil linhas bastam dez, porque . Donde a vantagem do método: seu custo cresce como o logaritmo do tamanho, e não exige compreender todo o programa, mas saber decidir, em um ponto, se o estado é o devido. Ora, essa decisão é a parte difícil: as especificações e os invariantes são o critério com que se julga cada estado intermediário.
Carregando o conteúdo…