|
Appunti informatica |
|
Visite: 1664 | Gradito: | [ Medio appunti ] |
Leggi anche appunti:C come cesareC come Cesare Il Cesare in questione è proprio Caio Giulio Cesare, il noto imperatore romano I device driverI device driver Sempre più difficile: dopo avere affrontato i TSR (pag. 169) Il flusso elaborativoIl flusso elaborativo Qualsiasi programma può venire codificato in un linguaggio |
Qualsiasi programma può venire codificato in un linguaggio di programmazione usando tre sole modalità di controllo del flusso elaborativo: l'esecuzione sequenziale, l'esecuzione condizionale e i cicli.
L'esecuzione sequenziale è la più semplice delle tre e spesso non viene pensata come una vera e propria modalità di controllo; infatti è logico attendersi che, in assenza di ogni altra specifica, la prossima istruzione ad essere eseguita sia quella che nella codifica segue quella attuale.
Le altre due strutture di controllo richiedono invece qualche approfondimento.
Il linguaggio C dispone di due diversi strumenti per condizionare il flusso di esecuzione dei programmi. Vale la pena di analizzarli compiutamente.
L'esecuzione condizionale nella forma più semplice è specificata tramite la parola chiave if, la quale indica al compilatore che l'istruzione seguente deve essere eseguita se la condizione, sempre specificata tra parentesi, è vera. Se la condizione non è verificata, allora l'istruzione non è eseguita e il flusso elaborativo salta all'istruzione successiva. L'istruzione da eseguire al verificarsi della condizione può essere una sola linea di codice, chiusa dal punto e virgola, oppure un blocco di linee di codice, ciascuna conclusa dal punto e virgola e tutte quante comprese tra parentesi graffe. Esempietto:
if(a == b)
printf('a è maggiore di bn');
if(a == c)
Nel codice riportato, se il valore contenuto in a è uguale a quello contenuto in b viene visualizzata la stringa 'a è maggiore di b'; in caso contrario la chiamata a printf() non è eseguita e l'elaborazione prosegue con la successiva istruzione, che è ancora una if. Questa volta, se a è uguale a c viene eseguito il blocco di istruzioni comprese tra le parentesi graffe, altrimenti esso è saltato 'a pié pari' e il programma prosegue con la prima istruzione che segue la graffa chiusa.
Come regola generale, una condizione viene espressa tramite uno degli operatori logici del C (vedere pag. ) ed è sempre racchiusa tra parentesi tonde.
La if è completata dalla parola chiave else, che viene utilizzata quando si devono definire due possibilità alternative; inoltre più strutture ifelse possono essere annidate qualora serva effettuare test su più 'livelli' in cascata:
if(a == b)
printf('a è maggiore di bn');
else
Quando è presente la else, se la condizione è vera viene eseguito solo ciò che sta tra la if e la else; in caso contrario è eseguito solo il codice che segue la else stessa. L'esecuzione dei due blocchi di codice è, in altre parole, alternativa.
E' estremamente importante ricordare che ogni else viene dal compilatore riferita all'ultima if incontrata: quando si annidano costruzioni ifelse bisogna quindi fare attenzione alla costruzione logica delle alternative. Cerchiamo di chiarire il concetto con un esempio.
Supponiamo di voler codificare in C il seguente algoritmo: se a è uguale a b allora si controlla se a è maggiore di c. Se anche questa condizione è vera, si visualizza un messaggio. Se invece la prima delle due condizioni è falsa, cioè a non è uguale a b, allora si assegna a c il valore di b. Vediamo ora un'ipotesi di codifica:
if(a == b)
if(a > c)
printf('a è maggiore di cn');
else
c = b;
I rientri dal margine sinistro delle diverse righe evidenziano che le intenzioni sono buone: è immediato collegare, da un punto di vista visivo, la else alla prima if. Peccato che il compilatore non si interessi affatto alle indentazioni: esso collega la else alla seconda if, cioè all'ultima if incontrata. Bisogna correre ai ripari:
if(a == b)
if(a > c)
printf('a è maggiore di cn');
else;
else
c = b;
Quella appena vista è una possibilità. Introducendo una else 'vuota' si raggiunge lo scopo, perché questa è collegata all'ultima if incontrata, cioè la seconda. Quando il compilatore incontra la seconda else, l'ultima if non ancora 'completa', risalendo a ritroso nel codice, è la prima delle due. I conti tornano ma c'è un modo più elegante.
if(a == b)
else
c = b;
In questo caso le parentesi graffe indicano chiaramente al compilatore qual è la parte di codice che dipende direttamente dalla prima if e non vi è il rischio che la else sia collegata alla seconda, dal momento che questa è interamente compresa nel blocco tra le graffe e quindi è sicuramente 'completa'.
Come si vede, salvo alcune particolarità, nulla diversifica la logica della if del C da quella delle if (o equivalenti parole chiave) disponibili in altri linguaggi di programmazione
La if gestisce ottimamente quelle situazioni in cui, a seguito della valutazione di una condizione, si presentano due sole possibilità alternative. Quando le alternative siano più di due, si è costretti a utilizzare più istruzioni if nidificate, il che può ingarbugliare non poco la struttura logica del codice e menomarne la leggibilità.
Quando la condizione da valutare sia esprimibile mediante un'espressione restituente un int o un char, il C rende disponibile l'istruzione switch, che consente di valutare un numero qualsiasi di alternative per il risultato di detta espressione. Diamo subito un'occhiata ad un caso pratico:
#define EOF -1
#define LF 10
#define CR 13
#define BLANK ' '
char c;
long ln = 0L, cCount = 0L;
.
switch(c = fgetc(inFile))
Il frammento di codice riportato fa parte di una funzione che legge il contenuto di un file carattere per carattere ed esegue azioni diverse a seconda del carattere letto: in particolare, la funzione fgetc() legge un carattere dal file associato al descrittore inFile e lo restituisce. Tale carattere è memorizzato nella variabile c, dichiarata di tipo char. L'operazione di assegnamento è, in C, un'espressione che restituisce il valore assegnato, pertanto il valore memorizzato nella variabile c è valutato dalla switch, che esegue una delle possibili alternative definite. Se si tratta del valore definito dalla costante manifesta EOF la funzione termina; se si tratta del carattere definito come LF viene valutato quante righe sono già state scandite per decidere se terminare o no; se si tratta di un LF o di un BLANK è incrementato un contatore; i caratteri definiti come CR e NULL (il solito zero binario) vengono semplicemente ignorati; qualsiasi altro carattere è copiato in un buffer il cui puntatore è incrementato di conseguenza.
E' meglio scendere in maggiori dettagli. Per prima cosa va osservato che l'espressione da valutare deve trovarsi tra parentesi tonde. Inoltre il corpo della switch, cioè l'insieme delle alternative, è racchiuso tra parentesi graffe. Ogni singola alternativa è definita dalla parola chiave case, seguita da una costante (non sono ammesse variabili o espressioni non costanti) intera (o char), a sua volta seguita dai due punti (' '). Tutto ciò che segue i due punti è il codice che viene eseguito qualora l'espressione valutata assuma proprio il valore della costante tra la case e i due punti, fino alla prima istruzione break incontrata (se incontrata!), la quale determina l'uscita dalla switch, cioè un salto alla prima istruzione che segue la graffa chiusa. La parola chiave default seguita dai due punti introduce la sezione di codice da eseguire qualora l'espressione non assuma nessuno dei valori specificati dalle diverse case . Ma non finisce qui.
Tra le parentesi graffe deve essere specificata almeno una condizione: significa che la switch potrebbe essere seguita anche da una sola case o dalla default, e che quindi possono esistere delle switch prive di default o di case. La default, comunque, se presente è unica. Complicato? Più a parole che nei fatti
Torniamo all'esempio: cosa accade se c vale EOF? viene eseguito tutto ciò che segue i due punti, cioè l'istruzione return. Questa ci 'catapulta' addirittura fuori dalla funzione eseguita in quel momento, quindi della switch non siparla proprio più
Se invece c vale LF, l'esecuzione salta alla if che segue immediatamente la seconda case. Se la condizione valutata dalla if è vera addio funzione; altrimenti l'esecuzione prosegue con l'istruzione immediatamente successiva. E' molto importante sottolineare che, a differenza di quanto si potrebbe pensare, la presenza di altre case non arresta l'esecuzione e non produce l'uscita dalla switch: viene quindi incrementata la variabile cCount. Solo a questo punto l'istruzione break determina l'uscita dalla switch
L'incremento della cCount è invece la prima istruzione eseguita se c vale BLANK, ed è anche l'ultima perché subito dopo si incontra la break. Se c vale CR o NULL si incontra immediatamente la break, e quindi si esce subito dalla switch. Da ciò si vede che quando in una switch è necessario trattare due possibili casi in modo identico è sufficiente accodare le due case. Infine, se in c non vi è nessuno dei caratteri esaminati, viene eseguito ciò che segue la default
E' forse superfluo precisare che le break, se necessario, possono essere più di una e possono dipendere da altre condizioni valutate all'interno di una case, ad esempio mediante una if. Inoltre una case può contenere un'intera switch, nella quale ne può essere annidata una terza tutto sta a non perdere il filo logico dei controlli. Esempio veloce:
switch(a)
.
break;
case 1:
.
break;
case 2:
.
}
Se a è uguale a viene eseguita la seconda switch, al termine della quale si rientra nella prima (e sempre nella parte di codice dipendente dalla case per ). La prima switch, inoltre, non ha la default: se a non vale , né , né l'esecuzione salta direttamente alla prima istruzione che segue la graffa che la chiude.
I blocchi di istruzioni dipendenti da una case, negli esempi visti, non sono mai compresi tra graffe. In effetti esse non sono necessarie (ma lo sono, ripetiamolo, per aprire e chiudere la switch), però, se presenti, non guastano. In una parola: sono facoltative.
Il C supporta un'istruzione che ha il formato generale:
goto etichetta;
.
etichetta:
oppure:
etichetta:
.
goto etichetta;
L'etichetta può essere un qualsiasi nome (sì, anche Pippo o PLUTO) ed è seguita dai due punti (' '). L'istruzione goto è detta 'di salto incondizionato', perché quando viene eseguita il controllo passa immediatamente alla prima istruzione che segue i due punti che chiudono l'etichetta. E' però possibile saltare ad una etichetta solo se si trova all'interno della stessa funzione in cui si trova la goto; non sono consentiti salti interfunzione.
Per favore, non usate mai la goto. Può rendere meno chiaro il flusso elaborativo alla lettura del listato ed è comunque sempre possibile ottenere lo stesso risultato utilizzando un'altra struttura di controllo tra quelle disponibili, anche se talvolta è meno comodo.
La giustificazione più usuale all'uso di goto in un programma C è relativa alla possibilità di uscire immediatamente da cicli annidati al verificarsi di una data condizione, ma anche in questi casi è preferibile utilizzare metodi alternativi.
Il linguaggio C dispone anche di istruzioni per il controllo dei cicli: con esse è possibile forzare l'iterazione su blocchi di codice più o meno ampi.
Mediante l'istruzione while è possibile definire un ciclo ripetuto finché una data condizione risulta vera. Vediamo subito un esempio:
while(a < b)
Le due righe comprese tra le graffe sono eseguite finché la variabile a, incremento dopo incremento, diventa uguale a b. A questo punto l'esecuzione prosegue con la prima istruzione che segue la graffa chiusa.
Vale la pena di addentrarsi un poco nell'algoritmo, esaminando con maggiore dettaglio ciò che accade. Come prima operazione viene valutato se a è minore di b (la condizione deve essere espressa tra parentesi tonde). Se essa risulta vera vengono eseguiti la printf() e l'autoincremento di a, per ritornare poi al confronto tra a e b. Se la condizione è vera il ciclo è ripetuto, altrimenti si prosegue, come già accennato, con quanto segue la parentesi graffa chiusa.
Se ne trae, innanzitutto, che se al primo test la condizione non è vera, il ciclo non viene eseguito neppure una volta. Inoltre è indispensabile che all'interno delle graffe accada qualcosa che determini le condizioni necessarie per l'uscita dal ciclo: in questo caso i successivi incrementi di a rendono falsa, prima o poi, la condizione da cui tutto il ciclo while dipende.
Esiste però un altro metodo per abbandonare un ciclo al verificarsi di una certa condizione: si tratta dell'istruzione break . Esempio:
while(a < b)
In questo caso a è incrementata e poi confrontata con il valore : se uguale, il ciclo è interrotto, altrimenti esso prosegue con il decremento di c. E' anche possibile escludere dall'esecuzione una parte del ciclo e forzare il ritorno al test:
while(a < b)
Nell'ultimo esempio presentato, a viene confrontata con c ed incrementata. Se, prima dell'incremento essa è minore di c il flusso elaborativo ritorna al test dell'istruzione while; la responsabile del salto forzato è l'istruzione continue, che consente di iniziare da capo una nuova iterazione. In caso contrario viene chiamata printf() e, successivamente, viene effettuato il nuovo test con eventuale uscita dal ciclo.
I cicli while possono essere annidati:
while(a < b)
All'interno del ciclo per (a > b) ve n'è un secondo, per (c < x). Già nella prima iterazione del ciclo 'esterno', se la condizione (c < x) è vera si entra in quello 'interno', che viene interamente elaborato (cioè c è incrementata finché assume valore pari ad x) prima che venga eseguita la successiva istruzione del ciclo esterno. In pratica, ad ogni iterazione del ciclo esterno avviene una serie completa di iterazioni nel ciclo interno.
Va sottolineato che eventuali istruzioni break o continue presenti nel ciclo interno sono relative esclusivamente a quest'ultimo: una break produrrebbe l'uscita dal ciclo interno e una continue il ritorno al test, sempre del ciclo interno.
Si può ancora notare, infine, che il ciclo per (c < x) si compone di una sola istruzione: proprio per questo motivo è stato possibile omettere le parentesi graffe.
I cicli di tipo dowhile sono, come si può immaginare, 'parenti stretti' dei cicli di tipo while. Vediamone subito uno:
do while(a < b);
Non a caso è stato riportato qui uno degli esempi utilizzati poco sopra con riferimento all'istruzione while: in effetti i due cicli sono identici in tutto e per tutto, tranne che per un particolare. Nei cicli di tipo dowhile il test sulla condizione è effettuato al termine dell'iterazione, e non all'inizio: ciò ha due conseguenze importanti.
In primo luogo un ciclo dowhile è eseguito sempre almeno una volta, infatti il flusso elaborativo deve percorrere tutto il blocco di codice del ciclo prima di giungere a valutare per la prima volta la condizione. Se questa è falsa il ciclo non viene ripetuto e l'elaborazione prosegue con la prima istruzione che segue la while, ma resta evidente che, comunque, il ciclo è già stato compiuto una volta.
In secondo luogo l'istruzione continue non determina un salto a ritroso, bensì in avanti. Essa infatti forza in ogni tipo di ciclo un nuovo controllo della condizione; nei cicli while la condizione è all'inizio del blocco di codice, e quindi per poterla raggiungere da un punto intermedio di questo è necessario un salto all'indietro, mentre nei cicli dowhile il test è a fine codice, e viene raggiunto, ovviamente, con un salto in avanti.
Per ogni altro aspetto del comportamento dei cicli dowhile, in particolare l'istruzione break , valgono le medesime considerazioni effettuate circa quelli di tipo while
Tra le istruzioni C di controllo dei ciclo, la for è sicuramente la più versatile ed efficiente. La for è presente in tutti (o quasi) i linguaggi, ma in nessuno ha la potenza di cui dispone in C. Infatti, in generale, i cicli di tipo while e derivati sono utilizzati nelle situazioni in cui non è possibile conoscere a priori il numero esatto di iterazioni, mentre la for, grazie alla sua logica 'punto di partenza; limite; passo d'incremento', si presta proprio ai casi in cui si può determinare in partenza il numero di cicli da compiere.
Nella for del C è ancora valida la logica a tre coordinate, ma, a differenza della quasi totalità dei linguaggi di programmazione, esse sono reciprocamente svincolate e non necessarie. Ciò significa che, se in Basic la for agisce su un'unica variabile, che viene inizializzata e incrementata (o decrementata) sino al raggiungimento di un limite prestabilito, in C essa può manipolare, ad esempio, tre diverse variabili (o meglio, tre espressioni di diverso tipo); inoltre nessuna delle tre espressioni deve necessariamente essere specificata: è perfettamente lecita una for priva di condizioni di iterazione.
A questo punto, tanto vale esaurire le banalità formali, per concentrarsi poi sulle possibili modalità di definizione delle tre condizioni che pilotano il ciclo. Sia subito detto, dunque, che anche la for vuole che le condizioni siano specificate tra parentesi tonde e che se il blocco di codice del ciclo comprende più di una istruzione sono necessarie le solite graffe, aperta e chiusa. Anche nei cicli for possiamo utilizzare le istruzioni break e continue : la prima per 'saltar fuori' dal ciclo; la seconda per tornare 'a bomba' alla valutazione del test. Anche i cicli for possono essere annidati, e va tenuto presente che il ciclo più interno compie una serie completa di iterazioni ad ogni iterazione di quello che immediatamente lo contiene.
E vediamo, finalmente, qualche ciclo for dal vivo: nella sua forma banale, quasi 'Basic‑istica', può assumere il seguente aspetto:
for(i = 1; i < k; i++)
Nulla di particolare. Prima di effettuare la prima iterazione, la variabile i è inizializzata a . Se essa risulta minore della variabile k il ciclo è eseguito una prima volta. Al termine di ogni iterazione essa è incrementata e successivamente confrontata con la k; se risulta minore di quest'ultima il ciclo è ripetuto.
Vale la pena di evidenziare che le tre coordinate logiche stanno tutte quante all'interno delle parentesi tonde e sono separate tra loro dal punto e virgola (' '); solo la sequenza deve obbligatoriamente essere presente in un ciclo for
In effetti possiamo avere una for come la seguente:
for( ; ; )
Qual è il suo significato? Nulla è inizializzato. Non viene effettuato alcun test. Non viene modificato nulla. Il segreto consiste nel fatto che l'assenza di test equivale a condizione sempre verificata: la for dell'esempio definisce quindi un'iterazione infinita. Il programma rimane intrappolato nel ciclo finché si verifica una condizione che gli consenta di abbandonarlo in altro modo, ad esempio con l'aiuto di una break
Ma si può fare di meglio
for(i = 0; string[i]; )
++i;
Il ciclo dell'esempio calcola la lunghezza della stringa (terminatore nullo escluso). Infatti i è inizializzata a e viene valutato se il carattere ad offset in string è nullo; se non lo è viene eseguita l'unica istruzione del ciclo, che consiste nell'incrementare i. A questo punto è valutato se è nullo il byte ad offset in string, e così iterando finché string[i] non è proprio il NULL finale. L'esempio appena presentato è del tutto equivalente a
for(i = 0; string[i]; i++);
Il punto e virgola che segue la parentesi tonda indica che non vi sono istruzioni nel ciclo. Le sole cose da fare sono, perciò, la valutazione della condizione e l'incremento di i finché, come nel caso precedente, string[i] non punta al NULL che chiude la stringa. Se poi volessimo includere nel calcolo anche il NULL, ecco come fare:
for(i = 0; string[i++]; );
Sissignori, tutto qui. Anche questo ciclo non contiene alcuna istruzione; tuttavia, in questo caso, l'incremento di i fa parte della condizione e (trattandosi di un postincremento; vedere pag. ) viene effettuato dopo la valutazione, quindi anche (per l'ultima volta) quando string[i] punta al NULL. E che dire della prossima?
for( ; *string++; )
Nulla di particolare, in fondo: viene verificato se *string è un byte non nullo e string è incrementato. Se la verifica dà esito positivo viene eseguito il codice del ciclo. Viene poi nuovamente effettuata la verifica, seguita a ruota dall'incremento, e così via. Quanti si sono accorti che questo ciclo for è assolutamente equivalente a un ciclo while? Eccolo:
while(*string++)
In effetti si potrebbe dire che l'istruzione while, in C, è assolutamente inutile, in quanto può essere sempre sostituita dalla for, la quale, anzi, consente generalmente di ottenere una codifica più compatta ed efficiente dell'algoritmo. La maggiore compattezza deriva dalla possibilità di utilizzare contestualmente alla condizione, se necessario, anche un'istruzione di inizializzazione ed una di variazione. La maggiore efficienza invece dipende dal comportamento tecnico del compilatore, il quale, se possibile, gestisce automaticamente i contatori dei cicli for come variabili register (vedere pag.
Gli esempi potrebbero continuare all'infinito, ma quelli presentati dovrebbero essere sufficienti per evidenziare, almeno a grandi linee, le caratteristiche salienti dei cicli definiti mediante l'istruzione for. E' forse il caso di sottolineare ancora una volta che il contenuto delle parentesi tonde dipende fortemente dal ciclo che si vuole eseguire e dall'assetto elaborativo che gli si vuole dare, ma l'uso dei due punto e virgola è obbligatorio. Il primo e l'ultimo parametro non devono essere necessariamente inizializzare ed incrementare (o decrementare) il contatore (o il medesimo contatore), così come il parametro intermedio non deve per forza essere una condizione da valutare. Ciascuno di questi parametri può essere una qualunque istruzione C o può venire omesso. Il compilatore, però, interpreta sempre il parametro di mezzo come una condizione da verificare, indipendentemente da ciò che è in realtà: detto parametro è quindi sempre valutato come vero o falso , e da esso dipendono l'ingresso nel ciclo e le successive iterazioni.
Tra l'altro, in C la if non è mai seguita da una parola chiave tipo 'then', o simili. Può sembrare banale sottolinearlo, ma chi viene ad esempio dalla programmazione in Basic sembra convincersene con qualche difficoltà.
In C, uno dei modi per manipolare il contenuto di un file consiste nell'aprire uno stream, cioè un 'canale di flusso' col file stesso mediante un'apposita funzione, che restituisce il puntatore ad una struttura i cui campi sono utilizzati poi da altre funzioni, tra cui la fgetc(), per compiere operazioni sul file stesso. Tale puntatore è detto 'descrittore' del file: vedere pag.
L'istruzione break, se usata in una struttura di controllo switch (pag. ), determina l'uscita dalla stessa. Si può dire che la break, laddove lecita, ha sempre lo scopo di interrompere la fase elaborativa corrente per proseguire con il normale flusso del programma: essa esercita infatti la medesima funzione nei cicli dowhile (vedere di seguito) e for (pag.
Appunti su: flusso elaborativo, |
|