Introduzione alla Gestione degli Errori in Linguaggio C
- Un errore è una condizione anomala che può verificarsi durante l'esecuzione di un programma, impedendone il normale funzionamento.
- Esistono diversi tipi di errori: errori di compilazione, errori logici ed errori di runtime (eccezioni).
- In C, non esiste un sistema di gestione delle eccezioni integrato, ma è possibile implementare strategie per gestire gli errori di runtime.
- Una strategia comune in C è quella di utilizzare valori di ritorno speciali o parametri di output per segnalare errori alle funzioni chiamanti.
- Molte funzioni della libreria standard C utilizzano questo approccio per segnalare errori, e i programmatori devono sempre controllare questi valori per gestire correttamente le condizioni di errore.
Cos'è un Errore
In generale, durante l'esecuzione di un programma possono verificarsi delle condizioni anomale che non ne permettono il normale funzionamento.
Tali condizioni anomale prendono il nome di Errori.
Bisogna, però, fare delle dovute distinzioni tra i vari tipi di errori che possono verificarsi in un programma.
- Errori di Compilazione: sono errori che si verificano durante la fase di compilazione del codice sorgente. Questi errori impediscono al compilatore di tradurre il codice in linguaggio macchina. Esempi comuni includono errori di sintassi, dichiarazioni di variabili non valide, o l'uso di funzioni non definite.
- Errori Logici: sono errori che si verificano durante l'esecuzione del programma, ma sono dovuti a una logica errata implementata dal programmatore. Questi errori potrebbero provocare il crash del programma o produrre risultati inattesi. Ad esempio, un ciclo infinito o un calcolo matematico errato. Spesso questi errori prendono il nome di Bug.
- Errori di Runtime: sono errori che si verificano durante l'esecuzione del programma, ma non sono necessariamente dovuti a errori logici. Questi errori possono essere causati da condizioni esterne al programma, come la mancanza di memoria, l'accesso a file inesistenti, o un input errato da parte dell'utente. Questo tipo di errori viene spesso chiamato Eccezione.
Nei primi due casi, la responsabilità dell'errore ricade sugli sviluppatori del software che quindi, possono correggerli modificando il codice sorgente.
Il terzo caso, invece, è più complesso da gestire in quanto le condizioni che lo causano possono essere imprevedibili e fuori dal controllo dello sviluppatore.
Ad esempio, un programma che tenta di aprire un file potrebbe non riuscirci se il file non esiste o se l'utente non ha i permessi necessari. Oppure, un programma che richiede in input un numero potrebbe ricevere un valore non numerico, causando un errore durante la conversione.
Il punto è che mentre i primi due tipi di errori sono gestiti in fase di sviluppo, le eccezioni devono essere gestite in fase di esecuzione del programma per garantire che il software possa continuare a funzionare correttamente anche in presenza di condizioni anomale.
I linguaggi più moderni, come il C++, Java e Python, offrono meccanismi integrati per la gestione delle eccezioni, permettendo agli sviluppatori di scrivere codice più robusto e resiliente.
In C, purtroppo, non esiste un sistema di gestione delle eccezioni integrato nel linguaggio. Ciononostante, è possibile implementare delle strategie per gestire gli errori di runtime in modo efficace.
Strategie di Gestione degli Errori in C
Per capire come gestire gli errori in C, è importante porsi delle domande fondamentali.
Dal momento che, in ultima analisi, tutto il codice C può essere visto come una serie di funzioni che si chiamano a vicenda, quando scriviamo una funzione, quest'ultima potrebbe incontrare un errore durante la sua esecuzione. In generale, tale funzione potrebbe essere in grado di gestire l'errore da sola, oppure potrebbe essere necessario che tale funzione segnali o comunichi l'errore alla funzione chiamante, in modo che quest'ultima possa gestirlo.
Partiamo da un esempio semplice in cui abbiamo una funzione, leggi_numero, che legge un numero intero dall'input standard. Abbiamo due possibilità:
- Se l'input non è valido (ad esempio, l'utente inserisce una stringa invece di un numero), la funzione
leggi_numeropuò gestire l'errore internamente, ad esempio chiedendo all'utente di inserire nuovamente il numero. - Se l'input non è valido, la funzione
leggi_numeropuò segnalare l'errore alla funzione chiamante.
Il primo caso può essere gestito con un ciclo while che continua a chiedere l'input finché non viene fornito un valore valido:
#include <stdio.h>
int leggi_numero() {
int numero;
int risultato;
while (1) {
printf("Inserisci un numero intero: ");
risultato = scanf("%d", &numero);
if (risultato == 1) {
return numero; // Input valido
} else {
printf("Input non valido. Riprova.\n");
// Pulire il buffer di input
while (getchar() != '\n');
}
}
}
La funzione leggi_numero gestisce l'errore internamente, chiedendo all'utente di riprovare in caso di input non valido.
Nel secondo caso, invece, la funzione leggi_numero deve comunicare l'errore alla funzione chiamante. Tipicamente, l'approccio classico in C è quello di utilizzare un valore di ritorno speciale per indicare un errore, oppure utilizzare un parametro di output per restituire lo stato dell'operazione.
La funzione leggi_numero, però, restituisce un intero, quindi non possiamo utilizzare il valore di ritorno per segnalare un errore senza perdere il numero letto. Allora, dobbiamo modificare la funzione per utilizzare un parametro di output, ossia un puntatore a intero, per restituire il numero letto, e utilizzare il valore di ritorno per segnalare lo stato dell'operazione:
#include <stdio.h>
int leggi_numero(int *numero) {
int risultato;
printf("Inserisci un numero intero: ");
risultato = scanf("%d", numero);
if (risultato == 1) {
return 0; // Successo
} else {
// Pulire il buffer di input
while (getchar() != '\n');
return -1; // Errore
}
}
int main() {
int numero;
int stato = leggi_numero(&numero);
if (stato == 0) {
printf("Hai inserito il numero: %d\n", numero);
} else {
printf("Errore: input non valido.\n");
}
return 0;
}
In questo esempio, la funzione leggi_numero restituisce 0 in caso di successo e -1 in caso di errore. La funzione chiamante (main) può quindi controllare lo stato dell'operazione e gestire l'errore di conseguenza.
Questa strategia di gestione degli errori è molto comune in linguaggio C, al punto che molte funzioni della libreria standard C utilizzano questo approccio per segnalare errori. Si pensi ad esempio alle funzioni di I/O come fopen, fread, fwrite, ecc. Queste funzioni restituiscono valori speciali (come NULL o EOF) per indicare errori, e il programmatore deve sempre controllare questi valori per gestire correttamente le condizioni di errore.
Quindi, possiamo costruire uno schema generale per la gestione degli errori in C:
int valore;
valore = funzione_che_puo_fallire(parametri);
if (valore indica successo) {
// Procedi con l'elaborazione normale
} else {
// Gestisci l'errore
}
Usare un valore di ritorno per segnalare errori ha anche un altro vantaggio. Dal momento che stiamo usando un intero non è detto che ci dobbiamo limitare a due stati (successo/errore). Possiamo definire una serie di codici di errore per rappresentare diverse condizioni di errore, permettendo così una gestione più fine delle eccezioni.
Ad esempio, potremmo definire dei codici di errore come segue:
#define SUCCESSO 0
#define ERRORE_FILE_NON_TROVATO -1
#define ERRORE_LETTURA -2
#define ERRORE_SCRITTURA -3
int funzione_che_puo_fallire(parametri) {
// Logica della funzione
if (file_non_trovato) {
return ERRORE_FILE_NON_TROVATO;
}
if (errore_lettura) {
return ERRORE_LETTURA;
}
if (errore_scrittura) {
return ERRORE_SCRITTURA;
}
return SUCCESSO;
}
In questo modo, non solo stiamo segnalando che si è verificato un errore, ma stiamo anche fornendo informazioni specifiche sul tipo di errore che si è verificato, permettendo alla funzione chiamante di gestirlo in modo più appropriato.
In conclusione, anche se il linguaggio C non offre un sistema di gestione delle eccezioni integrato, è possibile implementare strategie efficaci per gestire gli errori di runtime, garantendo così la robustezza e l'affidabilità del software sviluppato.
Nella prossima lezione esploreremo una seconda tecnica di gestione delle eccezioni in C, usata da alcune funzioni della libreria standard, che si basa sull'uso della variabile globale errno.