Gli errori più comuni che commettono i firmwaristi

Le insidie della programmazione sono centinaia se non migliaia. Ogni volta che scriviamo una riga di codice dobbiamo possedere elevate capacità di autocritica per comprendere se quella determinata riga o quella funzione svolge le operazioni come le abbiamo pensate. Spesso si tralasciano piccoli particolari che, in un futuro molto vicino, ci creeranno diversi grattacapi e ore di sonno perse per capire quale riga di codice non si comporta come pensavamo. In questo articolo affronteremo alcuni degli errori più comuni nella programmazione in ANSI C, che il compilatore non ci segnala, ma che sicuramente possiamo fare a meno di commettere.

Introduzione

Chi non ha mai commesso un errore le cui conseguenze sono emerse a lavoro completato? E casomai anche consegnato? La programmazione in ANSI C o, equivalentemente con gli altri linguaggi di programmazione, presenta sempre delle insidie: qualsiasi riga di codice, istruzione, costrutto e via dicendo dovrà essere “interpretata” dal compilatore che effettuerà dei check di compliance ad un set di regole dello standard (punteggiatura, tipologia di parentesi, tipologia di operazioni, etc.) e tradurrà ciò che noi abbiamo scritto ad “alto livello” in un linguaggio di basso livello, che più si adatta alla piattaforma sulla quale verrà eseguito. Così che cicli, confronti, somma, moltiplicazioni e tant’altro diverranno semplici operazioni eseguite da una ALU con eventuali salti tra registri e locazioni di memoria. Problemi di programmazione li possiamo avere anche nei nostri progetti con Arduino, Raspberry o qualsiasi altra piattaforma embedded. In questo articolo vi mostrerò alcuni dei più comuni errori che non vengono individuati dal compilatore ma che poi si presentano in maniera devastante nell’esecuzione dell’applicazione. Per supportare gli esempi, ho eseguito il codice di esempio con l'applicazione devC++ reperibile sul sito ufficiale.

L’ordine delle operazioni

Quando si scrive una funzione su un’unica riga di codice che include svariate operazioni è sempre necessario prestare estrema attenzione all’ordine con cui queste verranno eseguite, come già si fa normalmente per le espressioni matematiche. In realtà, quando si programma, non solo le operazioni aritmetiche vengono eseguite in un certo ordine ma qualsiasi altra operazione come le assegnazioni ed i confronti, oppure le condizioni all’interno dei costrutti. Questo è dovuto al fatto che la maggior parte degli ambienti di programmazione sfruttano linguaggi sequenziali (C, C++, anche l’IDE di Arduino) per cui ciò che scriviamo nel codice viene “tradotto” (ossia compilato) in un linguaggio di più basso livello. La traduzione rispetta delle regole ben precise tra cui l’ordine delle singole operazioni matematiche o logiche.

Di seguito elenco alcune delle principali precedenze:

  1. Operazioni di moltiplicazione, divisione e modulo
  2. Addizione e sottrazione
  3. Operazioni di shift a destra e a sinistra
  4. Operatori relazionali (minore, maggiore)
  5. Operatori relazionali (uguale e non uguale)
  6. AND bit a bit (simbolo &)
  7. OR esclusivo bit a bit (simbolo ^)
  8. OR bit a bit (simbolo |)
  9. AND logico (simbolo &&)
  10. OR logico (simbolo ||)
  11. Operatori di assegnazione

Di seguito riporto un esempio pratico:

int operazione;
operazione =0;
printf ("Esempio Operazioni\n");
operazione = 7*1+5;
printf ("Il risultato della prima operazione e' %d\n", operazione);
operazione = 7*(1+5);
printf ("Il risultato della seconda operazione e' %d\n", operazione);

Messaggio mostrato in Console:

Esempio Operazioni
Il risultato della prima operazione è 12
Il risultato della seconda operazione è 42

Le parentesi nelle macro

Ogni bravo progettista sa che utilizzare in maniera corretta le macro gli consente di incrementare e migliorare la leggibilità del software che sta realizzando, nonché velocizzare il tempo di scrittura dello stesso, soprattutto quando si utilizzano più volte le stesse procedure. Inoltre, grazie alla macro è possibile ovviare alla necessità delle funzioni che, seppur ottimizzando la dimensione del software, creano non pochi problemi in termini di velocità di esecuzione dovendo gestire gli accessi allo stack.
La maggior parte dei firmwaristi tende a trascurare il corretto utilizzo delle parentesi poiché considerate superflue. In realtà, in molte applicazioni e utilizzi massivi delle macro, l’assenza delle parentesi può portare a piccoli bug difficili da individuare quando si esegue il codice.
Un esempio è il seguente:

#define funzione(x) x*x+1 /*Definizione della macro senza parentesi*/

Utilizzo nel codice: result= funzione (a+b)
Risultato ottenuto: result = a+b * a+b +1
Per la priorità dell’operazione di moltiplicazione sull’addizione si nota che il risultato ottenuto non era di fatto quello desiderato. Utilizzando correttamente le parentesi si può ovviare all’inconveniente. Infatti:

#define funzione(x) (x)*(x)+1 /*Definizione della macro con parentesi*/

Utilizzo nel codice: result= funzione (a+b)
Risultato ottenuto: result = (a+b) * (a+b) +1
L’utilizzo della parentesi è fondamentale non solo per le macro ma anche in diversi altri aspetti della programmazione come le operazioni sui puntatori.

int operazione,a, b;
operazione =0;
a=7;
b=5;
printf ("Esempio Parentesi macro\n");
operazione = macro1(a+b);
printf ("Il risultato della prima operazione e' %d\n", operazione);

operazione = macro2(a+b);
printf ("Il risultato della seconda operazione e' %d\n", operazione);

Messaggio mostrato in Console:

Esempio Parentesi macro
Il risultato della prima operazione è 48
Il risultato della seconda operazione è 145

Le operazioni tra tipi diversi

Tra gli errori più comuni durante la programmazione troviamo sicuramente l’uso di operazioni tra variabili di tipo diverso: interi, interi senza segno, float, double, booleani e via dicendo. Se nei casi più estremi, determinate operazioni non sono consentite da parte del compilatore (viene generato un errore in fase di compilazione ad esempio tra operazioni che includono variabili di tipo char) per le operazioni che interessano variabili numeriche, il compilatore non restituisce alcun errore forzando eventuali cast di tipo implicito.

In generale, mischiare variabili di tipo diverso nelle operazioni matematiche è sconsigliato, soprattutto quando si lavora con variabili con segno e senza segno (signed ed unsigned). Questo perché le operazioni avvengono in un “dominio comune” e quindi vengono effettuate dal compilatore delle conversioni automatiche (cast impliciti). Essendo queste delle conversioni senza controllo, possono portare ad errate interpretazioni. Per ovviare a questo problema si effettuano delle conversioni forzate (cast espliciti) in modo tale da non indurre il compilatore ad errate interpretazioni.

#define macro1(x) x*x+1
#define macro2(x) (x)*(x)+1
/****************************/

unsigned int a = 50;
signed int b = -1;
printf ("Esempio Tipi diversi\n");
printf ("Risultato senza cast\t");
if (a > b) printf("A e' maggiore di b\n");
else printf("A e' minore/uguale a B\n");
printf ("Risultato con cast\t");
if ((signed int)a > b) printf("A e' maggiore di b\n");
else printf("A e' minore/uguale a B\n");

Messaggio mostrato in Console:

Esempio Tipi diversi
Risultato senza cast A è minore/uguale a B
Risultato con cast A è maggiore di B

Lavorare con gli Array

Secondo voi quale errore si commette maggiormente con gli Array? Accedere in maniera errata agli indirizzi. Ricordiamo che, definito un array con una certa dimensione, l’indice dello stesso è compreso tra 0 e la "dimensione-1". Se in un’istruzione viene utilizzato un valore di indice errato (ossia superiore alla "dimensione-1") il compilatore non ha alcun meccanismo per accorgersene. Cosa succederà? L’indice di un array non è altro che un incremento del puntatore inizializzato al primo elemento dell’array stesso. Operare su un indice di array superiore alla dimensione dello stesso comporta operare in una locazione della memoria che è assegnata ad un’altra variabile: in pratica stiamo modificando o leggendo qualcos’altro senza saperlo.

[...]

ATTENZIONE: quello che hai appena letto è solo un estratto, l'Articolo Tecnico completo è composto da ben 2060 parole ed è riservato agli ABBONATI. Con l'Abbonamento avrai anche accesso a tutti gli altri Articoli Tecnici che potrai leggere in formato PDF per un anno. ABBONATI ORA, è semplice e sicuro.

Scarica subito una copia gratis

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend