Capitolo 6 - Il controllo delle alternative

 

 

 

 

            6.1 - Il flusso di esecuzione del programma

 

 

                Nel capitolo 2, in 2.4.a abbiamo brevemente considerato la necessità di descrivere sequenze operative non esprimibili in modo puramente lineare, cioè con la logica: eseguita l’istruzione corrente passa a quella fisicamente successiva fino alla richiesta esplicita di arresto. Sono stati esaminati i casi delle alternative e dei cicli, che hanno in comune appunto il dovere trasformare in logicamente successiva una istruzione diversa da quella che nel flusso fisico é la prossima.

 

                Il primo metodo utilizzato nei linguaggi di programmazione per ottenere questo scopo é stato il salto diretto, direttamente derivato dalla struttura delle operazioni di macchina, per le quali é anzi l’unico modo di ottenere questo scopo; in C la sintassi dell’istruzione di salto é:

 

  (6.1.1)                   goto etichetta ;

 

in cui goto é una parola chiave del linguaggio, ed etichetta é un identificatore creato dall’utente con le stesse regole utilizzate per i nomi delle variabili; il medesimo nome deve comparire premesso ad una istruzione del programma (ed a non più di una) seguito dal simbolo ‘:’, il quale ne qualifica appunto la natura. E’ abitudine generale indicare le etichette con nomi composti di sole lettere maiuscole, per renderle più evidenti.

 

                Il modo di operare dell’istruzione di salto é immediatamente intuitivo e non richiede ulteriori commenti; essa é in grado di interrompere l’esecuzione sequenziale delle istruzioni per organizzare al loro interno ‘cammini’ complessi quanto si vuole: nella sintassi non é infatti specificato se l’etichetta ‘bersaglio’ del salto si debba trovare dopo o prima dello stesso goto.

 

                Con un po’ di esperienza si può provare che l’abitudine al largo impiego di etichette, che interrompono e frammentano le sequenze logiche nella scrittura del programma, ha come conseguenza uno stato di cose che è stato a volte descritto come ‘matassa di spaghetti’. La logica del programma tende cioè ad esserne fortemente oscurata e diviene difficile la ricerca degli errori o l’attuazione di modifiche ed ampliamenti successivi.

 

                Per questo motivo sono state introdotte forme linguistiche evolute, molto simili alle espressioni del linguaggio naturale cui si fa ricorso nell’analisi logica degli algoritmi, e si é fortemente ridotta la presenza delle istruzioni di salto diretto, che sono anzi in teoria eliminabili del tutto, con il passaggio da un estremo all’altro.

 

                Una soluzione così radicale non é però raccomandabile, poiché possono spesso verificarsi casi in cui un impiego ben delimitato di istruzioni goto è l’unica alternativa ragionevole ad una grande quantità di forme linguistiche strutturate annidate l’una nell’altra, che riprodurrebbero la ‘massa di spaghetti’, semplicemente cambiandone la marca e l’aspetto, per di più in condizioni apparentemente più sicure.

 

                La regola generale é quindi quella di evitare il salo esplicito, ma allo stesso tempo di non fare della sua presenza un tabù, utilizzandolo al pari di ogni altro strumento del linguaggio quando esso costituisce la soluzione più ‘pulita’ per una determinata circostanza.

 

 

 

 

            6.2 - Istruzione if

 

 

                Abbiamo già incontrato l’operatore condizionale ‘?’, specifico del linguaggio C, che nella forma presentata in (4.4.9) permette di esprimere alternative del tipo  se .. allora .. altrimenti .. , limitate a semplici espressioni in ognuno dei tre contesti marcati dai puntini, anche se ‘espressione semplice’ in C può divenire tutt’altro che ‘semplice’.


 

                Per esprimere alternative più complesse il mezzo più comune é la struttura condizionale aperta dalla istruzione if (in inglese, se), cui può fare seguito un’alternativa, che é una nuova istruzione aperta dalla parola chiave else (in inglese, altrimenti); la sintassi completa della costruzione é:

 

  (6.2.1)                   if ( espressione ) istruzione_1 ;  [ else istruzione_2 ; ]

 

in cui si vede bene come l’istruzione che inizia con else sia separata ed indipendente; essa é però una istruzione speciale, perché non può mai trovarsi da sola, ma soltanto come seconda clausola in una costruzione condizionale.

 

                In (6.2.1) if ed else sono parole chiave, mentre espressione é una costruzione comunque complessa, di cui interessa solo il valore finale falso / vero, ossia nullo / non nullo; infine, istruzione_1 ed istruzione_2 possono essere istruzioni semplici o composte (liste sequenziali separate da virgole), o blocchi racchiusi in parentesi graffe.

 

                La modalità operativa é: se espressione é vera, allora viene eseguito quanto richiesto da istruzione_1, mentre se essa é falsa si esegue istruzione_2 (se esiste). Alla conclusione, il controllo del flusso del programma passa in ogni caso all’istruzione immediatamente successiva alla costruzione if .. ; else .. ; .

 

                Consideriamo un semplice esempio di programma per la prova della tastiera; la libreria standard contiene la funzione getche ( ), capace di rilevare il codice ASCII di ogni tasto premuto, mostrandolo anche sullo schermo (“eco”) e restituirlo al programma come un valore di tipo char, unsigned char o int come ‘risultato’ delle operazioni della funzione. Dalla documentazione si ha, infine, che la funzione richiede che venga incluso il file di testata <conio.h>.

 

                Si vuole scrivere un programma che ‘legga’ la tastiera, scrivendo a video il valore ASCII ed il simbolo grafico associato; le operazioni debbono arrestarsi quando viene premuto il tasto Esc (o Escape), il cui codice ASCII è 27; segue il testo del programma:

 

  (6.2.2)                   /*  GETCHE01.C -  rilevazione di un tasto dalla tastiera e sua

                             rappresentazione finché diverso da ESCAPE

            */

            include <stdio.h>

            include <conio.h>

            #define ESCAPE 27

 

            main ( )

            {

                  char k ;

 

            GET:  k = getche ( ) ;

                  if ( k != ESCAPE )

                    { printf ( “\n ASCII %4.3d, simbolo : %c”, k, k ) ;

                      goto GET ;

                    }

            }

 

                Il lettore é invitato a riprodurre questo breve programma così come é scritto, compilarlo, sperimentarlo e quindi a provare ad introdurre varianti rispetto allo schema base, ad esempio nella scrittura, eseguendo il conto del numero di tasti premuti, o qualunque cosa la fantasia possa suggerire. Lo stesso vale per tutti i programmi di esempio che utilizzeremo di qui in avanti. Nella fase di sperimentazione si dovrebbero utilizzare tutti i tasti, compresi quelli ‘di controllo’, osservando con attenzione cosa accade a video quando si premono i tasti di Invio, Tab, ed altri simili.

 

                Rispetto agli esempi incontrati finora, printf contiene alcune novità: l’opzione ‘%4.3d’ é la richiesta di ordinaria scrittura decimale di un numero, ma utilizzando quattro colonne e scrivendo in ogni caso tre cifre, con il ricorso ad eventuali zeri non significativi; l’opzione ‘%c’ é invece la richiesta di trasmissione a video del carattere senza conversione di nessun tipo, per ottenerlo quindi nella sua forma grafica.          La parte finale di printf contiene poi la lista dei valori da scrivere, che in questo caso si riducono alla sola variabile k, ma citata due volte, per ottenere le due diverse rappresentazioni a video, numero e carattere.

 

                E’ interessante notare come l’introduzione di due soli elementi di controllo del flusso, il salto goto ed il condizionale if, quest’ultimo nella forma più semplice possibile, abbia permesso di costruire un programma non banale e di un certo interesse, anche se ‘rudimentale’ per una programmazione ‘accettabile’.


 

            6.3 - Alternative multiple

 

 

                La struttura condizionale (6.2.1) può essere utilizzata in due forme: la prima, con il solo if, permette di descrivere una azione condizionata da eseguire se la condizione risulta vera, senza dire nulla di una alternativa, mentre la seconda, con la clausola else, permette di esprimere due alternative mutuamente esclusive.

 

                Nel caso che le alternative che si escludono a vicenda siano tre (o più), come può ad esempio accadere con la soluzione di un’equazione di secondo grado, una attenta lettura della (6.2.1) prova che non é necessario arricchire la sintassi, ma semplicemente di utilizzare bene le potenzialità che essa fornisce.

 

                Ci riferiamo in particolare alla componente istruzione_2 che compare in (6.2.1); di essa si é detto che può trattarsi di qualunque istruzione legittima per il C; ne segue che, in particolare, essa può essere una nuova struttura condizionale aperta da un secondo if, nella forma:

 

  (6.3.1)                   if ( espressione_1 ) istruzione_1 ;

                               else if  ( espressione_2 ) istruzione_2 ;

                               [ else istruzione_3 ; ]

                               ...............

 

che é adatta ad esprimere appunto tre alternative mutuamente esclusive:

 

  (6..3.2)                  se espressione_1 é vera allora viene eseguita istruzione_1,

                               altrimenti se espressione_2 é vera, allora viene eseguita istruzione_2,

                               altrimenti (vale a dire se entrambe sono false) viene eseguita istruzione_3

 

di cui una, e solo una, viene eseguita; fatto ciò, l’esecuzione del programma prosegue dal punto evidenziato in (6.3.1) con i puntini, cioè dalla istruzione successiva alla struttura condizionale.

 

                Si noti come la descrizione in linguaggio naturale sia strettamente parallela alla formulazione sintattica del C, in cui manca solo l’equivalente della particella allora, che in questo linguaggio si é preferito sopprimere a differenza di quanto accade in altri (ancora la concisione ...).

 

                Il seguente segmento di programma mostra un caso semplice, ma significativo, in cui si suppone che nella parte rappresentata con puntini venga definito un valore per la variabile intera k:

 

  (6.3.3)                   int k ;

                               .......

                               if  ( k < 0 )  printf ( “\n --- numero negativo ---” ) ;

                               else if ( k > 0)  printf ( “\n +++ numero positivo +++” ) ;

                               else  printf ( “\n *** valore nullo ***” ) ;

 

                Non vi sono difficoltà nell’estensione dello schema (6.3.1) ad un numero di casi superiore:

 

  (6.3.4)                   if ( espr_1 )  istr_1 ;

                               else if ( espr_2 )  istr_2 ;

                               else if ( espr_3 )  istr_3

                               .....

                               else if ( espr_n )  istr_n ;

                               [ else  istruzione ; ]

 

in cui il numero di casi deve ovviamente essere finito. La scrittura di un alto numero di casi tende in genera ad una scomoda ripetitività; se l’espressione da controllare é sempre la stessa e deve essere confrontata con una sequenza di costanti, esiste una ‘quasi’ alternativa con la parola chiave switch, che considereremo oltre.

 

                La costruzione di alternative multiple ha a volte esigenze più complesse, per le quali é necessario fare ricorso a blocchi con parentesi graffe.  Si consideri l’esempio (6.2.2), con le seguenti varianti: mantenendo il tasto Esc come criterio di arresto, per il quale si richiede però una scrittura di commento, si vuole stavolta solo notificare a video che é stato premuto un tasto alfabetico, uno numerico, o nessuno dei due tipi di tasto.


(6.3.5)                     /*  GETCHE02.C -  commento ‘alfabetico/numerico’ per un

                             tasto fino all’arresto con ESCAPE

            */

            include <stdio.h>

            include <conio.h>

            #define ESCAPE 27

 

            main ( )

            {

                  char k ;  int num, alf ;

 

            GET:  k = getche ( ) ;

                  num = k >= ‘0’ && k <= ‘9’ ;

                  alf = ( k >= ‘A’ && k <= ‘Z’ ) ||

                        ( k >= ‘a’ && k <= ‘z’ ) ;

                  if ( k == ESCAPE )  printf ( “\n - fine programma - “ ) ;

                  else  { if ( num )  printf ( “\n -- numerico “ ) ;

                          else if ( alf )  printf ( “\n --- alfabetico” ) ;

                          else  printf ( “\n ---- non alfanumerico” ) ;

                          goto GET ;

                        }

            }

 

                Sarebbe stato più corretto calcolare il valore (logico) di num ed alfa solo quando essi sono necessari, cioè nel blocco più interno, ma si é preferita questa forma per mostrare meglio le relazioni delle alternative, che richiedono una

seconda costruzione if subordinata ad else, che deve in ogni caso concludersi con un ritorno alla funzione di input.

 

                Si osservi anche il modo in cui sono state calcolate le espressioni logiche per num ed alf: non si tratta di espressioni semplici, ma di proposizioni piuttosto complesse; nel primo caso non sono necessarie parentesi, perché dalla tabella della gerarchia degli operatori (4.4.6) risulta che le relazioni <= e >= hanno la precedenza sulla congiunzione &&, e poiché quest’ultima ha la precedenza sulla disgiunzione ||, non sarebbero state necessarie le parentesi della seconda assegnazione, che sono state incluse per maggiore chiarezza.

 

                Considerando ancora la definizione iniziale (6.2.1), l’osservazione su istruzione_2, che può diventare comunque complessa, si può applicare  anche ad istruzione_1, ottenendo strutture condizionali annidate, come:

 

  (6.3.6)                   if ( espr_1 )

                                  if (espr_2 )  istruzione ;

 

di cui si può preferire la scrittura in questa forma invece che in quella, equivalente:

 

  (6.3.7)                   if ( espr_1 && espr_2 )  istruzione ;

 

                Quando di più strutture linguistiche accade che una é interna ad un’altra, quindi subordinata ad essa, si dice che la prima é annidata (in inglese, nested) nella seconda. Nel caso dei due if appena considerati l’annidamento é solo apparente, per l’equivalenza logica tra (6.3.6) e (6.3.7).

 

                Se però compare la parola chiave else per stabilire alternative, nasce un problema; scrivendo in una sola riga per evidenziare la difficoltà:

 

  (6.3.8)                   if ( espr_1 )  if ( espr_2 )  istruz_1 ;   else  istruz_2 ;

 

si deve intendere la clausola else come alternativa al primo if, ossia con il senso di:

 

  (6.3.9)                   if ( espr_1 )

                              { if (espr_2 )  istruz_1 ;

                               }

                               else  istruz_2 ;

 

oppure al secondo, cioè come:


 

  (6.3.10) if ( espr_1 )

                                  { if ( espr_2 )  istruz_1 ;

                                     else  istruz_2 ;

                                   }

 

                I due casi sono logicamente ben distinti: se nel primo espr_1 é falsa, viene eseguita istruz_2, mentre nel secondo non é prevista alcuna alternativa, ed il contrario accade per espr_2. Si potrebbe obbligare a scrivere le parentesi graffe in ogni caso, ma é preferibile mantenere il significato più vicino a quello delle espressioni logiche del linguaggio corrente con la seguente convenzione:

 

  (6.3.11) Se una catena di if annidati é seguita dalla clausola else senza indicazione di blocco

                               con parentesi graffe, else si intende riferito all’ultimo if della sequenza.

 

vale a dire, (6.3.8) é logicamente equivalente a (6.3.10); ne segue che se l’esigenza logica del programma é diversa, cioè come quella in (6.3.9), o altre più complesse derivate da essa, la presenza delle parentesi graffe di blocco per il secondo if é obbligatoria.

 

                Consideriamo come esempio un programma che, confrontando un numero reale x con i due estremi dell’intervallo [ x_min, x_max ], con x_min < x_max, generi una scrittura per notificare che x appartiene alla prima metà dell’intervallo, oppure alla seconda ( estremi destri inclusi), ma non esegua alcuna altra azione nel caso opposto; il segmento rilevante del programma può quindi essere scritto:

 

  (6.3.12) double x, x_min, x_max, x_mez ;

                               ........

                               x_mez = ( x_min + x_max ) / 2 ;

                               if ( x > x_min && x <= x_max )

                                  if ( x <= x_mez )  printf ( “\n -- nella prima parte” ) ;

                                  else  printf ( “\n --- nella seconda parte ) ;

 

                Esistono anche altri modi di organizzare simili strutture condizionali, magari migliori, ma per l’esempio considerato abbiamo scelto questa forma per mostrare, come si può provare completando il programma e compilando, in che senso debba essere intesa la convenzione in (6.3.11).

 

 

 

 

 

 

            6.4 - Il selettore switch per casi costanti

 

 

                Nel caso, brevemente citato in precedenza, che esistano più alternative nelle quali la stessa espressione deve essere messa a confronto con una lista di costanti per decidere azioni alternative, la struttura condizionale if può essere utilizzata in una forma come la (6.3.4), ossia ripetendo la scrittura della espressione in tutte le parentesi che esprimono le condizioni logiche per gli if.

 

                Se l’espressione non si riduce ad una variabile, la ripetizione della sua elaborazione é una perdita di tempo, e si dovrebbe considerare l’opportunità di utilizzare una variabile in più, assegnandole il valore dell’espressione prima di dare inizio ai controllo della struttura condizionale.

 

                Il C offre però una conveniente alternativa, che non é l’esatto equivalente logico di una sequenza di else if consecutivi, ma può diventarlo; si tratta del selettore, basato sulla parola chiave switch, da utilizzare nella forma:

 

  (6.4.1)                   switch    ( espressione )

                                               { case cost_1 :     [ operazioni_1 ]

                                                  case cost_2 :     [ operazioni_2 ]

                                                  ......

                                                  case cost_n :     [ operazioni_n ]

                                                  [ default :  [ operazioni ] ]

                                               }


 

che deve essere esaminata (ed utilizzata) con una certa attenzione.

 

                In (6.4.1), ove le parentesi quadre hanno di nuovo il consueto significato di ‘opzione’, espressione é una qualunque espressione legittima del C, che genera un valore di tipo ben determinato (intero o non intero); gli identificatori cost_1, .., cost_n che seguono la parola chiave case debbono essere costanti dello stesso tipo, eventualmente in forma di espressioni costanti.

 

                Con operazioni si indica poi qualunque sequenza di istruzioni, semplici, composte a blocco, che non richiedono la chiusura in parentesi graffe; alla conclusione, la parola chiave default denota l’inizio di una possibile ulteriore sequenza di istruzioni non associate ad una case specifica. Infine, le parentesi graffe che marcano apertura e chiusura del contesto di selezione sono obbligatorie,

 

                La descrizione della modalità operativa é:

 

  (6.4.2)                   (a) - valuta l’espressione e confrontala con le costanti di case, in sequenza; appena si

                               verifica una coincidenza tra espressione e costante, esegui le operazioni descritte in

                               corrispondenza di quella costante;

 

                               (b) - se non si ha coincidenza con nessuna costante ed é presente l’opzione default,

                               esegui le operazioni in corrispondenza ad essa;

 

                               (c) - ottenuta la coincidenza con una costante ed eseguite le relative operazioni, non

                               vengono più controllate le case successive e si eseguono tutte le operazioni da esse

                               descritte, eventuale default compresa;

 

                               (d) - se nel corso delle operazioni compare la parola chiave break,  si ha una uscita

                               immediata dal contesto di selezione, con passaggio del controllo del flusso alla prima

                               istruzione successiva ad esso.

 

                Contrariamente a quelli di altri linguaggi, il selettore switch del C non é quindi una costruzione equivalente ad una sequenza di else if con eventuale else finale. Le costanti debbono invece essere considerate come speciali etichette, analogia rafforzata dalla presenza dei due punti ‘:’, perché quando una di esse viene ‘agganciata’, si ‘filtra’ attraverso le successive, salvo esplicita richiesta contraria.

 

                In quanto alla parola chiave per l’interruzione, che costituisce una istruzione autonoma:

 

  (6.4.3)                   break ;

 

essa é una istruzione di salto, simile ad un goto, ma che non richiede l’indicazione di una etichetta, che viene implicitamente definita come inizio della prima istruzione relativa ad un particolare contesto, che in questo caso é l’intera costruzione del selettore.

 

                L’impiego dell’istruzione break non é limitata al solo contesto switch, del resto abbastanza raro nei programmi; essa é invece più frequente nei casi dei cicli, di cui ci occupiamo nel prossimo capitolo, e per i quali l’individuazione del contesto é un po’ diversa da quella attuale.

 

                Il selettore switch ha impiego abbastanza frequente nei casi in cui si ha una logica di distribuzione a menù, nei quali in genere si utilizza una variabile tipo char (o int), il cui valore deve essere messo a confronto con una lista di costanti che contraddistinguono procedure specifiche.

 

                Questo é il caso in cui la ‘strana’ proprietà (d) in (6.4.2) può risultare conveniente per raggruppare diverse etichette case senza operazioni proprie, in modo che la selezione operi su tutto il gruppo. Ciò risulta particolarmente comodo, perché nella definizione (6.4.1) e nella formulazione (6.4.2)  nulla impone che le costanti di case si trovino in qualche ordine particolare, alfabetico o numerico.

 

                Si consideri il programma che segue, ancora basato sull’elaborazione di un singolo carattere dalla tastiera, questa volta utilizzando la variante getch ( ), che non genera eco sullo schermo; si richiede che Esc sia ancora il tasto di arresto delle operazioni, ma per il resto si vuole solo commentare il tasto premuto con la descrizione “vocale” oppure “altro”, a seconda che si tratti di una vocale dell’alfabeto, oppure no.


 

  (6.4.4)                   /*  GETCH03.C - prova del selettore switch

            */

            #include <stdio.h>

            #include <conio.h>

            #define ESCAPE 27

 

            main ( )

            {

                  char sel ;

 

            GET: sel = getch ( ) ;

                  if ( sel != ESCAPE )

                    { if ( sel >= 'a' && sel <= 'z' )  sel -= 32 ;

                      switch ( sel )

                        { case 'E' :

                          case 'A' :

                          case 'I' :

                          case 'O' :

                          case 'U' : printf ( "\n vocale %c ", sel ) ; break ;

                          default  : printf ( "\n altro  %c ", sel ) ;

                        }

                      goto GET ;

                    }

            }

 

                La struttura switch si rivela appropriata per le specifiche di questo programma, permettendone la scrittura con poche istruzioni semplici e leggibili. Come alternativa si può considerare una istruzione come:

 

  (6.4.5)                   if ( sel == ‘A’ || sel == ‘E’ || sel == ‘I’ ||  sel == ‘O’ || sel == ‘U’ )  .....          

 

e la scelta tra una struttura if ed una switch é in definitiva materia di preferenze personali.

 

                La sola istruzione che richiede un commento addizionale é:

 

  (6.4.6)                   if ( sel >= ‘a’  &&  sel <= ‘z’ )  sel -= 32 ;

 

il cui scopo é la trasformazione dei caratteri alfabetici minuscoli in maiuscoli per evitare lo sdoppiamento dei controlli; ciò viene eseguito osservando che:

 

                (a) nella tabella ASCII tutti i caratteri alfabetici sono consecutivi, nell’ordine consueto

                (b) tutte le minuscole hanno codice ASCII maggiore di 32 rispetto alle omologhe maiuscole.

 

                Esistono funzioni di libreria predisposte per scopi come questo; quella che serve in questo caso é toupper, richiede il file di testata <ctype.h> e deve essere utilizzata come in:

 

  (6.4.7)                   char c ;

                               .....

                               c = toupper ( c ) ;

 

                I vantaggi delle funzioni di libreria rispetto alla scrittura esplicita di istruzioni come la (6.4.6) sono molteplici: esse sono ottimizzate dal punto di vista operativo, certamente prive di errori e di semplice impiego. In un caso come l’attuale, per di più, l’istruzione (6.4.6) é corretta solo su sistemi in cui i caratteri sono rappresentati con i codici ASCII, mentre l’alternativa (6.4.7) é corretta in ogni caso.