Comments
Description
Transcript
Costrutti per il controllo di sequenza
Capitolo 3 Costrutti per il controllo di sequenza Nella descrizione di un algoritmo è fondamentale poter specificare l’ordine in cui si susseguono le diverse istruzioni ovvero, il controllo del flusso d’esecuzione (o controllo di sequenza). Nei diagrammi di flusso, abbiamo visto, l’ordine è specificato molto semplicemente tramite frecce che connettono le diverse istruzioni. Nei linguaggi di basso livello, tipicamente, sono presenti a questo scopo istruzioni di salto che permettono di modificare, quando opportuno, il comportamento di “default” della macchina hardware che è quello di eseguire le singole istruzioni una dopo l’altra, in sequenza, cosı̀ come appaiono memorizzate nella memoria principale del calcolatore. I linguaggi di programmazione ad alto livello, di tipo convenzionale, offrono un più ricco insieme di costrutti sintattici per specificare, in termini più astratti, diverse tipiche forme (o strutture) di controllo di sequenza. In tutti questi linguaggi sono presenti statement per specificare (almeno) le seguenti strutture di controllo: • sequenzializzazione • selezione • iterazione • salto. Per quanto riguarda la sequenzializzazione i linguaggi ad alto livello adottano solitamente soluzioni molto semplici come, ad esempio, l’uso esplicito di un operatore di sequenzializzazione (ad esempio il ; in Pascal) o l’assunzione che la sequenzializzazione sia la regola di default. Ad esempio, in C++ gli statement sono eseguiti in sequenza, uno dopo l’altro, come appaiono nel testo, a meno che non si trovino all’interno di uno statement che modifica esplicitamente il flusso d’esecuzione. Nota. In C++ il ; è semplicemente un “terminatore” di statement (e di dichiarazioni), e non un operatore che indica la sequenzializzazione di due statement, come accade ad 68 esempio in Pascal. Per questo in C++, qualsiasi statement (o dichiarazione, tranne lo statement composto) deve essere terminata da un ;, indipendentemente da ciò che segue (ad esempio, in C++, l’ultimo statement del programma, prima della parentesi graffa finale, va comunque terminato da un ;). Nei paragrafi successivi descriveremo in dettaglio gli statement offerti dal C++ per modellare le diverse strutture di controllo sopra citate. In particolare analizzeremo: • gli statement if e switch per le strutture di controllo selettive; • gli statement while, do-while e for per le strutture di controllo iterative; • gli statement break, continue e goto per i salti espliciti. Altri costrutti e meccanismi che permettono di realizzare altre forme di astrazione sul controllo, quali quelli per la definizione e la chiamata di sottoprogrammi ed il meccanismo della ricorsione, verranno presi in esame in un capitolo successivo (Capitolo 5). Prima di descrivere in dettaglio i singoli statement, introduciamo la nozione di statement composto che utilizzeremo poi nella presentazione degli statement stessi. Nota. Si osservi che le espressioni, introdotte nel capitolo precedente, costituiscono già una forma di controllo del flusso d’esecuzione. Nella valutazione di un’espressione, infatti, si procede seguendo un ben preciso ordine tre le varie parti costituenti l’espressione: tipicamente, valutando prima gli operandi, da sinistra a destra, e poi applicando l’operatore e, nel caso in cui un’espressione sia composta da più operatori, seguendo precise regole di precedenza ed associatività. 3.1 Statement composto I linguaggi di programmazione convenzionali offrono normalmente un costrutto—detto statement composto—che permette di raggruppare più statement per formare un unico statement. Lo statement composto in C++ ha la seguente forma:. { dichiarazioni ∪ statement } e cioè un insieme di dichiarazioni (eventualmente vuoto) unito ad un insieme di statement (eventualmente vuoto), il tutto racchiuso tra una coppia di parentesi graffe. Esempio 3.1 (Statement composto) Il seguente frammento di codice C++ costituisce un esempio di statement composto: 69 { int x; cin >> x; float y; y = x/2.0; cout << y; } La regione di testo racchiusa tra le due graffe costituisce un blocco, nozione fondamentale per la strutturazione dei programmi nei linguaggi di programmazione moderni che incontreremo nuovamente e preciseremo nel sottocapitolo 3.10 dedicato ai problemi di visibilità degli identificatori. Per ora anticipiamo soltanto che tutti gli identificatori eventualmente dichiarati all’interno di un blocco sono locali al blocco e cioè utilizzabili soltanto all’interno di quel blocco o di eventuali altri blocchi in esso contenuti. Si osservi che un programma principale, come quello mostrato nel sottocapitolo 1.4.2, è costituito da una testata, che di base ha la forma int main(), seguita dal corpo del programma che di fatto è uno statement composto. 3.2 Statement if Tra le strutture di controllo selettive più comuni figurano senz’altro quelle che permettono l’“esecuzione condizionale” di un’istruzione e la “biforcazione”, già incontrate nel sottocapitolo 1.2 relativo ai diagrammi di flusso. Queste strutture di controllo sono realizzate nella maggior parte dei linguaggi di programmazione attuali da una o più forme di statement if. Vediamo sintassi e semantica di questo statement nel caso del linguaggio C++. 3.2.1 Caso base Sintassi if (E) S; dove E: espressione booleana; S: statement qualsiasi. Semantica (informale) Calcola il valore dell’espressione E; se E ha valore “vero” allora esegue lo statement S e quindi termina l’esecuzione dello statement; altrimenti, termina immediatamente. Il caso descritto è rappresentabile con un diagramma di flusso nel seguente modo (selezione singola): 70 E Sı̀ No S Si tratta di una forma di esecuzione condizionale: lo statement S viene eseguito solo se la condizione E è vera. Esempio 3.2 Il seguente semplice frammento di programma calcola il valore assoluto di un numero intero letto da standard input: int x; cin >> x; if (x < 0) x = -x; cout << "Il valore assoluto e’ " << x; Lo statement x = -x viene eseguito soltanto se la condizione x < 0 è vera. In ogni caso, terminata l’esecuzione dell’if, si passa allo statement successivo, che, in questo esempio, provvederà a stampare il valore corrente della variabile x. Qualora le istruzioni da eseguire quando la condizione dell’if è vera siano più di una, è necessario racchiuderle all’interno di uno statement composto, in modo che possano essere viste come un unico statement (si osservi che la sintassi dell’if prevede che S sia un singolo statement, che potrebbe comunque essere anche lo statement composto). Come esempio, si supponga di voler modificare il frammento di programma mostrato nell’Esempio 3.2 in modo che, quando x < 0 è vero, allora venga stampato anche un messaggio che informa che il numero dato è negativo. Lo statement if dell’esempio 3.2 viene sostituito da: if (x < 0) { cout << "Il numero dato e’ negativo" << endl; x = -x; } Nota. Si osservi che senza racchiudere tra graffe i due statement da eseguire quando la condizione dell’if è vera il programma risultante int x; cin >> x; if (x < 0) cout << "Il numero dato e’ negativo" << endl; x = -x; cout << "Il valore assoluto e’ " << x; 71 avrebbe, in generale, un comportamento totalmente diverso (e non corretto rispetto alle nostre intenzioni originarie). Infatti, in questo caso, il compilatore considera come parte del costrutto if l’istruzione di stampa cout << ... << endl, ma non l’istruzione di assegnamento successiva, che pertanto è eseguita sempre, indipendentemente dal valore dell’espressione x < 0. Dunque, se, ad esempio, il dato in input è -3, il programma stampa correttamente Il numero dato e’ negativo Il valore assoluto e’ 3 ma, se il dato in input è 3, il programma stampa Il valore assoluto e’ -3 che evidentemente non è il risultato desiderato. 3.2.2 Caso if-else Lo statement if ammette anche un’altra forma, in cui è prevista una parte introdotta dalla parola chiave else. Sintassi if (E) S1 ; else S2 ; dove E: espressione booleana; S1 , S2 : statement qualsiasi. Semantica (informale) Calcola il valore dell’espressione E; se E ha valore “vero” allora esegue lo statement S1 , altrimenti esegue lo statement S2 ; quindi, in entrambi i casi, termina l’esecuzione dello statement. Anche in questo caso la situazione è rappresentabile con un diagramma di flusso (selezione doppia): Sı̀ E S1 No S2 Si ha dunque una biforcazione nel flusso di esecuzione: si esegue uno tra gli statement S1 o S2 in base al verificarsi o meno della condizione E. 72 Esempio 3.3 Il seguente semplice frammento di programma determina il maggiore tra due numeri interi letti da standard input e lo scrive sullo standard output: int x, y, max; cin >> x >> y; if (x < y) max = x; else max = y; cout << "Il maggiore e’ " << max; (per il diagramma di flusso relativo allo statement if-else di questo esempio si veda il sottocapitolo 1.2). Anche per lo statement che compare nella parte else valgono le stesse considerazioni fatte nel caso dell’if semplice: qualora le istruzioni da eseguire siano più di una è necessario che siano racchiuse tra le parentesi graffe di uno statement composto, come mostra il seguente frammento di codice: if (x >= y) { max = x; min = y; } else { max = y; min = x; } Si osservi che lo statement composto non vuole il “;” dopo la “}”. 3.2.3 Statement if-else annidati Gli statement S, S1 ed S2 delle diverse forme di if possono essere statement qualsiasi. In particolare possono essere a loro volta statement if, nel qual caso si parla di if “annidati” (o “nidificati”). Un caso particolarmente interessante di if annidati è quello che realizza una struttura di controllo del tipo “test multiplo” (o selezione multipla): if (E1 ) S1 ; else if (E2 ) S2 ; else ... else if (En ) Sn ; else Sn+1 ; 73 Come mostra l’indentazione, il ramo else relativo al primo if contiene a sua volta un if; il ramo else relativo al secondo if contiene ancora un if e cosı̀ via. La struttura di controllo realizzata da queste istruzioni è quella descritta dal seguente diagramma di flusso: E1 Sı̀ S1 No E2 Sı̀ S2 No .. . .. . En Sı̀ Sn No Sn+1 Si osservi che le espressioni booleane E1 , E2 , . . . , vengono valutate nell’ordine in cui compaiono. Pertanto lo statement Si eseguito sarà quello associato alla prima condizione Si che risulta vera. In particolare, lo statement Sn viene eseguito solamente nel caso in cui tutte le precedenti espressioni booleane abbiano valore falso. In ogni caso, soltanto uno degli statement Si viene eseguito, mentre tutti gli altri vengono ignorati. Nota. Per evidenziare meglio la struttura del test multiplo può essere conveniente utilizzare una diversa indentatura degli statement if annidati, come mostrato qui di seguito: if (E1 ) S1 ; else if (E2 ) S2 ; ... else if (En ) Sn ; else Sn+1 ; Il seguente esempio di programma completo mostra l’utilizzo di più if annidati per realizzare un test multiplo. Esempio 3.4 (Conversione voti → giudizi ) Problema. Scrivere un programma in grado di convertire un voto numerico tra 0 e 10 in un giudizio, secondo il seguente schema: 74 voto ≤ 5, 5 < voto ≤ 6.5, 6.5 < voto ≤ 7.5, voto > 7.5, giudizio: giudizio: giudizio: giudizio: insufficiente sufficiente buono ottimo Input: un numero reale. Output: una stringa di caratteri (cioè il giudizio) oppure un messaggio di errore. Programma: #include <iostream> using namespace std; int main() { float voto; cout << "Dammi il voto numerico (tra 0 e 10)" << endl; cin >> voto; if (voto < 0 || voto > 10) cout << "voto non valido" << endl; else if (voto <= 5) cout << "insufficiente" << endl; else if (voto <= 6.5) cout << "sufficiente" << endl; else if (voto <= 7.5) cout << "buono" << endl; else cout << "ottimo" << endl; cout << "arrivederci" << endl; return 0; } Esempio d’esecuzione (le parti sottolineate rappresentano input): Dammi il voto numerico (tra 0 e 10) 6 sufficiente arrivederci Nota. Si noti che nei test relativi ai giudizi “sufficiente” e “buono” non è necessario specificare anche l’estremo inferiore del voto. Ad esempio, per il giudizio buono basta dare la condizione (voto <= 7.5) e non necessariamente (voto > 6.5 && voto <= 7.5) dato che, nel momento in cui si esegue il test, sicuramente sono già stati effettuati i test precedenti e quindi sono già stati esclusi i valori di voto inferiori o uguali a 5. Esercizio 3.5 Scrivere un programma C++ che legge da standard input due numeri interi e determina se i due numeri sono uguali o se uno è maggiore dell’altro e, nel primo caso, stampa il messaggio “I due numeri 75 sono uguali” mentre, nel secondo caso, stampa il maggiore tra i due numeri (SUGG.: completare il frammento di programma dell’Esempio 3.3, utilizzando due if annidati). Variante #1 per l’esempio 3.4. Il programma dell’esempio 3.4 può essere riscritto utilizzando una sequenza di statement if semplici, piuttosto che più if-else annidati. Mostriamo qui soltanto le parti del programma modificate: ... cin >> voto; if (voto < 0 || voto > 10) cout << "voto non valido" << endl; if (voto > 0 && voto <= 5) cout << "insufficiente" << endl; if (voto > 5 && voto <= 6.5) cout << "sufficiente" << endl; if (voto > 6.5 && voto <= 7.5) cout << "buono" << endl; if (voto > 7.5 && voto <= 10) cout << "ottimo" << endl; cout << "arrivederci" << endl; ... I risultati prodotti dalle due versioni del programma sono gli stessi, ma la nuova versione (variante #1) risulta meno soddisfacente per almeno due motivi: • è più pesante da scrivere, in quanto richiede di esplicitare tutte le condizioni, indicando sia estremo inferiore che superiore dei diversi intervalli; • è più inefficiente, in quanto richiede di eseguire sempre comunque tutti i test, anche se uno soltanto avrà effettivamente esito positivo (e cioè la condizione risulterà vera). Esercizio 3.6 Scrivere i diagrammi di flusso delle due versioni dei programmi per la conversione di voti in giudizi e confrontarli tra loro. Approfondimento (Ambiguità tra if annidati). L’utilizzo di if-else annidati può portare a situazioni di possibile ambiguità. Si consideri ad esempio il seguente frammento di programma: if (trovato == true) if (a == 0) cout << "a e’ nullo" << endl; else cout << "a non e’ nullo" << endl; A quale if corrisponde il ramo else? L’ambiguità è risolta in C++—come nella maggior parte dei linguaggi convenzionali—assumendo che un else faccia riferimento sempre all’if più vicino che non abbia già un else associato. Quindi, nell’esempio di sopra, l’else si riferisce al secondo if (se ci fosse anche un altro else, successivo a quello già presente, questo ulteriore else farebbe chiaramente riferimento al primo if, in quanto il secondo sarebbe già “completo”). L’indentatura del programma può facilitare la comprensione dell’annidamento degli if. Conviene quindi scrivere l’esempio di sopra come: 76 if (trovato == true) if (a == 0) cout << "a e’ nullo" << endl; else cout << "a non e’ nullo" << endl; Se vogliamo che il ramo else si riferisca al primo if è necessario inserire il secondo if all’interno di uno statement strutturato nel modo seguente: if (trovato == true) { if (a == 0) cout << "a e’ nullo" << endl; } else cout << "Non e’ stato trovato l’elemento cercato" << endl; 3.3 Statement while Un altro tipo di struttura di controllo che si incontra con grande frequenza nella progettazione di algoritmi è la struttura di controllo iterativa, caratterizzata dall’esecuzione ripetuta di una sequenza di istruzioni (un ciclo). Nella pratica, si individuano diverse tipologie di strutture iterative, distinte, ad esempio, in base alla posizione in cui compare la condizione d’uscita dal ciclo o al tipo di azioni che si compiono all’interno del ciclo. Tutti i linguaggi di programmazione convenzionali forniscono uno o più costrutti linguistici che permettono di realizzare, in modo più o meno naturale, le diverse tipologie di strutture di controllo iterative. Il C++, in particolare, offre tre diversi statement, while, do-while e for, specificatamente rivolti alla realizzazione di cicli, oltre alle istruzioni di salto esplicito che possono comunque essere utilizzate per la realizzazione di cicli. In questo sottocapitolo descriveremo lo statement while, mentre gli altri statement iterativi e quelli di salto verranno descritti nei paragrafi successivi di questo sottocapitolo. Sintassi while (E) S; dove E: espressione booleana; S: statement qualsiasi. 77 Semantica (informale) Calcola il valore dell’espressione E; se E ha valore “vero” allora esegue lo statement S e ripete dall’inizio; se E ha valore “falso” allora termina l’esecuzione dello statement. La situazione è descrivibile con un diagramma di flusso nel seguente modo: No E Sı̀ S Lo statement while realizza in modo naturale una struttura di controllo iterativa in cui la condizione d’uscita dal ciclo è posta all’inizio ed in cui le azioni da ripetere (il “corpo del ciclo”) possono essere qualsiasi. Di fatto, il ciclo continua finchè la condizione E non diventa falsa. Ogni ripetizione dello statement S viene detta iterazione del ciclo. In generale, non è possibile stabilire a priori il numero di iterazioni, ma si osservi che, nel caso in cui l’espressione booleana E risulti subito falsa, allora non vi è alcuna iterazione (lo statement S non viene quindi eseguito, e si passa subito allo statement successivo al while). Come nel caso dello statement if, qualora le istruzioni all’interno del ciclo siano più di una, è necessario che vengano racchiuse in uno statement composto, ossia tra parentesi graffe. Un semplice esempio di utilizzo del while è mostrato nel seguente frammento di programma. Esempio 3.7 Stampa i quadrati dei primi 10 numeri interi positivi: int i = 1; while (i <= 10){ cout << i * i << endl; i = i + 1; } cout << "terminato!" << endl; L’esecuzione di questi statement produrrà il seguente output: 1 4 9 ... 100 terminato! 78 Il corpo del while può contenere anche altri statement strutturati, in particolare altri statement while (cicli annidati, di cui vedremo esempi più avanti) o statement if. Il seguente programma completo mostra un esempio di while contenente uno statement if. Esempio 3.8 (Conta numero totale di vocali) Problema. Scrivere un programma che legge da standard input una sequenza di caratteri terminata da un punto, determina il numero di vocali minuscole presenti nella sequenza e quindi scrive il numero calcolato sullo standard output. Programma: #include <iostream> using namespace std; int main() { char c; int vocali_minusc = 0; cout << "Inserisci sequenza di caratteri terminata da ." << endl; cin >> c; //legge un carattere e lo assegna a c while (c != ’.’){ if (c==’a’ || c==’e’ || c==’i’ || c==’o’ || c==’u’) ++vocali_minusc; cin >> c; } cout << "La sequenza data contiene " << vocali_minusc << " vocali minuscole" << endl; return 0; } L’esecuzione del programma, con la sequenza di input Una frase di prova. produce il seguente output: La sequenza data contiene 6 vocali minuscole. Esercizio 3.9 Scrivere un programma C++ che legge da standard input una sequenza di numeri interi terminata da un numero negativo, calcola la media aritmetica dei numeri (non negativi) letti, e scrive il risultato sullo standard output (il programma controlla anche che la sequenza non sia vuota, nel qual caso informa l’utente con opportuno messaggio in output). (SUGG.: la struttura del programma è essenzialmente la stessa del programma dell’Esempio 3.8; si utilizza una variabile somma in cui “accumulare”, man mano, la somma parziale dei numeri letti da input, inserendo all’interno del ciclo uno statement somma = somma + x, dove x è la variabile che contiene l’ultimo numero letto da input, . . . ). 79 Esercizio 3.10 Scrivere un programma che legge da standard input una sequenza di caratteri terminata da un punto e determina e stampa il numero di “doppie” presenti nella sequenza, dove per “doppie” si intende una sequenza di due caratteri consecutivi qualsiasi (ma diversi da spazio e a capo) identici. Ad esempio, data in input la sequenza Arrivero’ appena possibile. il programma deve indicare che sono presenti 3 doppie. Nota (Cicli senza fine). Un aspetto critico delle strutture di controllo iterative, come quelle realizzate tramite il while, è dato dalla possibilità di scrivere cicli non terminanti. In generale, è compito di chi progetta l’algoritmo assicurarsi che i cicli presenti terminino in un tempo finito. Un primo semplice controllo è assicurarsi che il valore dell’espressione che costituisce la condizione d’uscita dal ciclo venga in qualche modo modificata all’interno del ciclo (altrimenti la condizione, se inizialmente vera, rimarrebbe tale per sempre e quindi non porterebbe mai all’uscita dal ciclo). Si consideri, ad esempio, il seguente frammento di programma C++: int i = 0; while (i < 10) somma = somma + 1; Poichè il valore della variabile che funge da variabile di controllo del ciclo (in questo caso la variabile i) non viene mai modificata all’interno del corpo del while e la condizione del while è inizialmente vera, si è sicuramente in presenza di un ciclo infinito. Un altro semplice controllo da effettuare è di verificare che l’espressione booleana che funge da condizione d’uscita non sia sempre banalmente vera, indipendentemente dal valore delle variabili che eventualmente appaiono in essa. Ad esempio, il ciclo int i = 1; while (i > 0 || i <= 10) i = i + 1; è chiaramente senza fine (probabilmente il programmatore, inesperto, voleva dire che i può variare tra 1 e 10 ma ha sbagliato connettivo logico . . . ). Non sempre è cosı̀ immediato individuare la presenza di un ciclo senza fine. I tre frammenti di programmi C++ che seguono sono altrettanti esempi di situazioni di ciclo senza fine. Probabilmente sono tutti frutto di errori di programmazione che, purtroppo, non è cosı̀ infrequente commettere . . . .1 Lasciamo al lettore attento scoprire perchè si ha un ciclo senza fine e qual’è la probabile versione corretta del codice. int i = 1, j = 1; while (i = 1){ j = j + 1; if (j > 10) i = 0; } 1 Si noti che si tratta di errori semantici non sintattici e quindi non rilevabili dal compilatore; in pratica, il programma non fa quello che si vorrebbe, ma è sintatticamente corretto. 80 (SUGG.: si ricordi che l’assegnamento può essere usato anche come espressione, che il valore 1 è interpretato come valore ”vero” in un’espressione booleana, che la relazione di uguaglianza in C++ si scrive == e non =, . . . ). int i = 1; while (i <= 10); {cout << i * i << endl; i = i + 1; } cout << "terminato!" << endl; (SUGG.: si consideri che il C++ ammette anche lo statement vuoto, e che lo statement del while può essere uno statement qualsiasi (anche vuoto) . . . ). int i = 1; while (i <= 10) cout << i * i << endl; i = i + 1; cout << "terminato!" << endl; (SUGG.: si ricordi che il corpo del while è costituito da un solo statement (che però può essere anche lo statement composto) . . . ). Approfondimento (Il problema della terminazione). Il problema della terminazione dei programmi è senz’altro molto complesso: a partire dagli anni ‘70 hanno iniziato a nascere metodi formali per la verifica (statica) di correttezza dei programmi che comprendono tecniche per la dimostrazione formale di proprietà di terminazione dei cicli. Per un approfondimento si veda ad esempio il testo [7]. Gli statement visti finora, in particolare gli statement if e while, sono sufficienti a scrivere qualsiasi programma (ovvero a descrivere la soluzione di qualsiasi problema computabile). In altri termini, non è strettamente necessario introdurre altri costrutti; in particolare, non è richiesta la presenza nel linguaggio di programmazione di istruzioni di salto esplicito, quali il goto. Normalmente, però, i linguaggi di programmazione convenzionali offrono anche altri statement per il controllo di sequenza, quali lo statement for, lo switch (o case), ecc. Questi ulteriori costrutti sono introdotti, in generale, non per aumentare le capacità computazionali del linguaggio (che, come si diceva, sono già complete con i pochi tipi di statement visti fin qui), ma per motivi essenzialmente metodologici, quali: • facilitare la scrittura dei programmi, offrendo all’utente statement che modellano in modo più naturale le strutture di controllo che si vogliono realizzare; • migliorare la comprensibilità del programma, permettendo l’utilizzo di diverse forme di astrazione sul controllo • aumentare l’affidabilità dei programmi, riducendo le possibilità di commettere errori grazie all’utilizzo di costrutti più specializzati, ad esempio per realizzare cicli limitati, test multipli, ecc. 81 Approfondimento (Teorema di Böhm-Jacopini). Il Teorema di Böhm-Jacopini, pubblicato nel 1966, afferma che qualsiasi algoritmo (o meglio qualsiasi funzione computabile) puó essere realizzata con l’utilizzo di sole tre strutture di controllo, la sequenza, la selezione e il ciclo. In termini di linguaggi di programmazione convenzionali, come il C++, questo significa che sono sufficienti i soli costrutti if-else e while (oltre alla ovvia possibilità di sequenzializzare due o più istruzioni). Questo risultato è servito, tra l’altro, a chiudere la disputa sulla necessità o meno di istruzioni di salto esplicito (come l’istruzione goto). Dunque qualsiasi programma può essere scritto senza l’uso di goto, ma usando soltanto if-else e while che hanno il pregio rispetto al goto di garantire la costruzione di programmi strutturati (vedere sottocapitolo 3.8). 3.4 Statement do-while Spesso si ha a che fare con strutture di controllo iterative in cui il test d’uscita è posto alla fine del corpo del ciclo. Per modellare nel modo più naturale possibile queste situazioni, molti linguaggi di programmazione, tra cui il C++, offrono uno statement apposito. Nel caso del C++ si tratta dello statement do-while. Sintassi do S; while (E); dove E: espressione booleana; S: statement qualsiasi. Semantica (informale) Esegue lo statement S; quindi valuta l’espressione E: se E ha valore “vero” allora ripete dall’inizio; altrimenti termina l’esecuzione dello statement. La situazione è descrivibile con un diagramma di flusso nel seguente modo: S Sı̀ E No In altri termini, si esegue ripetutamente lo statement S per tutto il tempo che la condizione E rimane “vera” (si esce quando E assume valore “falso”). 82 Si osservi che, a differenza dello statement while, nel do-while prima si esegue S e poi si valuta E. Dunque, con il do-while, si esegue sempre almeno un’iterazione. Come già sottolineato, il do-while non è strettamente necessario e può essere sempre sostituito da un while. Precisamente, il generico costrutto do-while mostrato sopra è equivalente al frammento di programma: S; while (E) S; Esercizio 3.11 Realizzare il comportamento del costrutto while (E) S; tramite do-while. (SUGG.: inserire uno statement if all’inizio del corpo del do-while . . . ). Due tipiche situazioni in cui l’utilizzo dello statement do-while risulta particolarmente comodo sono le seguenti: Controllo dell’input: ripeti la lettura di un dato di input finchè il dato letto non soddisfa certe condizioni prestabilite. Ad esempio, se si deve leggere un numero intero x da standard input e si vuol imporre che il numero sia non negativo, si può sostituire la semplice istruzione cin >> x; con l’istruzione do-while seguente: do cin >> x; while (x < 0); che impone al programma di ripetere la lettura se il dato letto è negativo (scartando di fatto tutti gli eventuali dati negativi letti). Dunque, all’uscita dal ciclo il dato contenuto in x sarà sicuramente non negativo. Ripetizione programma: ripetere l’esecuzione di un programma fornendo ogni volta nuovi dati di input fintantochè l’utente non indichi esplicitamente di voler smettere. Come esempio per illustrare questo modo di utilizzare il do-while, mostriamo come modificare il programma dell’Esempio 3.4 per permetterne l’esecuzione ripetuta. Esempio 3.12 (Conversione voti → giudizi ripetuta) Problema. Scrivere un programma in grado di eseguire ripetutamente la conversione di un voto numerico tra 0 e 10 in un giudizio secondo lo schema indicato nell’ Esempio 3.4. Al termine di ogni operazione di conversione, il programma dovrà richiedere all’utente se vuole continuare o no ed in caso di risposta positiva (carattere ’s’) dovrà ripetere dall’inizio. Programma: 83 #include <iostream> using namespace std; int main() { float x; char ripeti; do { cout << "Dammi il voto numerico (tra 0 e 10)" << endl; cin >> voto; if (voto < 0 || voto > 10) cout << "voto non valido" << endl; else if (voto <= 5) cout << "insufficiente" << endl; else if (voto <= 6.5) cout << "sufficiente" << endl; else if (voto <= 7.5) cout << "buono" << endl; else cout << "ottimo" << endl; cout << "Altra conversione? (’s’ per continuare): "; cin >> ripeti; } while (ripeti == ’s’); cout << "Arrivederci" << endl; } Esercizio 3.13 Riscrivere il programma dell’Esempio 3.8 usando il costrutto do-while invece che while. 3.5 Statement for Un’altra forma molto comune di struttura di controllo iterativa è quella del cosiddetto ciclo limitato (o controllato, o iterazione determinata) che può essere schematicamente espressa nel modo seguente: “ripeti una certa azione per tutti i valori di x appartenenti ad un dato insieme finito di valori”. Ad esempio: “stampa tutti i quadrati dei numeri interi da 1 a 100” (ovvero, “per tutti gli x tra 1 e 100 calcola e stampa x ∗ x”) Il C++, come la maggior parte dei linguaggi di programmazione ad alto livello, offre uno specifico costrutto, lo statement for, che permette di realizzare (tra l’altro) questo tipo di cicli. Sintassi for (E1 ; E2 ; E3 ) S; 84 dove E1 , E2 , E3 : espressioni qualsiasi; S: statement qualsiasi. Semantica (informale) (i) Valuta l’espressione E1 ; (ii) valuta l’espressione E2 : se E2 ha valore “falso” termina l’esecuzione del for; altrimenti, se E2 ha valore “vero”, esegue lo statement S e quindi valuta l’espressione E3 e ripete da (ii). La situazione è descrivibile con un diagramma di flusso nel seguente modo: E1 No E2 Sı̀ S E3 Nota. Il ciclo realizzato tramite for può sempre essere realizzato, in alternativa, utilizzando uno statement while. Precisamente, il generico costrutto for mostrato sopra è equivalente al seguente frammento di programma: E1 ; while (E2 ) { S; E3 ; } da cui risulta evidente, tra l’altro, che, come nel caso del while, potrebbe non esserci alcuna esecuzione dello statement S. Si osservi anche che le espressioni E1 ed E3 sono di fatto utilizzate come statement e quindi saremo verosimilmente interessati al possibile side-effect da esse provocato piuttosto che al valore ritornato dalla loro valutazione. Questa forma molto generale di for può essere opportunamente specializzata per ottenere diverse forme di ciclo limitato o di struttura iterativa in genere. Analizziamo dapprima in dettaglio come realizzare la forma base del ciclo limitato sopra menzionata; poi discuteremo brevemente le altre forme ed usi del for. 85 3.5.1 Ciclo limitato: caso base Vogliamo eseguire lo statement S per tutti i valori di una variabile x compresi tra un valore iniziale ei ed un valore finale ef . Per ottenere questo comportamento è sufficiente specializzare lo statement for nel modo seguente: for (x = ei ; x <= ef ; x++) S; dove x: variabile di tipo compatibile con int; ei , ef : espressioni di tipo compatibile con int; S: statement qualsiasi. il cui significato è esprimibile con il seguente diagramma di flusso (in pratica si tratta di sostituire in modo opportuno le espressioni generali E1 , E2 ed E3 con le espressioni più specifiche che appaiono in questo uso particolare del for): x ← ei No x ≤ ef Sı̀ S x←x+1 x costituisce la variabile di controllo del ciclo, mentre ei ed ef rappresentano, rispettivamente, il valore iniziale e finale di x. La prima espressione del for permette di inizializzare la variabile di controllo; la seconda costituisce la condizione d’uscita dal ciclo; la terza specifica il passo usato per l’incremento della variabile di controllo. Il seguente frammento di programma C++ mostra un semplice esempio di utilizzo del for per la realizzazione di un ciclo limitato. Esempio 3.14 Stampa tutti i numeri interi compresi tra 0 e 9 in ordine crescente: int i; for (i = 0; i <= 9; i++) cout << i << ’ ’; 86 Il tipo della variabile di controllo è normalmente int, ma può essere anche un qualsiasi tipo compatibile con int (ovvero un tipo scalare), come char, bool, un tipo definito per enumerazione, o un sottotipo di int, come unsigned int, short, ecc. In realtà il compilatore C++ non esegue alcun controllo sul tipo della variabile di controllo (nè tantomeno su quello delle espressioni ei ed ef ) dato che in generale le espressioni del for possono essere di tipo qualsiasi. È perciò compito del programmatore fare in modo che tutte le espressioni coinvolte in un for usato come ciclo limitato abbiano tipo int o compatibile con int; il non rispetto di questo vincolo (utilizzando ad esempio tipi float) potrebbe portare alla non terminazione del ciclo. In C++ la variabile di controllo del for può essere dichiarata direttamente all’interno del for stesso.2 Ad esempio, il frammento di programma mostrato sopra può essere scritto alternativamente nel modo seguente: for (int i = 0; i <= 9; i++) cout << i << ’ ’; In questo caso, la variabile di controllo è locale al corpo dello statement for e quindi non può essere utilizzata al suo esterno. Nota. Si osservi che le espressioni x = ei e x++ usate nello statement for sono in realtà statement di assegnamento che, come detto in precedenza, il C++ permette di usare anche come espressioni. Si osservi anche l’uso—non strettamente necessario, ma assai frequente—della forma contratta dell’assegnamento (x++) per realizzare l’incremento della variabile di controllo. In questo caso si potrebbe utilizzare in modo del tutto equivalente anche la forma prefissa (++x) in quanto qui si è interessati alla modifica della variabile x piuttosto che al valore ritornato dall’assegnamento. Dunque usare (x++) o (++x) in questo contesto è solamente una questione di stile e noi abbiamo scelto di utilizzare in questo testo sempre la forma postfissa. Anche nel caso del for qualora le istruzioni all’interno del ciclo siano più di una è necessario racchiuderle in un blocco, ossia tra parentesi graffe. Vedremo altri esempi più complessi di utilizzo dello statement for per realizzare cicli limitati quando introdurremo le strutture dati di tipo array (si veda il sottocapitolo 4.3). Approfondimento (Controllo ciclo limitato). Il C++ non prevede alcun controllo per garantire che un ciclo realizzato tramite for sia davvero un ciclo limitato e quindi termini in un numero finito di passi. In particolare in C++ è possibile modificare liberamente la variabile di controllo e i suoi limiti iniziale e finale all’interno del corpo del ciclo. Ad esempio possiamo scrivere for (int i = 0; i <= 9; i++) i--; che porta chiaramente ad un ciclo infinito. Per garantire che un ciclo limitato termini sempre in un tempo finito bisognerebbe porre delle forti restrizioni sui valori iniziale e finale della variabile di controllo, sulla condizione di uscita dal ciclo e sul modo in cui la variabile di controllo viene modificata 2 A rigore questo significa che la sintassi del for non è esattamente quella mostrata all’inizio di questo sottocapitolo, ma andrebbe estesa in modo da prevedere che l’espressione E1 possa essere sostituita anche da una dichiarazione di variabile. 87 all’interno del ciclo stesso. In certi linguaggi di programmazione, come ad esempio il Pascal, queste restrizioni sono imposte nella forma sintattica del for, permettendo cosı̀ di effettuare i controlli che garantiscono la finitezza del ciclo for già a tempo di compilazione. La scelta del C++, invece, è come al solito quella di favorire la flessibilità d’uso dei suoi costrutti, lasciando completamente al programmatore la responsabilità di garantire il funzionamento corretto dei programmi realizzati. Come detto sopra, lo statement for del C++ è un costrutto molto più generale del for di altri linguaggi, utilizzabile per realizzare svariate strutture di controllo, non soltanto il ciclo limitato. Esercizio 3.15 Scrivere un programma C++ che calcoli e stampi il prodotto dei primi 10 numeri naturali positivi (ovvero 10!) utilizzando uno statement for (SUGG.: si utilizzi una variabile i che assume, uno alla volta, i valori da 1 a 10, e per ogni nuovo valore di i si calcoli il prodotto tra i e il prodotto parziale calcolato in precedenza ed “accumulato” in una variabile p, . . . ). Esercizio 3.16 Come per l’Esercizio 3.15, ma con il numero n dei naturali positivi da moltiplicare tra loro letto da standard input (ovvero, calcola n!). (SUGG.: il programma inizia leggendo in una variabile N il numero n, possibilmente verificando anche che sia ammissibile, e cioè > 0); quindi procede come nell’Esercizio 3.15, ma con il valore finale del ciclo for costituito da N (invece che da 10) . . . ). 3.5.2 Altri utilizzi dello statement for Come più volte sottolineato, il C++ è molto permissivo riguardo all’utilizzo delle espressioni all’interno del costrutto for, dando cosı̀ la possibilità di utilizzare questo costrutto anche per realizzare strutture di controllo diverse da quella del ciclo limitato di base visto finora. In particolare, possiamo facilmente realizzare altre forme di ciclo limitato, ma con passo diverso da +1. Per ottenere questo è sufficiente specificare in modo opportuno la terza espressione (E3 ) dello statement for. I seguenti esempi mostrano due cicli limitati, rispettivamente con passo −1 e con passo +2. Esempio 3.17 Stampa tutti i numeri interi compresi tra 0 e 9 in ordine decrescente: for (i = 9; i >= 0; i--) cout << i << ’ ’; Esempio 3.18 Stampa tutti i numeri pari compresi tra −10 e 10, in ordine crescente: for (int i = -10; i <= 10; i = i + 2) cout << i << ’ ’; 88 Le espressioni dello statement for possono essere anche espressioni più complesse di quelle viste finora. In particolare l’espressione E2 , che funge da condizione d’uscita dal ciclo, può essere un’espressione booleana composta costruita con i soliti connettivi logici (ad esempio, i < 10 && i != j). Si può anche utilizzare, al posto di una qualsiasi delle tre espressioni del for, la concatenazione di due espressioni ottenuta tramite l’operatore ’,‘, come mostrato nel seguente esempio: Esempio 3.19 Stampa contemporaneamente tutti i numeri interi compresi tra 0 e 9 in ordine crescente e tra 1 e 10 in ordine decrescente: int x; int y; for (x = 0, y = 10; x < 10; ++x, --y) cout << x << ’ ’ << y << ’\n’; Si osservi che è la variabile x a fungere da variabile di controllo all’interno del ciclo. Nota. L’operatore (infisso) ’,‘ permette di concatenare due o piu espressioni qualsiasi. Il significato di E1 , E2 , con E1 , E2 espressioni qualsiasi, è: valuta E1 , valuta E2 e restituisci come risultato dell’espressione composta il valore di E2 . Ad esempio, x = 1, y = 2, x + y valuta le tre sottoespressioni, nell’ordine da sinistra verso destra (l’operatore ’,‘ è associativo a sinistra) e restituisce come risultato finale dell’intera espressione il valore 3. Infine, tutte e tre le espressioni del for possono essere l’espressione vuota e quindi, di fatto, essere omesse (si noti che i “;” che separano le espressioni del for vanno comunque inseriti, anche se le espressioni sono mancanti). Ad esempio, il programma mostrato nell’Esempio 3.14 potrebbe essere scritto alternativamente come: int i = 0; for ( ; i < 10; ) { cout << i << ’ ’; i++; } In questo caso si sta di fatto utilizzando lo statement for come un while. Come altro esempio, il programma mostrato nell’Esempio 3.17 può essere scritto equivalentemente nel seguente modo: for (i = 10; i-- > 0; ) cout << i << ’ ’; In questo caso si è omessa la terza espressione del for ed inserito il decremento della variabile di controllo all’interno della seconda. Per ultimo, il seguente frammento di programma realizza il calcolo del fattoriale di un numero intero n, utilizzando un for combinato con un’espressione condizionale: 89 int fatt, n; cin >> n; for (fatt = (n ? n : 1) ; n > 1; ) fatt *= (--n); Si tratta evidentemente di un codice particolarmente sintetico, in cui vengono sfruttate varie pecularietà del C++ (che lasciamo da scoprire ed analizzare al lettore attento). È altrettanto evidente, comunque, che la sinteticità è ottenuta a discapito della chiarezza del codice. La possibilità di omettere le espressioni del for rende ancor più evidente il fatto che con il for del C++ è possibile scrivere cicli infiniti. In particolare, se manca l’espressione E2 del for e non sono presenti interruzioni forzate del ciclo (per esempio attraverso l’utilizzo di un break), allora il for provoca sicuramente un ciclo infinito. L’utilizzo di queste forme particolari di for può rendere il programma più difficile da comprendere ed è quindi, in generale, sconsigliato. Come regola generale, è sempre preferibile utilizzare il costrutto sintattico che modella in modo più naturale la struttura di controllo (iterativa) che si deve realizzare, sebbene la flessibilità del linguaggio di programmazione permetta di utilizzare indifferentemente uno qualsiasi dei costrutti offerti. 3.6 Statement switch Come abbiamo già avuto modo di evidenziare, una struttura di controllo molto comune è quella del test multiplo. In particolare, è frequente la situazione in cui si deve scegliere di eseguire una fra k possibili azioni in base ad m valori distinti (m ≥ k) che una data espressione E può assumere. Questa situazione può essere modellata tramite if-else annidati. Ma molti linguaggi, tra cui il C++, offrono un apposito costrutto sintattico (case, switch, ...) che permette di modellare in modo più naturale ed immediato questa forma di controllo del flusso. Nel caso del C++ lo statement in questione è lo switch. Vediamo come utilizzare questo statement per realizzare la struttura di controllo di test multiplo sopra descritta, nel caso m = k. 90 Sintassi switch (E) { case C1 : S Seq1 ; break; case C2 : S Seq2 ; break; ... case Ck : S Seqk ; break; default: S Seq; } dove E: espressione di tipo compatibile con int; C1 , C2 , . . . , Ck (k ≥ 0): espressioni costanti di tipo compatibile con int; S Seq1 ,. . . , S Seqk , S Seq: sequenze di statement qualsiasi. Semantica (informale) Valuta l’espressione E; se esiste i, con i = 1, . . . , k, tale che il valore di E è uguale al valore di Ci , allora esegue la sequenza di statement S Seqi e quindi termina; altrimenti, esegue lo statement S Seq, se presente, e quindi termina. La situazione è descrivibile con un diagramma di flusso nel seguente modo: E = C1 Sı̀ S Seq1 Sı̀ S Seq2 No E = C2 No .. . .. . Sı̀ E = Ck No S Seq 91 S Seqk Si tratta di un test multiplo in cui si sceglie una tra k possibili sequenze di statement S Seq1 , . . . , S Seqk , in base al valore di un’espressione E. La parte introdotta dalla parola chiave default è opzionale e viene eseguita soltanto se il valore di E non combacia con nessuna delle costanti specificate nei rami case.3 Il seguente frammento di programma mostra un semplice esempio di questo particolare utilizzo dello statement switch. Scopo del programma è di stampare il nome italiano dell’operatore aritmetico rappresentato dal carattere contenuto nella variabile c. ... char c; switch (c) case ’+’: cout << case ’-’: cout << case ’*’: cout << case ’/’: cout << default: cout << } { c << " -> addizione" << endl; break; c << " -> sottrazione" << endl; break; c << " -> moltiplicazione" << endl; break; c << " -> divisione" << endl; break; "Non e’ un operatore aritmetico!" << endl; In questo esempio (come spesso accade), l’espressione di controllo è semplicemente una variabile singola. Si osservi che le costanti C1 , . . . , Ck non possono essere espressioni qualsiasi, ma devono essere necessariamente espressioni costanti, e cioè espressioni il cui valore è determinabile a tempo di compilazione (ad esempio 2 o 2+3, ma non 2+x, con x variabile). Inoltre i valori di C1 , . . . , Ck devono essere tutti diversi tra di loro (in caso contrario il compilatore segnala un errore). Infine, il tipo di C1 , . . . , Ck e quello di E devono essere compatibili tra loro e tutti di tipo scalare (e cioè, compatibile con int). Per quanto riguarda il “corpo” di ogni alternativa case si osservi che si tratta di una sequenza di statement e non di un singolo statement: questo implica tra l’altro che, nel caso il corpo dell’alternativa debba contenere più statement, non è necessario racchiuderli tra parentesi graffe in modo da ottenere lo statement composto. Esercizio 3.20 Scrivere un programma C++ che legge da standard input un numero intero e stampa il corrispondente mese dell’anno o un opportuno messaggio di errore se il numero non è compreso tra 1 e 12. 3 Se presente, l’alternativa default deve essere unica, e può comparire ovunque nello switch (anche se è prassi inserirla sempre come ultima alternativa). 92 Approfondimento (Implementazione dello switch). Lo statement switch viene normalmente implementato in modo particolarmente efficiente su una macchina convenzionale. Senza entrare nei dettagli di un’implementazione reale, si può pensare che le costanti delle diverse alternative case vengano viste a livello di linguaggio Assembly come altrettante etichette associate al codice della sequenza di statement che segue il relativo case. L’esecuzione di uno switch allora procede valutando dapprima l’espressione di controllo dello switch stesso e quindi eseguendo un salto al codice che ha come etichetta il valore dell’espressione appena calcolato. Questa tecnica di implementazione spiega varie caratteristiche dello switch tra cui il fatto che le costanti associate ai case debbano essere valutabili a tempo di compilazione e debbano essere tutte diverse tra loro. Inoltre si osservi che il codice relativo alle sequenze di statement dei diversi rami case sarà disposto in memoria in modo consecutivo, in base all’ordine che gli statement hanno nello switch, e che quindi, una volta effettuato un salto alla porzione di codice individuata dall’etichetta corrispondente al valore assunto dall’espressione di controllo, l’esecuzione prosegue sul codice che segue, a meno che non sia prevista un’istruzione di salto esplicito che permette di trasferire il controllo oltre il codice dello switch. Nella forma di switch fin qui considerata, ogni alternativa, tranne l’ultima, termina con uno statement break. L’esecuzione di questo statement—su cui ritorneremo in modo più approfondito nel sottocapitolo successivo— provoca la terminazione immediata dello statement switch (e quindi la prosecuzione dell’esecuzione dallo statement successivo lo switch).4 In realtà lo statement switch del C++ non richiede, in generale, che un’alternativa case termini necessariamente con uno statement break: la sequenza di statement successivi alla parola chiave case può essere una sequenza qualsiasi (anche vuota). Dunque, nel caso più generale (cioè senza i break), la semantica dello switch diventa: valuta l’espressione E; se esiste i, con i = 1, . . . , k, tale che il valore di E è uguale al valore di Ci , allora esegue nell’ordine le sequenze di statement S Seqi , S Seqi+1 . . . , S Seqk e quindi termina; altrimenti, esegue lo statement S Seq, se presente, e quindi termina. Con un diagramma di flusso la situazione è rappresentata nel modo seguente: 4 Si noti che dopo uno statement break in un’alternativa case non ha senso che compaiano altri statement all’interno della stessa alternativa in quanto non potrebbero mai essere eseguiti. 93 E = C1 Sı̀ S Seq1 Sı̀ S Seq2 No E = C2 No .. . .. . Sı̀ E = Ck S Seqk No S Seq In altri termini, l’esecuzione del corpo dello switch comincia, in generale, dall’alternativa case a cui è associato un valore uguale al valore della condizione dello switch e continua attraverso le successive alternative fino a che non termina lo switch o incontra un break (o un’altra istruzione che provoca forzatamente il salto fuori dallo switch). Si tratta dunque di una struttura di controllo decisamente diversa da quella del test multiplo descritto all’inizio del paragrafo sottocapitolo. Riferendoci al frammento di programma mostrato sopra, l’assenza dell’istruzione break all’interno per esempio del case ’*’ provocherebbe come output * -> moltiplicazione * -> divisione Non e’ un operatore aritmetico! in risposta al carattere ’*’ fornito come input che, evidentemente, non è quanto si voleva ottenere. Si noti come la mancanza dei break renda significativo l’ordine in cui appaiono le diverse alternative dello switch. Nota. Il costrutto switch viene utilizzato nella maggior parte dei casi per realizzare la struttura di controllo di test multiplo considerata all’inizio del sottocapitolo, che richiede necessariamente l’utilizzo di un break alla fine di ogni ramo case dello switch (tranne l’ultimo che non necessita del break non essendo seguito da altre alternative case). Dunque l’assenza di un break è molto probabilmente segnale di un possibile errore e quindi si consiglia di evidenziare con un commento un’eventuale omissione voluta di un break. La forma più generale dello switch permette facilmente di modellare anche la situazione di test multiplo in cui ci siano più valori possibili in corrispondenza a ciascuna alternativa dello switch (caso m > k). Ad esempio 94 supponiamo che la sequenza di statement S Seqi , 1 ≤ i ≤ k, debba essere eseguita quando l’espressione E assume uno tra tre possibili valori, Ci1 , Ci2 , Ci3 , e cioè, con un diagramma di flusso: .. . E = Ci1 or E = Ci2 or E = C i3 Sı̀ S Seqi No Per modellare questa situazione basta inserire nello switch tre alternative case consecutive nel modo seguente: switch (E) { ... case Ci1 : case Ci2 : case Ci3 : S Seqi ; break; ... } Si noti che i primi due case sono alternative dello switch con la parte relativa alla sequenza di statement vuota (in particolare, senza break) e quindi la loro esecuzione comporta semplicemente la prosecuzione sullo statement dell’alternativa successiva. Esempio 3.21 (Conta numero vocali) Problema. Scrivere un programma che legge da standard input una sequenza di caratteri terminata da un punto, determina il numero di vocali (maiuscole o minuscole) presenti nella sequenza e quindi stampa il numero di vocali presenti per ciascuna delle 5 vocali (si veda per confronto il programma dell’Esempio 3.8). Programma: #include <iostream> using namespace std; int main() { char c; int n_a = 0, n_e = 0, n_i = 0, n_o = 0, n_u = 0; 95 cout << "Inserisci sequenza di caratteri terminata da ." << endl; cin >> c; //legge un carattere e lo assegna a c while (c != ’.’){ switch(c) { case ’a’: case ’A’: n_a++; break; case ’e’: case ’E’: n_e++; break; case ’i’: case ’I’: n_i++; break; case ’o’: case ’O’: n_o++; break; case ’u’: case ’U’: n_u++; } cin >> c; } cout << "La sequenza data contiene " << endl; cout << n_a << " vocali a" << endl; cout << n_e << " vocali e" << endl; cout << n_i << " vocali i" << endl; cout << n_o << " vocali o" << endl; cout << n_u << " vocali u" << endl; return 0; } Approfondimento (switch vs if). Uno statement switch può essere facilmente sostituito da una serie di statement if-else annidati. Ad esempio, la situazione descritta dallo statement switch switch (E) { case C1 : case C2 : case C3 : S Seq1 ; break; case C4 : S Seq2 ; break; default: S Seq; } può essere realizzata in modo equivalente (almeno per quanto riguarda il comportamento esterno) tramite if-else annidati nel modo seguente: if (E == C1 || E == C2 || E == C3 ) {S Seq1 } else if (E == C4 ) {S Seq2 } else {S Seq} Il codice scritto utilizzando il costrutto switch presenta alcuni vantaggi rispetto al codice che utilizza if-else annidati: 96 • risulta, in generale, più leggibile; • può essere eseguito, in generale, in modo più efficiente grazie alla tecnica di implementazione dello switch che richiede sempre un solo test per individuare l’alternativa da selezionare (per contro, con gli if-else annidati, se l’alternativa è l’n-esima verranno effettuati n test prima di poterla selezionare). Si osservi che il test usato per selezionare un’alternativa in uno statement switch è necessariamente un test di uguaglianza (il valore di E è uguale al valore di Ci ?). Questo rende molto più problematico realizzare tramite switch un test multiplo in cui le condizioni di selezione delle diverse alternative siano in generale delle disuguaglianze. Ad esempio, una situazione del tipo “se C1 ≤ E < C2 allora esegui S1 ; se C2 ≤ E < C3 allora esegui S2 ; altrimenti esegui S3 ” viene realizzata in modo molto più naturale tramite if-else annidati (si veda ad esempio il programma di conversione di voti in giudizi mostrato nell’Esempio 3.4). Nota. Si osservi che è possibile ottenere il comportamento di un generico if-else if (E) S1 else S2 utilizzando uno statement switch nel modo seguente: switch (E) { case true: S1; break; case false: S2: break; } È chiaro che si tratta di un “trucco“ di programmazione che sicuramente non contribuisce alla chiarezza del programma e che quindi andrebbe di norma evitato. 3.7 Statement break Finora abbiamo visto strutture di controllo per l’iterazione non limitata in cui la condizione d’uscita dal ciclo è posta o all’inizio o alla fine del ciclo, e che possono essere realizzate in modo naturale rispettivamente tramite gli statement while e do-while. Nella pratica, però, sono frequenti anche casi in cui la condizione d’uscita dal ciclo è posta in mezzo al ciclo, cioè preceduta e seguita da altre azioni, come illustrato nel seguente diagramma di flusso: S1 No E Sı̀ S2 Una simile struttura di controllo può ovviamente essere realizzata con gli statement while e do-while, ma, come vedremo, in modo non del tutto 97 naturale. Si consideri ad esempio la sequenza di istruzioni descritta dal seguente diagramma di flusso: cin >> x No x < 0 or x > 10 Sı̀ cout << input non valido che rappresenta una tipica situazione di lettura di un dato di input con controllo di validità del dato stesso e ripetizione della lettura in caso di dato non valido (preceduta da un opportuno messaggio d’errore). Questo schema può essere realizzato ad esempio utilizzando un costrutto do-while nel modo seguente: int x; do { cin >> x; if (x < 0 || x > 10) cout << "input non valido"; } while (x < 0 || x > 10); È evidente che questo codice realizza una struttura di controllo diversa (e senz’altro più complicata) rispetto a quella indicata sopra anche se il comportamento esterno delle due è lo stesso (lasciamo al lettore come esercizio la descrizione di questa struttura di controllo tramite un diagramma di flusso). In casi come questo può essere comodo avere a disposizione uno statement che permetta di “saltar fuori” da un ciclo in un punto qualsiasi del ciclo stesso. Il C++ offre una simile possibilità attraverso lo statement break. La semantica dell’istruzione break è quella del salto: l’esecuzione di un break provoca un salto al primo statement successivo al costrutto che lo contiene. Come costrutti contenenti il break si considerano soltanto quelli iterativi while, do-while, for e lo switch (ma non lo statement if o lo statement composto). L’utilizzo di un break all’interno di questi costrutti causa l’immediata uscita dal costrutto stesso, mentre il suo utilizzo all’esterno di questi costrutti (ad esempio all’interno di uno statement if non contenuto a sua volta all’interno di un ciclo o di uno switch) viene segnalato come errore dal compilatore. 98 Utilizzando il break è possibile riscrivere il codice dell’esempio mostrato sopra in modo che rispecchi in modo più preciso la struttura di controllo desiderata: int x; do { cin >> x; if (x < 0 || x > 10) cout << "input non valido"; else break; } while (true); Si osservi che il break comporta l’uscita dal costrutto do-while, ignorando il fatto che il break appare all’interno di un if-else. Si osservi anche che nel caso in cui il break sia all’interno di più costrutti iterativi o switch annidati la sua esecuzione provoca l’uscita soltanto dal costrutto che lo contiene direttamente e non da quelli più esterni. Vediamo ora un altro esempio (un programma completo) in cui si utilizza lo statement break. Esempio 3.22 (Conta il numero di lettere ‘a’) Problema. Leggere da standard input una sequenza di caratteri (di lunghezza massima 32) terminata da spazio o da “a capo” e determinare il numero di lettere ‘a’ presenti. Scrivere quindi il risultato su standard output. Nel caso in cui venga raggiunta la lunghezza massima della sequenza stampare anche un opportuno messaggio d’avviso. Programma: #include <iostream> using namespace std; int main() { int i = 0, num_a = 0; cout << "Inserisci una sequenza di caratteri " << " terminata da ’spazio’ o da ’a capo’" << endl; char c = cin.get(); ++i; while (c != ’ ’ && c != ’\n’) { if (i > 32) { cout << "Attenzione: la sequenza e’ stata troncata!" << endl; break; } if (c == ’a’) ++num_a; 99 c = cin.get(); ++i; } cout << "Il numero di caratteri ’a’ e’ " << num_a << endl; return 0; } Si presti attenzione al fatto che un utilizzo indiscriminato del break può portare a programmi più difficili da capire (e da dimostrare corretti). Risulta infatti più difficile, in generale, individuare e quindi controllare la condizione di uscita da ciclo che, con l’uso del break, piò essere “distribuita” in più parti del costrutto che realizza il ciclo. L’uso del break dovrebbe pertanto essere limitato ai soli casi in cui è evidente un effettivo miglioramento nella leggibilità del programma, come mostrato, ad esempio, nelle situazioni presentate all’inizio del sottocapitolo. Esercizio 3.23 Riscrivere il programma dell’Esempio 3.22 senza utilizzare l’istruzione break. (SUGG.: aggiungere la condizione i <= 32 all’interno dell’espressione booleana di controllo del ciclo while e portare il test per determinare se la sequenza è stata troncata dopo lo statement while). 3.8 Statement goto e programmazione strutturata Il C++, cosı̀ come la maggior parte dei linguaggi di programmazione convenzionali, offre anche un’istruzione di salto “incondizionato”: lo statement goto. La sintassi del goto è la seguente: goto L dove L è un identificatore associato ad uno statement s nel modo seguente: L: s; L è detta l’etichetta (in inglese label ) dello statement s. Il significato dello statement goto L è il seguente: l’esecuzione del programma continua dallo statement identificato dall’etichetta L. Esempio 3.24 ciao Ripeti: if (x<0) { s=s+x; x=x+1; goto Ripeti; } 100 Si osservi che questa sequenza di istruzioni realizza la stessa struttura di controllo realizzata dal costrutto while. Un’etichetta può essere associata a qualsiasi statement in un programma e deve essere unica. Un salto tramite goto può avvenire sia all’indietro che in avanti, e cioè sia ad un’etichetta che precede, nel testo del programma, lo statement goto, che ad un’etichetta che lo segue (nel caso di salto in avanti, si noti che l’etichetta viene utilizzata nel goto prima di essere stata introdotta). Inoltre con il goto è possibile saltare fuori/dentro cicli. Non è invece possibile saltare fuori/dentro una funzione. I costrutti utilizzati finora, e cioè i costrutti while, if-else, do-while, ecc., permettono di realizzare soltanto strutture di controllo con un unico punto di ingresso e un unico punto di uscita. Ad esempio, con il while si realizza la struttura di controllo a un ingresso e una uscita descritta dal seguente diagramma di flusso: No E Sı̀ S Un programma completo è ottenuto “connettendo” l’uscita di una struttura di controllo all’entrata della successiva. Il flusso di controllo del programma risulta pertanto molto semplice, lineare: Ciascun costrutto al suo interno potrà avere una struttura di controllo relativamente complessa (biforcazioni,ciao cicli, . . . ), ma comunque non visibile all’esterno. ... 101 La situazione può risultare drasticamente diversa con l’uso dell’istruzione goto. In questo caso si possono realizzare strutture di controllo con più uscite e più entrate. Si consideri ad esempio il seguente frammento di codice C++ in cui si fa un uso indiscriminato di goto. C: if (x > 0) goto A; else goto B; A: if (y < 0) goto C; else goto B; B: ... E’ evidente che in situazioni del genere il flusso di controllo del programma risulta molto più complesso, non descrivibile in generale come una semplice sequenza di costrutti connessi da un solo arco, ma piuttosto come un grafo complicato, con più archi uscenti da ogni costrutto. La situazione che si può creare è resa bene dal nome che è stato attribuito ad un codice di questo tipo: “codice spaghetti”. 3.8.1 Programmazione strutturata L’uso di strutture di controllo con una sola entrata e una sola uscita (senza l’uso di goto) è una delle caratteristiche fondamentali della cosiddetta programmazione strutturata. La programmazione strutturata è una metodologia di programmazione, sviluppata negli anni ’70-’80, allo scopo di rendere la struttura degli algoritmi realizzati dai programmi più facile da capire e da modificare. In particolare, la programmazione strutturata prevede l’uso disciplinato di: • strutture di controllo, che devono essere soltanto del tipo a 1-entrata 1-uscita; • convenzioni sul formato del programma, ovvero sulla forma grafica del codice sorgente (ad esempio, l’uso opportuno di “indentatura”); • commenti e altre convenzioni lessicali e grafiche (ad esempio, l’uso di nomi di variabili significativi); • opportune astrazioni sui tipi di dato, con l’utilizzo di tipi di dato definiti da utente. 102 I vantaggi dell’utilizzo della programmazione strutturata si possono riassumere nei seguenti punti:5 • migliora la leggibilità (ovvero la comprensibilità) del programma • semplifica l’individuazione di errori e la verifica (formale) di correttezza del programma (ad esempio, con l’utilizzo di pre- e post-condizioni, invarianti di ciclo) • semplifica la manutenzione del programma. E’ importante notare che la programmazione strutturata—ed in particolare l’utilizzo di sole strutture di controllo a 1-entrata 1-uscita—non limita in nessun modo il potere computazionale del linguaggio. Vale infatti il risultato enunciato nel già citato Teorema di Böhm-Jacopini (vedi sottocapitolo 3.3) in base al quale le strutture di controllo if-else e while, più la sequenza, sono un insieme di strutture “completo”, nel senso che sono sufficienti a scrivere qualsiasi programma (dunque, in particolare, il goto non è necessario). Va osservato, comunque, che a volte l’uso di strutture di controllo a 1-entrata 1-uscita può risultare un pò forzato e di fatto complicare la struttura dell’algoritmo invece che semplificarla. Inoltre il codice strutturato potrebbe risultare meno efficiente dal punto di vista dei tempi d’esecuzione di un codice equivalente, ma “meno strutturato” (problematico per alcune applicazioni real-time). In generale, i linguaggi di programmazione possono imporre più o meno il rispetto dei principi della programmazione strutturata, in particolare per quanto riguarda le strutture di controllo. Ad esempio il Pascal evita il più possibile l’utilizzo di costrutti linguistici che possano portare a strutture di controllo non ben strutturate. Il C++ invece è, come sempre, più permissivo e offre, oltre al goto, altre possibilità che facilitano la programmazione, ma possono portare a codice non ben strutturato (ad esempio, l’uso senza vincoli dello statement return—vedi Capitolo 5). Nel seguito dunque eviteremo di usare il goto in tutti i programmi che realizzeremo, che però potrebbero risultare, per motivi di convenienze, non sempre rigidamente ben strutturati, dal punto di vista della struttura di controllo. Cercheremo invece di attenerci sempre il pirù possibile alle altre indicazioni previste nella programmazione strutturata. 5 Si osservi che in questa discussione stiamo assumendo la fondamentale proprità che per uno stesso algoritmo si possano avere diversi programmi equivalenti, che potranno quindi essere piò o meno strutturati. Due programmi si dicono equivalenti se con gli stessi dati di input o non terminano o se terminano producono lo stesso effetto. 103 3.9 Controllo dei dati in input Come indicato nel paragrafo 2.8.1, il dato letto tramite l’operatore di estrazione s >> v, con s stream di input e v variabile di tipo t, deve essere una costante di tipo t, sintatticamente corretta. Altrimenti, il dato viene rifiutato (alla variabile v non viene assegnato alcun valore) e lo stream s viene impostato allo stato failed. È possibile verificare la presenza di questo stato tramite la funzione di libreria fail, che funziona nel modo seguente: s.fail() restituisce true se lo stream s si trova in uno stato failed, false altrimenti. Esempio 3.25 cout << "inserire un numero intero"; int n; cin >> n; if (cin.fail()) cout << "errore nel dato di input" << endl; else cout << "il dato inserito e’ corretto" << endl; Una volta individuato un errore nell’inserimento del dato, è frequente richiedere all’utente di inserire nuovamente il dato. Per far questo bisogna provvedere prima a ripristinare lo stato corretto dello stream di input s tramite la funzione di libreria clear, nel modo seguente: s.clear() Purtroppo questo non è ancora sufficiente. Infatti sullo stream di input è presente ancora il dato che l’operatore >> non è riuscito a leggere. Prima di ripetere la lettura bisogna pertanto rimuovere questo dato dallo stream, altrimenti la lettura fallisce nuovamente (andando quindi in loop). Un modo comodo per ottenere lo “svuotamento” dello stream di input è tramite la funzione di libreria ignore, che funziona nel modo seguente: s.ignore(n,delim) estrae i caratteri dallo stream di input s e li scarta; l’estrazione termina quando sono stati estratti e scartati n caratteri o quando si è trovato il carattere delim (dipende da quale delle due condizioni avviene per prima); nell’ultimo caso anche il carattere delimitatore viene estratto. Esempio 3.26 cout << "inserire un numero intero"; int n; do { cin >> n; if (cin.fail()) { cout << "errore - ripetere" << endl; cin.clear(); cin.ignore(256,’\n’); } else break; } while (true); cout << "il dato inserito e’ corretto" << endl; 256 è un numero arbitrario; in questo modo si fa l’ipotesi di poter leggere e scartare, per ogni lettura che sia fallita, al massimo 256 caratteri consecutivi. 104 Modi alternativi per il test dello stream Per verificare lo stato di uno stream è possibile usare direttamente il nome dello stream come condizione booleana. Esempio 3.27 ... int n; cin >> n; if (cin) cout << "il dato inserito e’ corretto" << endl; else cout << "errore nel dato di input" << endl; Nota Questo è possibile perchè la classe istream fornisce una funzione che può convertire un oggetto di tipo istream, come ad es. cin, in un valore booleano. Questa funzione viene chiamata quando lo stream (ad es. cin) appare in una posizione in cui è richiesta un’espressione boolena, come ad esempio nella condizione di un if o di un while. Un altro modo alternativo per ottenere lo stesso risultato è di controllare direttamente il risultato della valutazione dell’operatore >>. Siccome questo operatore restituisce come suo risultato uno stream (precisamente, lo stream modificato a seguito dell’estrazione del dato di input), è possibile testare lo stream risultante esattamente come nella prima soluzione alternativa. Esempio 3.28 ... int n; if (cin >> n) cout << "il dato inserito e’ corretto" << endl; else cout << "errore nel dato di input" << endl; L’uso della funzione fail risulta semanticamente più chiaro, ma spesso si preferiscono le soluzioni alternative perchè più sintetiche. Nota A rigore l’uso degli stream come condizione di test risulta un pò più generale dell’uso del fail perchè permette di testare anche altre possibili cause di fallimento, come ad esempio errori di disco. 3.10 Regole di “scope” Una dichiarazione (di variabile, di costante, di tipo, di funzione, ...) introduce un legame (“binding”) tra un nome ed un oggetto denotabile. I linguaggi di programmazione di solito permettono di utilizzare lo stesso nome, in contesti diversi, per denotare oggetti diversi. Un’associazione nome—oggetto, dunque, non rimane attiva necessariamente per tutto il programma, ma può essere attiva in certi punti del programma e non attiva in altri, e in quest’ultimi potrebbe essere attiva invece un’associazione relativa allo stesso nome, ma con un oggetto diverso. 105 Si pone quindi il problema quando si incontra un nome nel programma, di capire a quale dichiarazione (e quindi a quale oggetto denotabile) esso si riferisce. La risposta a questa domanda è fornita dalle regole di “scope”: regole che permettono di determinare il campo d’azione (o “scope”) di ogni dichiarazione (ovvero, in quale parte del programma è attiva quella dichiarazione), in modo tale che ogni nome che si incontra nel programma sia associato ad una ed una sola dichiarazione. Nei punti del programma in cui una dichiarazione per un nome N è attiva diremo che il nome N è visibile. Nei linguaggi di programmazione convenzionali le regole di “scope” sono solitamente basate sulla nozione di blocco (regole di “scope” statiche). Blocco: parte del programma (= insieme di dichiarazioni e statement) delimitata da opportuni costrutti sintattici (ad esempio, parentesi graffe aperte e chiuse, coppie di parole chiave begin-end, ecc.). ++ Per esempio, lo statement composto del C { dich. + stmt } definisce un blocco. Dunque, il corpo del main (e più in generale, come vedremo in seguito, di una qualsiasi funzione), cosı̀ come il corpo di uno statement strutturato, costituiscono altrettanti blocchi. Si noti che nel caso dello statement for del C++ si considerano parte del blocco del for anche le tre espressioni che compaiono nella testata del costrutto for. Ad esempio.: for(int i=0; i<n; i++) {...} il blocco di questo statement for è costituito dalle dichiarazioni e statement presenti nel suo corpo + la dichiarazione ed espressioni presenti nella testata del for. Nota. Non è cosı̀ ad esempio per il do-while; ad es. nello statement: do { ... } while (i < 10); l’espressione i < 10 è esterna al blocco rappresentato dal corpo del while. Inoltre, per comodità di trattazione, indicheremo come blocco del programma l’insieme delle eventuali dichiarazioni poste nella parte più esterna 106 del programma (e quindi non contenute nel blocco del main né di nessun’altra funzione). Esempio 3.29 (Blocchi in un programma) ciao const int max = 100; int main(...) { ... while(...) {... } ... } In questo programma si evidenziano tre blocchi: • blocco del programma, che contiene una dichiarazione per la costante max • blocco del main, contenuto nel blocco del programma (= blocchi annidati) • blocco del while, contenuto nel blocco del main. Regole di “scope”. Il campo d’azione di una dichiarazione per un nome N contenuta in un blocco B si estende dal punto in cui essa compare fino al termine del blocco B stesso. Il campo d’azione della dichiarazione si estende anche agli eventuali blocchi contenuti in B (sotto-blocchi), a meno che quest’ultimi non contengano a loro volta una dichiarazione per lo stesso nome N, nel qual caso il campo d’azione della dichiarazione in B è sospeso fino al termine del sotto-blocco che la contiene. Dunque le regole di “scope” stabiliscono che un nome N è visibile all’interno del blocco in cui dichiarato, ma non visibile al suo esterno. Inoltre, nel caso in cui un sotto-blocco contenga una nuova dichiarazione per N, la nuova dichiarazione “nasconde” temporaneamente quella vecchia. Nota. Vale comunque sempre la regola che un nome N deve essere in ogni istante associato ad uno ed un solo oggetto denotabile. Quindi, ad esempio, la presenza di due dichiarazioni per lo stesso nome N all’interno dello stesso blocco viene segnalata (dal compilatore) come un errore. Viceversa, uno stesso oggetto può essere associato in un certo istante a più nomi (“aliasing”). Questo capita ad esempio con il passaggio parametri per riferimento (si veda sottocapitolo 5.5). Se una funzione f ha un parametro formale x passato per riferimento e si richiama f specificando come parametro attuale una variabile a, all’interno della funzione f il nome x sarà associato allo stesso oggetto denotato da a. Quest’ultima associazione verrà eliminata nel momento in cui la funzione f terminerà, mentre verrà mantenuta l’associazione con il nome a. Esempio 3.30 (Regole di scope) ciao 107 const int max = 100; int n = max; int main() { int x = 0; int y = 1 cout << n; cout << x; for (int i=0; i<n; i++) { int x = 1; cout << x << endl; x = y + 1; } cout << x; cout << i; // errore! return 0; } Il campo d’azione delle dichiarazioni per max e n è l’intero programma. Il campo d’azione della prima dichiarazione per x è il main, però con un “buco” dovuto alla presenza all’interno del sotto-blocco del for di un’altra dichiarazione per lo stesso nome x. In questo sotto-blocco la prima dichiarazione per x non è più attiva; risulta invece attiva la seconda dichiarazione per x, quella interna al corpo del for. Dunque la stampa di x fatta all’interno del for fa riferimento alla x “locale” al for stesso (e quindi il primo valore stampato sarà 1). Nel blocco del for non c’e’ alcun modo per riferirsi all’oggetto creato con la prima dichiarazione per x. Quest’ultima ritonerà attiva invece al termine del blocco del for nel momento in cui si rientra nel blocco del main. L’istruzione di stampa di x successiva al blocco del for perciò farà riferimento alla x dichiarata nel main e quindi produrrà il valore 0. La successiva istruzione di stampa della variabile i invece viene rilevata dal compilatore come errata in quanto il nome i a cui si fa riferimento in essa non è visibile in quel punto, essendo i dichiarato in un blocco più interno. L’insieme delle associazioni nomi–oggetti denotabili esistenti in un certo momento dell’esecuzione in un dato punto del punto del programma è detto ambiente di riferimento (o, semplicemente, ambiente). Le associazione nomi–oggetti denotabili sono introdotte nell’ambiente in particolare tramite le dichiarazioni. Ad esempio, nel programma mostrato sopra, nel punto immediatamente precedente il for, l’ambiente è costituito dalle associazioni introdotte dalle dichiarazioni: - per max e n, contenute nel blocco del programma - per x e y, contenute nel blocco del main. 108 All’interno del for, dopo la dichiarazione di x, invece, l’ambiente è costituito dalle associazioni introdotte dalle dichiarazioni: - per per per per max e n, contenute nel blocco del programma y, contenuta nel blocco del main x, contenuta nel blocco del for i, contenuta nel blocco del for. Le associazioni nomi-oggetti contenute in un certo ambiente, relativo ad un certo punto del programma, all’interno di un blocco B, si distinguono in: • locali, se la corrispondente dichiarazione è interna a B • non locali, se la corrispondente dichiarazione non è interna a B (e quindi interna ad un blocco contenente B). In particolare, le associazioni non locali relative a dichiarazioni contenute nel blocco del programma (il blocco più esterno) sono dette globali. I nomi introdotti da queste dichiarazioni (che diremo, appunto, dichiarazioni globali ) sono visibili in tutto il programma, a meno che non vengano ridefiniti in blocchi più interni. Con riferimento all’esempio di sopra, nell’ambiente all’interno del for, dopo la dichiarazione di x, abbiamo che: - le associazioni relative a x e i sono locali - l’associazione relativa a y è non locale - le associazioni relative a max e n sono globali. 3.11 Domande per il Capitolo 3 1. Qual è il risultato prodotto dallesecuzione del seguente frammento di programma C++, con x = 3 e y = 5? (attenzione alle parentesi . . . ) if (x>y) max = x; cout << Il maggiore e << x << endl; if (x<=y) max = y; cout << Il maggiore e << y << endl; 2. Qual è il risultato prodotto dallesecuzione del seguente statement if, con x = 5? if (x % 2) cout << "then"; else cout << "else"; 109 3. Dare il significato dello statement do S; while(E); tramite un diagramma di flusso. 4. Dare il significato dello statement for(int x=e1; x < e2; x++)S; (e1, e2: espressioni intere) tramite un diagramma di flusso. 5. È sempre possibile realizzare la struttura di controllo del for tramite while? E viceversa? Giustificare le risposte. 6. Citare almeno due motivi per cui è utile, in generale, introdurre in un linguaggio di programmazione altri costrutti oltre all’if-else ed al while. 7. Dare il significato dello statement switch(e) {case c1: case c3: ... case cn: default: } case c2: S1; break; S2; break; Sm; break; S; (dove e è un’espressione di tipo t e c1, c2, . . . , cn sono costanti di tipo t) tramite un diagramma di flusso. 8. Indicare vantaggi/svantaggi dello statement switch rispetto allo statement if-else. 9. È sempre possibile realizzare la struttura di controllo dello switch tramite if-else annidati? E viceversa? Giustificare le risposte. 10. Dare il significato dello statement switch(e) { case c1: case c2: ... case cn: default: } S1; S2; Sn; Sn+1; 110 (dove e è un’espressione di tipo t e c1, c2, . . . , cn sono costanti di tipo t) tramite un diagramma di flusso (n.b.: gli statement S1, S2, . . . , Sn non contengono lo statement break}). 11. Dare il significato dello statement switch(e) { case c1: case c2: case c3: S1; break; case c4: case c5: S2; } (dove e è un’espressione di tipo t e c1, c2, . . . , c5 sono costanti di tipo t) tramite un diagramma di flusso. 12. Cosa prevede il Teorema di Böhm-Jacopini? Quali sono le sue implicazioni sui linguaggi di programmazione? 13. Qual è la forma sintattica ed il significato dello statement goto in C++? Cosa si intende con il termine programmazione strutturata? 14. A cosa servono in generale le “regole di scope” in un linguaggio di programmazione? A quali identificatori si riferiscono in generale? 15. Cosa si intende con il termine “blocco” in un linguaggio di programmazione? Quali dichiarazioni comprende in particolare il blocco di una funzione, quello dello statement for, e quello dellambiente globale in un programma C++? 16. Cosa affermano le “regole di scope” del C++? 17. Cosa si intende con ambiente di riferimento “locale” e “non locale”? 18. In base alle “regole di scope” del C++, cosa accade se in un blocco B1 interno ad un blocco B0 viene ridichiarata una variabile x già dichiarata nel blocco B0 ? È possibile riferirsi alla x del blocco B0 allinterno di B1 ? E allesterno del blocco B1 ? 19. Cosa si intende con il termine “programmazione strutturata”? 20. Che caratteristica devono avere le strutture di controllo in un linguaggio per la programmazione strutturata? 21. Quali vantaggi offre la programmazione strutturata? 22. Cosa si intende con il termine “codice spaghetti”? 111