3.6 — Utilizzo di un debugger integrato:Stepping

3.6 — Utilizzo di un debugger integrato:Stepping

Quando esegui il programma, l'esecuzione inizia nella parte superiore del principale funzione, e quindi procede in sequenza istruzione per istruzione, fino alla fine del programma. In qualsiasi momento mentre il tuo programma è in esecuzione, il programma tiene traccia di molte cose:il valore delle variabili che stai usando, quali funzioni sono state chiamate (in modo che quando quelle funzioni ritornano, il programma saprà dove a cui tornare) e il punto di esecuzione corrente all'interno del programma (quindi sa quale istruzione eseguire successivamente). Tutte queste informazioni monitorate sono chiamate stato del tuo programma (o semplicemente stato , in breve).

Nelle lezioni precedenti, abbiamo esplorato vari modi per modificare il codice per facilitare il debug, inclusa la stampa di informazioni diagnostiche o l'utilizzo di un logger. Questi sono metodi semplici per esaminare lo stato di un programma mentre è in esecuzione. Sebbene possano essere efficaci se usati correttamente, hanno comunque degli svantaggi:richiedono la modifica del codice, che richiede tempo e può introdurre nuovi bug, e ingombrano il tuo codice, rendendo più difficile la comprensione del codice esistente.

Dietro le tecniche che abbiamo mostrato finora c'è un presupposto non dichiarato:che una volta eseguito il codice, verrà eseguito fino al completamento (solo facendo una pausa per accettare l'input) senza alcuna possibilità per noi di intervenire e ispezionare i risultati del programma in qualsiasi momento vogliamo.

Tuttavia, cosa accadrebbe se potessimo rimuovere questa ipotesi? Fortunatamente, la maggior parte degli IDE moderni viene fornita con uno strumento integrato chiamato debugger progettato per fare esattamente questo.

Il debugger

Un debugger è un programma per computer che consente al programmatore di controllare come viene eseguito un altro programma ed esaminare lo stato del programma mentre quel programma è in esecuzione. Ad esempio, il programmatore può utilizzare un debugger per eseguire un programma riga per riga, esaminando il valore delle variabili lungo il percorso. Confrontando il valore effettivo delle variabili con ciò che ci si aspetta o osservando il percorso di esecuzione attraverso il codice, il debugger può aiutare immensamente a rintracciare gli errori semantici (logici).

Il potere dietro il debugger è duplice:la capacità di controllare con precisione l'esecuzione del programma e la capacità di visualizzare (e modificare, se lo si desidera) lo stato del programma.

I primi debugger, come gdb, erano programmi separati che avevano interfacce a riga di comando, in cui il programmatore doveva digitare comandi arcani per farli funzionare. I debugger successivi (come le prime versioni del turbo debugger di Borland) erano ancora autonomi, ma venivano forniti con i propri front-end "grafici" per semplificare il lavoro con essi. Molti IDE moderni disponibili in questi giorni hanno un debugger integrato, ovvero un debugger utilizza la stessa interfaccia dell'editor di codice, quindi puoi eseguire il debug utilizzando lo stesso ambiente che usi per scrivere il codice (piuttosto che dover cambiare programma).

Mentre i debugger integrati sono molto convenienti e consigliati ai principianti, i debugger a riga di comando sono ben supportati e comunemente usati in ambienti che non supportano le interfacce grafiche (ad es. sistemi embedded).

Quasi tutti i debugger moderni contengono lo stesso set standard di funzionalità di base, tuttavia, c'è poca coerenza in termini di come sono organizzati i menu per accedere a queste funzionalità e ancora meno coerenza nelle scorciatoie da tastiera. Sebbene i nostri esempi utilizzino schermate di Microsoft Visual Studio (e tratteremo anche come fare tutto in Code::Blocks), dovresti avere pochi problemi a capire come accedere a ciascuna funzionalità di cui discutiamo, indipendentemente dall'IDE che stai utilizzando .

Suggerimento

Le scorciatoie da tastiera del debugger funzioneranno solo se l'IDE/debugger integrato è la finestra attiva.

Il resto di questo capitolo sarà dedicato all'apprendimento dell'uso del debugger.

Suggerimento

Non trascurare di imparare a usare un debugger. Man mano che i tuoi programmi diventano più complicati, la quantità di tempo che dedichi a imparare a utilizzare il debugger integrato in modo efficace impallidirà rispetto alla quantità di tempo che risparmi per trovare e risolvere i problemi.

Avvertimento

Prima di procedere con questa lezione (e le successive lezioni relative all'uso di un debugger), assicurati che il tuo progetto sia compilato utilizzando una configurazione di build di debug (vedi 0.9 -- Configurazione del compilatore:Configurazioni di build per maggiori informazioni).

Se invece stai compilando il tuo progetto utilizzando una configurazione di rilascio, la funzionalità del debugger potrebbe non funzionare correttamente (ad es. quando provi ad entrare nel tuo programma, eseguirà semplicemente il programma).

Per Codice::Blocca utenti

Se stai usando Code::Blocks, il tuo debugger potrebbe o non potrebbe essere impostato correttamente. Controlliamo.

Innanzitutto, vai su menu Impostazioni> Debugger... . Quindi, apri il debugger GDB/CDB albero a sinistra e scegli Predefinito . Dovrebbe aprirsi una finestra di dialogo simile a questa:

Se vedi una grande barra rossa dove dovrebbe trovarsi il "Percorso eseguibile", devi individuare il tuo debugger. Per farlo, fai clic su ... pulsante a destra del Percorso eseguibile campo. Quindi, trova il file "gdb32.exe" sul tuo sistema:il mio era in C:\Programmi (x86)\CodeBlocks\MinGW\bin\gdb32.exe . Quindi fare clic su OK .

Per Codice::Blocca utenti

Sono stati segnalati che il debugger integrato Code::Blocks (GDB) può avere problemi nel riconoscere alcuni percorsi di file che contengono spazi o caratteri non inglesi. Se il debugger sembra non funzionare correttamente durante queste lezioni, questo potrebbe essere un motivo.

Fare un passo

Inizieremo la nostra esplorazione del debugger esaminando prima alcuni degli strumenti di debug che ci consentono di controllare il modo in cui un programma viene eseguito.

Stepping è il nome di un insieme di funzionalità del debugger correlate che ci consentono di eseguire (scorrere) il nostro codice istruzione per istruzione.

Ci sono una serie di comandi stepping correlati che tratteremo a turno.

Entra in

Il comando step into esegue l'istruzione successiva nel normale percorso di esecuzione del programma, quindi interrompe l'esecuzione del programma in modo da poter esaminare lo stato del programma utilizzando il debugger. Se l'istruzione in esecuzione contiene una chiamata di funzione, entrare fa sì che il programma salti all'inizio della funzione chiamata, dove si fermerà.

Diamo un'occhiata a un programma molto semplice:

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

Eseguiamo il debug di questo programma utilizzando il passo in comando.

Innanzitutto, individua e quindi esegui il passo in comando debug una volta.

Per gli utenti di Visual Studio

In Visual Studio, il entro è possibile accedere al comando tramite menu Debug> Entra in o premendo il tasto di scelta rapida F11.

Per Codice::Blocca utenti

In Code::Blocks, entra in è possibile accedere al comando tramite menu Debug> Entra in o premendo Maiusc-F7

Per altri compilatori

Se utilizzi un IDE diverso, probabilmente troverai il entrare in comando in un menu Debug o Esegui.

Quando il tuo programma non è in esecuzione ed esegui il primo comando di debug, potresti vedere accadere un bel po' di cose:

  • Il programma verrà ricompilato se necessario.
  • Il programma inizierà a essere eseguito. Poiché la nostra applicazione è un programma console, dovrebbe aprirsi una finestra di output della console. Sarà vuoto perché non abbiamo ancora prodotto nulla.
  • Il tuo IDE potrebbe aprire alcune finestre diagnostiche, che potrebbero avere nomi come "Strumenti diagnostici", "Stack chiamate" e "Guarda". Tratteremo quali sono alcuni di questi in seguito, per ora puoi ignorarli.

Perché abbiamo fatto un passo dentro , dovresti ora vedere una sorta di indicatore apparire a sinistra della parentesi graffa di apertura della funzione main (riga 9). In Visual Studio, questo indicatore è una freccia gialla (Code::Blocks usa un triangolo giallo). Se stai utilizzando un IDE diverso, dovresti vedere qualcosa che ha lo stesso scopo.

Questa freccia indica che la linea puntata verrà eseguita successivamente. In questo caso, il debugger ci dice che la riga successiva da eseguire è la parentesi graffa di apertura della funzione main (riga 9).

Scegli entra in (usando il comando appropriato per il tuo IDE, elencato sopra) per eseguire la parentesi graffa di apertura e la freccia si sposterà all'istruzione successiva (riga 10).

Ciò significa che la riga successiva che verrà eseguita sarà la chiamata alla funzione printValue .

Scegli entra in ancora. Perché questa istruzione contiene una chiamata di funzione a printValue , entriamo nella funzione e la freccia si sposterà nella parte superiore del corpo di printValue (riga 4).

Scegli entra in di nuovo per eseguire la parentesi graffa di apertura della funzione printValue , che farà avanzare la freccia alla riga 5.

Scegli entra in ancora una volta, che eseguirà l'istruzione std::cout << value e sposta la freccia sulla riga 6.

Avvertimento

Poiché operator<<è implementato come una funzione, il tuo IDE potrebbe invece passare all'implementazione di operator<<.

Se ciò accade, vedrai il tuo IDE aprire un nuovo file di codice e l'indicatore della freccia si sposterà all'inizio di una funzione denominata operator<<(questo fa parte della libreria standard). Chiudi il file di codice appena aperto, quindi trova ed esegui uscita comando debug (le istruzioni sono sotto nella sezione "uscita", se hai bisogno di aiuto).

Ora perché std::cout << value è stato eseguito, dovremmo vedere il valore 5 appaiono nella finestra della console.

Suggerimento

In una lezione precedente, abbiamo menzionato che std::cout è memorizzato nel buffer, il che significa che potrebbe esserci un ritardo tra quando chiedi a std::cout di stampare un valore e quando lo fa effettivamente. Per questo motivo, a questo punto potresti non visualizzare il valore 5. Per assicurarti che tutto l'output da std::cout venga emesso immediatamente, puoi aggiungere temporaneamente la seguente istruzione all'inizio della tua funzione main():

std::cout << std::unitbuf; // enable automatic flushing for std::cout (for debugging)

Per motivi di prestazioni, questa istruzione dovrebbe essere rimossa o commentata dopo il debug.

Se non vuoi aggiungere/rimuovere/commentare/decommentare continuamente quanto sopra, puoi racchiudere l'istruzione in una direttiva del preprocessore di compilazione condizionale (trattata nella lezione 2.10 -- Introduzione al preprocessore):

#ifdef DEBUG
std::cout << std::unitbuf; // enable automatic flushing for std::cout (for debugging)
#endif

Dovrai assicurarti che la macro del preprocessore DEBUG sia definita, da qualche parte al di sopra di questa istruzione o come parte delle impostazioni del tuo compilatore.

Scegli entra in di nuovo per eseguire la parentesi di chiusura della funzione printValue . A questo punto, printValue ha terminato l'esecuzione e il controllo viene restituito a principale .

Noterai che la freccia punta di nuovo a printValue !

Anche se potresti pensare che il debugger intenda chiamare printValue ancora una volta, in realtà il debugger ti sta solo facendo sapere che sta tornando dalla chiamata di funzione.

Scegli entra in altre tre volte. A questo punto, abbiamo eseguito tutte le righe del nostro programma, quindi abbiamo finito. Alcuni debugger interromperanno automaticamente la sessione di debug a questo punto, altri no. Se il tuo debugger non lo fa, potresti dover trovare un comando "Interrompi debug" nei menu (in Visual Studio, questo si trova in Debug> Interrompi debug ).

Tieni presente che Interrompi il debug può essere utilizzato in qualsiasi momento del processo di debug per terminare la sessione di debug.

Congratulazioni, ora hai eseguito un programma e hai guardato l'esecuzione di ogni riga!

Fai un passo avanti

Come entrare in , Il scavalcamento comando esegue l'istruzione successiva nel normale percorso di esecuzione del programma. Tuttavia, mentre entra in inserirà le chiamate di funzione e le eseguirà riga per riga, oltrepassa eseguirà un'intera funzione senza fermarsi e restituirà il controllo all'utente dopo che la funzione è stata eseguita.

Per gli utenti di Visual Studio

In Visual Studio, il passo oltre è possibile accedere al comando tramite menu Debug> Passa oltre o premendo il tasto di scelta rapida F10.

Per Codice::Blocca utenti

In Code::Blocks, il scavalca il comando si chiama Riga successiva invece, e vi si può accedere tramite menu Debug> Riga successiva o premendo il tasto di scelta rapida F7.

Diamo un'occhiata a un esempio in cui si scavalca la chiamata di funzione a printValue :

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

Per prima cosa, usa entra in sul tuo programma fino a quando l'indicatore di esecuzione è sulla riga 10:

Ora scegli scavalca . Il debugger eseguirà la funzione (che stampa il valore 5 nella finestra di output della console) e poi restituisci il controllo all'istruzione successiva (riga 12).

Il passo oltre Il comando fornisce un modo conveniente per saltare le funzioni quando sei sicuro che funzionino già o non sei interessato a eseguirne il debug in questo momento.

Esci

A differenza degli altri due comandi stepping, Step out non esegue solo la riga di codice successiva. Al contrario, esegue tutto il codice rimanente nella funzione attualmente in esecuzione, quindi ti restituisce il controllo quando la funzione è tornata.

Per gli utenti di Visual Studio

In Visual Studio, l'uscita è possibile accedere al comando tramite menu Debug> Esci o premendo la combinazione di scelta rapida Maiusc-F11.

Per Codice::Blocca utenti

In Code::Blocks, l'uscita è possibile accedere al comando tramite menu Debug> Esci o premendo la combinazione di scelta rapida ctrl-F7.

Diamo un'occhiata a un esempio di questo utilizzando lo stesso programma di cui sopra:

#include <iostream>

void printValue(int value)
{
    std::cout << value << '\n';
}

int main()
{
    printValue(5);

    return 0;
}

Entra il programma finché non si è all'interno della funzione printValue , con il marker di esecuzione sulla riga 4.

Quindi scegli esci . Noterai il valore 5 appare nella finestra di output e il debugger ti restituisce il controllo dopo che la funzione è terminata (alla riga 10).

Questo comando è molto utile quando sei entrato accidentalmente in una funzione di cui non vuoi eseguire il debug.

Un passo troppo avanti

Quando si passa attraverso un programma, normalmente è possibile solo fare un passo avanti. È molto facile oltrepassare (oltrepassare) accidentalmente il luogo che volevi esaminare.

Se oltrepassi la destinazione prevista, la solita cosa da fare è interrompere il debug e riavviare il debug, facendo un po' più attenzione a non superare il tuo obiettivo questa volta.

Fai un passo indietro

Alcuni debugger (come Visual Studio Enterprise Edition e GDB 7.0) hanno introdotto una funzionalità di passaggio generalmente denominata indietro o debug inverso . L'obiettivo di un passo indietro consiste nel riavvolgere l'ultimo passaggio, in modo da poter riportare il programma a uno stato precedente. Questo può essere utile se si va oltre o se si desidera riesaminare un'istruzione appena eseguita.

Fai un passo indietro per l'implementazione richiede molta sofisticazione da parte del debugger (perché deve tenere traccia di uno stato del programma separato per ogni passaggio). A causa della complessità, questa funzionalità non è ancora standardizzata e varia in base al debugger. Al momento della scrittura (gennaio 2019), né l'edizione della community di Visual Studio né l'ultima versione di Code::Blocks supportano questa funzionalità. Si spera che ad un certo punto in futuro ricada in questi prodotti e sia disponibile per un uso più ampio.