Capitolo 7 - Le strutture di ciclo

 

 

 

 

            7.1 - Organizzazione dei cicli.

 

 

                I programmi visti in (6.2.2) e (6.4.4) forniscono due semplici esempi di ciclo, il cui elemento fondamentale é l’istruzione di salto ad una etichetta che nel flusso fisico del programma si trova prima del punto in cui avviene il salto (goto); ciò ha come conseguenza che le istruzioni che si trovano tra i due punti, etichetta e relativo salto, possono essere percorse più di una volta.

 

                Questo modo di riutilizzare un certo insieme di istruzioni eseguendo su di esse più iterazioni (ripetizioni), in cui la struttura delle operazioni é sempre la stessa, ma da una volta all’altra esse agiscono su dati potenzialmente diversi, é la ragione principale della efficacia e flessibilità dei linguaggi di programmazione.

 

                Trattandosi di uno strumento di grande ‘potenza’, il ciclo é allo stesso tempo assai ‘pericoloso’ se male utilizzato; questa considerazione non é nuova, avendone già incontrato esempi con la disponibilità degli indici e dei puntatori, e non é casuale che le maggiori possibilità applicative derivano dall’impiego nei cicli delle variabili dotate di indice.

 

                Nell’esempio  (6.2.2) si immagini di eliminare il condizionale if, riducendo il ciclo a:

 

  (7.1.1)                   GET:      k = getche ( ) ;

                                               printf ( ........ ) ;

                                               goto GET ;

 

                Questo insieme di istruzioni permette di ottenere lo scopo principale, che é di rilevare il codice del tasto premuto e scriverne il valore, ma il problema é che ora esso é un ciclo chiuso, che tiene bloccata la macchina sulla esecuzione di questo gruppo di istruzioni, senza più alternativa se non un intervento drastico a livello del sistema operativo, che per un Personal Computer può significare dovere riavviare la macchina.

 

                Questo problema é evitato appunto dall’istruzione condizionale, che assicura che il ciclo non viene percorso sempre, ma solo se si verificano le condizioni prestabilite.

 

  (7.1.2)                   Diciamo ciclo ogni organizzazione del flusso di operazioni in cui é prevista la possibilità

                               di iterazione di una parte di esse, ossia la ripetizione per più di una volta delle medesime

                               istruzioni; del ciclo debbono essere ben definiti il dominio, o corpo, che é l’insieme delle

                               istruzioni interessate alla ripetizione, ed il criterio di arresto, o interruzione.

 

                Nell’esempio citato, il criterio di arresto é dato dal condizionale

 

  (7.1.3)                   if ( k != ESCAPE )

 

che controlla il ciclo, facendo in modo che esso venga eseguito solo se la condizione é vera; poiché in (6.2.2) non si hanno altre istruzioni in alternativa, il programma si arresta.

 

                La costruzione esplicita di un ciclo in tutte le sue componenti: criterio di arresto, salto ed etichetta ha interesse didattico per la comprensione della dinamica operativa;  essa comporta però una fastidiosa serie di passi ripetitivi, per di più con consistenti probabilità di errori di scrittura.

 

                Consideriamo come esempio un segmento di programma che, data una matrice intera mat di n righe ed m colonne, debba assegnare a mat[i][j] il valore i + j; per il (piccolo) algoritmo necessario é immediata l’analisi, formulata in termini di linguaggio naturale:


 

  (7.1.4)                   per riga che va da 0 ad n-1 con passo 1

                                    per colonna che va da 0 ad m-1 con passo 1

                                          porre mat[riga][colonna] = riga + colonna

 

in cui é evidente la presenza di due cicli operativi, il secondo contenuto, cioè annidato nel primo.

 

                Utilizzando salti e controlli espliciti la trascrizione minimale in linguaggio C, supposte al solito ben dichiarate le variabili interessate, é:

 

  (7.1.5)                                   riga = 0 ;

                               J_INI:    colonna = 0 ;

                               CALC:   mat[riga][colonna] = riga + colonna

                                               if ( colonna < n - 1 )  { colonna++ ;  goto CALC; }

                                               if ( riga < n - 1 )  { riga++ ;  goto J_INI ; }

 

che é corretta, ma assai lontana dalla chiarezza espositiva e dalla semplicità logica di (7.1.4): é necessario creare identificatori per le etichette, assicurarsi che esse siano citate nel giusto modo ed al giusto posto, ecc..

 

                Per comprendere la reale natura del problema, si esamini cosa accade se si invertono le etichette nei due goto, o addirittura se si scambiano tra loro le ultime due righe; il programma é sempre sintatticamente corretto, ma le sequenza operative sono certamente sbagliate e, almeno nel primo caso, generano un ciclo senza uscita.

 

                E’ quindi evidente l’opportunità di introdurre strutture linguistiche più evolute adatte al controllo dei cicli, che riducano al minimo la scrittura degli elementi ‘automatici’ che, come nel caso dei due cicli in (7.1.5), debbono provvedere a quattro esigenze:

 

  (7.1.6)                   (a) - inizializzazione di un elemento di controllo del ciclo ;

                               (b) - verifica della condizione di arresto ;

                               (c) - variazione dell’elemento di controllo ;

                               (d) - ritorno all’inizio del ciclo .

 

 

 

 

 

            7.2 - Cicli basati sul conteggio - struttura for

 

 

                Quando il linguaggio naturale viene utilizzato in modo semiformalizzato per la descrizione dell’analisi di un algoritmo, le frasi come la prima e la seconda riga che compaiono in (7.1.4) contengono tutti gli elementi necessari per determinare quante volte un ciclo debba essere percorso; fornendone valore iniziale, finale ed variazione della variabile di controllo é implicito il conto delle ripetizioni.

 

                Ogni linguaggio di programmazione possiede una sua variante di questa struttura linguistica: do per il Fortran e for per il Basic ed il Pascal; anche il C adotta la parola chiave for (per, in inglese) con una sintassi particolare, che ne fa uno strumento molto più potente e flessibile dei suoi “concorrenti”.

 

                La parola for é seguita da una coppia di parentesi, in cui sono inclusi tre elementi funzionali; alla chiusura della parentesi segue il corpo del ciclo:

 

  (7.2.1)                   for ( [ inizializzazione ] ; [ controllo ] ; [ variazione ] )  [ istruzione ] ;

 

in cui inizializzazione, variazione sono istruzioni semplici o composte (non blocchi), e controllo é una espressione, mentre la parte finale, istruzione, che figura dopo la chiusura delle parentesi tonde, é il corpo del ciclo, che può essere una istruzione semplice, composta, o un blocco.


 

                Le modalità operative sono definite da:

 

  (7.2.2)                   (a) - si esegue l’istruzione inizializzazione

                               (b) - si valuta la espressione controllo

                               (c) - se essa é vera, si esegue istruzione

                               (d) - se invece é falsa, il controllo passa alla istruzione che segue la struttura for

                               (e) - si esegue l’istruzione variazione

                               (f) - si torna al punto (b)

 

 

                Rispetto alle strutture di controllo di altri linguaggi, la singolarità della definizione (7.2.1) sta nel fatto che tutti gli elementi costruiti dall’utente sono opzionali e come caso limite potrebbero mancare tutti; vale a dire la scrittura:

 

  (7.2.2)                   for ( ; ; ) ;

 

é una istruzione legittima del C, anche se priva di significato, perché non descrive nessun tipo di azione, ed é inoltre molto pericolosa; inserirla in un programma può avere effetti diversi a seconda del compilatore: ad esempio, in un Personal Computer essa si traduce in uno stretto ciclo operativo chiuso, che provoca il blocco totale delle operazioni del microprocessore.

 

                La forma più comune di una istruzione for impiega una sola variabile (detta di controllo) ed i tre elementi in parentesi si riducono ad una istruzione semplice; ad esempio, per costruire un vettore di interi contenenti potenze di 2, si può utilizzare il segmento di programma:

 

  (7.2.3)                   int k, pot2[11] ;

                               ...

                               pot2[0] = 1

                               for ( k = 1 ; k <= 10 ; k++ )  pot2[k] = 2 * pot2[k-1] ;

 

ma la sintassi del C permette di ottenere lo stesso scopo con diverse varianti; per citarne solo una, non del tutto ovvia, le due righe ‘operative’ in (7.2.3) possono essere riunite in una sola, come in:

 

  (7.2.4)                   for ( *pot2 = 1, k = 1 ;  ++k <=10 ; )  pot2[k] = pot2[k-1] << 1 ;

 

in cui abbiamo utilizzato molti elementi caratteristici del C: impiego di un puntatore, operatore di deferimento, istruzione composta (l’inizializzazione), controllo e variazione unite con il preincremento, operatore di shift a sinistra al posto della moltiplicazione.

 

                Questa seconda forma é probabilmente più efficiente della prima, ma la sua logica é certamente più oscura e non é raccomandabile la pratica di simili forme di scrittura, almeno nella fase di apprendimento, mentre la loro ‘decodifica’ può essere un utile esercizio. Si osservi, in particolare, che mentre in (7.3.2) é indifferente scrivere la variazione di k come postincremento o preincremento, perché essa avviene comunque prima del controllo, nelle (7.2.4) la logica dell’operazione non ammette alternativa.

 

                Come già osservato per if, la struttura for é una istruzione legittima del C, e non vi sono limitazioni ad impiegarla nel ruolo di istruzione in (7.2.1), vale a dire che un for può essere incluso, o annidato, nel dominio di un altro.

 

                L’esempio in (7.1.5) può quindi essere riformulato come:

 

  (7.2.5)                   for ( riga = 0 ; riga < n ; riga++ )

                                    for ( colonna = 0 ; colonna < m ;  colonna++)

                                         mat[riga][colonna] = riga * colonna ;

 

che riflette molto bene la linearità logica dell’analisi ( 7.1.4) essendone, anzi, quasi una trascrizione letterale


 

            7.3 - Esempio: scrittura di una tavola pitagorica

 

 

                Consideriamo un esempio di programma, che utilizza molti elementi del linguaggio C finora acquisiti; si vuole eseguire la scrittura a video di una tavola pitagorica, permettendo all’utente di scegliere un numero indefinito di volte la forma (ed il contenuto) decimale, oppure esadecimale.

 

  (7.3.1)

       /*  PITAGFOR.C - costruzione di tavola pitagorica decimale o esadecimale

 

         Analisi della struttura dell'algoritmo:

 

           richiedi scelta tra decimale, esadecimale o nulla ;

           se nulla, fine operazioni ;

           altrimenti:

             componi la testata delle colonne ;

             per tante righe quanto Š necessario, componi la riga ;

           torna a proporre la scelta .

       */

 

       #include <stdio.h>

       #include <conio.h>

 

       main ( )

       {

          int  massimo, riga, colonna, sel_esa ;

          char scelta, *form, dec_form[] = "%4d", esa_form[] = "%4X" ;

 

       /*---  Scelta su menu'  ---*/                                      /*  <1>  */

 

       MENU:  printf ( "\n\n T A V O L A   P I T A G O R I C A" ) ; /*  <2>  */

             printf ( "\n\n D = decimale" ) ;

             printf (   "\n H = esadecimale" ) ;

             printf ( "\n\n     scegliere ( altro = nulla ) : " ) ;

 

             scelta = toupper ( getche ( ) ) ;                     /*  <3>  */

 

             if ( sel_esa = scelta == 'H' )                              /*  <4>  */

               { massimo = 16, form = esa_form ; }

             else   { if ( scelta != 'D' )   exit ( 0 ) ;                 /*  <5>  */

                      massimo = 10, form = dec_form ;

                    }                                                     /*  <6>  */

 

       /*---   Intestazione tabella  */

 

             printf ( "\n\n r\\c " ) ;                                   /*  <7>  */

             for ( colonna = 1 ; colonna <= massimo ; colonna++ )

                printf ( form, colonna ) ;                               /*  <8>  */

             printf ( "\n" ) ;                                           /*  <9>  */

 

       /*---   Valori tabella   ---*/

 

             for ( riga = 1 ; riga <= massimo ; riga++ )

                { printf ( "\n " ) ;  printf ( form, riga) ;

                  for ( colonna = 1 ; colonna <= massimo ; colonna++ )

                      printf ( form, riga * colonna ) ;

                }

             goto MENU ;                                                 /* <10> */

       }

 

                Il programma contiene molti elementi di interesse, che commentiamo brevemente.


 

·         <1>: pur trattandosi di un programma di struttura molto semplice, le parti funzionalmente distinte che lo compongono sono state separate con commenti in ruolo di ‘titoletti’, che ne evidenziano gli scopi funzionali. E’ opportuno cercare di applicare subito tecniche come questa durante la scrittura del programma per renderne più agevole il controllo; esse sono anche molto utili come criterio di verifica dell’analisi, che in questo esempio é stata presentata succintamente nel commento iniziale.

 

·         <2>: viene proposta una scelta basata sulla logica dei menù, con cui la selezione di un elemento in una lista permette di avviare una intera procedura di elaborazione; in questo caso le scelte ‘attive’ sono fatte premendo i tasti ‘D’ o ‘H’ (senza bisogno di Invio), mentre ogni altro tasto arresta l’elaborazione.

 

·         <3>: si utilizza la funzione getche di rilevazione diretta di un tasto (con eco), il cui valore viene utilizzato come argomento per la funzione toupper, che trasforma i caratteri minuscoli nei corrispondenti maiuscoli.

 

·         <4>: questa riga svolge tre funzioni diverse, unificabili per la flessibilità della sintassi del C. Essa consiste nella assegnazione alla variabile sel_esa del valore di una espressione logica e può essere scritta senza fare ricorso a parentesi, come in sel_esa = ( scelta == ‘H’) , poiché la gerarchia degli operatori la risolve in modo non ambiguo; infine, essendo l’assegnazione anche una espressione, il suo valore può essere il soggetto del test logico che determina la condizione per l’istruzione if. In altri linguaggi le medesime operazioni richiedono sempre l’impiego di due istruzioni separate che, si ricorda, sono in genere consigliabili anche in C.

 

·         <5>: la funzione exit ( numero ) svolge il ruolo intuibile dal suo nome: se si giunge a questo punto, che corrisponde alla scelta di non ‘H’ e non ‘D’, le operazioni del programma sono logicamente terminate e si può quindi uscire dal suo contesto per ritornare a quello da cui é stato avviato, cioè al sistema operativo. In genere il sistema é predisposto per accettare da un programma terminato un numero, che può venire utilizzato per condizionare le operazioni successive; molto spesso questa informazione non viene utilizzata, ma la funzione exit richiede comunque un numero intero in parentesi. Qui zero non ha significato particolare e può essere sostituito da qualunque altro numero.

 

·         <6>: questo é un caso interessante di puntatore utilizzato con la stessa tecnica delle variabili ordinarie; in fase di dichiarazione sono state definite due stringhe, dec_form ed esa_form, che sono due diverse opzioni di formato utilizzabili in una printf. Entrambe le stringhe sono allocate in memoria, ovviamente con indirizzi diversi, che sono però disponibili come nomi degli array in cui le stringhe sono contenute.  Con le assegnazioni contenute nelle due alternative del condizionale, l’indirizzo di stringa coerente con la scelta dell’utente é sempre disponibile sotto l’unico nome form, che é un puntatore.

 

·         <7>: scrittura di una stringa costante che apre l’intestazione della tabella; si noti il doppio simbolo ‘\\’, necessario per ottenerne uno solo in scrittura - si vedano i commenti a (4.2.7).

 

·         <8>: si constata ora il vantaggio del puntatore form: é possibile scrivere una sola istruzione printf capace di svolgere i due ruoli diversi di scrittura decimale o esadecimale - incidentalmente, ‘%X’ é l’opzione di formato per la scrittura esadecimale con caratteri alfabetici maiuscoli. Si può anche constatare che la stringa di formato in printf non deve necessariamente essere scritta come costante nella parentesi, poiché nel medesimo ruolo é accettabile un indirizzo.

 

·         <9>: questa istruzione é la prima immediatamente successiva al dominio del for, che é costituito da una sola istruzione printf , che non contiene richieste di invio a capo. Per una scrittura ordinata é quindi necessaria una soluzione come quella di questa istruzione.

 

·         <10>: il salto, che é anche l’ultima istruzione, organizza l’intero programma come un unico ciclo; questo é un esempio in cui salto e relativa etichetta non costituiscono un ostacolo per la leggibilità della logica del programma, anzi con una buona scelta dell’etichetta stessa possono costituire una buona forma di autodocumentazione.

 

                In tema di documentazione si può ancora osservare qualcosa sulla ‘pulizia’ della presentazione generale del programma; del commento iniziale che riassume l’analisi si é già detto. Si noti anche come la parte dichiarativa, che definisce i dati su cui si opera, é stata nettamente separata da quella operativa; anche questa é una buona abitudine, che dovrebbe divenire una ‘seconda natura’ fin dalle prime esperienze.


            7.4 - Cicli basati su condizioni logiche

 

 

                Il termine più importante tra quelli utilizzati trattando della struttura for é conteggio: infatti, quando si utilizzano le forme più comuni, come in:

 

  (7.4.1)                   for ( var = v_ini ; var <= v_fin ; var = var + passo )

 

é ben determinabile quante iterazioni verranno effettuate, semplicemente calcolando un rapporto.

 

                Può anche accadere che il risultato sia zero, ad esempio con:

 

  (7.4.2)                   for ( k = 0 ; k < 0 ; k++ )

 

in cui la condizione di controllo é falsa fin dall’inizio, quindi il ciclo non viene percorso neppure una volta. E’ ovvio che questo é probabilmente un errore nella scrittura del programma, ma una situazione simile potrebbe legittimamente insorgere ed essere elemento logico essenziale quando gli elementi che compaiono nei tre ruoli nella parentesi del for sono parametrizzati, ad esempio variabili come in (7.4.1) e dipendono da passi precedenti del programma.

 

                In altri linguaggi le strutture linguistiche corrispondenti al for sono progettate esclusivamente per il conteggio delle iterazioni; per il C le possibilità applicative della definizione (7.2.1) sono molto più generali, ma tale scopo é sempre quello primario.

 

                Esistono condizioni logiche in cui il conteggio preventivo delle iterazioni può essere solo parziale, o del tutto impossibile; consideriamone un esempio con una variante dei programmi già considerati per l’esplorazione della tastiera ai fini del riconoscimento di tasti numerici.

 

  (7.4.3)

       /*  GETCHNUM.C -    ricostruzione del valore di un numero intero di tipo

                           long con rilevazione diretta di tasti numerici; il primo

                           non numerico costituisce il criterio di arresto

       */

       #include <stdio.h>

       #include <conio.h>

       #include <ctype.h>

 

       main ( )

       {

         char c ;  int valido ;  long numero = 0 ;

 

       GET:   if ( valido = isdigit ( c = getche ( ) ) ) ;                       /* <1> */

               { numero *= 10 ;  numero += ( c - '0' ) ;  goto GET ; }          /* <2> */

             printf ( "\n -- valore acquisito : %ld", numero ) ;                /* <3> */

       }

 

                Commentiamo brevemente gli elementi nuovi presenti in questo programma.

 

·         <1>: si é utilizzata la proprietà per cui una assegnazione é anche una espressione; il carattere ottenuto da getche é utilizzato come argomento per la funzione isdigit, che appartiene alla ‘famiglia’ di toupper ( testata <ctype.h>); essa verifica se il carattere é numerico e restituisce un valore logico 0 / 1.

 

·         <2>: il numero viene ricostruito in modo naturale per successive posizioni decimali e la divisione in due operazioni distinte permette al compilatore di ottimizzare i calcoli; in quanto alla differenza c - ‘0’, essa si basa sulle proprietà numeriche dei codici ASCII, come già osservato.

 

·         <3>: il descrittore di formato ‘%ld’ é quello necessario per interi di tipo long.


 

                Ai fini attuali l’osservazione più importante al riguardo del programma (7.4.3) é che non é possibile fissare a priori il numero di iterazioni che verranno eseguite, poiché il numero di caratteri rilevati dipende dalle decisioni dell’utente durante l’esecuzione.

 

                La definizione di for nel C permette di ‘forzarne’ egualmente l’utilizzazione, ad esempio sostituendo le istruzioni del ciclo con le seguenti:

 

  (7.4.4)            char c ;  int valido = 1 ;  long numero = 0 ;

 

             for ( ; valido ; )

                { if ( valido = isdigit ( c = getche ( ) ) )

                     { numero *= 10 ;  numero += ( c - '0' ) ; }

                }

 

ma, nonostante l’eliminazione dell’etichetta e del relativo goto, é dubbio che questa soluzione si possa ritenere logicamente più chiara della precedente, perché almeno linguisticamente, l’impiego di for sembra fuori luogo; si é poi dovuta inizializzare la variabile di controllo prima del ciclo per permetterne l’esecuzione almeno una volta.

 

                Per concludere, la differenza tra cicli incontrati in (7.2.5) e (7.4.3) é che i primi dipendono da un conteggio aritmetico, mentre i secondi da una condizione logica, la quale determina direttamente il criterio di arresto

 

 

 

 

 

 

            7.5 - Strutture while e do .. while

 

 

                In altri l linguaggi le strutture ‘simili’ al for non permettono ‘trucchi’ quali quello visto in (7.4.4); per soddisfare le esigenze dei cicli regolati da un controllo logico, sono state introdotte varianti logiche, in genere basate sulla parola inglese while (in italiano, mentre, o finché).

 

                Pur non essendocene la necessità stretta, esse sono state introdotte anche nel C, al fine di permettere la scrittura di un programma in forma più vicina a quella dell’analisi degli algoritmi di quanto possibile con il solo for. Esistono due tali forme, che presentiamo subito nella definizione sintattica:

 

  (7.5.1)                   while ( espressione )  [ istruzione ] ;

 

  (7.5.2)                   do istruzione while ( espressione ) ;

 

in cui espressione é considerata, al solito, come valore logico, mentre istruzione può essere semplice, composta, a blocco o eventualmente vuota.

 

                In entrambi i casi le iterazioni proseguono finché il valore logico di espressione é falso (zero), che é quindi il criterio di arresto del ciclo; la differenza tra di esse é che in (7.5.1) il controllo viene eseguito all’inizio del ciclo, che potrebbe quindi non essere mai eseguito, mentre nel secondo esso avviene alla fine, che significa che il ciclo viene sempre percorso almeno una volta.

 

                Per quanto riguarda espressione, essa dovrebbe sempre contenere almeno un elemento che può essere modificato nel corso delle operazioni del ciclo; in caso contrario, cioè se l’espressione é una costante rispetto al ciclo, essa é sempre vera o sempre falsa. Se é falsa non c’è problema particolare, ma se é vera il ciclo non ammette uscita.

 

                Si noti poi che per il caso while, gli elementi di espressione debbono essere stati definiti prima dell’ingresso del ciclo per renderne possibile la prima valutazione, mentre ciò non é necessario nel caso do.


                Considerando l’esempio (7.4.4), la variante più ‘corretta’, ossia che utilizza meglio le risorse linguistiche del linguaggio C ed allo stesso tempo rappresenta bene la logica dell’algoritmo é:

 

  (7.5.5)            char c ;  int valido ;  long numero = 0 ;

 

             do { if ( valido = isdigit ( c = getche ( ) ) )

                    { numero *= 10 ;  numero += ( c - '0' ) ; }

                } while ( valido ) ;

 

mentre é ora evidente che l’impiego ‘anomalo’ dell’istruzione for nella (7.4.4) ne fa l’equivalente funzionale di un ciclo while.

 

                Questa osservazione fornisce l’occasione per riprendere l’asserzione fatta sopra che non vi sarebbe necessità stretta di definire altri tipi di ciclo oltre il for, pur essendo apparentemente il criterio di controllo logico più generale di quello del conteggio e quindi di portata applicativa maggiore. Ma il ciclo for del C non é limitato al solo conteggio, e sono immediate le due proprietà di natura generale:

 

  (7.5.6)                   Ogni ciclo while può essere riscritto come ciclo for lasciando vuoti il primo e terzo

                               elemento, ossia la portata applicativa di for é non minore di quella di while.

 

  (7.5.7)                   Allo stesso modo, ogni ciclo do .. while può essere riscritto come ciclo for, in cui con

                               con una opportuna inizializzazione l’espressione di controllo risulti vera almeno

                               alla prima iterazione.

 

                Sarebbe però errato dedurre che la struttura for è più generale delle altre due, perché:

 

  (7.5.8)                   Ogni ciclo for può essere riscritto come ciclo while spostandone l’inizializzazione

                               prima dell’istruzione while e la variazione del controllo all’interno del dominio

                               del ciclo.

 

  (7.5.9)                   Ogni ciclo while può essere riscritto nella forma do .. while, inserendo all’inizio

                               del secondo un condizionale che utilizza l’espressione di controllo ed é in grado

                               di evitare l’esecuzione anche della prima iterazione.

 

  (7.5.10) Le strutture di ciclo for, while, do .. while sono equivalenti, nel senso che ogni tipo

                               di ciclo può venire espresso con una qualunque di esse.

 

                L’equivalenza é però solo una proprietà logica astratta; vi sono invece differenze, che possono essere soggettive, di efficacia linguistica, ma ne esistono anche di oggettive, misurabili come numero di operazioni compiute, o di quantità di memoria richiesta, o entrambe, anche se in generale gli obiettivi di minimizzazione degli uni o dell’altra sono in contrasto tra loro e certe decisioni ‘critiche’ del programma riguardano proprio il ‘tasso di scambio’ tra le due esigenze.

 

                Ad esempio, l’inizializzazione di una matrice intera, supponiamo con valori tutti uguali ad 1, può essere effettuato con istruzioni come:

 

  (7.5.11)                 #define MAX_R  20

                                               #define MAX_C  30

                                               ...

                                               int k, mat[MAX_R][MAX_C], *mp ;

                                               ....

                                               mp = (int *) mat ;

                                               k = MAX_R * MAX_C ;

                                               while ( k-- )  *mp++ = 1 ;

 

che é probabilmente la soluzione minimale per quasi tutti i compilatori, Si osservi l’operatore di cast, o conversione di tipo, (int *), necessario perché mat non é un puntatore ad interi, ma un puntatore a puntatori.


 

            7.6 - Interruzioni di ciclo - istruzioni break e continue

 

 

                Riprendiamo l’esempio (7.3.1) dell’acquisizione di un dato numerico come sequenza di cifre decimali dalla tastiera, nella formulazione finale data in (7.5.5), ma introducendo come nuova condizione che il numero di cifre accettate abbia un massimo, supponiamo di 6.

 

                Ne segue che l’interruzione del ciclo operativo dipende ora da due cause: la decisione dell’utente, nella forma di un tasto non numerico, oppure il raggiungimento del massimo di cifre permesse; queste condizioni richiedono l’impiego di variabili addizionali ed una possibile soluzione é:

 

(7.6.1)              char c ;  long numero = 0 ;

               int valido, pos = 0, max_pos = 6 ;

 

             do { if ( valido = isdigit ( c = getche ( ) ) )

                    { numero *= 10 ;  numero += ( c - '0' ) ;  pos++ }

                } while ( valido && pos < max_pos ) ;

 

                La soluzione é abbastanza semplice, come del resto era il problema; si é però fatto ricorso ad una espressione di controllo più complessa; una soluzione diversa, forse più ‘leggibile’ perché differenzia i due criteri di arresto, é:

 

(7.6.2)              char c ;  int valido, cifre = 6 ;  long numero = 0 ;

 

             do { if ( valido = isdigit ( c = getche ( ) ) )

                    { numero *= 10 ;  numero += ( c - '0' ) ;  cifre-- }

                  if ( ! cifre )  break ;

                } while ( valido ) ;

 

che si rivela migliore anche in termini di efficienza in esecuzione: utilizza una sola variabile addizionale (questo sarebbe stato possibile anche nella versione precedente), ma soprattutto evita una operazione per ogni ciclo, la congiunzione logica presente nel primo caso nella espressione dopo while.

 

                Queste osservazioni possono sembrare marginali, ed in effetti lo sono in semplici programmi di questo tipo; ma porre l’attenzione su simili dettagli può fare grandi differenze quando si passa alla trattazione di problemi ‘veri’.

 

                In (7.6.2) abbiamo fatto ricorso ad una risorsa già nota: l’istruzione break definita in (6.4.3) come richiesta di uscita immediata da un contesto, che in quel caso era il dominio di un selettore switch, mentre in questo caso si tratta del dominio di un ciclo do .. while.

 

                Essa é utilizzabile in tutti i tipi di cicli, for, while, do .. while, sempre con il medesimo significato:

 

  (7.6.3)                   l’istruzione break impone l’interruzione immediata (o prematura) del ciclo con

                               trasferimento del controllo alla prima istruzione successiva al ciclo stesso.

 

                In altre condizioni, é possibile che sia necessario interrompere solo la iterazione corrente, compito per il quale é disponibile la parola chiave continue:

 

  (7.6.4)                   L’istruzione continue impone l’interruzione della iterazione corrente, trascurando le

                               le eventuali istruzioni residue del dominio del ciclo, con passaggio immediato alla

                               espressione di controllo, quindi alla possibile iterazione successiva.

 

                Anche continue, come break, é una forma di salto ‘mascherata’, cioè senza etichetta esplicita; in principio entrambe sono sempre sostituibili da qualche combinazione di strutture condizionali, spesso basate sulla inversione logica delle condizioni di controllo. Vale naturalmente quanto detto in passato, in contrasto con i ‘puristi’ dei linguaggi di programmazione: se esse sono la soluzione più lineare di un problema, non c’è motivo di evitarle.


 

            7.7 - Esempio: ordinamento con Bubble Sort

 

                Presentiamo un programma per l’ordinamento di una array di interi compresi tra 0 e 99, generati ricorrendo alla funzione di libreria rand ( ), che richiede la testata <stdlib.h> e nella versione di compilatore utilizzata genera interi pseudocasuali nell’intervallo 0 .. 32767. Il metodo utilizzato é il Bubble Sort, le cui modalità operative sono illustrate nella presentazione dell’analisi dell’algoritmo.

 

                Avvertiamo che al di là dell’interesse dell’ordinamento, lo scopo del programma é solo quello di mostrare un esempio di tutte le istruzioni relative ai cicli, quindi che esso non é formulato nella maniera ‘stilisticamente migliore’, in particolare per l’impiego dell’istruzione continue.

 

  (7.7.1)

       /*  BUBSORT.C -     generazione di un array di numeri casuali ed ordinamento

                           crescente con il metodo Bubble Sort.

 

             Analisi dell'algoritmo:

 

             costruisci la lista dei numeri casuali ;

             scrivila per controllo ;

             poni il limite delle operazioni al valore massimo ;

             ripeti: per k da o al limite :

                         confronta gli elementi di posto k e k+1 ;

                         se necessario, scambiali ;

                      decrementa il limite di uno ;

             finché gli elementi sono almeno due e scambi > 0 ;

             scrivi la lista ordinata.

       */

       #include <stdio.h>

       #include <stdlib.h>

       #define  MAX  20

 

       main ( )

       {

          int k, lim, scambi, x, dat[MAX], *dp ;

 

       /*---   Generazione casuale dati   ---*/

 

             k = MAX ;  dp = dat ;

             while ( k-- )  *dp++ = rand ( ) % 100 ;

             printf ( "\n lista dei dati originari : \n" ) ;

             for ( k = 0 ; k < MAX ; k++ )  printf ( " %2d", dat[k] ) ;

 

       /*---   Ordinamento   ---*/

 

             lim = MAX - 1 ;

             do { scambi = 0 ;

                  for ( k = 0 ; k < lim ; k++ )

                      { if ( dat[k] < dat[k+1] )  continue ;

                        scambi++ ;

                        x = dat[k] ; dat[k] = dat[k+1] ; dat[k+1] = x ;

                      }

                  if ( ! scambi )  break ;

                  lim-- ;

               } while ( lim > 0 ) ;

        printf ( "\n lista dei dati ordinati : \n" ) ;

        for ( k = 0 ; k < MAX ; k++ )  printf ( " %2d", dat[k] ) ;

}

 

                Questo esempio si presta bene a miglioramenti nello stile ed ampliamenti, ad esempio introducendo contatori del numero di if utilizzati e di scambi effettuati, al fine di confrontare la sua efficienza con quella di un altro algoritmo.