Cesare Rota
Programmare con C++
EDITORE ULRICO HOEPLI MILANO
Copyright © Ulrico Hoepli Editore S.p.A. 2009 via Hoepli 5, 20121 20121 Milano (Italy) tel. +39 02 864871 – fax f ax +39 02 8052886 e-mail
[email protected]
www.hoepli.it www .hoepli.it Tutti i diritti sono riservati a norma di legge e a norma delle convenzioni internazionali ISBN 978-88-203978-88-203-4248-7 4248-7 Ristampa: 4
3
2
1
0
2009
2010 20
2011 20
2012
2013
Realizzazione editoriale ART Servizi Editoriali S.p.A. - Bologna www.art.bo.it
Coordinamento editoriale : Coordinamento Redazione : Progetto grafico : Impaginazione :
Monica Monari Barbara Megali Marina Baldisserri Sonia Bertusi
Copertina: MN&CG S.r.l., Milano Stampa: Art Grafiche Franco Battaia Batt aia S.r.l. - Zibido San Giacomo (MI) Printed in Italy
Indice
Presentazione
5
Sezione Sezi one 1 - Prem Premess esse e
7
U.D. 1 - Sistemi di numerazione
1 .1 1 .2 1 .3 1 .4 1 .5 1.6 1.7 1.7 1.8
8 9 9 10 11 12
Sistema di numerazione decimale Notazione polinomiale Numeri decimali Sistema di numerazione binario Conversione da base 2 a base 10 Somm So mmaa e so sott ttra razi zion onee nel nel si sist stem emaa binario 13 Sist Si stem emaa di nu nume mera razi zion onee es esad adec ecim imal alee 14 Conv Co nver ersi sion onee da da bin binar ario io a esa esade deci cima male le e viceversa 16
Esercizi
18
U.D. 2 - Codifica delle informazioni
20 21 22
2 .1 2 .2 2.3 2. 3 2 .4
Introduzione Rappresentazione de dei ca caratteri Rapp Ra ppre rese sent ntaz azio ione ne in inte tern rnaa dei numeri interi Rappresentazione dei nu numeri re reali
24 28
Esercizi
31
Sezione 2 - Primi elementi Sezione di programmazione
33
U.D. 3 - Introduzione a C++
3 .1 3 .2 3 .3 3 .4 3 .5 3 .6 3 .7 3 .8 3 .9 3 . 10
Primo programma Analisi del codice Definizione e assegnazione Tipi di dato Tipo di dato intero Tipo char Numeri in virgola mobile Casting Tipo booleano Costanti
34 35 38 40 42 43 44 46 48 48 49
Esercizi
51
U.D. 4 - Visualizzazione e acquisizione acquisizione
54
4.1 4. 1
Acquis Acqu isiz izio ione ne de dell llee in info form rmaz azio ioni ni:: il metodo cin
55
4 .2 4 .3 4 .4
Output formattato Caratteri di escape Uso di “\\” e del carattere “@”
58 62 63
Esercizi
64
U.D. 5 - Operatori
66 67 73 78 80
5 .1 5 .2 5 .3 5 .4
Operatori aritmetici Operatori aritmetici composti Operatori relazionali Operatori logici
Esercizi
86
Sezion Sez ione e 3 - Organizzazion Organizzazione e degli algoritmi 89 U.D. 6 - Algoritmi e pseudocodifica
6 .1 6 .2 6 .3 6 .4 6 .5 6.6 6. 6
90 91 92 94 96 97
Analisi del problema Primo esempio Algoritmi Dati e istruzioni Istruzioni di assegnamento Istr Is truz uzio ioni ni di ing ingre ress sso o e di di usc uscit itaa dei dati 6.7 Teorema di Jacopini-Bö hm 6.8 Struttura di alternativa 6.9 Struttura di ripetizione 6.10 6.1 0 Co Cons nsid ider eraz azio ioni ni su sulla lla ps pseu eudo doco codi difi fica ca
99 100 102 104 107
Esercizi
109
U.D. 7 - Istruzioni di selezione
112 113 115 118
7 .1 7 .2 7 .3 7 .4 7 .5
Istruzione if Istruzione if..else If nidificati Istru ruzzione switch (selezione multipla) Raggruppamento dei case
120 122
Esercizi
125
U.D. 8 - Istruzioni di ripetizione
127 128 130 132 134
8 .1 8 .2 8 .3 8 .4
Istruzione while Tabulazione Istruzione do.. while Istruzione for
Esercizi
139
3
Indice
U.D. 9 - Le funzioni
9 .1 9 .2 9 .3 9 .4 9 .5 9 .6 9.7 9. 7
Le funzioni La funzione più impor tante Definizione delle funzioni Ambito delle variabili Variabili locali e globali Valori di ritorno Pass Pa ssag aggi gio o de dei para parame metr trii per valore 9.8 9. 8 Pa Pass ssag aggi gio o de dei para parame metr trii per riferimento 9.9 Ricorsione 9.10 Funzioni matematiche
1 57 161 163
Esercizi
165
Sezion Sez ione e 4 - Strutture dei dati U.D. 10 - Enumerazioni e array
10.1 10.2 10 .2 10.3 10.4 10.5 10. 5
Introduzione Tipi Ti pi enu num mer erat ativ ivii (e (en num umer eraz azio ioni ni)) Tipo vettore Vettori in C++ Carica Car icamen mento to di un vet vettor toree in memoria 10.6 Array di dimensione variabile 10.7 Matrici 10.8 10. 8 Pas Passag saggio gio di un un vett vettore ore com comee parametro a una funzione
1 53
Esercizi
222
U.D. 13 - Polimorfismo Polimorfismo ed ereditarietà ereditarietà
223 224 227 230 234 236 240
13.1 13.2 13.3 13.4 13.5 13.6
Costruttori Costruttori parametrizzati Membri static di una classe Overloading Polimor fismo Ereditarietà
216 218
167
Sezione Sezio ne 6 - Operare con gli archivi
251
168 169 169 16 9 171 172
U.D. 14 - Archivi
252 253 254 255
176 178 180 187
U.D. 11 - Stringhe e strutture
192 193 195 197 200 201 202 204
Esercizi
207
Sezione Sezio ne 5 - Classi e oggetti
209
U.D. 12 - Concetti generali
210 211 211 212
12.1 Introduzione alla OOP 12.2 Incapsulazione 12.3 Polimorfismo
213 215
249
190
Definizione di stringa Lunghezza di una stringa Concatenazione ed ed es estrazione Confronti tra stringhe Caratteri e stringhe C Dichiarazione di una struttura Metodi costruttori
12.4 Ereditarietà 12.5 Introduzione alle classi 12.6 12. 6 Term ermino inolog logia ia e rappre rapprese senta ntazio zione ne grafica 12.7 Dichiarazione degli oggetti
Esercizi
Esercizi
11.1 11.2 11.3 11.4 11.5 11.6 11.7
4
142 143 144 147 148 151 1 52
14.1 14.2 14.3 14.4 14 .4
Definizione di archivio Dati Definizione di record Oper Op eraz azio ioni ni fo fond ndam amen enta tali li sugli archivi 14.5 I/ 14.5 I/O O stan standa dard rd e su su memo memori riaa di massa 14.6 Tipi di archivio 14.7 Tipi di accesso
256 257 259 260
Esercizi
261
U.D. 15 - File di testo
15.1 Creazione di un file di testo 15.2 Lettura di un file di testo 15.3 Accodamento
262 263 266 268
Esercizi
271
Sezione Sezio ne 7 - Le eccezioni
273
U.D. 16 - Gestione delle eccezioni
16.1 Concetto di anomalia 16.2 Ec E ccezioni
274 275 277
Esercizi
283
Appendice Appen dice A - Riepilogo degli operatori operator i
285
Appendice B - Sequenze di caratteri Appendice carat teri escape
286
Indice analitico
287
Presentazione
Il presente volume espone, in modo chiaro ed efficace, le caratteristiche del linguaggio C++ e ha il duplice scopo di descriverne la sintassi e di evidenziarne le potenzialità. In particolare il libro:
si rivolge allo studente come un manuale di facile consultazione per la programmazione; presenta le basi teoriche per lo sviluppo delle applicazioni informatiche.
Il primo obiettivo si realizza tramite numerosi esempi presenti nel testo, che forniscono chiare indicazioni sulle caratteristiche sintattiche del linguaggio. Per quanto riguarda le basi teoriche sono stati messi in rilievo i fondamenti dei cinque argomenti di base per la programmazione: la rappresentazione dei dati, le strutture di controllo utilizzabili nella costruzione di un algoritmo, le strutture dei dati, la programmazione orientata agli oggetti e la gestione dei file. Il libro è suddiviso in sette sezioni. 1 La se sezione Premesse sviluppa gli argomenti della codifica binaria delle informazioni. programmazione vengono descritti i concetti di variabile, 2 In Primi elementi di programmazione vengono costante e tipi di dato, le operazioni di input/output da console e gli operatori aritmetici. algoritmi vienee intro 3 Nell llaa se sezi zio one Organizzazione degli algoritmi vien introdott dottaa la nozio nozione ne di algo algoritritmo e sono descritte le principali strutture di controllo sia in pseudocodifica sia in C++. dati vengono definite le principali strutture statiche dei 4 Nell llaa se sezi zio one Strutture dei dati vengono dati. 5 L’i ’int nter eraa sezi sezion onee Classi e oggetti è dedicata alle nozioni fondamentali della OOP e vengono presentati i concetti concetti principali della della programmazione programmazione orientata agli oggetoggetti quali l’incapsulamento, il polimorfismo e l’ereditarietà. 6 La sezione Operare con gli archivi spiega le nozioni di base per la definizione degli archivi di dati. 7 La se sezzione Le eccezioni descrive gli accorgimenti essenziali per la realizzazione di applicazioni “robuste”. Ogni sezione è suddivisa in Unità didattiche le quali contengono un numero limitato di paragrafi, la cui trattazione è, di norma, contenuta in circa due pagine. Ne risulta un testo
5
Presentazione
di facile lettura, che aiuta lo studente a concentrarsi, di volta in volta, su un singolo elemento del discorso. Tutti i concetti presentati sono accompagnati da un esempio, che mette in pratica quanto esposto. Ogni esempio contiene un listato di codice, una figura che illustra una prova di esecuzione del codice proposto e l’analisi dettagliata del codice stesso; quest’ultima parte dell’esempio presenta una descrizione dettagliata degli aspetti più significativi del linguaggio C++ presenti nell’esempio. Per ogni sezione sono indicati gli obiettivi generali che si vogliono raggiungere, mentre nella prima pagina di ogni Unità didattica è specificato per lo studente “Che cosa imparerai a fare” e “Che cosa dovrai studiare”. In concreto, gli obiettivi generali presentati all’inizio di ogni modulo descrivono le capacità che lo studente deve acquisire. Le voci “Che cosa imparerai a fare” e “Che cosa dovrai studiare” indicano rispettivamente le competenze e le conoscenze che devono essere apprese dall’alunno. Considerando l’ampiezza della trattazione, il libro include tutti i contenuti dei programmi didattici tradizionalmente affrontati nelle classi terze degli istituti tecnici. In particolare può essere adottato nella classe terza degli Istituti Tecnici Industriali con indirizzo ABACUS o informatica industriale, in quella degli Istituti Tecnici Commerciali con indirizzo MERCURIO o programmatori, nonché in quella dei Licei scientifici tecnologici. L’Autore
6
Sezione
1
Premesse
†
Obiettivi
◊ Nozioni fondamentali sui sistemi di numerazione
◊ Sistemi di numerazione in base diversa da 10 ◊ Introduzione alla codifica delle informazioni
&
Questa sezione contiene
U.D. 1
Sistemi di numerazione
U.D. 2
Codifica delle informazioni
Unità didattica
1
Sistemi di numerazione
CHE COSA IMPARERAI A FARE $
Rappresentare un numero con notazione polinomiale
$
Utilizzare la numerazione in base 2
$
Trasformare un numero da base 10 a base 2 e viceversa
$
Utilizzare la numerazione esadecimale
$
Trasformare un numero da base 10 a base 16 e viceversa
$
Trasformare un numero da base 2 a base 16 e viceversa
$
Eseguire le operazioni aritmetiche con basi diverse da 10
CHE COSA DOVRAI STUDIARE $
Notazione polinomiale dei numeri
$
Cifre del sistema binario e del sistema esadecimale
$
Procedure di trasformazione da base 10 a base diversa da 10
$
Metodologia di calcolo aritmetico con basi diverse da 10
1
Unità didattica Sistemi di numerazione
1.1 Sistema di numerazione decimale Il sistema di numerazione di più largo uso nel mondo è il sistema di numerazione decimale o a base 10. Esso si avvale di dieci simboli o cifre che, come è noto, sono: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 stabilendo che tali simboli rappresentino, rispettivamente, i primi dieci numeri naturali (lo zero compreso), cioè i numeri i cui nomi sono: zero, uno, due, tre, quattro, cinque, sei, sette, otto, nove. Il successivo di nove si chiama dieci. Si dice che dieci è la base del sistema di numerazione decimale.
Da quanto convenuto, segue che ogni numero contiene un certo numero di unità semplici, un certo numero di decine, un certo numero di centinaia ecc., pertanto ogni numero naturale può essere scritto come una somma i cui termini sono prodotti di una potenza di dieci per una opportuna cifra. Esempio..............................................................................................................................
Indicando, per ora, con la lettera d il numero dieci, si ha: novemilatrecentoquarantasette = 9 migliaia + 3 centinaia + 4 decine + 7 unità = 9 × d3 + 3 × d2 + 4 × d1 + 7 × d0
La scrittura di queste cifre, una dopo l’altra, costituisce la scrittura posizionale, in base 10, del numero dato. In tal modo, il numero “novemilatrecentoquarantasette” si scrive: 9347 ...........................................................................................................................................
1.2 Notazione polinomiale Il numero dieci si indica con il simbolo 10 (si legga: “uno, zero”). Il numero novemilatrecentoquarantasette dell’esempio precedente, si scrive: 9347 = 9 × 103 + 3 × 102 + 4 × 101 + 7 × 100 che è un polinomio ordinato secondo le potenze di 10. In generale, possiamo dire che un numero naturale viene così scritto: anan lan–2 ... a2ala0 = an × l0n + an–l × l0n −
−
l
+
... + a2 × l02 + al × l0 + a0
dove i simboli an sono le cifre che compongono il numero. 9
Sezione 1 - Premesse
Ogni cifra di un numero naturale ha un valore intrinseco, che è quello che ha in quanto numero, e un valore che dipende dalla posizione che occupa nel numero. Per esempio, nel numero: 444 i tre “4” hanno lo stesso valore intrinseco come cifre singole, ma valori diversi in base alla posizione che occupano nel numero: infatti, il primo “4” da destra rappresenta quattro unità semplici, il secondo rappresenta quattro decine e il terzo quattro centinaia. Le cifre, quindi, acquistano maggior significato da destra a sinistra. Per questo motivo la prima cifra a destra è la “meno significativa” e la prima cifra a sinistra è la “più significativa”.
1.3
Numeri decimali
Oltre ai numeri naturali, anche ai numeri frazionari si può dare la rappresentazione di un polinomio ordinato secondo le potenze di 10, come rappresentato di seguito. 0,1 = 10–1 0,01 = 10–2 ……………… ……………… 0,00…1 = 10–n {
n cifre decimali
L’espressione polinomiale di un numero frazionario è, quindi: N = an × 10n + an
−
1
×
10n
−
1
+
... + a1 × 10 + a0 × 100 + a
−
1
×
10
−
1
+
a
2
−
×
10
−
2
+
…
e la scrittura simbolica è: N = anan 1 … a1a0a 1a 2 … −
−
−
Invece del segno “,” (virgola) usato comunemente per separare le cifre della parte intera da quelle della parte non intera, si userà (da qui in avanti) il segno “.”, detto punto radice o punto decimale . Esempio..............................................................................................................................
Il numero N=7
×
103 + 3 × 10 + 2
×
10−1 + 3 × 10−3
scritto nella forma simbolica è: N = 7030.203 ...........................................................................................................................................
La scelta della base per la scrittura dei numeri è determinata soltanto da ragioni di comodo; un numero può essere scritto in una base qualsiasi senza che il suo intrinseco valore cambi perché tale valore è qualcosa che esiste realmente, indipendentemente dal sistema numerico usato. Per esempio, un sacchetto di caramelle ne contiene una certa quantità e questa quantità è sempre la stessa, qualunque sia il sistema numerico usato per esprimerla. Più in generale, possiamo dire che in un sistema numerico posizionale in base b (con b > 1) un numero naturale può essere così rappresentato: Nb = an × bn + an
−
1
×
bn
−
1
+
... + a1 × b + a0 × b0
Conveniamo di indicare sempre la base, se questa non è 10. Quindi i simboli N2, N5, N rappresentano numeri in base 2, 5, 10, rispettivamente. 10
Unità didattica 1 - Sistemi di numerazione
1.4
Sistema di numerazione binario
Molto interesse riveste, nell’aritmetica dei calcolatori, il sistema a base due, o sistema binario, le cui cifre sono: A = {0, 1} I numeri 110, 100 possono essere considerati numeri binari. Per distinguerli da quelli decimali è bene contrassegnarli con un piccolo 2 in basso a destra; perciò i due numeri scritti precedentemente vengono così indicati: 1102, 1002 e si leggono rispettivamente: “uno, uno, zero”; “uno, zero, zero”.
Un sistema di numerazione si dice binario quando la base è due. Esso usa solo i due simboli, 0 e 1 per rappresentare tutti i numeri. I simboli 0, 1 prendono il nome di bit (contrazione del termine inglese binary-digit , la cui traduzione è cifra binaria ). Il sistema binario è il più semplice sistema di numerazione che si possa considerare, perché è quello che richiede il minor numero di simboli per esprimere graficamente tutti i numeri. Anche in questo sistema il valore delle cifre dipende dalla posizione da esse occupata rispetto al punto radice. Spostando una cifra di una posizione verso sinistra, si moltiplica il suo valore per 2, mentre spostandola verso destra si divide il suo valore per 2. Quindi, anche i numeri binari si possono scrivere nella forma polinomiale: N2 = an × 2n + an
−
1
×
2n
−
1
+
... + a1 × 2 + a0 × 20 + a
−
1
×
2
−
1
+
a
2
−
×
2
−
2
+
...
e la scrittura simbolica di N2 è: N2 = anan 1 ... a1a0a 1a 2 ... −
−
−
dove i coefficienti ak possono assumere solo i valori 0 oppure 1. Per esempio, il numero binario: 11002 = 1 × 23 + 1 × 22 + 0 × 21 + 0 × 1 = 1210 ha le cifre ak che sono rispettivamente 1, 1, 0, 0. I numeri 0, 1 del sistema decimale corrispondono rispettivamente alle cifre 0 e 1 del sistema binario. Il numero 2 non ha un simbolo che lo rappresenti, nella forma binaria: in questo caso, poiché due unità del primo ordine formano una unità del secondo ordine e zero del primo, si ha: SISTEMA DECIMALE
SISTEMA BINARIO
0
=
02
1
=
12
2
=1+1=
102
3
= 2 + 1 =102 + 12 =
112
4
=
1002
5
=
1012
6
=
1102
7
=
1112
8
=
10002
9
=
10012
11
Sezione 1 - Premesse
1.5
Conversione da base 2 a base 10
Per convertire un numero dalla base 2 alla base 10 si calcola il valore del polinomio ordinato secondo le potenze del 2. Esempi ................................................................................................................................ 1. 110111 2 = 1 × 25 + 1 × 24 + 0 × 23 + 1 × 22 + 1 × 2 + 1 = 55
per cui 110111 scritto in base 2 equivale a 55 scritto in base 10. 2. 101.11 2 = 1 × 22 + 0 × 2 + 1 + 1 × 2–1 + 1 × 2–2 = 4 + 1 + 1/2 + 1/4 = 5.75 per cui 101.11 scritto in base 2 equivale a 5.75 scritto in base 10. ...........................................................................................................................................
La conversione dalla base 10 alla base 2 si effettua nel seguente modo: se il numero è naturale, si divide il numero per 2 e il resto rappresenta la cifra meno significativa in binario; si divide poi per 2 il quoziente ottenuto e il resto rappresenta la seconda cifra da destra e così si procede fino ad avere quoziente zero. L’ultimo resto ottenuto è la cifra più significativa del numero in scrittura binaria. Esempio.............................................................................................................................. Convertire il numero 35 in binario.
35 : 2 = 17 17 : 2 = 8 8:2= 4 4:2= 2 2:2= 1 1:2= 0
resto 1 resto 1 resto 0 resto 0 resto 0 resto 1
1 1 0 0 0 1
Cifre binarie:
1 0 0 0
1
1
Pertanto il numero 35, in base 2 è 100011. ...........................................................................................................................................
Se il numero è frazionario, per la parte intera si procede come indicato nell’esempio precedente, mentre le cifre della parte non intera si ottengono nel modo seguente: 1. 2. 3. 4.
si moltiplica per 2 la parte frazionaria in base 10; se il numero ottenuto è maggiore o uguale a 1, si sottrae 1 al risultato e si riporta 1 nelle cifre in base 2; in caso contrario, si riporta 0 nelle cifre in base 2; si ripetono le operazioni precedenti fino a ottenere un prodotto uguale a zero.
Esempio.............................................................................................................................. Convertire in base 2 il numero 0,625.
0,625 × 2 = 1,25 0,25 × 2 = 0,5 0,5 × 2 = 1 0 stop 12
tolgo 1 tolgo 1
Cifre binarie 1 0 1
Unità didattica 1 - Sistemi di numerazione
Risulta quindi: 0,62510 = 0,1012
Controprova: 0,1012 = 2−1 + 0 + 2−3 = 1/2 + 1/8 = 5/8 = 0,625 10
...........................................................................................................................................
1.6
Somma e sottrazione nel sistema binario
Le operazioni aritmetiche in base 2 (o in qualsiasi altra base) vengono eseguite seguendo i criteri di calcolo già noti per il sistema di numerazione in base 10. Vediamole singolarmente. Somma
Per la somma tra due numeri, vale la regola del riporto: per ogni cifra si esegue la somma e, se si raggiunge o si supera la base, si addiziona il riporto alle cifre dell’unità superiore. In pratica, per la somma di due cifre in base 2 vale la seguente tabella. SOMMA
0+0=0 0+1=1+0 1 + 1 = 10
RISULTATO
0 1 0 con riporto di 1
Una volta definita la somma di due numeri di una sola cifra, è facile eseguire l’addizione di due numeri binari, tenendo conto della regola del riporto.
Esempio.............................................................................................................................. Eseguire la somma: 110100 2 + 111012 Base 2 Riporti
Base 10
1 1 1 1
1
110100 + 11101 = 1010001
52 + 29 = 81
........................................................................................................................................... Per addizionare due o più numeri binari si addizionano i primi, al risultato si aggiunge il ter zo e così via. Così facendo, si evitano le complicazioni dei ripor ti. Sottrazione
La sottrazione può essere eseguita con la regola del prestito.
Esempio.............................................................................................................................. Eseguire la sottrazione 110101 2 – 101102 Base 2 Prestiti
Base 10
1 1 1 1
110100 – 10110 = 11111
53 – 22 = 31
........................................................................................................................................... 13
Sezione 1 - Premesse
1.7
Sistema di numerazione esadecimale
Fino ad ora sono stati presentati i sistemi di numerazione con base 10 (sistema decimale) o con base minore di 10, come il sistema binario. Ricordiamo che quest’ultimo è il più semplice dei sistemi usati. Si possono definire anche sistemi di numerazione con base maggiore di 10, come per esempio il sistema esadecimale, o con base 16. Questo sistema usa sedici simboli, sei in più di quelli del sistema decimale: per la rappresentazione di un valore si possono utilizzare, oltre alle dieci cifre del sistema decimale, altri sei simboli, nella fattispecie le prime lettere dell’alfabeto latino maiuscolo: A, B, C, D, E, F. Quindi i sedici simboli della numerazione esadecimale sono: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F I primi dieci simboli coincidono con quelli del sistema decimale, mentre per gli altri sei si ha la seguente corrispondenza: SISTEMA ESADECIMALE
SISTEMA DECIMALE
A16 B16 C16 D16 E16 F 16
10 11 12 13 14 15
Se si vuole rappresentare un numero maggiore o uguale a 16 in esadecimale, si ricorre alla rappresentazione posizionale: 16 = 1016, 17 = 1116, 18 = 1216 ... 31 = 1F16, 32 = 2016 ecc. ... La rappresentazione polinomiale secondo le potenze di 16 di un numero esadecimale è: N16 = an × 16n + an
−
1
×
16n
−
1
+ . . . + a1 × 1 6 + a0 × 160 + a
−
1
×
16
−
1
+a
−
2
×
16
−
2
+ ...
dove i coefficienti an possono assumere i valori: 0, 1, 2, ..., 8, 9, A, B, ..., F. La scrittura simbolica di N16 è: N = anan 1 … a1a0a 1a 2 … −
−
−
Conversione da base 16 a base 10 e viceversa
Se un numero è in forma esadecimale, per trovare la sua notazione decimale è sufficiente scrivere il numero in forma polinomiale. Esempio.............................................................................................................................. Trasformare in base 10 il numero N 16 = 2A.1116 2A.1116 = 2 × 161 + A × 160 + 1 × 16−1 + 1 × 16−2 = = 2 × 16 + 10 × 1 + 1 × 1/16 + 1 × 1/256 = = 32 + 10 + 0.0625 + 0.0039062 = 42.0664062 ...........................................................................................................................................
Viceversa, per scrivere un numero decimale in base 16 si distinguono due casi. 1.
14
N è un numero intero. La sua forma esadecimale si ottiene con il metodo delle divisioni successive per 16.
Unità didattica 1 - Sistemi di numerazione
Esempio.............................................................................................................................. Trasformare il numero 1970 in esadecimale. 1970 : 16 = 123 resto 2 123 : 16 = 7 resto 11 = B 7 : 16 = 0 resto 7 Cifre esadecimali:
7
B
2
Pertanto 1970 = 7B216. ........................................................................................................................................... 2.
N è un numero frazionario. In tal caso, procedendo come nella numerazione binaria, si converte la parte intera con il metodo appena visto e, per la parte non intera, si eseguono moltiplicazioni successive per 16: si toglie di volta in volta la parte eccedente l’unità e la si trascrive come cifra della parte frazionaria.
Esempio.............................................................................................................................. Sia N = 18.6206 Cifre esadecimali 9
0.6206
×
16 =
9.9296
tolgo
9
0.9296
×
16 = 14.8736
tolgo
14
E (= 14 10)
0.8736
×
16 = 13.9776
tolgo
13
D (= 13 10)
0.9776
×
16 = 15.6416
tolgo
15
F (= 15 10)
ecc.
Quindi 0.6206 = 0.9EDF... 16 Il calcolo è approssimato alla quarta cifra decimale. ........................................................................................................................................... Addizione
L’addizione tra due numeri esadecimali viene eseguita tenendo conto della regola del riporto, così come in base 10 e in base 2. Quando si eseguono i calcoli in base 16, occorre ricordare che il riporto de ve essere calcolato quando si raggiunge o si supera 16, e non 10 (come ci suggerisce l’abitudine).
Esempio.............................................................................................................................. Eseguire la somma 2A1C 16 + 9E316 Base 16 Riporti
1
2A1C + 9E3 = 33FF
Le somme eseguite, a partire da destra, sono, in base 16: C + 3 = F; E + 1 = F; A + 9 = 13 (scrivo 3, riporto 1); 2 + 1 = 3 ...........................................................................................................................................
15
Sezione 1 - Premesse
Sottrazione
Come nel sistema binario, anche in quello esadecimale la sottrazione si esegue con la regola del prestito. Esempio..............................................................................................................................
Eseguire la sottrazione 3C2E 16 – 4A516 Base 16 Prestito
1
3C2E – 4A5 = 3789
Le operazioni eseguite, procedendo da destra verso sinistra, sono, in base 16: E – 5 = 9; 2 – 9 = 8 (con il prestito di 1 da C); B – 4 = 7; 3 – 0 = 3 ...........................................................................................................................................
1.8
Conversione da binario a esadecimale e viceversa
La numerazione binaria, a causa del numero elevato delle cifre che in generale costituiscono N2 e a causa del monotono ripetersi di 0 e 1, è poco intelligibile all’uomo e può portare facilmente a errori. Per ov viare a questi inconvenienti si può ricorrere al sistema esadecimale, perché la scrittura di un numero in tale sistema è più compatta e risulta facile la conversione di N16 verso il sistema binario e viceversa. Osservando la tabella seguente. DECIMALE
ESADECIMALE
BINARIO
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0 1 2 3 4 5 6 7 8 9 A B C D E F
0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
si vede che a ogni cifra esadecimale corrisponde una quadrupla di cifre binarie. Ne consegue quanto riportato nella regola che segue. Per passare da un numero in forma binaria, intero o non intero, alla corrispondente forma esadecimale si suddivide il numero binario in gruppi di quattro bit, dal punto decimale verso sinistra e dal punto radice verso destra, completando se necessario l’ultimo gruppo di sinistra e l’ultimo di destra con degli zeri.Quindi, si sostituisce a ogni gruppo di 4 bit la corrispondente cifra esadecimale, secondo quanto indicato dalla tabella riportata sopra.
16
Unità didattica 1 - Sistemi di numerazione
Esempio.............................................................................................................................. N2 = 101101110.10111 = 0001 0110 1110 . 1011 1000 {
1
{
6
{
E
{
B
{
8
Quindi 101101110.10111 2 = 16E.B816
........................................................................................................................................... Vale anche il viceversa.
Dato un numero in forma esadecimale, per ottenere la sua forma binaria si sostituisce ogni cifra della forma esadecimale, con il corrispondente gruppo di quattro cifre binarie determinato sulla base della tabella riportata sopra.
Esempio.............................................................................................................................. N16 = A3516 = 1010 0011 0101 = 101000110101 2 {
A
{
3
{
5
........................................................................................................................................... I numeri esadecimali vengono usati in informatica per rappresentare i dati memorizzati in un elaboratore e per correggere er rori, ma raramente sono usati per l’input e l’output di dati. I numeri esadecimali hanno il vantaggio che con due cifre rappresentano otto bit e, come vedremo più avanti, possono esprimere in forma sintetica tutti i caratteri comunemente usati per trasmettere le informazioni.
17
Sezione 1 - Premesse
Esercizi Unità didattica 1 Per ognuno dei seguenti numeri, indicare il valore posizionale delle cifre 3, 7, 4.
37584 4735 78043
Scrivere i seguenti numeri decimali nella rappresentazione polinominale secondo le potenze di 10.
7039 180041 731.03
Quale numero indica la rappresentazione polinominale: 5
×
102 + 3 × 10? In quale sistema di numera-
zione? Quali delle seguenti uguaglianze sono vere e quali false?
807 = 8 × 10 + 1 × 10 + 7 807 = 8 × 102 + 7 × 10 807 = 8 × 102 + 7
Scrivere tutti i numeri binari formati da:
due cifre tre cifre quattro cifre
Convertire il numero binario 11101 nel corrispondente numero decimale. Convertire il numero decimale 827 nel corrispondente numero binario. È vera l’eguaglianza: 179 = 10110011 2? Eseguire le seguenti operazioni nel sistema binario.
11101 × 101 11101 : 101
Eseguire le seguenti operazioni nel sistema binario.
1100111011 + 110101 1100111011 – 110101 110101 × 1011 110101 : 1011
Verificare che i tre numeri binari 1001, 111, 1101 abbiano per somma 11101. Verificare nel sistema binario l’eguaglianza: ((11 + 10 + 101)
×
11 – 101) /101 = 101.
Quale dei seguenti tre numeri esadecimali rappresenta il polinomio A
18
16A AB5 AOB5
×
162 + B × 16 + 5?
Unità didattica 1 - Sistemi di numerazione
Esercizi Unità didattica 1 Calcolare il numero in base 10 cor rispondente a A25 16. Convertire il numero decimale 179 nel corrispondente numero esadecimale. Convertire il numero esadecimale 33B 16 nel corrispondente numero decimale. Eseguire le seguenti operazioni nel sistema esadecimale. 4A7 + BC5 ABC – 2A 46B + 2A
Verificare che 36A16 sia la somma dei numeri 335 16 e 3516. Convertire i seguenti numeri binari in esadecimali e calcolare il loro valore in decimale. 110100101 1110011101 1000110110
Verificare che il numero decimale 53 sia rappresentato da 110101 nella scrittura binaria e 35 nella scrittura esadecimale.
Convertire direttamente i seguenti numeri binari in numeri esadecimali.
101111 11101 1011.1 11.101
Convertire direttamente i seguenti numeri esadecimali in numeri binari. A45 5C 63D
Indicare quali dei seguenti numeri sono pari e quali dispari.
1102 11002 100112 101012
19
Unità didattica
2
Codifica delle informazioni CHE COSA IMPARERAI A FARE $ Applicare
un codice univoco a un insieme di oggetti
$
Codificare i caratteri che compongono le informazioni
$
Utilizzare il criterio di rappresentazione dei numeri interi all’interno di un elaboratore
$
Eseguire sottrazioni con il metodo del complemento a 2
$
Rappresentare i numeri reali in notazione esponenziale
CHE COSA DOVRAI STUDIARE $
Caratteristiche dei codici ASCII e UNICODE
$
Procedura per la rappresentazione binaria dei numeri interi
$
Definizione del complemento a 2
$
Notazione in virgola mobile
$
Mantissa e caratteristica di un numero reale
2
Unità didattica Codifica delle informazioni
2.1
Introduzione
Nel linguaggio naturale, le informazioni possono essere:
numeriche, se rappresentate da numeri; alfabetiche, se rappresentate da lettere; alfanumeriche, se rappresentate da numeri, lettere o caratteri speciali.
Di seguito si riportano alcuni esempi:
informazioni numeriche: informazioni alfabetiche: informazioni alfanumeriche:
375; 10111; –32.46; PREZZO; ALTEZZA; libro; MN32756; INT2; COLOR5.
Le informazioni devono essere elaborate e, a seconda del tipo di elaborazione cui vanno soggette e dei mezzi usati per tale trattamento, si presenta la necessità di dar loro una rappresentazione simbolica idonea, che si chiama codifica, mentre il sistema dei simboli usati si chiama codice. Si possono citare, come esempi di codifica, la classificazione dei libri di una biblioteca, l’attribuzione di un codice alla merce in vendita in un magazzino, l’immatricolazione di autovetture, ecc. Quando le informazioni sono trattate in modo automatico, è necessario che la loro rappresentazione simbolica sia intelligibile alla macchina. Per esempio, dovendo trattare informazioni numeriche con macchine aventi dispositivi di calcolo adatti al sistema decimale, come è il caso delle macchine calcolatrici e di quelle contabili, le informazioni devono essere presentate in forma numerica decimale e, quindi, il codice usato è il codice numerico decimale. Quando le informazioni devono essere trattate dai calcolatori elettronici, poiché i congegni che li compongono possono esistere solo sotto due stati (passaggio o interruzione di corrente, oppure magnetizzazione positiva o negativa, tensione positiva o negativa), è evidente che le informazioni de vono essere rappresentate da un linguaggio binario il cui alfabeto è riportato di seguito. A = {0, 1} Sappiamo che le cifre binarie sono gli elementi fondamentali della tecnica dei calcolatori elettronici e prendono il nome di bit. Ogni “carattere” e ogni “parola” devono essere ridotti a una successione di bit, cioè di zero e uno, perché il calcolatore possa riconoscerli, elaborarli e fornire delle risposte. I bit 0 e 1, presi due a due, possono dare quattro diverse configurazioni: 00; 01; 10; 11; quindi tante quante sono le disposizioni con ripetizione di due oggetti presi due a due, ovvero 22 = 4. Se i simboli binari sono presi quattro a quattro si ottengono 24 = 16 disposizioni con ripetizione e, quindi, 16 configurazioni diverse.
In generale, i due bit 0 e 1, presi n ad n, danno 2n configurazioni diverse.
21
Sezione 1 - Premesse
Per costruire un codice che rappresenti in forma binaria ogni carattere del linguaggio naturale, è necessario stabilire una corrispondenza biunivoca tra ciascun carattere e una particolare configurazione binaria e, per convenzione, stabilire che tale configurazione rappresenti sempre quel carattere. Si definisce codice del calcolatore l’insieme delle configurazioni binarie usate dalla macchina per rappresentare i caratteri. L’insieme dei caratteri che possono essere codificati è chiamato repertorio o set di caratteri del calcolatore.
I codici adottati per la rappresentazione in forma binaria delle informazioni numeriche e alfanumeriche possono variare in funzione delle tecniche costruttive del calcolatore. Quindi ogni tipo di calcolatore ha un proprio codice, detto codice macchina. Nei paragrafi che seguono verranno illustrati brevemente alcuni di tali codici. È bene, tuttavia, precisare che la codifica è un problema interno del calcolatore, che non riguarda direttamente l’operatore umano. I caratteri, sia numerici sia letterali, sono immessi ed estratti nella loro forma consueta.
2.2 Rappresentazione dei caratteri Per la codifica dei caratteri numerici, alfabetici e speciali (per esempio, i segni di interpunzione) si sono costruiti vari codici: tra tutti, presentiamo quelli di uso più comune. Codice ASCII
Ogni carattere viene rappresentato con otto bit e, quindi, questo codice può rappresentare 2 8 = 256 caratteri. Un gruppo di 8 bit prende il nome di byte.
Con un byte si possono rappresentare:
una lettera dell’alfabeto; una cifra numerica; un segno speciale.
Per rappresentare cifre, lettere e altri caratteri, come i segni di interpunzione, viene usato un codice a 7 posizioni: il codice ASCII ( American Standard Code for Information Interchange ).
22
Numero d’ordine
Codice ASCII
Binario
Esadecimale
0
nul
0000000
00
1
soh
0000001
2
stx
3
Numero d’ordine
Codice ASCII
Binario
Esadecimale
64
@
1000000
40
01
65
A
1000001
41
0000010
02
66
B
1000010
42
etx
0000011
03
67
C
1000011
43
4
eot
0000100
04
68
D
1000100
44
5
enq
0000101
05
69
E
1000101
45
6
ack
0000110
06
70
F
1000110
46
7
bel
0000111
07
71
G
1000111
47
8
bs
0001000
08
72
H
1001000
48
9
ht
0001001
09
73
I
1001001
49
10
lf
0001010
0A
74
J
1001010
4A
Unità didattica 2 - Codifica delle informazioni
Numer o d’ordine
Codi ce ASCII
Binario
11 12 13 14 15
vt ff cr so si
0001011 0001100 0001101 0001110 0001111
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
dle dcl dc2 dc3 dc4 nak syn etb can em sub esc fs gs rs us spazio ! “ # $ % & ‘ ( ) * + ‘ . / 0 1 2 3 4 5
0010000 0010001 0010010 0010011 0010100 0010101 0010110 0010111 0011000 0011001 0011010 0011011 0011100 0011101 0011110 0011111 0100000 0100001 0100010 0100011 0100100 0100101 0100110 0100111 0101000 0101001 0101010 0101011 0101100 0101101 0101110 0101111 0110000 0110001 0110010 0110011 0110100 0110101
Esadecimale
Nume ro d’ordine
Codice ASCII
Bi nario
E sadeci male
0B 0C 0D 0E 0F
75 76 77 78 79
K L M N O
1001011 1001100 1001101 1001110 1001111
4B 4C 4D 4E 4F
10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
P Q R S T U V W X Y Z [ \ ] ^ _ ‘ a b c d e f g h i j k l m n o p q r s t u
1010000 1010001 1010010 1010011 1010100 1010101 1010110 1010111 1011000 1011001 1011010 1011011 1011100 1011101 1011110 1011111 1100000 1100001 1100010 1100011 1100100 1100101 1100110 1100111 1101000 1101001 1101010 1101011 1101100 1101101 1101110 1101111 1110000 1110001 1110010 1110011 1110100 1110101
50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75
23
Sezione 1 - Premesse
Numero d’ordine
Codice ASCII
Binario
Esadecimale
54
6
0110110
36
55
7
0110111
56
8
57
Codice ASCII
Binario
Esadecimale
118
v
1110110
76
37
119
w
1110111
77
0111000
38
120
x
1111000
78
9
0111001
39
121
y
1111001
79
58
:
0111010
3A
122
z
1111010
7A
59
;
0111011
3B
123
{
1111011
7B
60
<
0111100
3C
124
|
1111100
7C
61
=
0111101
3D
125
}
1111101
7D
62
>
0111110
3E
126
~
1111110
7E
63
?
0111111
3F
1111111
7F
Numero d’ordine
127
I caratteri stampabili, quelli compresi dal 32 al 126, rappresentano tutti i simboli di cui un computer può aver bisogno per le proprie elaborazioni: lettere, segni d’interpunzione, simboli matematici, caratteri numerici e così via. I caratteri di controllo (quelli da 0 a 31 e il 127) sono caratteri non stampabili, che vengono impiegati per segnalare condizioni particolari, per esempio eot per individuare la fine di un testo o ack per indicare il riconoscimento di un segnale durante una trasmissione dati. I numeri che figurano nella tabella (i codici dal 48 al 57) non vanno confusi con i valori numerici propriamente detti, che abbiamo studiato in precedenza: in questo caso si tratta semplicemente dei simboli che servono a rappresentare le cifre. La codifica ASCII è realizzata con 7 bit, ma ciascun carattere occupa, in ogni caso, un byte (8 bit).
Per allargare l’insieme dei caratteri rappresentabili, includendo anche le vocali accentate e i segni diacritici tipici delle lingue latine, ai 128 simboli del codice ASCII a 7 bit sono stati aggiunti altri 128 caratteri. Il codice ASCII a 8 bit che permette di rappresentare 256 caratteri prende il nome di Codice ASCII esteso . Codice UNICODE
A differenza delle lingue anglosassoni, come inglese e tedesco, e neolatine, come italiano, francese e spagnolo, esistono molti linguaggi che non usano l’alfabeto latino: greco e cirill ico ne sono un tipico esempio. Per la loro rappresentazione si ricorre a speciali codici a 8 bit, che descrivono insiemi specifici di caratteri. Il computer di un utente russo, per esempio, deve poter rappresentare un insieme di caratteri ben diverso da quello di un utente italiano. Oltre ai vari tipi di codici a 8 bit esistono anche codici a 16 bit, che vengono utilizzati per linguaggi ancora più complessi come quelli dei paesi asiatici. Vista la varietà delle codifiche, si è cercato uno standard di rappresentazione che comprenda universalmente tutti i caratteri possibili: nasce con questo scopo il codice UNICODE, che con i suoi 2 byte di rappresentazione consente di descrivere ben 65.536 caratteri diversi.
2.3 Rappresentazione interna dei numeri interi La rappresentazione dei numeri, cifra per cifra, mediante i sistemi di codifica p resentati finora non è la più conveniente, poiché non tiene affatto conto della natura dei numeri (naturali, interi, razionali ecc.) e non permette di eseguire le operazioni aritmetiche. È, quindi, necessario avvalersi di altri sistemi di codifica sia per rappr esentare i numeri interi (negati vi e positivi) sia per rappresentare i numeri reali, quando su di essi si devono eseguire calcoli. Il calcolatore considera i numeri naturali come numeri interi positivi. 24
Unità didattica 2 - Codifica delle informazioni
Le informazioni vengono memorizzate all’interno del calcolatore secondo configurazioni binarie, ognuna delle quali è formata dallo stesso numero di bit ed è detta parola. La lunghezza della parola dipende dal codice interno del calcolatore e può essere di 8, 16, 32, 64 bit. Naturalmente, un numero intero non può essere rappresentato da un numero di bit maggiore di quello della parola del calcolatore, pertanto l’insieme degli interi memorizzabili all’interno della macchina è un sottoinsieme dell’insieme degli interi. Se n è il numero di bit contenuti in una parola di un calcolatore, tenuto conto che il primo bit a sinistra rappresenta il segno del numero, i numeri interi rappresentabili sono compresi tra: −
(2n
−
1
−
1)
e
2n
−
1
−
1
Per esempio, con parole di 8 bit si possono rappresentare gli interi compresi tra: 27 − 1
−
e
27 − 1
Il fatto di operare su un insieme limitato degli interi comporta notevoli conseguenze nell’aritmetica dei calcolatori. Infatti è indispensabile che i risultati delle operazioni, in fase di esecuzione, appartengano all’intervallo −2n − 1 e 2n − 1, altrimenti si determina una situazione di traboccamento o, in inglese, di overflow . In questo caso la macchina, in generale, opera un troncamento sul risultato oppure interrompe l’esecuzione, segnalando con un messaggio l’errore. Per esempio, nel caso di n = 8, la semplice espressione: (90 + 40) + (−20) risulta uguale a: 130 + (−20) e, poiché 130 > 27, si dice che “si va in overflow”. Anche se i calcolatori moderni permettono di operare su un sottoinsieme molto ampio degli interi, per cui l’overflow si presenta raramente, è buona norma di sicurezza prevedere questi casi di traboccamento, soprattutto per quelle operazioni come la moltiplicazione e la potenza, che portano con più velocità di altre operazioni a valori molto elevati o molto piccoli. Naturalmente, i limiti entro cui si opera dipendono, come già detto, dal tipo di calcolatore usato.
Rappresentazione in complemento a 2 Viene ora presentato il metodo universalmente riconosciuto come il più valido per la rappresentazione dei numeri interi relativi. La sua conoscenza permette di spiegare alcuni casi di errore di calcolo imprevisti, dovuti alla mancata segnalazione degli eventi di overflow da parte dell’elaboratore. La rappresentazione in complemento a 2 di un numero intero x, per parole di n bit è così definita: se il numero x è positivo, allora il primo bit a sinistra è impostato a zero e gli altri n – 1 bit disponibili sono utilizzati per la rappresentazione binaria del numero x dato; se il numero x è negativo, il primo bit a sinistra è 1 mentre il modulo è la rappresentazione binaria di: 2n–1 – |x| L’espressione precedente prende il nome di complemento a 2 di x.
25
Sezione 1 - Premesse
Esempio..............................................................................................................................
Per la rappresentazione di x = –12 con parole di 8 bit, il modulo da scrivere in binario è: 27 − 12 = 128 − 12 = 116 = 1110 100 2 quindi, la rappresentazione su 8 bit di x = −12 è: 1 1 1 1 0 1 0 0 ⇑ bit del segno ........................................................................................................................................... Con la rappresentazione in complemento a 2: lo zero ha la sola rappresentazione 000 ... 00 (n zeri), quindi la sua codifica è univoca; il maggior numero rappresentabile, con parole di n bit, è 2 n–1 – 1; i numeri negativi rappresentabili sono compresi tra –2 n–1 e –1, le cui rappresentazioni in
complemento a 2 sono rispettivamente: 100 ... 00
e
111 ... 11
In base a quanto detto, usando parole di n bit sono rappresentabili in complemento a due tutti i numeri compresi nell’intervallo: −2n−1 ; 2n−1 − 1 Se nella rappresentazione di un numero negativo cambiamo gli 1 in 0 e gli 0 in 1, si ottiene il valore assoluto del numero diminuito di 1.
Esempio..............................................................................................................................
Dall’esempio precedente si ha che la rappresentazione di –12 è 11110100; cambiando i bit, come detto, si ha 00001011, che rappresenta il numero decimale 11, che è il valore assoluto di –12 diminuito di 1. ...........................................................................................................................................
Tenendo conto di questa considerazione, è possibile ricorrere alla procedura seguente per calcolare in modo rapido la rappresentazione in complemento a 2 di un numero negativo.
Per rappresentare in complemento a 2 un numero negativo x: 1. 2. 3.
si trova la rappresentazione in valore assoluto e segno dell’opposto di x (cioè –x); si cambiano gli 0 in 1 e gli 1 in 0; si somma 1 al risultato.
Esempio..............................................................................................................................
Rappresentare in complemento a 2 il numero x = –18, su otto bit. +18 è uguale a 0 0 0 1 0 0 Scambio gli 0 con 1 e gli 1 con 0 1 1 1 0 1 1 Aggiungo 1 –18 è uguale a 1 1 1 0 1 1
⇓
1 0 + 1
0 1 1 0
bit del segno 27 – 18 = 110 ........................................................................................................................................... 26
Unità didattica 2 - Codifica delle informazioni
Quando si utilizza il criterio del complemento a 2 per rappresentare i numeri negativi, è indispensabile specificare con chiarezza il numero di bit disponibile per le operazioni aritmetiche di somma o sottrazione. All’interno di un elaboratore, tale numero è definito dalla lunghezza massima della parola (ovvero del numero di byte) con cui l’elaboratore opera. Il numero di bit utilizzati permette di conoscere la posizione del segno e di controllare eventuali over flow.
Esempio.............................................................................................................................. Supponendo di operare con parole di un byte (cioè con 8 bit), eseguire le seguenti operazioni: a) 15 − 12; b) −15 − 12; c) 100 + 30. a) a)
1 5 – 1 2 1 5 = 0 0 0 0 1 1 1 12 1 2 = 0 0 0 0 1 1 0 02
Gli zeri sulla sinistra non sono significativi, ma ricordano che i bit sono 8 Per rappresentare –12 si utilizza il complemento a 2
⇓
0 0 0 0 1 1 0 0 1 1 1 1 0 0 1 1 1 1 1 1 0 1 0 0
scambio 0 con 1 sommo 1
– 1 2 = 1 1 1 1 0 1 0 02 Quindi Base 10 +1 5 + –1 2 = +3
Base 12 0 0 0 0 1 1 1 1 + 1 1 1 1 0 1 0 0 = 1 0 0 0 0 0 0 1 1
=+3
⇓ Riporto da troncare b) b)
–1 5 – 1 2 1 5 = 0 0 0 0 1 1 1 12
Per rappresentare –15 si utilizza il complemento a 2
– 1 5 = 1 1 1 1 0 0 0 12 1 2 = 0 0 0 0 1 1 0 02
Per rappresentare –12 si utilizza il complemento a 2
– 1 2 = 1 1 1 1 0 1 0 02 Quindi Base 10 –1 5 + –1 2 = –2 7
Base 2 1 1 1 1 0 0 0 1 + 1 1 1 1 0 1 0 0 = 1 1 1 1 0 0 1 0 1
= complemento a 2 di 27
⇓ Riporto da troncare
27
Sezione 1 - Premesse
Verifica – 2 7 = 1 1 1 0 0 1 0 12
complemento a 2
⇓
1 1 1 0 0 1 0 1 0 0 0 1 1 0 1 0 0 0 0 1 1 0 1 1
scambio 0 con 1 sommo 1
+ 2 7 = 0 0 0 1 1 0 1 12 ⇓
c) c)
1 0 0 + 30 Poiché i bit disponibili sono 8, l’intervallo dei numeri interi rappresentabili è –27; 27 – 1 cioè –128; 127. La somma 100 + 30 = 130 eccede tale intervallo e il risultato dell’operazione è completamente errato. Infatti Base 10 1 0 0 + 3 0 = 1 3 0
Base 2 0 1 1 0 0 1 0 0 + 0 0 0 1 1 1 1 0 = 1 0 0 0 0 0 1 0
= –1 2 6
⇓ Bit del segno Verifica 1 2 6 = 0 1 1 1 1 1 1 02
complemento a 2
⇓
0 1 1 1 1 1 1 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0
scambio 0 con 1 sommo 1
⇓ – 1 2 6 = 1 0 0 0 0 0 1 02
...........................................................................................................................................
2.4 Rappresentazione dei numeri reali Mentre per i numeri interi le operazioni aritmetiche eseguite da un calcolatore danno risultati esatti, purché gli operandi e i risultati appartengano all’intervallo (MININT, MAXINT), dove con MININT e MAXINT intendiamo indicare il minore e il maggiore degli interi rappresentabili in un calcolatore, l’esattezza dei risultati non è più possibile quando si opera nell’insieme dei reali. Infatti, per l’insieme dei reali è possibile memorizzare solo valori compresi tra un valore minimo (MINREAL) e un valore massimo (MAXREAL). Inoltre, della parte non intera di un numero reale è possibile memorizzare solo un numero limitato di cifre, la cui entità dipende dal tipo di macchina usata: se un numero reale ha un numero di cifre decimali che supera tale limite subisce un troncamento, quindi del numero viene memorizzato solo un valore approssimato. Infine, l’insieme dei numeri reali è continuo e qualsiasi intervallo, per quanto piccolo, di numeri reali ne contiene infiniti. Il tipo reale dell’aritmetica dei calcolatori contiene, invece, un insieme discreto di valori e ogni valore, come già detto, è costituito da un numero finito di cifre decimali: per esempio, in un calcolatore 28
Unità didattica 2 - Codifica delle informazioni
la cui aritmetica ammetta solo quattro cifre decimali il numero reale 2 sarà rappresentato dal decimale finito 1.4142. Ogni decimale finito rappresenta inoltre un sottoinsieme di numeri reali: per esempio, 1.4142 è la rappresentazione di tutti i reali, razionali o irrazionali, le cui prime quattro cifre frazionarie sono 4, 1, 4, 2 (1.41427, 1.4142333..., 1.41424141..., sono esempi di elementi dell’insieme rappresentato dal decimale finito 1.4142). Da quanto esposto si può facilmente comprendere che sorgono notevoli problemi, sia di rappresentazione dei numeri reali sia di approssimazione, quando si tratta dell’aritmetica dei calcolatori. In molti problemi, soprattutto di carattere scientifico, vengono spesso trattati numeri molto piccoli o molto grandi, con numerosi zeri, per cui è necessario rappresentare i numeri in modo da evitare la scrittura ripetuta di molti zeri. In questi casi viene adottato un particolare metodo, detto della vir- gola mobile ( floating point , in inglese), che consente di ridurre notevolmente l’impiego di posizioni di memoria fisiche del calcolatore.
Con il metodo della virgola mobile i numeri vengono scritti in forma esponenziale, in modo che la parte intera sia sempre zero e la prima cifra frazionaria sia diversa da zero: ciò si ottiene moltiplicando o dividendo il numero per un’opportuna potenza di dieci. I numeri +32.415; −2130000; +0.0000003715 vengono rappresentati in virgola mobil e come segue: +3 2 . 4 1 5
=
–2 1 3 0 0 0 0 +0 . 0 0 0 0 0 0 3 7 1 5
0 . 3 2 4 1 5 * 1 0
=
–0 . 2 1 3
* 1 0
=
+0 .
* 1 0
3 7 1 5
2
7
–6
Nella scrittura esponenziale la parte decimale viene detta mantissa e l’esponente della potenza di 10 è detto caratteristica. Per esempio, nel numero +32.415 = +0.32415 * 102 il valore 32415 è la mantissa, mentre l’esponente 2 di 10 è la caratteristica. La scrittura in virgola mobile di un numero viene anche detta forma normalizzata del numero. In memoria centrale il numero, scritto in forma normalizzata, viene rappresentato secondo convenzioni particolari, che possono variare da calcolatore a calcolatore, ma che tendono tutte a rendere minimo il numero di posizioni che il numero deve occupare in memoria.
Un modo di rappresentazione può essere il seguente: 1. 2.
3.
non considerare lo zero della parte intera (che non viene memorizzato) e fissare un numero costante di cifre per la mantissa; stabilire che il campo di variabilità della caratteristica sia fisso (per esempio –50, +49: aggiungendo alla caratteristica +50,quest’ultima sarà sempre positiva e non servirà memorizzare il segno della caratteristica); memorizzare le tre parti rimaste (segno, caratteristica e mantissa) nel seguente ordine: segno
caratteristica
mantissa
I numeri di esempio citati sopra vengono così rappresentati in memoria. 29
Sezione 1 - Premesse
Numero
Virgola mobile
+3 2 . 4 1 5
In memoria
+ 0 . .3 2 4 1 5 * 1 0
–2 1 3 0 0 0 0 +0 . 0 0 0 0 0 .
3 7 1 5
+0 . 2 1 3
* 1 0
+0 . 3 7 1 5
* 1 0
2
7
–6
+5 2 3 2 4 1 5 –5 7 2 1 3 0 0 +4 4 3 7 1 5 0
Da questi esempi si può comprendere come questo tipo di rappresentazione dei numeri in memoria riduca notevolmente il numero di posizioni occupate, soprattutto quando i numeri contengono molti zeri. Esistono rappresentazioni in virgola mobile in cui sia la mantissa sia la caratteristica sono espresse in esadecimale e la caratteristica viene aumentata di 64: in tal modo, l’esponente convenzionale varia da 0 a 127 e quello effettivo da –64 a +63. Da quanto esposto si può concludere che l’aritmetica dei dati reali è un’aritmetica finita e discreta e l’insieme su cui opera è un piccolissimo sottoinsieme dei numeri reali. In questa esposizione ci siamo avvalsi della scrittura decimale ma, naturalmente, all’interno del calcolatore i numeri vengono espressi in forma binaria secondo le potenze di 2. Una rappresentazione che utilizza quattro byte potrebbe essere così strutturata: 1o byte
|±|
caratteristica
2o byte
|
3o byte
mantissa
4o byte
|
dove:
il primo bit di sinistra rappresenta il segno della mantissa; i restanti sette bit la caratteristica, o esponente; gli altri 24 bit servono per rappresentare la mantissa.
Poiché il massimo numero rappresentabile con 7 bit è 127, l’esponente convenzionale varia da 0 a 127 mentre l’esponente effettivo, che si ottiene togliendo 64, varia da –64 a 63. Supposto che l’esponente convenzionale sia 10001002, che in decimale corrisponde al numero 68, l’esponente effettivo sarà 68 – 64 = 4.
30
Unità didattica 2 - Codifica delle informazioni
Esercizi Unità didattica 2 Rappresentare tutte le configurazioni possibili che si possono ottenere con 4 bit. Quanti bit sono necessari per codificare 20 oggetti diversi? Quanti bit sono necessari per codificare i caratteri con cui vengono rappresentate le infor mazioni? Nella tabella del codice ASCII un bit resta sempre uguale a 0. Quale? Perché? Rappresentare la codifica ASCII binaria ed esadecimale della parola “casa”. Qual è il numero d’ordine del carattere ASCII “lf”. Completare la tabella seguente: NUMERO D’ORDINE
38 41 43
CARATTERE
BINARIO
ESADECIMALE
$
0100100 0100110
24 26 29
+
Quanti bit utilizza il codice UNICODE? Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 33 + 71 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 24 + 62 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 71 – 33 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 62 – 24 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 33 – 71 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 33 – 34 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 133 – 71 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dire perché il risultato è da ritenersi errato. Dopo aver trasformato gli operandi in base 2, eseguire l’operazione 64 + 64 utilizzando il criterio del complemento a 2 avendo a disposizione 8 bit. Dire perché il risultato è da ritenersi errato. 31
Sezione 1 - Premesse
Esercizi Unità didattica 2 Scrivere in base 2 tutti i numeri che possono esser e rappresentati con 4 bit quando si utilizza il metodo del complemento a 2.
Scrivere in forma esponenziale i seguenti numeri:
0.00012345 0.6789 1.234 5678
Per i numeri dell’esercizio precedente indicare la mantissa e la caratteristica. Scrivere in forma esponenziale i seguenti numeri:
0.000005 0.00005 0.0005 0.005 0.05 0.5
Scrivere in forma esponenziale i seguenti numeri:
300000 30000 3000 300 30 3
Usando 4 byte rappresentare in binario e in virgola mobile il numero 1 2.34.
32
Sezione
2
Primi elementi di programmazione
†
Obiettivi
◊ Acquisire
familiarità con la struttura di un programma
C++
◊ Apprendere ◊
i primi elementi del linguaggio
Scrivere, compilare ed eseguire brevi programmi in C++
&
Questa sezione contiene
U.D. 3 Introduzione a C++ U.D. 4 Visualizzazione e acquisizione U.D. 5 Operatori
Unità didattica
3
Introduzione a C++
CHE COSA IMPARERAI A FARE $
Riconoscere gli elementi fondamentali di un programma C++
$
Compilare ed eseguire un semplice programma
$
Scegliere i tipi di dato
$
Distinguere tra variabili e costanti
CHE COSA DOVRAI STUDIARE $
Compilazione ed esecuzione di un programma
$
Definizioni dei tipi di dato
$
Operazione di cast
$
Definizione di una costante
3
Unità didattica
Introduzione a C++
3.1 Primo programma Prima di entrare nel vivo della programmazione C++ è opportuno prendere un po’ confidenza con questo linguaggio, scrivendo un piccolissimo programma che ormai fa parte della storia della teoria della programmazione, a prescindere dal linguaggio che si vuole studiare: si tratta, naturalmente, del famoso “Ciao mondo”. Esempio Ciao .....................................................................................................................
Scrivere a video il messaggio di saluto “Ciao, mondo”. Di seguito è riportato il codice del programma. Codice Ciao.cpp 1 2
#include
using namespace std;
3 4
//inizio int main ()
5 6
{ //scrivi il messaggio di saluto
7 8 9
cout << “Ciao, mondo” << endl; //arresta l’esecuzione del programma system(pause);
10
//termina il programma
11 12
return 0; }
........................................................................................................................................... step by step
1. Copiare il seguente codice, senza i numeri di riga, in Blocco Note. 2. Nominare il file ciao.cpp .
È importante ricordare che si tratta di un file che contiene codice C++, che deve avere l’estensione .cpp. 3. Procedere alla compilazione del programma, creando il file eseguibile Ciao.exe . 4. Provare a eseguire il programma.
La prima cosa che serve per poter cominciare a creare un programma in C++ è un editor di testi.
Un editor di testi è un software che permette al programmatore di gestire (cioè creare, modificare e salvare) un file contenente caratteri alfanumerici. 35
Sezione 2
- Primi elementi di programmazione
Per programmi scritti in C++ è sufficiente un editor di base come Blocco Note di Windows. L’editor di testi permette di scrivere un programma, cioè un insieme finito di istruzioni composte da un limitato numero di parole chiave relative alla grammatica di un determinato linguaggio di programmazione.
Il file che contiene il testo del programma viene chiamato file sorgente, o programma sorgente, ed è un semplice file di testo che viene letto dal compilatore; quest’ultimo provvede alla codifica delle istruzioni scritte dal programmatore in istruzioni direttamente eseguibili dal processore. I file sorgente, perlomeno quelli di programmi di una certa dimensione, sono in genere più di uno e tra essi rivestono particolare importanza i file chiamati librerie (anche detti file d’intestazione), che tipicamente contengono insiemi di funzioni, procedure e classi utilizzati dal programma principale. Il meccanismo di inclusione dei file d’intestazione permette di realizzare programmi indipendenti dall’hardware e dal software di sistema, cioè dal compilatore e dal sistema operativo in uso sul computer sul quale il programma viene eseguito: infatti, basterà creare librerie di sottoprogrammi contenenti tutte le funzioni dipendenti dal tipo di elaboratore, che saranno richiamate nel codice sorgente del programma tramite il loro file d’intestazione. Affinché il programma sia completo deve essere collegato (mediante un apposito software, detto linker) con altri codici oggetto. I sottoprogrammi utilizzati dal linker per effettuare il collegamento sono moduli che possono essere forniti dal sistema operativo, scritti dal programmatore e/o forniti insieme al compilatore. La compilazione e il linking di un codice sorgente producono il file eseguibile (di estensione .exe), il quale viene memorizzato su un’apposita area di memoria di massa atta a contenere il codice eseguibile.
Un file eseguibile racchiude al suo interno una serie d’istruzioni in formato binario (quindi interpretabili dal processore) che devono essere eseguite secondo un ordine preciso. Il file eseguibile è strettamente dipendente dal sistema operativo della macchina su cui viene eseguito. Per esempio, non è possibile trasferire un file eseguibile da un sistema operativo Unix su una macchina Windows: l’insieme delle informazioni contenute nel file eseguibile destinato al sistema operativo Unix, infatti, contiene istruzioni che Windows non è in grado di eseguire. Il passaggio da codice sorgente a file eseguibile è detto compilazione e viene eseguito da un programma detto compilatore.
Per compilazione si intende il processo tramite il quale un programma, scritto in un qualsiasi linguaggio di programmazione, viene tradotto in istruzioni eseguibili dal processore della macchina su cui viene compilato. In ambiente C++ il processo di compilazione è in realtà diviso in due parti ben distinte: 1. 2.
parsing del codice sorgente; compilazione.
Il parsing del codice sorgente è la prima attività che viene svolta durante il processo di compilazione.
I file sorgente vengono esaminati da un sottoprogramma, detto parser, che ricerca eventuali errori sintattici e grammaticali del linguaggio in cui è scritto il file sorgente. 36
Unità didattica 3 - Introduzione a C++
Qualora siano presenti, gli errori di scrittura nel programma sorgente vengono segnalati, sotto forma di errori di compilazione, al programmatore che dovrà correggere il suo programma riprendendone la fase di editing. Una volta che il parser ha esaminato l’intero file e ne ha verificato la correttezza sintattica e grammaticale è la volta del compilatore vero e proprio, che deve trasformare le istr uzioni in linguaggio di programmazione contenute nel file sorgente in una sequenza di istruzioni in linguaggio macchina. Ogni linguaggio di programmazione provvede a individuare i propri file sorgente, obbligando il programmatore a salvarli con una determinata estensione: per esempio nel linguaggio C i file sorgente hanno estensione .c, in C++ hanno .cpp , in Java .java. Un file eseguibile, frutto della compilazione di un file sorgente, ha estensione .exe e contiene istruzioni direttamente eseguibili dalla CPU. La figura che segue riporta lo schema logico riassuntivo dei vari passaggi che portano dal programma sorgente al programma eseguibile. Programmatore Editor
Parser
File sorgente
Sì
Librerie
Errori
No
Moduli
Compilatore
File eseguibile
L’esecuzione del programma finale avviene sotto il controllo del sistema operativo, seguendo i passaggi principali indicati di seguito. 1. 2. 3. 4.
Trasferimento del contenuto del file eseguibile dalla memoria di massa nella RAM. Inizio dell’esecuzione del programma, mediante il richiamo della funzione main() da parte del sistema operativo. Svolgimento dell’elaborazione. Termine dell’elaborazione.
L’inizio dell’esecuzione di un programma finale scritto in C++ coincide con la chiamata alla funzione main() da parte del sistema operativo. Lo schema della figura applicato al primo programma ci mostra i diversi passaggi necessari per arri vare dalla scrittura del file sorgente Ciao.cpp fino all’esecuzione del programma eseguibile Ciao.exe . Dopo che il programma è stato compilato, è possibile mandarlo in esecuzione. 37
Sezione 2
- Primi elementi di programmazione
Prova di esecuzione
Come si nota, il programma esegue semplicemente la visualizzazione del messaggio “Ciao, mondo!”.
3.2
Analisi del codice
Riprendiamo il codice dell’esempio, che riportiamo di seguito: è arrivato il momento di fornire, per quanto possibile, una spiegazione chiara delle singole istruzioni. Si tenga comunque presente che alcune di esse introducono argomenti di una certa complessità, che è prematuro approfondire in queste prime lezioni. Ciao.cpp 1
#include
2
using namespace std;
3
//inizio
4
int main ()
5 6
{ //scrivi il messaggio di saluto
7
cout << “Ciao, mondo” << endl;
8
//arresta l’esecuzione del programma
9
system(pause);
10
//termina il programma
11 12
return 0; }
La riga 1 indica al compilatore di leggere la libreria di funzioni iostream e di inserirla nel codice. Tale file contiene la definizione delle funzioni e degli operatori per la gestione dei flussi di immissione e di emissione. La presenza di quest’istruzione permette l’esecuzione dell’istruzione cout << "Ciao, mondo!" << endl ; per questo è necessario includere un riferimento a questa libreria in tutti i programmi che leggono dati dalla testiera o scrivono dati a video. La riga 2 indica al compilatore che tutti i nomi utilizzati nel programma appartengono al namespace standard del C++. Nei programmi di grandi dimensioni è alquanto comune che programmatori diversi utilizzino gli stessi nomi per denotare elementi diversi: si possono evitare conflitti di nomi utilizzando namespace separati, tuttavia per i semplici programmi che verranno scritti in questo libro tale misura non è asolutamente necessaria. Le righe 3, 6, 8 e 10 non sono istruzioni, bensì commenti.
I commenti sono utilizzati in genere come una descrizione delle funzionalità di una istruzione, di un intero blocco di codice o come informazioni sull’autore del programma. In altre parole: tutto ciò che serve per informazioni e/o descrizioni, va inserito in un commento. Il compilatore, all’atto della generazione del codice eseguibile, ignora i commenti lavorando solo sul codice del programma. 38
Unità didattica 3 - Introduzione a C++
In molti linguaggi sono supportati due tipi di commenti:
commenti a riga singola , che iniziano con la combinazione di caratteri // e si estendono solo fino alla fine della riga; commenti delimitati , che iniziano con la combinazione di caratteri /*, terminano con la combinazione */ e possono estendersi su più righe, come nell’esempio riportato di seguito. /* Questo programma scrive un messaggio di benvenuto */
Il costrutto individuato dalle righe 4, 5, 11, 12 del codice definisce una funzione denominata main. Una funzione rappresenta una raccolta di istruzioni di programmazione che consente di portare a termine una determinata operazione. Ogni programma C++ deve disporre di una funzione main. La maggior parte dei programmi C++ contiene altre funzioni oltre a main, il cui ruolo spiegheremo più avanti. Le istruzioni del corpo della funzione main, vale a dire quelle all’interno delle parentesi graffe {}, vengono eseguite una per volta, in sequenza. Tutti i programmi C++ iniziano l’esecuzione con questo costrutto: nel nostro esempio le istruzioni che vanno da riga 6 a riga 12 sono delimitate dalle parentesi di riga 5 e riga 13, che definiscono un blocco di codice “appartenente” alla funzione main, a sua volta “appartenente” al programma Ciao.cpp . Come vedremo, esistono molti costrutti che utilizzano le parentesi graf fe per raggruppare un insieme di istruzioni. La riga 7 contiene l’istruzione che realizza concretamente quanto richiesto dal programma: infatti, il comando cout richiede che venga scritto a video quanto indicato dopo di esso. La sequenza di caratteri racchiusa da virgolette “Ciao, mondo!” viene denominata stringa. È necessario racchiuderne il contenuto all’interno delle virgolette, per fare in modo che il compilatore ne comprenda il significato letterale. In questo breve programma non è possibile alcuna confusione, ma si supponga di voler visualizzare il termine “main” sullo schermo: indicando il termine tra virgolette, vale a dire specificando la stringa “main” dopo il comando cout, il compilatore comprende che si tratta della sequenza di caratteri “main”, non della funzione denominata main . In C++ la semplice regola di racchiudere tutte le stringhe di testo tra virgolette è sufficiente per fare in modo che il compilatore le interpreti come semplice testo e non come istruzioni del programma. L’instruzione si conclude con la parola chiave endl (che sta per “end line”), che comunica al sistema operativo che la stringa è terminata e che il cursore dello schermo deve andare a capo dopo averla scritta. La riga 9 sospende l’esecuzione del programma e comunica all’utente il messaggio
Solo dopo che l’utente ha effettuato quanto indicato, l’esecuzione del programma procede alla riga successiva. Il comando di arresto system (“pause”); non è indispensabile e può essere omesso senza alterare la logica del programma, ma è molto utile perché permette all’utente di controllare quanto è stato realizzato dal programma e quali sono i risultati da esso prodotti. La riga 12, infine, chiude il codice della funzione main e restituisce il controllo al sistema operativo. 39
Sezione 2 - Primi elementi di programmazione
Si ricordi sempre che ogni istruzione C++ valida termina con un “;” (il carattere “punto e vir gola”), altrimenti il compilatore genera un errore di sintassi.
3.3 Definizione e assegnazione Durante la programmazione, è sempre necessario memorizzare dati e valori, sia temporanei sia definitivi. A tal proposito, vengono utilizzate le variabili.
Una variabile è una zona (locazione) di memoria che memorizza un determinato tipo di dato, identificato e accessibile tramite un nome mnemonico. Attribuire un nome a una locazione di memoria anziché avere a che fare con un incomprensibile indirizzo formato da lunghe sequenze esadecimali solleva il programmatore da un’enorme fatica: si immagini un programma che ha al proprio interno centinaia di variabili codificate con il loro indirizzo di memoria! Probabilmente tutti i linguaggi di alto livello come il C++ non avrebbero avuto motivo di esistere: si sarebbe continuato a programmare in codice macchina e si sarebbe fatta una fatica enorme a progettare le applicazioni visuali che oggi siamo abituati a utilizzare.
Per utilizzare una variabile, è necessario definirla (dichiarazione) all’interno del codice, nel seguente modo: nometipo nomevariabile;
dove: nometipo specifica il tipo di dato
che si intende memorizzare nella variabile (vedremo più avanti i vari tipi di dato); nomevariabile identifica la variabile tramite un nome. Per esempio, l’istruzione int numero;
dichiara una variabile di nome numero di tipo intero (cioè senza la parte decimale), che possiamo utilizzare all’interno di un programma. Una volta dichiarata una variabile, per assegnarle un valore si deve scrivere un’istruzione del tipo numero=122; . Vediamo un esempio pratico molto semplice. Esempio Numero ................................................................................................................
Definire una variabile numero di tipo intero e assegnarle il valore 122. Codice Numero.cpp
40
1 2 3
#include using namespace std; //inizio
4 5 6
int main () { //dichiara la variabile di tipo intero
Unità didattica 3 - Introduzione a C++
7 8 9 10 11
int numero;
12 13 14
//scrivi il valore di numero cout << numero << endl;
15 16 17 18 19
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma
20 21
//assegna il valore numero=122;
return 0 ; }
Prova di esecuzione
Analisi del codice
Il codice è simile a quello del primo programma tranne per il fatto che, anziché visualizzare una serie di caratteri (una stringa), visualizza il valore assegnato a una variabile. Per prima cosa alla riga 7 viene dichiarata una variabile di tipo intero (int) e di nome numero , mentre alla riga 10 viene assegnato un valore a tale variabile, cioè 122. Ora la variabile numero contiene il valore 122. Alla riga 13, con la funzione cout si visualizza il contenuto della variabile sullo schermo. Si noti che dopo il costrutto cout<< non c’è più una stringa di caratteri, ma una variabile: questo significa che l’istruzione riconosce automaticamente che deve essere v isualizzato il valore contenuto nella variabile numero. Le operazioni di dichiarazione e di assegnazione di una variabile sono molto semplici, ma bisogna considerare che il valore da assegnar e a una variabile può essere anche il risultato dell’esecuzione di un’espressione matematica. L’importante è dunque che il valore, comunque sia determinato, rispetti il tipo di dato associato alla variabile. ........................................................................................................................................... Nomi di variabili
Negli esempi visti fino a questo punto sono state utilizzate poche variabili, quindi non sono sorti problemi di chiarezza nell’interpretazione del codice. È utile, tuttavia, comprendere l’importanza di attribuire a una variabile il nome più adatto. Quando, infatti, si scriveranno programmi più complessi, con un numero considerevolmente più alto di variabili, è consigliabile che queste abbiano un nome il più possibile autodescrittivo. Per esempio, se si intende scrivere un programma che calcola l’area di un triangolo è opportuno attribuire alla variabile che contiene il valore della base il nome base e a quella che contiene l’altezza il nome altezza . Ciò si traduce in un immediato riconoscimento delle variabili e del loro utilizzo all ’interno del programma. 41
Sezione 2
- Primi elementi di programmazione
Questa regola si deve applicare ai programmi scritti da un singolo programmatore, ma specialmente ai programmi scritti in collaborazione tra più programmatori. La lettura del codice è molto più chiara e agevole, quindi il consiglio è quello di usare sempre questa semplice prassi. Il nome di una variabile può contenere sia caratteri sia cifre, tuttavia altezza1 è una variabile C++ valida, mentre 1altezza non lo è: infatti, tale linguaggio non ammette che il nome di una variabile cominci con una cifra, generando un errore in fase di compilazione. Il nome della variabile può anche contenere il carattere “trattino basso” (“_”, anche detto undersco- re ): per esempio, numero_1 è un nome di variabile valido. Il C++ è un linguaggio case sensitive, ossia distingue tra lettere maiuscole e minuscole, quindi le due variabili Numero1 e numero1 sono considerate due entità distinte. Per evitare confusione ed errate interpretazioni, è consigliabile non assegnare mai alle variabili di uno stesso programma nomi come Numero1 e numero1: è un pessimo stile di programmazione. I bravi programmatori di C++ usano la convenzione di scrivere i nomi delle variabili utilizzando solamnente caratteri minuscoli, per differenziarle dalle costanti (che vedremo più avanti, e che vengono scritte in maiuscolo). In ogni caso ognuno è libero di usare lo stile che preferisce, l’importante è che sia sempre coerente e chiaro.
3.4 Tipi di dato La dichiarazione di una variabile è il processo tramite il quale il compilatore stabilisce la quantità esatta di memoria da “allocare” per memorizzare il tipo di dato della variabile. Allocare significa fornire l’esatta quantità di “spazio” di memoria necessaria per memorizzare una variabile, un file o qualsiasi oggetto che debba essere memorizzato.
In C++ esistono molti tipi di dato, ognuno dei quali necessita di una quantità di memoria idonea a contenerlo. Per dichiarare una variabile bisogna sapere di che tipo è. Nell’ultimo esempio esaminato è stata dichiarata una variabile di tipo numerico, in particolare di tipo intero, denominata int. I tipi di dato numerici in C++ rientrano nelle seguenti quattro categorie: 1. interi 2. in virgola mobile 3. decimali 4. booleani
Il tipo di dato rappresenta il formato dell’informazione. Ogni informazione deve essere memorizzata rispettandone il tipo: una variabile di tipo intero, per esempio, deve essere memorizzata assegnando la quantità di byte corretta necessaria per memorizzarne il tipo di dato. Il C++ è un linguaggio di programmazione in cui è obbligatorio tipizzare le variabili: si dice, quindi, che il C++ è un linguaggio fortemente tipizzato. All’atto della compilazione del codice sorgente viene generato il codice ottimizzato in base al tipo delle variabili utilizzate all’interno del programma. È compito del pr ogrammatore stabilire durante la dichiarazione delle variabili il tipo più adatto, affinché non venga allocata inutilmente la memoria. Quindi la buona progettazione di un programma dipende anche dal corretto uso dei tipi. 42
Unità didattica 3 - Introduzione a C++
3.5 Tipo di dato intero Il tipo di dato intero rappresenta tutti i numeri in cui non compare la parte decimale. Una variabile dichiarata con uno di questi tipi di dato occupa una determinata quantità di memoria, di cui parleremo nei paragrafi che seguono. Il C++ dispone di tre tipi di dato intero, articolati nelle categorie riportate nella tabella che segue. TIPO C++
MIN
MAX
short
–32768
32767
int
–2.147.483.648
2.147.483.647
long
–9.223.372.036.854.775.808
9.223.372.036.854.775.807
Tipo int Un tipo di dato int occupa 4 byte di memoria, cioè 32 bit. Esso può contenere un numero intero con segno compreso tra: –2.147.483.648 e 2.147.483.647 corrispondenti alle espressioni –231 e 231 – 1.
Esempio Intero ................................................................................................................... Dichiarare una variabile num di tipo intero e scriverne a video il contenuto.
Codice
Intero.cpp
1 2 3
#include using namespace std; // inizio
4 5 6 7 8
int main () { //dichiara la variabile e assegnale un valore int num = 1234567890;
9 10 11 12
//scrivi num cout << "intero = " << num;
13 14 15 16
cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma
17 18
return 0 ;
//salta due righe
}
Prova di esecuzione
43
Sezione 2
- Primi elementi di programmazione
Analisi del codice
La riga 7 assolve a due funzioni. La prima parte (int num ) serve a definire una variabile di tipo int e di nome num ; la seconda parte (num = 1234567890 ) assegna a num un valore. Si noti che tale valore rientra nell’intervallo – 231 e 231 – 1. La riga 10 effettua sul monitor la scrittura del contenuto della variabile num : cout è infatti la funzione necessaria per visualizzare dati sul video. Dopo il comando cout possono essere indicati nomi di variabili o nomi di costanti, di ti po sia numerico sia stringa. Nel nostro caso i dati da scrivere sono la stringa “intero =” , che specifica all’utente che cosa viene scritto in seguito, e la variabile num , di cui in fase di esecuzione viene scritto il contenuto (cioè il valore 1.234.567.890). Ciascun elemento che segue il comando cout è preceduto dai caratteri di redirezione “<<”. ...........................................................................................................................................
Tipo short Il tipo di dato intero, come abbiamo visto, occupa 4 byte di memoria, ma spesso è necessario elaborare numeri molto piccoli, che sicuramente non eccedono i valori compresi tra –32.768 e 32.767 cioè –215 e 215 –1. In questo potremmo utilizzare agevolmente il tipo di dato short. Il tipo short occupa 2 byte, cioè 16 bit di memoria, e consente un notevole risparmio di memoria.
Tipo long Se in un programma si devono manipolare numeri interi molto grandi, c’è bisogno di un tipo di dato capace di contenere tali numeri. A tale scopo alcuni compilatori C++ (ma non tutti) mettono a disposizione il tipo long che utilizza ben 8 byte (cioè 64 bit) e può memorizzare un numero compreso tra: –9.223.372.036.854.775.808 e 9.223.372.036.854.775.807 cioè tra –263 e 263 –1. Quando si progetta un programma si deve tener presente il giusto utilizzo del tipo di dato, onde evitare inutili sprechi di memoria; è quindi consigliabile pensare bene a quale tipo di dato si adatti meglio all’elaborazione e al trattamento dei dati, prima di procedere con le fasi successive dello sviluppo del programma.
3.6 Tipo char Oltre ai numeri non è insolito, in un programma, dover memorizza re dei caratteri, dove per “caratteri” si intendono lettere come A, B, C, d, g, e, e simboli come !, $, /, ), e così via. Il C++ interpreta i caratteri in base alla codifica UNICODE, che utilizza 2 byte per memorizzare ciascun carattere. Ogni carattere o simbolo viene identificato con un numero intero compreso tra 0 e 65.535 I caratteri UNICODE sono valori a 16 bit che rappresentano gran parte delle lingue scritte conosciute.
44
Unità didattica 3 - Introduzione a C++
All’interno di un programma, per poter assegnare un carattere a una variabile è necessario racchiuderlo tra apici singoli. Di seguito riportiamo alcune righe di esempio. //dichiara una variabile di tipo char char car; //assegnale il carattere ‘a’ car=‘a’;
I caratteri possono essere espressi anche sotto forma di codici strettamente UNICODE . Quelli di uso più diffuso, ossia quelli occidentali, rientrano nell’intervallo di valori che va da 0 a 255 in notazione decimale, ma il computer li interpreta sempre secondo il sistema esadecimale (quindi non utili zza la base 10, ma la base 16). Si tenga presente che esiste sempre la possibilità di visualizzare il carattere corrispondente a un numero. Esempio Carattere ............................................................................................................. Visualizzare il carattere UNICODE corrispondente a un numero dato.
Codice Carattere.cpp 1
#include
2 3 4
using namespace std; //inizio int main ()
5 6 7 8 9
{ /*dichiara una variabile char e assegnale un numero intero compreso tra 0 e 255 */ char car=(char)125;
10 11 12 13
//scrivi il carattere corrispondente al numero 125 cout << car;
14 15 16 17
cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma
18 19
//salta due righe
return 0 ; }
Prova di esecuzione
Analisi del codice L’unica riga che necessita di commento è la riga 8 che, tramite il costrutto (char), comunica al compilatore che deve effettuare la conversione di un numero decimale “125” nel carattere corri45
Sezione 2 - Primi elementi di programmazione
spondente. Per il momento si segnala che questo tipo di conversione viene chiamato casting esplicito, in gergo informatico: più avanti se ne parlerà ancora. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
3.7 3. 7 Nu Nume meri ri in vi virg rgol ola a mob mobil ile e Nelle nostre applicazioni avremo sicuramente bisogno di gestire, oltre a numeri interi, anche numeri che hanno una parte decimale. Per esempio, per effettuare calcoli scientifici è indispensabile utilizzare variabili numeriche che consentano di memorizzare numeri con la virgola. In C++ il tipo di dato numerico in virgola mobile è il double. double è il primo tipo di dato che il C++ utilizza per memorizzare memor izzare i numeri con la virgola.
Ogni valore di questo tipo occupa 8 byte di memoria ed è compreso tra i seguenti intervalli di valori: –1,7 × 10308 e –5,0 × 10–324 5,0 × 10–324 e 1,7 × 10308 La precisione di un valore di tipo double è di circa 15 cifre. Per esempio si può scrivere, senza incorrere in errori, l’istruzione l’istr uzione di assegnazione riportata di seguito. double x=3.145926587412536;
Esempio Double ............... ............................... ............................... ............................... ................................ ................................ ................................ ................... ...
Dichiarare, assegnare e stampare a video una variabile di tipo double . Codice Double.cpp 1 2
#include using namespace std;
3 4 5 6
//inizio int main () { //dichiara la variabile di tipo double
7 8 9 10
double numero; //assegna un valore numero=3.145926587412;
11 12 13 14 15
46
//scrivi il valore cout << numero << endl; //salta due righe
16 17 18
cout << endl << endl; //arresta l'esecuzione del programma system ("pause");
19 20 21
//termina il programma return 0 ; }
Unità didattica 3 - Introduzione a C++
Prova di esecuzione
Come si può vedere, a video non vengono riportate tutte le cifre decimali: per maggiori dettagli su come gestire la precisione dell’output si rimanda al paragrafo sulla formattazione dell’Unità didattica 4. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
Intervalli di definizione e precisione dei numeri I valori definiti mediante i tipi int e double introducono un problema non indifferente, legato al fatto che l’intervallo di numeri interi o a virgola mobile rappresentabili è limitato. Alcuni compilatori C++ ammettono per i dati di tipo int un intervallo di valori particolarmente ridotto, che va da –32.768 a 32.767: ciò è dovuto al fatto che i numeri interi sono rappresentati su 16 bit, il che permette di codificare 65.536 valori diversi, metà dei quali (da –1 a–32.768) rappresentano numeri negativi, mentre per i numeri positivi il massimo valore possibile è 32.767, dato che occorre tenere conto della rappresentazione del valore 0. Legato ai numeri a virgola mobile c’è invece il problema pr oblema della precisione, dato che anche i numeri a virgola mobile a doppia precisione (cioè di tipo double ) non possono memorizzare più di quindici cifre decimali significative. Si consideri il seguente caso: un cliente può ritenere che il prezzo di trecento triliardi di dollari ($300.000.000.000.000) sia eccessivo per un determinato prodotto e vorrebbe ridurlo di 5 centesimi, portandolo alla cifra di $299.999.999.999.999,95. Se si prova a eseguire il programma indicato di seguito si visualizzerà il valore 0,0625, al posto di 0, 0,05 05.. #include using namespace std; int main() { double original_price = 3E14; double discounted_price = original_price - 0.05; double discount = original_price - discounted_price; /* la differenza dovrebbe valere 0.05 */ cout « discount « "\n"; /* viene visualizzato 0.0625 ! */ }
Nella maggior parte dei programmi, come pure in quelli riportati in questo libro, la precisione, nel trattamento dei numeri di tipo double , non costitui costituisce sce in ogni ogni caso caso un problem problema. a. C++ dispone di un altro tipo di numeri a virgola mobile, denominato float, che prevede una precisione molto più limitata, li mitata, ristretta a circa 7 cifre decimali. decimali. Questo tipo, di norma, non dovrebbe essere utilizzato nei programmi, dato che la sua limitata precisione può costituire un problema in molti casi e tutte le funzioni matematiche restituiscono dati di tipo double . Se si memorizzano i risultati di un’operazione matematica in una variabile di tipo float il compilatore segnala la possibile perdita di informazioni, quindi per evitare questi messaggi di avvertimento e per mantenere la precisione dei risultati è consigliabile evitare di utilizzare variabili di questo tipo. 47
Sezione 2 - Primi elementi di programmazione
3.8 3. 8 Ca Cast stin ing g Può capitare di memorizzare un valore di un certo certo tipo in una variabile di tipo differente. Ogni volta che si corre il rischio di perdere delle informazioni infor mazioni il compilatore lo segnala con un messaggio di avvertimento. Se, per esempio, si memorizza un valore double in una variabile int, si possono perdere informazioni per due motivi:
si perde la parte frazionaria; il valore del numero può essere troppo grande.
L’istruzione int p = 1.0E100; non è corretta, dato che 10100 è un valore superiore all’intero più grande memorizzabile in una variabile di tipo intero. A volte, comunque, si rende necessario convenire convenire un valore a virgola virgola mobile in un valore intero. Se Se è possibile perdere la parte frazionaria e se il numero a virgola mobile non supera il massimo valore intero possibile si può evitare il messaggio di avvertimento ricorrendo a un’operazione di casting, che consiste nella conversione da un tipo (per esempio, double ) in un altro (per esempio, int). L’istruzione riportata di seguito è un esempio di casting in C++. int n = (int)(y + 0.5);
3.9 Tip Tipo o boo boolea leano no A volte, ciò che interessa interessa sapere è se un enunciato enunciato è vero o falso falso (true oppure false): in questo caso, si può utilizzare il tipo bool.
Una variabile di tipo bool occupa 1 byte di memoria e la sua sintassi è molto semplice; per esempio, con: bool flag=true;
si è dichiarata una variabile di tipo bool con l’assegnazione del valore true (vero). Questa variabile si potrebbe utilizzare come segnalatore (flag) dell’esito di una elaborazione testandone il contenuto, contenuto, cioè true oppure false. Esempio Boolean ................ ............................... ............................... ................................ ................................ ............................... ............................... ..................
Dichiarare, assegnare e scrivere a video una variabile di tipo bool . Codice Boolean.cpp 1
#include
2 3 4 5 6 7
using namespace std; //INIZIO int main () { //dichiara e inizializza a true la variabile di tipo bool bool flag = true;
8 9 10 11
48
//scrivi il valore cout << "vero = " << flag << endl;
Unità didattica 3 - Introduzione a C++
12 13 14 15 16
//cambia il valore di flag flag = false; //scrivi il valore cout << "falso = " << flag << endl;
17 18 19 20 21 22 23 24
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0 ; }
Prova di esecuzione
Analisi del codice
Alla riga 7 alla variabile vari abile flag viene assegnato true, che è uno dei due valori possibili per il tipo riga 13 il valore valore di flag pasbool . Quando viene scritto il valore di flag, a video compare 1. Alla riga sa da true a false e, quando ne viene visualizzato il contenuto (mediante l’istruzione alla riga 16), a video compare 0. Si deve tenere in considerazione che i due valori true e false sono parole riservate del C++ e che, in quanto tali, non possono essere utilizzate come nomi di variabili. Si approfondirà il concetto quando si studieranno le strutture di controllo, più avanti nella trattazione. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
3.10 3.1 0 Cos Costan tanti ti In queste prime lezioni si è parlato di variabili come di entità utili per memorizzare valori che possono cambiare durante un’elaborazione. In tutti i linguaggi di programmazione sono previsti elementi che consentono di archiviare un valore che resta invariato in tutto il programma: sono le costanti costanti..
La sintassi per definire una costante è la seguente. tipo const tipo
nome nom e = valore ;
Un esempio è questo. const double
PiGreco=3.14F;
Con le costanti si è sicuri che il valore memorizzato resterà invariato durante la vita dell’applicazione: se si cerca di assegnare un valore a una costante viene infatti generato un errore in fase di compilazione. 49
Sezione 2 - Primi elementi di programmazione
Esempio Const .............. .............................. ................................ ................................ ............................... ............................... ................................ ...................... ...... Utilizzo di una costante.
Codice Const.cpp 1 2
#include using namespace std;
3 4 5 6 7 8
//INIZIO int main () { //dichiarazione di una costante di tipo double const double PIgreco = 3.1459;
9 10
//scrivi il valore della costante cout << PIgreco << endl;
11 12 13 14
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma
15 16 17 18
system ("pause"); //termina il programma return 0 ; }
Analisi del codice
Il listato ha la stessa struttura dei precedenti e serve ser ve solo per mostrare l’utilizzo di una costante di tipo double , definita alla riga 7. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
50
Unità didattica 3 - Introduzione a C++
Esercizi Unità didattica 3 Quanti e quali sono i tipi di commento in C++? A che cosa servono le parentesi graffe? Qual è il carattere che viene usato per terminare un’istruzione? Modificare il codice del listato del programma “Ciao.cpp” affinché scriva il vostro nome e cognome anziché il messaggio di benvenuto.
Salvare il codice precedentemente modificato con il nome “Mionome.cpp” ed eseguirlo. Modificare il codice del listato del programma “Ciao.cpp” affinché scriva il messaggio “Benvenuto in laboratorio”.
Salvare il codice precedentemente modificato con il nome “Benvenuto.cpp” ed eseguirlo. Nel seguente codice c’è un errore; individuare il relativo numero di riga e spiegare di che errore si tratta. n.r.
Ciao.cpp
1 2 3
#include using namespace std //INIZIO
4 5 6 7
int main () { //scrivi il messaggio di saluto cout << "Ciao, mondo!" << endl;
8 9 10 11 12
//arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Nel seguente codice c’è un errore; individuare il relativo numero di riga e spiegare di che errore si tratta. n.r.
Ciao.cpp
1 2 3 4
#include using namespace std; //INIZIO int main ()
5 6 7 8
{
9 10 11 12
//scrivi il messaggio di saluto cout << "Ciao, mondo! << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
51
Sezione 2 - Primi elementi di programmazione
Esercizi Unità didattica 3 Nel seguente codice c’è un errore; individuare il relativo numero di riga e spiegare di che errore si tratta. n.r.
Ciao.cpp
1
#include
2 3
using namespace std; //INIZIO
4 5
int main () {
6 7 8 9 10 11 12
//scrivi il messaggio di saluto cout << "Ciao, mondo! " endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0 ; }
Nel seguente codice c’è un errore; individuare il relativo numero di riga e spiegare di che errore si tratta. n.r. 1 2 3 4 5
Ciao.cpp #include using namespace std; //INIZIO int main () {
6 7 8 9 10 11 12
//scrivi il messaggio di saluto cout << "Ciao, mondo!" << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return "0" ; }
Nella tabella sottostante sono indicate alcune ipotesi di nomi di variabili; ricordando quali sono le regole per la scelta dei nomi di variabili, seleziona i nomi corretti. NOME VARIABILE
CORRETTO
ERRATO
Numero 1numero numero2 il numero Pi Greco angolo
Nel segmento di codice riportato sotto è presente un errore di sintassi: individualo e correggilo. integer numero; numero=122; cout << numero << endl;
52
Unità didattica 3 - Introduzione a C++
Esercizi Unità didattica 3 Nel segmento di codice riportato sotto è presente un errore di sintassi: individualo e correggilo. int numero; numero= 0.122; cout << numero << endl;
Nel segmento di codice riportato sotto è presente un errore di sintassi: individualo e correggilo. integer numero; numero = 122; cout << numero endl;
Indica la parola chiave per definire le variabili di tipo intero cor to, il minimo e il massimo valore che esse possono contenere.
Indica la parola chiave per definire le variabili di tipo intero, il minimo e il massimo valore che esse possono contenere.
Indica la parola chiave per definire le variabili di tipo intero lungo, il minimo e il massimo valore che esse possono contenere.
Indica la parola chiave per definire le variabili di tipo reale a doppia precisione e gli intervalli dei valori che esse possono contenere.
Indica la parola chiave per definire le variabili di tipo r eale a precisione singola e gli inter valli dei valori che esse possono contenere.
Dopo aver eseguito le istruzioni seguenti, che cosa contiene la variabile n? y=5; int n = (int)(y + 0.5);
53
Unità didattica
4
Visualizzazione e acquisizione CHE COSA IMPARERAI A FARE $
Sapere utilizzare le istruzioni di acquisizione
$
Gestire, in generale, una corretta visualizzazione dei risultati
$
Organizzare la formattazione dei dati di output
CHE COSA DOVRAI STUDIARE $
Istruzioni di acquisizione
$
Formattazione dell’output
$
Caratteri di escape
4 Visualizzazione e acquisizione
Unità didattica
I metodi per la scrittura delle informazioni visti finora sono in grado anche di gestire un sistema di formattazione dell’output. In questa Unità didattica viene fornita la descrizione di un primo tipo di formattazione dei risultati dell’esecuzione dei programmi. Quando si studieranno, invece, gli operatori si avrà modo di formattare i risultati in maniera più complessa ed efficiente.
4.1 Acquisizione delle informazioni: il metodo cin Nei programmi scritti nelle Unità didattiche precedenti sono state utilizzate unicamente istruzioni preposte alla visualizzazione delle informazioni, sia numeriche sia in forma testuale: è arrivato il momento di arricchire i programmi con istruzioni che consentano di acquisire dati forniti dall’utente. Nei prossimi paragrafi verranno illustrate le modalità di acquisizione delle informazioni da console. Il linguaggio C++ per la visualizzazione delle informazioni mette a disposizione, oltre al comando cout , anche altre istruzioni, che consentono di acquisire dati dalla tastiera: la prima che illustreremo è quella denominata cin. L’istruzione cin >> ... acquisisce quanto digitato sulla tastiera fino a quando l’utente non preme il tasto Invio.
L’esecuzione del metodo cin interrompe momentaneamente il flusso di elaborazione del programma e lascia il sistema in attesa finché l’utente, dopo aver digitato tutti i caratteri desiderati sulla tastiera, preme il tasto Invio. Esempio Cin ....................................................................................................................... Leggere un numero inserito da tastiera.
Codice Cin.cpp 1 2
#include using namespace std;
3 4 5 6 7
//INIZIO int main () { //dichiara una variabile di tipo intero int numero;
8 9 10 11 12 13 14 15
//messaggio per l’utente cout << "Inserisci un numero => "; //assegna a numero il valore letto da tastiera cin >> numero; //scrivi la variabile
55
Sezione 2 - Primi elementi di programmazione
16 17 18 19 20
cout << "hai scritto " << numero << endl;
21 22 23
system ("pause"); //termina il programma return 0;
24
//salta due righe cout << endl << endl; //arresta l’esecuzione del programma
}
Prova di esecuzione
Analisi del codice
Alla riga 7 è stata dichiarata la variabile di tipo intero numero , destinata a contenere il valore intero letto dalla tastiera. Eseguendo la riga 10 viene visualizzato il messaggio che richiede all’utente di inserire un numero: questo tipo di istruzione è importante, dato che serve per guidare l’interazione dell’utente con il programma. L’assegnazione del dato letto alla variabile numero viene effettuata alla riga 13, nella quale compare l’istruzione cin >> ... , che legge quanto digitato sulla tastiera e lo assegna alla variabile nu mero (che conterrà anche l’ultimo carattere digitato, quello relativo al tasto Invio). Infine alla riga 16 viene scritto sul video, a titolo di verifica, il contenuto della variabile numero , vale a dire il dato letto dalla tastiera. ...........................................................................................................................................
Naturalmente, l’istruzione cin >> ... permette anche la lettura di numeri di tipo double . Esempio Cin_double .......................................................................................................... Leggere e scrivere un numero di tipo double .
Codice Cin_double.cpp 1
#include
2 3 4 5 6 7
using namespace std; //INIZIO int main () { //dichiara una variabile di tipo double double numero;
8 9 10 11 12
56
//messaggio per l’utente cout << "Inserisci un numero => " ; //assegna a numero il valore letto da tastiera
Unità didattica 4 - Visualizzazione e acquisizione
13 14 15 16 17
cin >> numero;
18 19 20
//salta due righe cout << endl << endl; //arresta l’esecuzione del programma
21 22 23 24
system ("pause"); //termina il programma return 0;
//stampa della variabile cout << "hai scritto " << numero << endl ;
}
Prova di esecuzione
Analisi del codice
L’unica differenza con l’esempio precedente è alla riga 7, dove la variabile che acquisisce il valore è definita di tipo double . Dalla prova di esecuzione si nota che non tutte le cifre vengono visualizzate. Per questo problema si rimanda ai prossimi paragrafi. ...........................................................................................................................................
È possibile leggere più di un valore contemporaneamente. Per esempio, l’istruzione di richiesta di input cin >> num1 >> num2 >> num3;
legge tre valori digitati sulla tastiera: si ricorda che se tali valori sono disposti su un’unica riga occorre separare ciascun dato dal successivo con uno spazio bianco. Esempio Cin_multiplo ........................................................................................................ Leggere tre numeri scritti in sequenza e scriverli su tre righe diverse.
Codice Cin_multiplo.cpp 1 2 3
#include using namespace std; //INIZIO
4 5 6 7 8
int main () { //dichiara le variabili di tipo double double numero1; double numero2;
9 10
double numero3;
57
Sezione 2 - Primi elementi di programmazione
11 12 13 14 15
//messaggio per l’utente cout << "Inserisci 3 numeri => "; //assegna alle variabili i valori letti da tastiera cin >> numero1 >> numero2 >> numero3;
16 17 18
//scrivi le variabili cout << "hai scritto " << endl << numero1 << endl << numero2 << endl << numero3 << endl;
19 20 21 22 23 24 25 26
//salta due righe cout << endl << endl; //arresta l’esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
La riga 15 contiene una chiamata del metodo cin simile a quelle degli esempi precedenti, in questo caso seguita però dai nomi di tre variabili, che hanno il compito di contenere i tre valori digitati dall’utente. Come si nota dalla prova di esecuzione, i tre valori sono scritti sulla stessa riga, separati da uno spazio bianco. L’istruzione alla riga 18 (che è stata distribuita su due linee solamente per motivi di spazio) serve a scrivere i tre valori su tre righe diverse: per questo dopo ogni valore è presente il carattere endl . ...........................................................................................................................................
4.2 Output formattato Quando si visualizzano più dati numerici, ciascun dato viene visualizzato con il numero minimo di cifre necessario per mostrarne il valore. Ciò comporta spesso un output poco ordinato. Esempio Scrivi .................................................................................................................... Scrivere i numeri seguenti: 0,01 ; 0,6 ; 30. Codice Scrivi.cpp 1 2
58
#include using namespace std;
Unità didattica 4 - Visualizzazione e acquisizione
3 4 5 6 7
//INIZIO int main () { //dichiara variabili e assegna valori double n1 = 0.01;
8 9 10
double n2 = 0.6; double n3 = 30.8;
11 12 13 14 15
//scrivi cout << n1 << endl; cout << n2 << endl; cout << n3 << endl;
16 17 18 19
//salta due righe cout << endl << endl; //arresta l’esecuzione del programma system ("pause");
20 21 22
//termina il programma return 0; }
Prova di esecuzione
Che confusione! Come si può vedere, i numeri non sono incolonnati in modo corretto. ...........................................................................................................................................
Per ovviare all’inconveniente evidenziato nell’esempio è necessario formattare l’output, stabilendo, per esempio, che ciascun numero deve occupare 8 caratteri e definendo una precisione di due cifre per i numeri decimali. Il manipolatore di stream setw permette di impostare la larghezza del successivo campo di output; per esempio, se si vuole scrivere il numero all’interno di una colonna larga 8 caratteri occorre utilizzare l’istruzione riportata di seguito. cout << setw(8);
Il metodo setw non produce alcun output: elabora semplicemente il flusso dei dati verso lo schermo in modo da modificare la formattazione dell’output del valore che segue. Per utilizzare i manipolatori di stream occorre includere nel programma il riferimento all’header iomanip, mediante l’istruzione indicata di seguito. #include
Si utilizza un altro manipolatore, setprecision , per impostare la precisione con cui visualizzare le cifre decimali dei numeri a virgola mobile. Per esempio, l’istruzione cout << setprecision(2);
richiede che a video vengano indicate solo le prime due cifre decimali. 59
Sezione 2 - Primi elementi di programmazione
I manipolatori possono essere usati in contemporanea nelle istruzioni di output, come si vede nell’istruzione che segue. cout << setprecision(2) << setw(8) << x;
Questo comando visualizza il valore della variabile x entro un campo di larghezza 8 caratteri e con una precisione di due cifre decimali. Nell’esempio di output che segue è riportata la formattazione che avrebbe l’output del valore –34,95 (ogni carattere “•” corrisponde a uno spazio). • • -34.95
L’impostazione della precisione non influenza i campi interi. Sfortunatamente il metodo setprecision non consente di visualizzare gli zeri non significativi: per esempio, il valore 0.1 sarebbe comunque visualizzato come 0.1, e non come 0.10. Per ottenere l’introduzione degli zeri non significativi è necessario impostare il cosiddetto formato di visualizzazione fìsso , mediante il metodo fixed , da chiamarsi come indicato di seguito. cout << fixed;
Alcuni vecchi compilatori non supportano il manipolatore fixed: in questo caso si può utilizzare il più oscuro comando indicato di seguito. cout << setiosflags(ios::fixed);
La combinazione dei tre manipolatori appena visti riesce finalmente a farci ottenere il risultato di formattazione desiderato. Ecco l’istruzione di output completa. cout << fixed << setprecision(2) << setw(8) << x;
Fortunatamente, i manipolatori setprecision e fixed devono essere utilizzati una sola volta: lo stream, infatti, ricorda le impostazioni di formattazione anche per i successivi output. Al contrario, la chiamata del metodo setw deve essere ripetuta per ciascuna istanza di cout , cioè per ciascun valore da mostrare a video. Esempio Tabula .................................................................................................................. Scrivere i tre numeri dell’esempio precedente, incolonnandoli a destra.
Codice Tabula.cpp
60
1 2 3 4
#include #include using namespace std; //INIZIO
5 6
int main () {
7 8 9 10
//dichiara variabili e assegna valori double n1 = 0.01; double n2 = 0.6; double n3 = 30.8;
11 12
//imposta numero cifre decimali
13
cout << setprecision(3);
Unità didattica 4 - Visualizzazione e acquisizione
14 15 16 17 18
//imposta uso della virgola "fissa" cout << fixed; //imposta ampiezza totale
19 20 21
cout << setw(8);
22 23 24 25 26
cout << n1 << endl;
27 28 29 30
//scrivi il valore cout << n2 << endl;
31 32 33 34
cout << setw(8);
//scrivi il valore
//imposta ampiezza totale cout << setw(8);
//imposta ampiezza totale
//scrivi il valore cout << n3 << endl;
35 36 37 38 39 40 41 42
//salta due righe cout << endl << endl; //arresta l’esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
La prima novità si trova alla riga 2, dove viene richiamato l’header che contiene i metodi capaci di impostare la presentazione dei dati di input e output. Un’altra particolarità si trova alla riga 13, dove con il metodo setprecision viene indicato il numero di cifre decimali che deve essere visualizzato al momento della scrittura delle variabili di tipo double , mentre alla riga 15, mediante il metodo fixed, si specifica che la scrittura dei dati di tipo double deve avvenire con il criterio della virgola fissa. Alle righe 19, 25 e 31, con il metodo setw , viene impostata la lunghezza complessiva in caratteri che i dati devono occupare: nell’esempio viene impostata una lunghezza di otto caratteri di ingombro per ogni numero scritto. Si noti che i metodi setprecision e fixed vengono utilizzati una volta sola all’inizio del programma, mentre il metodo setw deve essere specificato prima di ogni istruzione cout << .... ........................................................................................................................................... 61
Sezione 2 - Primi elementi di programmazione
4.3 Caratteri di escape A conclusione del discorso sulla formattazione dell’output non si potevano non menzionare quei caratteri particolari che possono essere utilizzati nelle istruzioni di output, e che spesso sono di grande aiuto nella formattazione dell’output: si tratta dei caratteri di escape . Nella tabella a destra ne sono presentati alcuni.
CARATTERE \a \r \v \f \b \n \t \\ \’ \”
SIGNIFICATO Segnale sonoro Ritorno a capo Tabulazione ver ticale Avanzamento modulo (pagina) Backspace A capo (nuova riga) Tabulazione orizzontale Back slash Virgoletta singola Virgolette doppie
Come si nota dalla tabella ciascuno di questi caratteri speciali è preceduto da una barra rovesciata (back slash , in inglese), per indicare appunto che si tratta di un carattere di escape. Invece di descrivere il significato di ogni singolo carattere, vediamone l’utilizzo con un breve programma di esempio. Esempio Escape ................................................................................................................. Codice Escape.cpp
62
1
#include
2 3 4 5
using namespace std; //INIZIO int main () {
6 7 8 9 10
//dichiara tre variabili intere e assegna i valori int numero1 = 35; int numero2 = 115; int numero3 = 220;
11 12 13
/*emetti un suono \a, vai a capo \n e scrivi il valore della variabile numero1*/ cout << "\a\nPrimo valore = " << numero1;
14 15 16 17 18
/*vai a capo due volte \n\n, inserisci una tabulazione orizzontale \t e scrivi il valore della variabile numero2*/ cout << "\n\n\tSecondo valore = " << numero2;
19 20 21 22
/*vai a capo due volte \n\n e scrivi il valore della variabile numero3*/ cout << "\n\nTerzo valore = " << numero3;
23 24 25 26
cout << endl << endl; //arresta l’esecuzione del programma system ("pause"); //termina il programma
27 28
return 0;
//salta due righe
}
Unità didattica 4 - Visualizzazione e acquisizione
Prova di esecuzione
Analisi del codice
Il codice appena presentato illustra come utilizzare i caratteri di escape nelle istruzioni di visualizzazione. Nella riga 13 compaiono i due caratteri di escape \a e \n. Il carattere \a non rappresenta un’azione direttamente collegabile alla scrittura, ma fa in modo che il computer emetta un “beep”che, in questo caso, ha solo scopo illustrativo, ma in un programma più complesso potrebbe servire per segnalare la presenza, per esempio, di un’incongruenza o di un errore di immissione dei dati. Il carattere \n ha invece un senso nell’istruzione, dato che impone di saltare una riga e andare a capo prima di visualizzare il messaggio che segue. Il numero di caratteri \n all’interno di un’istruzione di stampa dipende da come il programmatore intende effettuare la visualizzazione dei risultati dell’elaborazione. Un altro carattere interessante compare nella riga 17, ed è il carattere \t, che è il cosiddetto carat- tere di tabulazione . Il carattere di tabulazione \t inserisce un certo numero di spazi prima di effettuare la scrittura sul video. Nell’esempio, gli spazi inseriti sono otto, come si vede nella prova di esecuzione. Procedendo nella trattazione si presenterà ancora l’occasione di utilizzare questo carattere, per ottenere la visualizzazione tabellare dei risultati. ...........................................................................................................................................
4.4
Uso di “\\” e del carattere “@”
Nel paragrafo precedente abbiamo visto che i caratteri di escape sono sempre preceduti dalla barra inversa “\”. A volte, però, può presentarsi il problema di dover scrivere a video una sequenza di caratteri in cui la barra inversa è parte integrante del testo da scrivere. Per esempio, se all’interno di un programma si scrive cout << ="c:\esempi\Error.txt";
il compilatore segnala un errore perché interpreta come caratteri di escape le sequenze “ \e” e “\E”, che non riconosce come caratteri di escape validi. Per ovviare a questo problema è sufficiente raddoppiare le barre inverse, quindi il modo corretto di scrivere l’istruzione precedente è: cout << ="c:\\esempi\\Error.txt";
Un secondo metodo, più elegante, prevede che la stringa da visualizzare sia preceduta dal carattere “@”. La nostra istruzione di esempio assume così la forma indicata di seguito. messaggio=@"c:\esempi\Error.txt";
63
Sezione 2 - Primi elementi di programmazione
Esercizi Unità didattica 4 Le istruzioni seguenti contengono un errore di sintassi: individualo e correggilo. int numero; cout << "Inserisci un numero => "; cin << numero;
Le istruzioni seguenti contengono un errore di sintassi: individualo e correggilo. int numero; cout << "Inserisci un numero => "; cin >> " scrivi il numero " >> numero;
Supponendo che a fronte della richiesta del programma “Scrivi due numeri”, l’utente digiti 12
21
che cosa viene scritto a video dopo l’esecuzione delle istruzioni seguenti? int numero1; int numero2; cout << "Inserisci due numeri => "; cin >> numero1 >> numero2; cout << numero2 << numero1;
Dire se è meglio scrivere: cout << "Risultato =" << ris;
oppure cout << "Risultato = " << ris;
e spiegare perché.
Dire se è meglio scrivere: cout << "Risultato =" << ris << "\n" ;
oppure cout << "Risultato = " << ris;
e spiegare perché. , setprecision e Quale header bisogna inserire nel programma per usare le formattazioni setw fixed?
Le due istruzioni indicate sotto vengono eseguite in sequenza. Che cosa compare a video? cout << setw(8)<< 5 << endl; cout << 5 << endl;
64
Unità didattica 4 - Visualizzazione e acquisizione
Esercizi Unità didattica 4 Che cosa producono in output le seguenti istruzioni? double num = 0.12345678e3; cout << "\n" << setw(15)<< fixed << num;
Che cosa producono in output le seguenti istruzioni? double num = 0.12345678e3; cout << "\n" << setw(15)<< setprecision(5) << num;
Quanti caratteri occupa in memoria la stringa "\n"? Quanti caratteri occupa sul video la stringa "\n"? Che cosa compare a video con la seguente istruzione? cout << "abcd\aabcd";
Che cosa compare a video con la seguente istruzione? cout << "\nabcd\refg";
Che cosa compare a video con la seguente istruzione? cout << "\nabcd\tabcd";
Che cosa compare a video con la seguente istruzione? cout << "abcd\babcd";
65
Unità didattica
5
Operatori CHE COSA IMPARERAI A FARE $
Scrivere espressioni aritmetiche
$
Comporre gli operatori con l’operatore di assegnazione
$ Verificare $
relazioni
Creare espressioni logiche
CHE COSA DOVRAI STUDIARE $
Simboli per gli operatori aritmetici
$
Sintassi degli operatori aritmetici composti
$
Simboli per gli operatori relazionali
$
Tabelle di verità degli operatori logici
Unità didattica
5
Operatori
A conclusione della trattazione sui vari tipi di dato disponibili in C++, vediamo quali operazioni aritmetiche possono essere eseguite su di essi.
5.1 Operatori aritmetici In C++ sono presenti (come in tutti i linguaggi) le operazioni fondamentali dell’aritmetica, riassunte nella tabella che segue. OPERAZIONE
C++
Addizione
+
Sottrazione
–
Moltiplicazione
*
Divisione
/
Modulo (resto)
%
Gli operatori riportati nella tabella vengono definiti operatori binari, perché indicano operazioni effettuabili su due operandi,che possono essere variabili o espressioni.
Addizione e sottrazione Il modo più semplice per eseguire la somma è scrivere l’istruzione che segue. somma = numero1 + numero2;
Questa istruzione somma il contenuto della prima variabile con il contenuto della seconda e il risultato dell’operazione viene memorizzato nella variabile somma . Fin qui niente di nuovo. Si possono anche sommare una variabile e un numero, come nell’istruzione riportata di seguito. somma = numero1 + 8;
Quanto indicato per la somma vale, chiaramente, anche per la sottrazione. Bisogna però fare attenzione ai tipi delle variabili che vengono utilizzate in un’espressione. Esempio Somma ................................................................................................................ Sommare due variabili di tipo int.
Codice Somma.cpp 1 2
#include using namespace std;
67
Sezione 2 - Primi elementi di programmazione
3 4 5 6 7
//INIZIO int main () { //dichiara e valorizza la prima variabile intera int numero1 = 235;
8 9 10
//dichiara la seconda variabile intera int numero2 = 265;
11 12 13 14 15
//dichiara la variabile per il risultato int somma; //esegui l'operazione
16 17 18 19
somma = numero1 + numero2; //scrivi il risultato cout << "Somma = " << somma << endl;
20 21 22 23 24 25 26 27
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
L’unica riga significativa è la riga 16, nella quale viene eseguita l’operazione di somma e il risultato viene assegnato alla variabile somma. ...........................................................................................................................................
Moltiplicazione L’operazione di moltiplicazione viene effettuata con l’ausilio dell’operatore *. Per moltiplicare due valori si utilizza la sintassi indicata di seguito. prod = numero1 * numero2;
La variabile prod contiene il prodotto ottenuto dalla moltiplicazione delle due variabili numero1 e numero2 . Vediamo un esempio pratico. Esempio Prodotto............................................................................................................... Moltiplicare due variabili dichiarate di tipo int.
68
Unità didattica 5 - Operatori
Codice Prodotto.cpp 1 2 3
#include using namespace std; //INIZIO
4 5 6 7 8
int main () { //dichiara e valorizza la prima variabile intera int numero1 = 150;
9 10 11 12
//dichiara e valorizza la seconda variabile intera int numero2 = 20;
13 14 15 16
int prodotto;
//dichiara la variabile per il risultato
//esegui l'operazione prodotto = numero1 * numero2;
17 18
//scrivi il risultato
19 20 21 22 23 24
cout << "prodotto = " << prodotto << endl;
25 26
//termina il programma return 0;
27
/ salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause");
}
Prova di esecuzione
Analisi del codice
Il codice è molto simile al precedente; cambia solo la riga 16, che è diventata una moltiplicazione. ........................................................................................................................................... Divisione
Per effettuare una divisione si utilizza l’operatore /. Per esempio, l’istruzione val = numero1 / numero2;
memorizza nella variabile val il risultato della divisione indicata a destra dell’operatore =. Se i due operandi sono di tipo intero il risultato della divisione è anch’esso di tipo int, mentre se uno dei due operandi è di tipo double il risultato è di tipo double . Vediamo un esempio pratico. 69
Sezione 2 - Primi elementi di programmazione
Esempio Divisione .............................................................................................................. Eseguire la divisione tra due variabili di tipo int.
Codice Divisione.cpp 1 2 3 4 5
#include using namespace std; //INIZIO int main () {
6 7 8 9
//dichiara e valorizza la prima variabile intera int numero1 = 150; //dichiara e valorizza la seconda variabile intera
10 11 12 13
int numero2 = 20; //dichiara la variabile per il risultato int divisione;
14 15 16 17 18
//esegui l'operazione divisione = numero1 / numero2; //scrivi il risultato
19 20 21
cout << "divisione = " << divisione << endl;
22 23 24 25 26
cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0;
27
//salta due righe
}
Prova di esecuzione
Analisi del codice
Le uniche modifiche al codice, rispetto a quello precedente, riguardano la sola riga 16, dove viene utilizzato l’operatore di divisione. ...........................................................................................................................................
Come si nota nella figura, il risultato non è quello che ci si aspettava: l’utilizzo di operandi tutti di tipo int ha prodotto infatti un troncamento della parte decimale, visualizzando come risultato “7” (anziché il valore corretto “7,5”). Per ottenere il risultato giusto è indispensabile che la variabile che contiene il risultato della divisione sia dichiarata di tipo double e che almeno una delle altre due 70
Unità didattica 5 - Operatori
(numero1 e/o numero2 ) sia dichiarata come di tipo double . Vediamo la dimostrazione pratica di quanto detto, utilizzando alcune variabili di tipo double .
Esempio Divisione_double ................................................................................................. Codice Divisione_double.cpp 1 2 3
#include using namespace std; //INIZIO
4 5 6 7 8
int main () { //dichiara e valorizza la prima variabile intera int numero1 = 150;
9 10 11
//dichiara e valorizza la seconda variabile intera double numero2 = 20;
12 13 14 15 16
//dichiara la variabile per il risultato double divisione; //esegui l'operazione divisione = numero1 / numero2;
17 18 19 20
//scrivi il risultato cout << "divisione = " << divisione;
21 22 23 24
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause");
25 26 27
//termina il programma return 0; }
Prova di esecuzione
Analisi del codice Nel codice compaiono due variabili dichiarate di tipo double , rispettivamente la variabile numero2 e la variabile divisione che contiene il risultato. Alla riga 16, l’espressione a destra dell’operatore di assegnazione = contiene due variabili di tipo differente: la prima (numero1) è di tipo int e la seconda (numero2) è di tipo double . In fase di compilazione, per rendere compatibile il risultato con il tipo della variabile divisione (che è di tipo double ), la variabile numero1 viene per così dire “promossa” al tipo double , dando luogo al calcolo del risultato corretto. ...........................................................................................................................................
71
Sezione 2 - Primi elementi di programmazione
Operatore modulo Le quattro operazioni appena studiate consentono di effettuare calcoli di qualsiasi genere ma, a volte, può essere necessario conoscere solo il resto di una divisione. A questo scopo il C++ mette a disposizione l’operatore modulo, rappresentato dal simbolo %. Esso viene utilizzato come l’operatore di divisione /, solo che il risultato dell’operazione è il resto del quoziente. Per esempio, l’istruzione resto = numero1 % numero2;
consente di memorizzare nella variabile resto il resto della divisione indicata a destra dell’operatore =. Esempio Resto .................................................................................................................... Calcolare il resto della divisione tra due interi.
Codice Resto.cpp 1
#include
2 3 4 5 6
using namespace std; //INIZIO int main () { //dichiara e valorizza la prima variabile intera
7 8 9
int numero1 = 150; //dichiara e valorizza la seconda variabile intera
10 11 12 13 14
int numero2 = 20;
15 16 17
//esegui l'operazione divisione = numero1 % numero2;
18 19 20 21 22
//scrivi il risultato cout << "resto = " << resto;
23 24 25 26
//arresta l'esecuzione del programma system ("pause"); //termina il programma return 0;
27
//dichiara la variabile per il risultato int resto;
//salta due righe cout << endl << endl;
}
Prova di esecuzione
72
Unità didattica 5 - Operatori
Analisi del codice L’esempio segue la struttura dei precedenti, è cambiata solo la solita istruzione di riga 16 nella quale compare l’operatore modulo. Alla variabile numero1 è stato assegnato il valore 150, mentre alla variabile numero2 è stato assegnato il valore 20. Si sa che il 20 nel 150 sta 7 volte con il resto di 10, che è proprio quello che viene memorizzato nella variabile resto . ...........................................................................................................................................
5.2 Operatori aritmetici composti Molto spesso si ha la necessità di sommare una certa quantità a una variabile: per esempio, per maggiorare il valore della variabile var di 25 unità si deve scrivere l’istruzione indicata di seguito, che ov viamente è una assegnazione valida. var = var + 25;
Il significato dell’istruzione è il seguente: al contenuto della variabile var viene sommato il valore 25. In una istruzione del genere possono essere utilizzati tutti gli operatori aritmetici visti finora. Nell’esempio viene utilizzato il numero 25, ma al suo posto può trovarsi una variabile, come nell’istruzione che segue, che significa “al contenuto della variabile var deve essere sommato il valore contenuto nella variabile num ”. var = var + num;
Il linguaggio C++ consente di scrivere operazioni di questo tipo in maniera più sintetica ed elegante, utilizzando i cosiddetti operatori aritmetici composti . La prima istruzione viene modificata come segue. var += 25;
L’operatore += è stato posto tra la variabile e la quantità da aggiungere a essa: il suo significato è quello descritto prima. La stessa cosa avviene per la seconda istruzione, che si trasforma come segue. var += num;
Nella tabella sottostante sono riassunti tutti gli operatori composti. OPERATORE
ESEMPIO
ISTRUZIONE EQUIVALENTE
+=
var += 25;
var = var + 25;
-=
var -= 87;
var = var - 87;
*=
var *= 3;
var = var * 3;
/=
var /= 2;
var = var / 2;
%=
var %= 5;
var = var % 5;
Esempio Imponibile ........................................................................................................... Applicare l’IVA del 20% all’importo, espresso in euro, inserito da tastiera.
Codice Imponibile.cpp 1 2
#include using namespace std;
73
Sezione 2
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
- Primi elementi di programmazione
//INIZIO int main () { //definisci la variabile importo double importo; //definisci la variabile IVA double IVA; //definisci la costante per la percentuale IVA const int PERC=20; //richiedi il valore dell'importo cout << "\nInserisci l'importo "; //leggi importo cin >> importo; //calcola l'IVA sull'importo //N.B.: l'uso delle parentesi in questo caso è facoltativo IVA = (importo*PERC)/100; //aggiungi IVA importo += IVA; //scrivi l'importo con IVA cout << "\nImporto con IVA = " << importo; //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
Sono state dichiarate due variabili di tipo double, rispettivamente alla riga 7 e alla riga 10: la prima contiene l’importo in euro inserito da tastiera, la seconda l’IVA relativa all’importo inserito. Alla riga 13 è stata dichiarata una costante, nella quale viene memorizzata la percentuale dell’IVA da utilizzarsi nel calcolo dell’imponibile. Alla riga 26 è stato usato l’operatore composto +=, per sommare all’importo inserito l’IVA calcolata nell’istruzione precedente. ...........................................................................................................................................
74
Unità didattica 5 - Operatori
Operatori unari Il C++, oltre a mettere a disposizione gli operatori binari visti in precedenza, dispone di particolari operatori utilizzabili su una singola variabile, i quali permettono l’incremento (o il decremento) di una unità del valore contenuto nella variabile: si tratta degli operatori unari . Per esempio, per incrementare la variabile numero si utilizza l’operatore di incremento ++, all’interno del costrutto ++numero;
che equivale all’istruzione indicata di seguito. numero = numero + 1;
Il medesimo concetto è da applicarsi al decremento di una variabile, mediante l’operatore di decremento --. L’istruzione --numero;
equivale a quella indicata di seguito. numero = numero - 1;
Esempio Unario .................................................................................................................. Incrementare e decrementare due variabili intere.
Codice Unario.cpp 1 2 3 4
#include using namespace std; //INIZIO int main ()
5 6
{ //dichiara due variabili intere
7 8 9 10
//e ponile = 0 int num1=0; int num2=0;
11 12
//scrivi i loro valori iniziali cout << "\n\nVALORI INIZIALI\n";
13 14
cout << "num1 = " << num1 << "\n"; cout << "num2 = " << num2 << "\n";
15 16 17 18 19 20 21 22 23 24 25 26 27
//incrementa la prima ++num1; //decrementa la seconda --num2; //scrivi i loro valori finali cout << "\n\nVALORI FINALI\n"; cout << "num1 = " << num1 << "\n"; cout << "num2 = " << num2 << "\n"; //salta due righe
75
Sezione 2
28 29 30 31 32 33
- Primi elementi di programmazione
cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
Nel codice vengono utilizzati gli operatori unari, rispettivamente alla riga 17, per incrementare la variabile num1 di una unità, e alla riga 20, per decrementare la variabile num2 di una unità. Infine, alle righe 12, 13, 14 vengono scritti i valori iniziali e, per confronto, alle righe 23, 24, 25 vengono visualizzati i valori finali. ...........................................................................................................................................
Operatori unari pre-fissi e post-fissi L’uso degli operatori unari è molto pratico, perché consente di scrivere l’istruzione in modo sintetico. Nell’esempio appena visto sono stati utilizzati operatori che precedono la variabile, e che perciò vengono chiamati operatori pre-fissi. Tali operatori possono essere utilizzati anche all’interno di un’istruzione di assegnazione di un’altra variabile, come mostra la riga di esempio riportata di seguito. num1 = ++num2;
Una simile istruzione consente di assegnare a num1 il valore della variabile num2 incrementato di una unità. Lo stesso discorso vale anche per l’operatore di decremento (--). Il C++ permette anche di utilizzare gli operatori unari come operatori post-fissi , da collocare dopo la variabile su cui devono operare, come nel costrutto num1++; . Utilizzato così, l’operatore si comporta in maniera perfettamente identica a quello pre-fisso: infatti, se nell’esempio “Unario” si prova a cambiare le istruzioni di riga 16 e riga 22, sostituendole rispettivamente con le istruzioni num1++; e num2--; , il risultato del programma non cambia. Diverso è invece il discorso per le operazioni di assegnazione. Nell’istruzione di esempio precedente, in cui veniva eseguita l’assegnazione num1 = ++num2; , l’incremento viene effettuato prima di assegnare il nuovo valore alla variabile num1, mentre nell’analoga istruzione di assegnazione con operatore post-fisso (num1 = num2++; ) prima viene assegnato il valore di num2 alla variabile num1 e poi viene effettuato l’incremento della variabile num2. Una tale situazione deve essere presa in seria considerazione in fase di programmazione, in quanto si potrebbero ottenere risultati non desiderati. Vediamo un esempio pratico. 76
Unità didattica 5 - Operatori
Esempio Pre_e_post ........................................................................................................... Verificare la dif ferenza tra operatori pre-fissi e post-fissi.
Codice Pre_e_post.cpp 1 2
#include using namespace std;
3 4 5 6 7 8
//INIZIO int main () { //dichiara e inizializza una variabile intera int num = 10;
9 10
//incremento post-fisso cout << "num con incremento post-fisso = " << num++ << "\n";
11 12 13 14
//ripristina num num = 10;
15 16 17 18
//incremento pre-fisso cout << "num con incremento pre-fisso = " << ++num <<"\n";
19 20 21 22 23
cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0;
24
//salta due righe
}
Prova di esecuzione
Analisi del codice Alla riga 10 la variabile num viene prima scritta e poi incrementata; alla riga 16 la variabile num viene prima incrementata e poi scritta. ........................................................................................................................................... Alla luce dei risultati dell’esempio, si può affermare quanto segue.
Se l’operatore di incremento (o decremento) agisce come operatore pre-fisso il valore della variabile cui è applicato viene incrementato (o decrementato) prima di ogni altra operazione, mentre se agisce come operatore post-fisso il valore viene incrementato (o decrementato) dopo ogni altra operazione. 77
Sezione 2 - Primi elementi di programmazione
È importante tenere bene a mente queste differenze operative per non incappare in errori che, a volte, possono risultare difficili da scovare.
5.3 Operatori relazionali Durante la scrittura di un programma capita spesso di dover eseguire un blocco di codice in base al risultato di un’operazione di controllo eseguita su alcuni valori. L’operazione di controllo mette a confronto due operandi utilizzando i cosiddetti operatori relazionali, detti anche operatori di confronto.
Il risultato di un’operazione di confronto può assumere solo due valori, quindi è di tipo booleano: vero o falso (True o False ). Il C++ mette a disposizione gli operatori relazionali indicati nella tabella che segue. OPERATORE
DESCRIZIONE
==
Uguale a
<
Minore di
>
Maggiore di
<=
Minore o uguale a
>=
Maggiore o uguale a
!=
Diverso
Operatore di uguaglianza (==)
L’operatore di uguaglianza consente di verificare se i due operandi sono uguali. Un esempio è l’istruzione bool test = num1==num2;
in cui alla variabile test viene assegnato il risultato dell’operazione di confronto num1==num2 . Esempio Test.......................................................................................................................
Verificare se due numeri acquisiti da tastiera sono uguali. Codice Test.cpp 1 2 3 4
#include using namespace std; //INIZIO int main ()
5 6 7
{
8 9 10 11 12
78
//definisci due variabili intere int num1; int num2; //dichiara una variabile per il confronto bool test;
Unità didattica 5 - Operatori
13 14 15 16 17
//chiedi e leggi il primo numero cout << "\nInserisci il primo numero "; cin >> num1;
18 19 20
cout << "\nInserisci il secondo numero "; cin >> num2;
21 22 23 24 25
//esegui il confronto test = num1==num2;
//chiedi e leggi il secondo numero
//scrivi il risultato cout << "\n\ntest = " << test << "\n";
26 27 28 29 30 31 32 33
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prove di esecuzione
Nella prima esecuzione i due numeri inseriti sono diversi e il contenuto della variabile booleana test risulta essere 0 (che corrisponde a false ), mentre nella seconda prova i due numeri digitati sulla tastiera sono uguali e, quindi, test contiene 1 (true). Analisi del codice
Alle righe 7 e 8 sono state dichiarate due variabili di tipo int, in cui vengono memorizzati i valori letti dalla tastiera (righe 15 e 19). 79
Sezione 2
- Primi elementi di programmazione
Alla riga 11 è stata dichiarata la variabile test di tipo bool, che contiene il risultato (booleano) del confronto tra num1 e num2 (num1==num2; ). L’istruzione test = num1==num2; verifica, tramite l’operatore di uguaglianza, che i due valori inseriti siano uguali e memorizza il risultato nella variabile booleana test. Infine, alla riga 25 viene visualizzato il risultato del confronto. ...........................................................................................................................................
Operatore minore di (<) Questo operatore consente di verificare se il primo operando è minore del secondo. Un esempio è l’istruzione bool test = num1 < num2; . Operatore maggiore di (>) Questo operatore consente di verificare se il primo operando è maggiore del secondo. Un esempio è l’istruzione bool test = num1 > num2; . Operatore minore o uguale a (<=) Questo operatore consente di verificare se il primo operando è minore o uguale al secondo. Un esempio è l’istruzione bool test = num1 <= num2; . Operatore maggiore o uguale a (>=) Per questo tipo di operatore vale il discorso inverso rispetto all’operatore precedente (<=), in quanto esso verifica che il primo operando sia maggiore o uguale al secondo. Un esempio è l’istruzione bool test = num1 >= num2; . Operatore di disuguaglianza (“diverso da”, !=) Questo operatore verifica se i due operandi sono diversi. Un esempio è l’istruzione bool test = num1!= num2; .
5.4 Operatori logici Con gli operatori relazionali si è in grado di verificare il risultato di un test applicato a due operandi. Tuttavia, non è rara la necessità di confrontare non due singoli operandi, ma due espressioni booleane. Le espressioni booleane sono quelle utilizzate nei paragrafi precedenti a proposito degli operatori relazionali. Una tipica espressione booleana è quella riportata di seguito. (num1>=num2);
Il confronto, racchiuso tra parentesi tonde, fornisce infatti come risultato true o false.
Gli operatori logici operano su due o più espressioni booleane. C++ mette a disposizione gli operatori logici riportati nella tabella che segue.
80
OPERATORE
DESCRIZIONE
&
AND (completo)
&&
AND (cortocircuito)
|
OR (completo)
||
OR (cortocircuito)
!
NOT (negazione)
^
XOR
Unità didattica 5 - Operatori
Operatore AND completo (&) Questo operatore consente di verificare la contemporaneità di due (o più) condizioni e solo se entrambe le condizioni (operandi o espressioni booleane) risultano true, il risultato è true, come mostrato nella seguente tabella di verità. OP1
OP2
OP1 & OP2
false
false
false
false
true
false
true
false
false
true
true
true
Esempio TestAnd ................................................................................................................ Verificare che un numero letto da tastiera rientri nell’intervallo che va da 10 a 100.
La prima condizione è che il numero deve essere maggiore o uguale al limite inferiore, cioè 10; tradotto in codice, significa che deve essere vera l’espressione booleana (num1>=10 ). La seconda condizione è che il numero deve essere anche minore o al massimo uguale a 100; l’espressione tradotta in codice è (num1<=100 ). Ora, ricorrendo all’operatore logico & è possibile creare l’istruzione bool test = (num1>=10) & (num1<=100);
in cui la variabile test contiene il risultato della valutazione della contemporaneità delle due espressioni: test sarà true se entrambe le condizioni sono verificate, false se almeno una delle due è false.
Codice TestAnd.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
#include using namespace std; //INIZIO int main () { //dichiara una variabile intera int num; //dichiara una variabile per il test bool test; //chiedi e leggi un numero cout << "\nInserisci un numero "; cin >> num; //esegui il test test = (num>=10) & (num<=100); //scrivi il risultato cout << "\n\nTest = "<< test << "\n"; //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
81
Sezione 2
- Primi elementi di programmazione
Prova di esecuzione
Analisi del codice
Alla riga 17 viene eseguito il test con l’istruzione test=(num1>=10)&(num1<=100);
che esamina l’espressione contenuta nella prima coppia di parentesi tonde, poi quella nella seconda coppia di parentesi e infine, tramite l’operatore &, le mette in relazione logica tra loro. Nella figura si vede che il contenuto di test è 1 (true) perché il numero inserito da tastiera è compreso nell’intervallo 1-100. ...........................................................................................................................................
Per concludere, come è descritto nella tabella di verità, l’uso dell’operatore & consente di avere come risultato true solo se entrambe le espressioni booleane sono true. L’operatore logico & viene anche definito operatore logico di valutazione completa , in quanto può mettere in relazione tutte le diverse espressioni booleane.
Operatore AND di cortocircuito (&&) Pur essendo fondamentalmente identico all’operatore &, questo operatore si differenzia per un aspetto che riguarda la valutazione delle espressioni. Come si è visto, l’operatore & valuta tutte le espressioni booleane presenti, mentre l’operatore && valuta solo la prima e solo se questa risulta verificata procede con la valutazione della seconda e così via. Quindi, è sufficiente che la prima espressione risulti false per far sì che il controllo non proceda con le successive valutazioni: infatti, basta che la prima condizione non sia verificata per ottenere dall’AND un risultato false (come appare chiaro osservando la tabella di verità dell’operatore &). Riprendendo il listato dell’esempio precedente, si può agevolmente sostituire l’istruzione di riga 15 (test=(num1>=10)&(num1<=100); ) con l’istruzione test=(num1>=10)&&(num1<=100);
che dà il medesimo risultato a tutto vantaggio della velocità di esecuzione, dato che se il primo test risulta non verificato non si procede alla valutazione della seconda espressione (il risultato sar ebbe in ogni caso false). Tale tecnica di valutazione viene definita di cortocircuito.
Operatore OR ( | ) Questo operatore valuta due espressioni booleane e restituisce true se almeno una di esse è true. Dalla tabella di verità riportata a destra si evince che solo se entrambe le espressioni risultano non verificate (false) il risultato del confronto è false, mentre è true in tutti gli altri casi. 82
OP1
OP2
OP1 | OP2
false
false
false
false
true
true
true
false
true
true
true
true
Unità didattica 5 - Operatori
Esempio TestOr ................................................................................................................... Verificare la terza riga della tabella di verità dell’operatore OR.
Codice
TestOr.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include using namespace std; //INIZIO int main () { //dichiara due variabili booleane e assegna due valori opposti bool OP1 = true; bool OP2 = false; //visualizza i valori delle variabili cout << "\nOperatore 1 = " << OP1; cout << "\nOperatore 2 = " << OP2; //scrivi il risultato cout << "\n\nOP1 or OP2 = " << (OP1 | OP2); //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice Alla righe 7 e 8 sono state dichiarate le due variabili di tipo booleano test1 e test2 , alle quali vengono loro assegnati due valori booleani opposti. Alla riga 18 viene visualizzato il risultato del test. Si noti l’utilizzo dell’espressione direttamente nell’istruzione di stampa, anziché definire un’altra variabile. ...........................................................................................................................................
83
Sezione 2 - Primi elementi di programmazione
Operatore OR di cortocircuito (||) Questo operatore valuta la prima espressione e, se questa risulta true, non ha bisogno di valutare la seconda perché comunque il risultato sarà true; se invece la prima espressione risulta false, allora è costretto a valutare anche la seconda. Riprendendo il codice precedente, si può sostituire l’espressione test1 | test2 contenuta nell’istruzione di stampa alla riga 18 con l’espressione indicata di seguito.
test1 || test2 Il risultato non cambia. In questo caso, la valutazione del secondo operando test2 non viene effettuata, perché qualsiasi sia il suo valore il risultato del confronto è sempre true.
Operatore NOT (!) Questo operatore è definito, in gergo, operatore di negazione . Esso inverte il valore booleano del suo operando, come risulta chiaro dalla sua tabella di verità. OP1
OP1
false true
true false
L’operando può essere una singola variabile oppure un’espressione booleana, come si può vedere nelle due istruzioni di esempio riportate di seguito.
bool test1 = !test2; bool test1= !(num1>=num2); Operatore XOR (^) Questo operatore booleano restituisce true se i valori degli operandi sono diversi tra loro; di seguito è riportata la sua tabella di verità. OP1
OP2
OP1 ^ OP2
false false true true
false true false true
false true true false
Esempio TestXor .................................................................................................................
Verificare la quarta riga della tabella di verità dell’operatore XOR. Codice
TestXor.cpp 1 2 3 4 5 6
84
#include using namespace std; //INIZIO int main () { //dichiara due variabili booleane e assegna due valori uguali
Unità didattica 5 - Operatori
7 8 9 10 11
bool OP1 = true; bool OP2 = true;
12 13 14
cout << "\nOperatore 2 = " << OP2;
15 16 17 18 19
cout << "\n\nOP1 xor OP2 = " << (OP1 ^ OP2);
20 21 22 23
system ("pause"); //termina il programma return 0;
//visualizza i valori delle variabili cout << "\nOperatore 1 = " << OP1;
//scrivi il risultato
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma
}
Prova di esecuzione
Analisi del codice
Il risultato della figura conferma quanto evidenziato nella tabella di verità dell’operatore XOR: infatti, le due variabili sono state entrambe valorizzate a true e quindi, essendo i valori identici, il risultato dell’operazione è false. ...........................................................................................................................................
85
Sezione 2 - Primi elementi di programmazione
Esercizi Unità didattica 5 Scrivere in C++ le espressioni matematiche ripor tate di seguito. 1 s = s 0 + v 0t + – gt 2 2
a G = 4π2 ––––––––––––– p 2 (m 1 + m 2)
Scrivere le seguenti espressioni C++ utilizzando la notazione matematica. dm = m * volume = volume = p = z/(x
(1 + v / e) / (l - v / e) ; 3.14 * r * r * h; 4 * 3.14* r*r*r/ 3; * x + y * y)) ;
Che cosa c'è di sbagliato nella seguente versione della f ormula risolutiva dell'equazione di secondo grado? xl = (-b - sqrt(b*b-4*a*c))/2*a; x2 = (-b + sqrt(b*b-4*a*c))/2*a; // sqrt() = radice quadrata
Date le istruzioni: int n; double x = 5.5; n = x;
quale segnalazione di errore dà il compilatore?
Come possono essere corrette le istruzioni dell’esercizio precedente? Che cosa contiene la variabile z dopo le seguenti istruzioni? double x = 2.5; double y = -1.5; int n = 4 ; double z; z=(x+n*y-x+n)*y;
Che cosa contiene la variabile z dopo le seguenti istruzioni? int m = 18; int n = 4 ; int z; z = m/n+m%n;
Che cosa contiene la variabile z dopo le seguenti istruzioni? double x = 2.5; int n = 4 ; double z; z = 5*x-n/5;
86
Unità didattica 5 - Operatori
Esercizi Unità didattica 5 Che cosa contiene la variabile z dopo le seguenti istruzioni? int n = 4 ; int z; z=1 - (1 - (1 - (1 - (1 - n) ) ) )
Scrivere un programma che visualizzi il quadrato e il cubo dei numeri da 1 a 5. Scrivere un programma che richieda all'utente due valori interi, e che calcoli e visualizzi: la somma; la differenza; il prodotto; la media.
Qual è il risultato della seguente espressione:
15 % 4?
Quali sono gli operatori relazionali? Date le istruzioni seguenti: int numero1; int numero2; bool test=(numero1<=numero2);
quanto vale test per i valori di numero1 e numero2 indicati nella tabella? NUMERO1
NUMERO2
3
5
3
3
5
3
TEST
Qual è il carattere che viene utilizzato per l’operazione AND (completa)? Qual è il carattere che viene utilizzato per l’operazione AND di cor tocircuito? Qual è il carattere che viene utilizzato per l’operazione OR (completo)? Qual è il carattere che viene utilizzato per l’operazione OR di cor tocircuito? Nel seguente codice, che controlla se un numero è compreso in un intervallo tra 25 e 90, c’è un errore logico: quale? cout << "\nInserisci il numero "; cin >> num1; test=(num1>25)&(num1>90);
Dare il resto. Implementare un programma che aiuti un cassiere a stabilire il resto dovuto. Il programma prevede due dati in input: la somma dovuta e la quantità di denaro ricevuta dal cliente. Il pr ogramma calcola la differenza e stabilisce gli euro da restituire al cliente. 87
Sezione 2 - Primi elementi di programmazione
Esercizi Unità didattica 5 Date le istruzioni seguenti: int n1; int n2; bool test=(numero1==numero2)|( numero1
N2
3
5
3
3
5
3
TEST
Date le istruzioni seguenti: int n1; int n2; bool test=(numero1==numero2)&( numero1
N2
3
5
3
3
5
3
TEST
Date le istruzioni seguenti: int n1; int n2; bool test=(numero1==numero2)^( numero1
N2
3
5
3
3
5
3
TEST
Che cosa scrive in output il frammento di codice seguente (le variabili sono tutte di tipo int)? x = 2; y = x + x; t = y + x; cout << t;
88
Sezione
3
Organizzazione degli algoritmi
†
Obiettivi
◊
Strutturare un algoritmo in modo logicamente corretto
◊ Applicare
i principi base della programmazione
strutturata
◊
Trasferire nel linguaggio di programmazione le strutture fondamentali di un algoritmo
&
Questa sezione contiene
U.D. 6 Algoritmi e pseudocodifica U.D. 7 Istruzioni di selezione U.D. 8 Istruzioni di ripetizione U.D. 9 Le funzioni
Unità didattica
6
Algoritmi e pseudocodifica CHE COSA IMPARERAI A FARE $ Analizzare
un problema per la costruzione di un algoritmo
$
Realizzare i primi semplici algoritmi
$
Istruzioni di assegnamento, di ingresso e di uscita dei dati
$ Applicare $
i principi della programmazione strutturata
Scrivere un algoritmo utilizzando la pseudocodifica
CHE COSA DOVRAI STUDIARE $
Definizione generale di algoritmo
$
Forma delle istruzioni di assegnamento, di input e di output in pseudocodifica
$
Teorema di Jacopini-Böhm
$
Pseudocodifica delle strutture di sequenza, alternativa e ripetizione
6 Algoritmi e pseudocodifica
Unità didattica
6.1 Analisi del problema Ognuno di noi, nel corso della giornata, incontra molti problemi, semplici o complessi, da risolvere. In questa sede vogliamo trattare solo una categoria di problemi e, precisamente, quelli i cui risultati si ottengono mediante l’elaborazione di un numero sufficiente di informazioni. Sono, per esempio, problemi della categoria considerata:
scomporre in fattori primi un numero naturale; stabilire, su una cartina stradale, il percorso più breve tra Mantova e Milano; accendere il televisore.
Non sono problemi della classe considerata:
stabilire qual è la regione più bella d’Italia; stabilire quale squadra di calcio della serie A vincerà lo scudetto del prossimo campionato.
Infatti, il primo tipo di problema richiede il possesso di conoscenze intuitive non formalizzabili, mentre il secondo non fornisce un numero sufficiente di informazioni iniziali.
Risolvere un problema significa compiere delle azioni (elaborazione) su oggetti (di natura concreta o astratta), detti dati iniziali, modificandone lo stato per raggiungere lo scopo voluto (ossia i risultati o dati finali). Colui che si propone di risolvere il problema, cioè il risolutore, deve svolgere un’attività creativa nella ricerca della soluzione al problema e, avvalendosi di un patrimonio di conoscenze precedentemente acquisite, deve: 1. interpretare l’enunciato del problema; 2. evidenziare i dati che l’enunciato fornisce (dati iniziali); 3. ricercare tutte le azioni (operazioni) da compiere sui dati e collegarle logicamente in funzione
del raggiungimento dei risultati o dati finali; 4. eseguire nell’ordine le operazioni descritte al punto 3; 5. verificare i risultati.
L’attività che il risolutore svolge per realizzare i punti 1), 2), 3) prende il nome di analisi del problema. Quando il risolutore realizza il punto 4), cioè quando esegue le azioni descritte nel punto 3), assume il ruolo di esecutore. Spesso, il risolutore e l’esecutore sono la stessa persona ma, a volte, non è così: può presentarsi il caso che il risolutore demandi l’esecuzione della soluzione del problema a un’altra persona oppure a un esecutore automatico quale, per esempio, un calcolatore elettronico. In questo caso, il risolutore deve preparare un’accurata descrizione del procedimento risolutivo costituito da un insieme di comandi da impartire all’esecutore. Tale descrizione è, naturalmente, condizionata dalle caratteristiche dell’esecutore. 91
Sezione 3 - Organizzazione degli algoritmi
Nella figura è schematizzato il processo di soluzione di un problema descritto poco sopra.
Problema Risolutore Procedimento risolutivo
Dati iniziali
Esecutore
Analisi del problema
Risultati
Verifica dei risultati
Il risolutore deve sapere:
quale linguaggio comprende l’esecutore; quali sono le azioni operative e logiche che l’esecutore sa eseguire.
La descrizione della risoluzione del problema prende il nome di algoritmo.
La risoluzione del problema deve soddisfare le seguenti condizioni: 1. contenere il nome di tutti gli oggetti da manipolare (dati iniziali); 2. contenere la descrizione di un insieme finito delle azioni effettivamente eseguibili che devono
operare sugli oggetti; 3. ogni azione descritta deve svolgersi in un tempo determinato, cioè con un inizio e una fine; 4. le azioni sono descritte in una sequenza logicamente strutturata e, quindi, devono essere specificate le eventuali condizioni affinché a un’azione ne segua una e non un’altra; 5. tutte le azioni necessarie devono essere presenti e ogni azione deve essere interpretabile in un unico modo (completezza e univocità).
6.2 Primo esempio Per chiarire quanto esposto, diamo un esempio di algoritmo che sia familiare a ognuno di noi. Esempio Televisore............................................................................................................. Algoritmo per l’uso di un apparecchio televisivo.
Pseudocodifica
1 2 3 4 92
INIZIO oggetti da manipolare: televisore, telecomando; premere il tasto di accensione; selezionare un canale con il telecomando;
Unità didattica 6 - Algoritmi e pseudocodifica
5 6 7 8 9 10
se il programma del canale selezionato non è gradito, allora ripetere l’azione 4 altrimenti assistere alla trasmissione; se nessun programma è gradito o le trasmissioni sono terminate, allora spegnere il televisore; FINE
Analisi della pseudocodifica
Possiamo affermare che la soluzione del problema è un algoritmo perché sono rispettate tutte e cinque le condizioni precedentemente elencate. Infatti: contiene l’elenco di tutti gli oggetti da manipolare (dati iniziali): televisore e telecomando; contiene la descrizione delle azioni da compiere sugli oggetti: queste azioni sono eseguibili e sono in numero finito; ogni azione descritta si svolge in un tempo determinato; la sequenza delle azioni da compiere è logicamente strutturata; l’algoritmo è esaustivo (contempla tutte le possibilità) e ogni istruzione è interpretabile in modo univoco (senza ambiguità). ...........................................................................................................................................
Osserviamo che l’algoritmo descrive il comportamento che un esecutore deve tenere per risolvere il problema. Possiamo anche dire che è stata fatta la descrizione di un programma di comportamento, in cui le azioni sono espresse in modo imperativo e rappresentano dei comandi; inoltre, sono presenti azioni che vengono eseguite solo se si verificano determinate condizioni come, per esempio: 5 6 7
se il programma del canale selezionato non è gradito, allora ripetere l’azione 4 altrimenti assistere alla trasmissione;
Frasi come questa descrivono delle azioni, dette di controllo, che impongono delle condizioni (programma non gradito) il cui verifìcarsi o meno determina il comando di esecuzione di una sequenza di azioni piuttosto che di un’altra.
I comandi di un algoritmo prendono anche il nome di istruzioni che vengono fornite all’esecutore affinché egli sappia che cosa fare. La frase: premere il tasto di accensione descrive, quindi, un comando o istruzione. Le istruzioni sono state contrassegnate da numeri per poterle individuare meglio. Le istruzioni contrassegnate dai numeri 1 e 10 avvertono l’esecutore quando inizia e quando termina la sequenza delle istruzioni, rispettivamente. La descrizione di una esecuzione dell’algoritmo da parte di un esecutore potrebbe essere la seguente: 1. 2. 3. 4.
l’esecutore preme il tasto di accensione; seleziona il primo canale con il telecomando; assiste alla trasmissione; la trasmissione è terminata e l’esecutore spegne il televisore. 93
Sezione 3 - Organizzazione degli algoritmi
Dunque, mentre la descrizione dell’algoritmo è unica, l’esecutore può realizzare più esecuzioni e non è detto che tutte le esecuzioni siano uguali. Per esempio, l’azione 2 della procedura potrebbe essere così eseguita: l’esecutore seleziona il secondo canale con il telecomando. È evidente che l’esecutore deve avere le seguenti competenze:
conoscere il tasto di accensione ed essere in grado di premerlo; saper selezionare i canali con il telecomando; saper spegnere il televisore.
6.3 Algoritmi Nell’algoritmo per l’uso di un apparecchio televisivo appena descritto, gli og getti da manipolare (tasti, telecomando) sono concreti e non formalizzabili. Descriviamo ora una procedura in cui gli oggetti da manipolare sono astratti e formalizzabili. Esempio Area .....................................................................................................................
Calcolare l’area di un triangolo sapendo che b e h sono, rispettivamente, le misure della base e dell’altezza del triangolo, e che l’area del triangolo si calcola mediante la formula: b×h area = —-— 2 Pseudocodifica
INIZIO dati iniziali: b e h; moltiplica b per h; dividi il prodotto per 2 e ottieni l’area: area = (b × h) / 2; comunica il risultato, area; FINE ...........................................................................................................................................
Questo algoritmo vale per calcolare l’area di qualsiasi triangolo purché si conoscano le misure della base e dell’altezza. In questo caso, si dice che l’algoritmo risolve una classe di problemi. L’insieme di tutti i problemi che vengono risolti dallo stesso algoritmo costituisce una classe di problemi.
L’algoritmo che descrive l’uso dell’apparecchio televisivo e quello per il calcolo dell’area del triangolo presentano una differenza sostanziale: il primo opera su oggetti fisici (il televisore e il telecomando) facilmente individuabili e, quindi, per essi non è necessario dare una descrizione dettagliata; il secondo opera su oggetti che non sono fisici ma astrazioni mentali (segmenti, figure piane) dalla cui rappresentazione simbolica (numeri interi o decimali) non si può prescindere nella descrizione della procedura risolutiva. In generale, un algoritmo non serve mai per risolvere un singolo problema ma per risolvere più problemi che differiscono solo per i dati iniziali. 94
Unità didattica 6 - Algoritmi e pseudocodifica
Una definizione informale di algoritmo è la seguente.
Per algoritmo si intende una successione finita di passi contenenti le istruzioni che specificano le operazioni da compiere per risolvere una classe di problemi. Quasi tutti gli algoritmi, tranne forse i più semplici, ricevono dei dati, li trasformano e, quindi, li comunicano all’utente. Un programma di elaborazione testi riceve i propri dati sotto forma di parole; tali parole vengono formattate dal programma per dare loro un aspetto gradevole, quindi il programma le stampa ordinatamente su carta. Un archivio anagrafico riceve i propri dati sotto forma di nomi, indirizzi e numeri telefonici, memorizza le informazioni sui propri supporti magnetici e poi visualizza i dati in un formato considerato utile per l’utente. Un sistema di guida per missili nucleari riceve i propri dati sotto forma di coordinate del bersaglio. Qualunque programma che abbia un minimo di funzionalità segue queste tre fasi fondamentali: 1. 2. 3.
acquisizione di dati iniziali; elaborazione dei dati; presentazione dei risultati.
La fase di acquisizione dei dati viene anche definita fase di input o di immissione. La fase di presentazione dei risultati prende il nome di output o di emissione. In un computer, lo strumento attraverso il quale avviene la maggior parte delle operazioni di input è la tastiera (standard input) mentre l’unità più utilizzata per la presentazione dei risultati è il video (standard output); questo processo è schematizzato nella figura seguente.
Output: emissione dei dati Input: immissione dei dati
Il risultato dell’elaborazione compiuta dall’algoritmo è costituito da un insieme di uno o più valori, detti dati di output, che vengono messi a disposizione all’esterno visualizzandoli sul monitor o scri vendoli su file o su carta. Si tratta della fase in cui il programma comunica i dati finali, ottenuti grazie all’elaborazione. Quando si descrive un algoritmo, si indica la fase di emissione dei dati di output con istruzioni del tipo “comunica”, “scrivi” oppure “visualizza”, seguite dal nome dei dati che vengono comunicati all’utente. Può succedere che, per ottenere i dati di output a partire dai dati di input, sia necessario passare attraverso risultati intermedi. Tali dati non sono né il frutto di operazioni di acquisizione né sono utilizzati in operazioni di emissione, ma sono dati temporanei che si ottengono nel corso dell’elaborazione. Essi prendono il nome di variabili di lavoro.
I dati di input sono i valori iniziali che servono all’algoritmo per elaborare la soluzione del problema. Le variabili di lavoro sono dati non provenienti dall’esterno, bensì rappresentano risultati intermedi ottenuti durante l’elaborazione. I dati di output costituiscono il risultato delle operazioni effettuate dall’algoritmo. Essi vengono resi disponibili all’esterno mediante visualizzazione o stampa. 95
Sezione 3
- Organizzazione degli algoritmi
Lo schema seguente illustra i passaggi appena trattati. Dati di input
6.4
ELABORAZIONE
Dati di output
Dati e istruzioni
Se esaminiamo gli algoritmi precedentemente descritti, notiamo che ogni passo dell’algoritmo è una proposizione che descrive l’azione da eseguire e gli oggetti su cui questa opera.
Ogni passo dell’algoritmo si compone di due par ti distinte:
la descrizione dell’azione che deve essere eseguita, che prende il nome di comando o istruzione; uno o più oggetti su cui ogni istruzione opera, che prendono il nome di dati o argomenti.
Sono, per esempio, istruzioni: acquisisci il numero; moltiplica ...; comunica ...; mentre sono dati: b, h, area. In generale, i dati si indicano simbolicamente con stringhe (cioè successioni di caratteri) formate da una o più lettere maiuscole o minuscole (ma noi conveniamo di usare lettere minuscole) dell’alfabeto inglese e da cifre numeriche, con la condizione che il primo carattere sia una lettera. Le stringhe: a, b, bc, area, media1, area; rappresentano nomi di dati. Non sono nomi di dati: 3a, paga netta, volume sfera1; il primo perché inizia con una cifra e gli altri due perché sono formati da due stringhe. Lo sono, invece: a3, paganetta, volumesfera1. Ricordiamo, quindi, che il nome di un dato non deve contenere spazi.
I dati possono essere:
costanti: il loro valore non cambia nel tempo; variabili: durante l’esecuzione dell’algoritmo, può cambiare il loro valore.
Per esempio, nella formula: 2πR che calcola la lunghezza della circonferenza, π è considerato dato costante; R è considerato dato variabile. Finora, per descrivere algoritmi, ci siamo serviti del linguaggio naturale, cercando di espri96
Unità didattica 6 - Algoritmi e pseudocodifica
mere le varie azioni in modo univoco e il più possibile conciso. Ma il linguaggio naturale, a causa della sua complessità e ambiguità, non è sempre adatto per descrivere in modo adeguato algoritmi, soprattutto quando questi non sono semplici come quelli trattati finora. È, quindi, necessario costruire un linguaggio che consenta una giusta interpretazione delle istruzioni da eseguire da parte dell’operatore, soprattutto se questo è una macchina, come nel caso di un elaboratore elettronico. D’ora in avanti, introdurremo gradualmente il linguaggio della pseudocodifica. La pseudocodifica non è un linguaggio di programmazione, ma viene utilizzato per descrivere il procedimento risolutivo contenuto in un algoritmo. Un algoritmo in pseudocodifica non è pronto per essere utilizzato da un calcolatore, ma serve a produrre il programma che è scritto in un particolare linguaggio di programmazione e che viene caricato in un elaboratore per la sua esecuzione.
6.5 Istruzioni di assegnamento Esaminando l’algoritmo dell’esempio Area, troviamo l’istruzione: dividi il prodotto per 2 e ottieni l’area: area = (b × h) / 2 Con questa istruzione, l’esecutore attribuisce alla variabile di nome area la metà del prodotto dei valori attribuiti alle variabili b e h con l’istruzione di acquisizione dei dati iniziali. L’istruzione mediante la quale a una variabile viene attribuito, cioè assegnato, un valore appartenente a un dato insieme, detto insieme di definizione della variabile, prende il nome di istruzione di assegnamento.
In pseudocodifica, l’istruzione di assegnamento viene denotata secondo la seguente sintassi: nome della variabile ← valore della variabile Per esempio, con: a ← 5; si intende che alla variabile a viene assegnato il valore 5 e si legge: a prende il valore 5 oppure: 5 viene assegnato ad a Se una variabile ha già un valore, questo viene sostituito dal nuovo valore assegnato. Per esempio, se alla variabile a, a cui è già stato assegnato il valore 5, vogliamo assegnare il valore 7, il valore 5 viene cancellato. Quindi le istruzioni: a ← 5; a ← 7; si possono visualizzare come nella figura seguente.
5
5
A A
7
A 5
A
7
97
Sezione 3 - Organizzazione degli algoritmi
Una variabile si comporta come una scatola su cui è scritto il nome (o identificatore) della variabile e che contiene il valore assegnato (che rappresenta il contenuto, come indicato nella figura a destra).
Contenuto 5 Nome
A
Non si deve mai confondere il contenitore con il contenuto (cioè con il valore assegnato alla variabile). Elenchiamo alcuni tipi di assegnamento: 1. 2. 3.
assegnamento per costante: a ← 9; e b ← 3; sono due esempi; assegnamento per espressione: alla variabile viene assegnato il valore di un’espressione aritmetica che deve essere prima valutata; assegnamento per nome: eseguendo l’istruzione b ← a;
4.
alla variabile b viene assegnato il valore della variabile di nome a; assegnamento per calcolo: eseguendo le istruzioni a ← 14; b ← a + 1;
5.
a b viene assegnato il valore 15; assegnamento per ridefinizione: a ← a + 1; alla variabile a viene assegnato il suo valore attuale aumentato di 1; quindi, se il valore attuale di a è 5, dopo l’assegnazione sarà 6.
Si deve sempre tenere presente che se a una variabile viene assegnato un valore, allora il valore pr ecedentemente contenuto viene distrutto.
Una variabile, prima di essere trattata, deve contenere già un valore, ovvero essere già stata inizializzata. Esempi ................................................................................................................................
Data la sequenza di istruzioni: a b c
← ← ←
5; a + 1; a + b;
dopo l’esecuzione della terza istruzione il contenuto di ogni variabile è: a = 5; b = 6; c = 11. Data la sequenza di istruzioni: a b c
← ← ←
5; a + c; a + 1;
dall’esame della sequenza notiamo che nella seconda istruzione non è possibile assegnare un valore alla variabile b perché non è conosciuto il contenuto di c: la variabile c non è stata inizializzata, cioè non le è stato assegnato alcun valore. ........................................................................................................................................... 98
Unità didattica 6 - Algoritmi e pseudocodifica
6.6 Istruzioni di ingresso e di uscita dei dati L’istruzione per l’acquisizione dei dati di ingresso prende il nome di istruzione di lettura o di input.
In pseudocodifica, si conviene di esprimere tale istruzione con la parola “leggi” seguita dal nome della variabile racchiusa tra parentesi tonde: leggi (nome della variabile); Per esempio, se la variabile è a, si scrive: leggi (a); Se le variabili sono più di una, aventi per esempio i nomi a, b, c, l’istruzione di lettura può essere così denotata: leggi (a, b, c); L’istruzione termina con un punto e virgola per far capire all’esecutore che l’istruzione è terminata e ne segue un’altra. Per comunicare i risultati, ci si serve di un’istruzione che prende il nome di istruzione di scrittura o di output.
In pseudocodifica, questa istruzione viene descritta con la parola “scrivi” seguita dal nome della variabile contenente il risultato, compresa entro parentesi tonde: scrivi (nome della variabile); ma il risultato può anche essere rappresentato da una o più espressioni e, quindi, si ha: scrivi (espress1, espress2, ..., espressN); Per esempio, nell’istruzione: scrivi (a, a – b, 10); se 15 e 3 sono rispettivamente i valori di a e b, l’esecutore comunica: 15 12 10
Spesso, per rendere comprensibili i risultati da parte dell’utente, si usano frasi racchiuse tra apici, dette costanti letterali.
Per esempio, nell’istruzione: scrivi (“Il prezzo della merce è di euro”, p); se p ha il valore 100, all’esterno viene comunicato: Il prezzo della merce è di euro 100
Esempio Rettangolo........................................................................................................... Scrivere in pseudocodifica l’algoritmo per calcolare l’area del rettangolo, note le misure dei lati.
Per le operazioni di input le variabili sono b e h, mentre il risultato finale viene memorizzato in area, che è la variabile per l’output.
99
Sezione 3
- Organizzazione degli algoritmi
Pseudocodifica
input : b, h output : area INIZIO leggi (b, h) area ← b * h scrivi (“Area del rettangolo = ”, area) FINE Dall’esame dell’algoritmo risulta che: le istruzioni sono comprese entro le parole INIZIO e FINE e sono scritte leggermente spostate a de-
stra rispetto a tali parole; prima della parola INIZIO, vengono definite le variabili, ovvero le “cose” su cui operano le istruzioni dell’algoritmo; le variabili sono divise in variabili di input e variabili di output: le prime sono argomento di istr uzione di tipo leggi, mentre le seconde risultano all’interno di istruzioni di output. ...........................................................................................................................................
6.7 Teorema di Jacopini-Böhm Lo studio degli algoritmi ebbe un notevole sviluppo negli anni ’60 del secolo scorso, quando i programmi raggiunsero notevoli dimensioni in termini di righe di codice ed elevati gradi di complessità. In quegli anni, il linguaggio grafico dei diagrammi a blocchi era l’unico disponibile per descrivere algoritmi. Ma, inizialmente, lo studio degli algoritmi si sviluppò in modo disordinato, tanto che E.W. Dijrska nel 1968 denunciò su una rivista del settore la necessità di razionalizzare tale studio, protestando per l’eccessiva soggettività degli algoritmi che, quindi, risultavano non facilmente accessibili. Il processo di razionalizzazione dello studio degli algoritmi trae fondamento dal teorema di Jacopini-Böhm, enunciato di seguito.
Ogni algoritmo può essere trasformato in un algoritmo equivalente che faccia uso solo di alcune strutture di controllo. Tali strutture di controllo sono:
sequenza; alternativa; ripetizione.
Due algoritmi sono equivalenti quando, per gli stessi dati di ingresso, producono i medesimi risultati in uscita.
Nei prossimi paragrafi daremo una definizione precisa di sequenza, alternativa e ripetizione. Gli algoritmi descritti nel rispetto del teorema enunciato prendono il nome di algoritmi strutturati e il loro studio si situa nel processo di razionalizzazione e standardizzazione logica iniziato negli anni ’60. Le strutture di controllo per creare algoritmi strutturati sono descritte in pseudocodifica mediante espressioni linguistiche che esprimono in modo sintetico il significato di tali str utture. Ogni struttura è considerata come un blocco di istruzioni avente una sola entrata e una sola uscita. 100
Unità didattica 6 - Algoritmi e pseudocodifica
La presentazione di un algoritmo in pseudocodifica descrive la strategia risolutiva del problema, indipendentemente dalle caratteristiche del linguaggio di programmazione che si vuole utilizzare. Un programma per l’elaborazione automatica è composto da diverse righe di codice che rispettano le regole sintattiche del linguaggio utilizzato. Lo schema logico che serve per strutturare il programma è la base razionale che sta al di sotto del codice, è indipendente dal linguaggio di programmazione ed è quello che fa in modo che il programma produca risultati utilizzabili e consistenti a partire dai dati iniziali. Tale schema logico, ovvero l’algoritmo risolutivo, può essere descritto in pseudocodifica. L’utilizzo di quest’ultima permette di “dimenticare” le specificità del linguaggio di programmazione e consente di concentrarsi sui passaggi razionali che costituiscono l’elaborazione dei dati, cioè la trasformazione dei dati iniziali in risultati attesi affidabili. La pseudocodifica, quindi, serve a descrivere la strategia per affrontare il problema e a organizzare in modo razionale l’algoritmo risolutivo.
È ovvio che, per costruire un programma sicuro che non si blocchi al secondo tentativo di esecuzione, risulta fondamentale organizzare in modo razionale l’algoritmo risolutivo e, allo scopo, è fondamentale il ricorso alla pseudocodifica e al rispetto del teorema di Jacopini-Böhm. Inoltre, da ogni algoritmo realizzato in pseudocodifica può essere derivato un programma in un qualsiasi linguaggio, mentre un programma costruito senza rispettare il teorema di Jacopini-Böhm non può essere “tradotto” in pseudocodifica e rende difficoltoso il controllo della sua strategia risolutiva (ammesso che ne abbia una). Struttura di sequenza
Lo schema della struttura di sequenza di un algoritmo è indicato di seguito: ogni elemento B i può rappresentare un’istruzione semplice (come, per esempio, leggi, scrivi, assegna, ...) ed è detto blocco semplice , oppure può essere costituito, a sua volta, da una delle strutture fondamentali (sequenza, alternativa e ripetizione) e, in questo caso, è detto blocco composto . La pseudocodifica della struttura di sequenza ha la seguente sintassi: INIZIO B1 B2 ... ... Bn FINE Tornando all’esempio “Rettangolo”, le istruzioni comprese tra INIZIO e FINE sono indicate in sequenza e l’esecutore deve eseguirle nell’ordine in cui sono disposte. Pseudocodifica
input : b, h output : area INIZIO leggi (b, h) area ← b * h scrivi (“Area del rettangolo = ”, area) FINE 101
Sezione 3 - Organizzazione degli algoritmi
6.8
Struttura di alternativa
Lo schema di alternativa è costituito da un blocco di controllo che contiene una condizione il cui verifìcarsi o meno determina l’esecuzione delle istruzioni di un blocco B1 o di un blocco B2. Lo schema sintattico della struttura di alternativa binaria è: SE condizione ALLORA B1 ALTRIMENTI B2 FINE SE Come lo schema sintattico mette in evidenza, prima viene valutata la condizione; se questa è vera, viene eseguito B1, se è falsa viene eseguito B2. Esempio Verifica.................................................................................................................
Decidere quanto studiare nell’eventualità di una verifica. Pseudocodifica
INIZIO SE devi fare la verifica ALLORA ripassa gli argomenti vecchi ALTRIMENTI studia lezione del giorno FINE SE FINE Le due parole INIZIO e FINE delimitano il blocco di istruzioni di cui è formato l’intero programma. Le righe dopo ALLORA e dopo ALTRIMENTI corrispondono, rispettivamente, ai blocchi B 1 e B2 indicati nello schema sintattico precedente. In questo caso, entrambi i blocchi sono composti da una sola istruzione.
........................................................................................................................................... Esempio Assoluto1 .............................................................................................................
Costruire l’algoritmo per calcolare il valore assoluto di un numero intero a. Pseudocodifica
input : a output : assoluto INIZIO leggi (a) SE a >= 0 ALLORA assoluto ← a ALTRIMENTI assoluto ← –a FINE SE scrivi (“valore assoluto = ”, assoluto) FINE 102
Unità didattica 6 - Algoritmi e pseudocodifica
Trattandosi di un esempio numerico, nelle prime due righe sono state definite le variabili su cui opera il programma, distinguendo tra le variabili di input e quelle di output.
........................................................................................................................................... Alternativa a una via
La struttura alternativa a una via presenta uno schema diverso dal precedente e non contiene il “ramo” ALTRIMENTI. Lo schema sintattico della struttura alternativa a una via è: SE condizione ALLORA B1 FINE SE
Esempio Ombrello.............................................................................................................. Decidere se uscire con l’ombrello.
Pseudocodifica INIZIO SE piove ALLORA esci con l’ombrello FINE SE FINE
........................................................................................................................................... Esempio Assoluto2 ............................................................................................................. L’algoritmo dell’esempio “Assoluto” può essere così modificato:
Pseudocodifica input : a output : assoluto INIZIO leggi (a) assoluto ← a SE a < 0 ALLORA assoluto ← –a FINE SE scrivi (“valore assoluto = ”, assoluto) FINE
........................................................................................................................................... L’esempio successivo mostra come è possibile combinare tra loro diverse strutture di selezione: si parla in questo caso di strutture di selezione nidificate.
Esempio Targhe alterne ..................................................................................................... In una domenica ecologica vige il regime di traffico a targhe alterne: se il giorno corrente è pari, circolano solo i veicoli con targa pari, altrimenti solo quelli con targa dispari.
103
Sezione 3
- Organizzazione degli algoritmi
Pseudocodifica
INIZIO SE data odierna è pari ALLORA SE la targa finisce con una cifra pari ALLORA usa l’auto ALTRIMENTI esci a piedi FINE SE ALTRIMENTI SE la targa finisce con una cifra dispari ALLORA usa l’auto ALTRIMENTI esci a piedi FINE SE FINE SE FINE Si noti che le istruzioni (nell’esempio “usa l’auto” e “esci a piedi”) sono scritte rientrate rispetto alle parole ALLORA e ALTRIMENTI e che queste ultime occupano da sole un’intera riga. Inoltre le parole delle coppie SE-FINE SE e ALLORA-ALTRIMENTI sono sempre incolonnate tra loro. Da ultimo, la parola SE deve sempre avere il corrispondente FINE SE, mentre è possibile trovare delle strutture di alternativa che richiedono soltanto le parole SE-A LLORA-FINE SE, senza, dunque, l’alternativa ALTRIMENTI. ...........................................................................................................................................
6.9 Struttura di ripetizione La struttura di ripetizione permette di eseguire una o più volte un blocco B di istruzioni. La ripetizione presenta due schemi di composizione:
ripetizione precondizionale (con controllo in testa); ripetizione postcondizionale (con controllo in coda).
Ripetizione precondizionale La struttura di ripetizione precondizionale ha, in pseudocodifica, la seguente sintassi: MENTRE condizione B FINE MENTRE Il blocco B viene eseguito una o più volte MENTRE la condizione è vera; quando questa diventa falsa, la ripetizione si interrompe. Il blocco B potrebbe anche non essere mai eseguito se la condizione non è mai vera. Chiariamo quanto detto con due esempi. 104
Unità didattica 6 - Algoritmi e pseudocodifica
Esempio Semaforo ............................................................................................................. Descrivere il comportamento da seguire davanti a un semaforo in funzione.
Pseudocodifica INIZIO MENTRE semaforo rosso Aspetta FINE MENTRE Attraversa FINE Così come la struttura di alternativa è racchiusa tra le parole SE e FINE SE, anche l’iterazione ha una parola iniziale, che è MENTRE, e parole di chiusura, ossia FINE MENTRE. Tali parole vanno incolonnate. L’iterazione continua se la condizione indicata dopo MENTRE risulta vera; quando la condizione diventa falsa l’esecuzione del programma procede con l’istruzione presente subito dopo le parole FINE MENTRE.
........................................................................................................................................... Esempio Somma primi 4 .................................................................................................... Descrivere l’algoritmo per calcolare la somma dei primi quattro numeri naturali.
Analisi I numeri da sommare sono: 1, 2, 3, 4. Data la variabile somma, inizializzata con zero (somma ← 0) deve essere eseguita la seguente sequenza di istruzioni: 1 somma ← somma + 1 2 somma ← somma + 2 3 somma ← somma + 3 4 somma ← somma + 4 con l’istruzione n. 4 la variabile somma contiene il risultato. Notiamo che le quattro istruzioni sono tutte del tipo: somma ← somma + k dove k è una variabile a cui assegnare, di volta in volta, i valori 1, 2, 3, 4.
Pseudocodifica output : somma variabile di lavoro : k INIZIO somma ← 0 k ← 1; MENTRE k <= 4 somma ← somma + k k←k+1 FINE MENTRE scrivi (“risultato = ”, somma) FINE
105
Sezione 3 - Organizzazione degli algoritmi
L’esecutore, per realizzare la ripetizione MENTRE, deve: controllare se la condizione di controllo k <= 4 è verificata; eseguire, se k <= 4, le istruzioni del ciclo: somma ← somma + k; k ← k + 1; tra queste istruzioni ve ne deve essere sempre una, detta istruzione di modifica, che modifica il valore della variabile di controllo (in questo caso k); nell’esempio tale istruzione è: k←k+1 che, a ogni ripetizione incrementa di una unità il valore di k; ripetere le azioni dei punti a) e b), uscendo dal ciclo di ripetizione quando la condizione k <= 4 è falsa.
Le istruzioni fuori ciclo: somma ← 0 e k ← l che inizializzano le due variabili somma e k, prendono il nome di istr uzioni di inizializzazione del ciclo. ...........................................................................................................................................
Struttura di ripetizione postcondizionale
La struttura di ripetizione postcondizionale, ha in pseudocodifica la seguente sintassi: RIPETI B FINCHÉ condizione
Il blocco B viene eseguito una o più volte FINCHÉ la condizione è falsa; quando la condizione di venta vera, la ripetizione si interrompe: il blocco B viene quindi ripetuto fino a quando la condizione indicata dopo FINCHÉ da falsa diventa vera. In questa ripetizione, il blocco B viene eseguito almeno una volta perché il controllo viene dopo il blocco B, a differenza della ripetizione MENTRE nella quale il blocco di controllo viene prima del blocco di istruzioni da eseguire. Esempio Asso di cuori ........................................................................................................
Determinare, all’interno di un mazzo di car te, in quale posizione si trova l’asso di cuori. Analisi
Il problema si risolve concretamente scoprendo una carta alla volta e contando le car te che man mano vengono scoperte. Se si vuole formalizzare l’algoritmo, una soluzione può essere la seguente. Pseudocodifica
INIZIO conta ← 0 RIPETI scopri una carta conta ← conta + 1 FINCHÉ è l’asso di cuori Scrivi (“L’asso di cuori si trova alla posizione = ”, conta) FINE Anche in questo caso le istruzioni da ripetere sono racchiuse da una parola iniziale (RIPETI) e da una parola finale (FINCHÉ). ........................................................................................................................................... 106
Unità didattica 6 - Algoritmi e pseudocodifica
Esempio Somma primi 4bis................................................................................................
L’algoritmo per il calcolo della somma dei primi quattro numeri naturali, precedentemente descritto, può essere modificato come segue. Pseudocodifica
output : somma variabile di lavoro : k INIZIO somma ← 0 k←4 RIPETI somma ← somma + K k←k–1 FINCHÉ k = 0 scrivi (“risultato = ”, somma) FINE Anche per l’istruzione RIPETI FINCHÉ si distinguono:
le istruzioni di inizializzazione del ciclo: somma ← 0; K ← 4; la condizione di controllo: k = 0; le istruzioni del ciclo: somma ← somma + k e k ← k – 1; l’istruzione di modifica: k ← k – 1.
Negli esempi relativi alla somma dei primi 4 numeri, possiamo notare che nelle due str utture di ripetizione MENTRE-FINE MENTRE e RIPETI-FINCHÉ sono sempre presenti:
una (ma in molti casi anche più di una) variabile di controllo inizializzata prima del ciclo, che è k, inizializzata con k ← 0 nel ciclo MENTRE e con k ← 4 nel ciclo RIPETI; una o più istruzioni che devono essere eseguite una o più volte, tra le quali c’è sempre una istruzione di modifica della variabile di controllo (k ← k + 1, k ← k –1, rispettivamente); la condizione di uscita dal ciclo, falsa per MENTRE e vera per FINCHÉ;
Le due iterazioni si differenziano perché in MENTRE la condizione viene controllata prima di ogni ciclo e in RIPETI tale controllo viene eseguito dopo l’esecuzione di ogni ciclo. Inoltre, se in MENTRE il blocco di istruzioni potrebbe non essere mai eseguito, in RIPETI il blocco è eseguito almeno una volta. ...........................................................................................................................................
6.10 Considerazioni sulla pseudocodifica Gli esempi visti nei paragrafi precedenti mostrano algoritmi risolutivi, descritti dalla pseudocodifica mediante costrutti che ben si adeguano alle strutture del linguaggio di programmazione e che perciò permettono di costruire programmi affidabili. La pseudocodifica non è altro che l’elencazione dei passaggi logici che conducono al risultato finale, espressi utilizzando termini di uso comune. Allo scopo di rendere comprensibile il linguaggio usato in questa prima fase di definizione dell’algoritmo, dato che ognuno di noi potrebbe utilizzare termini diversi per dire la stessa cosa, sono state definite alcune regole di massima, che sono elencate di seguito. 107
Sezione 3
- Organizzazione degli algoritmi
Ogni algoritmo deve iniziare con la parola INIZIO e finire con la parola FINE. La selezione è descritta dal costrutto SE ...ALLORA ... ALTRIMENTI ... FINE SE. La ripetizione precondizionale è descritta dal costrutto MENTRE ... FINE MENTRE. La ripetizione postcondizionale è descritta dal costrutto RIPETI ... FINCHÉ. È possibile assegnare a un blocco di istruzioni un nome che ne descrive le funzionalità, che potrà essere costituito anche da più parole: l’iniziale di ciascuna parola dovrà essere maiuscola. Per le strutture più complesse, che verranno descritte più avanti, si possono usare anche i costrutti SCEGLI ... CASO ... FINE SCEGLI; PER ... DA ... A ... PASSO ... FINE PER.
Nelle strutture di iterazione, le parole MENTRE e FINCHÉ sono seguite da una condizione. Anche se queste due parole hanno una funzione simile, il loro significato è ben diverso e va chiarito con precisione. MENTRE richiede che la ripetizione sia continuata quando la condizione che la segue è vera; la ripetizione si arresti quando la condizione è falsa. FINCHÉ richiede che la ripetizione sia continuata quando la condizione che la segue è falsa; la ripetizione si arresti quando la condizione è vera. Nella ripetizione precondizionale, la condizione che indica se si devono eseguire le istr uzioni da iterare è posta all’inizio; ne consegue che l’iterazione può non essere eseguita mai (se la condizione indicata dopo MENTRE non è verificata). Nella ripetizione postcondizionale la condizione che indica se si devono eseguire le istruzioni da ripetere è posta alla fine e, quindi, le istruzioni da iterare vengono sempre eseguite almeno una volta. Le parole MENTRE e FINCHÉ possono essere scambiate, se la condizione che le segue è sostituita con la sua negazione. L’esempio dell’algoritmo della ricerca dell’asso di cuori in un mazzo di carte può avere anche la seguente versione. INIZIO conta ← 0 RIPETI Scopri una carta conta ← conta + 1 MENTRE non è l’asso di cuori Comunica conta FINE In sintesi, valgono le seguenti uguaglianze MENTRE = FINCHÉ non e FINCHÉ = MENTRE non. L’algoritmo in pseudocodifica si presenta come uno schema che può essere codificato senza sforzo in un qualsiasi linguaggio di programmazione: per questo la sua costruzione riveste un’importanza primaria. Concludendo, l’algoritmo descritto in modo strutturato rappresenta l’anello di congiunzione tra l’analisi del problema e la stesura del codice del programma risolutivo, come si può vedere nella figura seguente. PROGRAMMATORE
Analisi del problema
108
ELABORATORE
Dati
Algoritmo in pseudocodifica
Codice di programma
Unità didattica 6 - Algoritmi e pseudocodifica
Esercizi Unità didattica 6 Si consideri l’algoritmo: // input a // output assoluto INIZIO leggi (a) SE a >= 0 ALLORA assoluto <-- a ALTRIMENTI assoluto <-- –a FINE SE scrivi (“valore assoluto = “, assoluto) FINE se a = –39, quale istruzione di assegnamento viene eseguita?
Descrivere l'algoritmo che, dati due interi a, b, scriva il valore assoluto della differenza tra il primo e il secondo numero.
Che cosa viene scritto se si esegue il segmento di algoritmo indicato? A <-- 5 SE A <= 2 ALLORA scrivi(“A è piccolo”) ALTRIMENTI SE A <= 10 ALLORA Scrivi(“A è medio”) ALTRIMENTI Scrivi(“A è grande”) FINE SE FINE SE
Che cosa viene scritto se si esegue il segmento di algoritmo indicato? i <-- 0 RIPETI i <-- i + 1 scrivi(i) FINCHE’ i < 5
Che cosa viene scritto se si esegue il segmento di algoritmo indicato? i <-- 0 RIPETI i <-- i + 1 109
Sezione 3 - Organizzazione degli algoritmi
Esercizi Unità didattica 6 scrivi(i) FINCHE’ i >= 5 Per gli esercizi dal numero 6 al numero 10 indicare i dati di input, di output e le variabili di lavoro e strut- turare l’algoritmo risolutivo .
In un piano cartesiano si conoscono le coordinate di un ver tice e la lunghezza del lato di un quadrato. Si consideri che i lati del quadrato sono paralleli agli assi e che il ver tice di cui si conoscono le coordinate è quello in basso a sinistra.
Dati i coefficienti di un'equazione di secondo grado, calcolare, se esistono, le due soluzioni reali. Data in input una serie di numeri contare quelli pari e quelli dispari; comunicare anche la percentuale sul totale dei pari e dei dispari. Suggerimento: si devono ripetere le seguenti istruzioni. leggi numero SE numero è pari ALLORA conta pari ALTRIMENTI conta dispari FINE SE Chiedi “serie finita ?” leggi risposta
Scrivere un programma che legge una serie di numeri di tipo double e, alla fine, comunicare la media dei numeri.
Dati tre numeri indicare il minimo e il massimo. Dato un elenco di persone di cui sono indicati il nome e l’anno di nascita, sc rivere il nome del più vecchio e del più giovane.
Dati due numeri interi e positivi calcolare il loro quoziente intero utilizzando solo le operazioni di somma e dif ferenza.
Dato il segmento di pseudocodifica: a = 15 SE a <= 2 ALLORA x = “a è piccolo” ALTRIMENTI SE a <= 10 ALLORA x = “a è medio” ALTRIMENTI x = “a è grande” FINE SE FINE SE Scrivi (x) che cosa compare a video? 110
Unità didattica 6 - Algoritmi e pseudocodifica
Esercizi Unità didattica 6 Date le coordinate di due punti del piano car tesiano calcolare la loro distanza. Date le coordinate di due punti del piano cartesiano determinare i coefficienti della retta passante per essi (controllare che la retta non sia verticale).
Dati il nome e l’età di due persone indicare prima il nome del più giovane e dopo quello del più vecchio. Dato in input un numero controllare se è compreso tra 0 e 10. Dato in input un numero controllare se è esterno all’intervallo [5, 10]. Scrivere un programma che legge tre numeri in virgola mobile e che sceglie e scrive il maggiore dei tre.
111
Unità didattica
7
Istruzioni di selezione
CHE COSA IMPARERAI A FARE $
Usare le istruzioni di selezione
$
Utilizzare le diverse varianti dell’istruzione if
$
Organizzare la scelta multipla con switch
$
Realizzare semplici applicazioni con algoritmi strutturati
$
Lavorare con i connettivi logici
CHE COSA DOVRAI STUDIARE $
Sintassi dell’istruzione if
$
Come organizzare if nidificati
$
Sintassi dell’istruzione switch
7
Unità didattica Istruzioni di selezione
I programmi scritti finora per illustrare in pratica i concetti che sono stati sviluppati non erano dotati di particolari strutture che consentissero un minimo controllo sul flusso del programma. Sono stati trattati anche tutti gli operatori relazionali e logici, ed è ora il momento di utilizzarli in contesti in cui poter apprezzare le loro effettive qualità nella gestione del flusso del codice.
7.1
Istruzione if
Gli operatori relazionali assumono la loro importanza dal fatto che consentono di prendere decisioni in base al loro risultato. “Prendere una decisione” significa essere in grado di eseguire un blocco di codice al verificarsi di una determinata condizione. C++, come tutti i linguaggi di alto livello, mette a disposizione l’istr uzione if che consente al programma di eseguire un blocco di codice solo in base al verificarsi di una determinata condizione. La sua sintassi è: if (condizione ) { //blocco di codice }
dove condizione è un test in cui vengono messi in relazione due operandi tramite un operatore relazionale o logico. L’istruzione if, con la sintassi vista sopra, realizza quanto visto in pseudocodifica per la struttura di selezione a una via. SE condizione ALLORA blocco di istruzioni FINE SE Quindi le istruzioni, all’interno del blocco, vengono eseguite solo se la condizione risulta verificata (true). Esempio If1 ........................................................................................................................
Verificare che un numero intero letto da tastiera sia maggiore di 50 con la scrittura di un messaggio in caso affermativo. Pseudocodifica
INIZIO leggi (numero) SE numero > 50 ALLORA scrivi (“il numero è maggiore di 50”) FINE SE FINE 113
Sezione 3
- Organizzazione degli algoritmi
Codice
If1.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
include using namespace std; //INIZIO int main () { //dichiara la variabile intera int numero; //chiedi numero cout << "\nInserisci un numero "; //leggi un numero cin >> numero; //controlla se è > di 50 if (numero>50) { //scrivi se la condizione è verificata (true) cout << "\nIl numero è maggiore di 50"; } //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
Alla riga 16 l’istruzione if controlla la condizione indicata tra parentesi tonde (numero>50): se il test restituisce il valore true, cioè se la condizione è verificata, viene eseguito il blocco di codice delimitato dalle parentesi graffe. In questo esempio il blocco di codice è composto solo da un’istruzione di scrittura a video, che notifica che il numero inserito risulta maggiore di 50. Se la condizione fosse risultata false il blocco di codice non sarebbe stato eseguito, proseguendo con l’elaborazione dell’istruzione immediatamente successiva (riga 23). ...........................................................................................................................................
Il codice del programma precedente informa l’utente solo al verificarsi della condizione; in caso contrario, non emette alcun messaggio. 114
Unità didattica 7 - Istruzioni di selezione
7.2
Istruzione if..else
La soluzione adottata nel codice del programma precedente non risulta ottimale; per questo è possibile utilizzare un altro costrutto, che estende l’istruzione if rendendola più efficiente: il costrutto else . La sintassi di un’istruzioni if completa è la seguente: ) if (condizione { //1° blocco di codice } else { //2° blocco di codice }
Qui, il 2o blocco di codice viene eseguito quando la condizione non è verificata (false), consentendo così l’esecuzione di codice in alternativa alla condizione imposta dall’if. In questo caso viene realizzata la struttura di selezione completa vista nell’Unità didattica 6, relativa alle strutture di controllo. SE condizione ALLORA B1 ALTRIMENTI B2 FINE SE Esempio If_Else ................................................................................................................... Verificare che un numero intero letto da tastiera sia maggiore di 50. In caso affermativo, scrivere un messaggio di conferma; nel caso contrario, segnalare che il numero non è maggiore di 50.
Pseudocodifica INIZIO leggi (numero) SE numero > 50 ALLORA scrivi (“Il numero è maggiore di 50”) ALTRIMENTI scrivi (“Il numero non è maggiore di 50”) FINE SE FINE Codice If_Else.cpp 1 2 3 4
#include using namespace std; //INIZIO int main ()
115
Sezione 3 - Organizzazione degli algoritmi
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
{ //dichiara una variabile intera int numero; //chiedi numero cout << "\nInserisci un numero "; //leggi un numero cin >> numero; //controlla se è > di 50 if (numero>50) { //scrivi che la condizione è verificata (true) cout << "\nIl numero è maggiore di 50"; } else { //scrivi che la condizione non è verificata (false) cout << "\nIl numero non è maggiore di 50"; } //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
L’istruzione alla riga 16 riporta la versione estesa dell’istruzione if dove, alla riga 21, compare “l’alternativa” else. Se il test risulta verificato viene eseguita l’istruzione di riga 19, altrimenti il flusso del programma “salta” alla riga 24, dove inizia il blocco di istruzioni relative all’alternativaelse. ...........................................................................................................................................
La condizione che segue la parola if può essere semplice, come negli esempi precedenti, oppure può essere una condizione composta da più enunciati legati tra loro dai connettivi logici And, Or, Xor e Not. Esempio If_And...................................................................................................................
Verificare che un numero letto da tastiera sia compreso nell’intervallo che va da 10 a 100. Il numero è compreso tra 10 e 100 quando risulta maggiore o uguale a 10 e minore o uguale a 100. 116
Unità didattica 7 - Istruzioni di selezione
Pseudocodifica
INIZIO leggi (numero) SE numero >= 10 AND numero <= 100 ALLORA scrivi (“Il numero è compreso nell’intervallo”) ALTRIMENTI scrivi (“Il numero è esterno all’intervallo”) FINE SE FINE Codice
If_And.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#include using namespace std; //INIZIO int main () { //dichiara una variabile intera int numero; //chiedi numero cout << "\nInserisci un numero "; //leggi un numero cin >> numero; //controlla se è compreso nell'intervallo if ((numero>=10)&&(numero<=100)) { //scrivi che la condizione è verificata (true) cout << "\nIl numero è compreso"; } else { //scrivi che la condizione non è verificata (false) cout << "\nIl numero non è compreso"; } //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
117
Sezione 3 - Organizzazione degli algoritmi
Analisi del codice
L’introduzione dell’istruzione if..else ha consentito un maggior controllo sul dato inserito da tastiera, verificando tutte le situazioni possibili. Infatti, alla riga 16 l’istruzione if, tramite l’operatore logico &&, verifica se il numero inserito è compreso nell’intervallo. Se il test risulta verificato viene eseguita l’istruzione di stampa alla riga 19, altrimenti il flusso del programma “salta” alla riga 24, dove inizia il blocco di codice relativo all’alternativa else. ...........................................................................................................................................
7.3
If nidificati
Riprendiamo l’esempio “If_Else” relativo all’utilizzo della struttura di alternativa. Si vuole verificare che un numero intero inserito da tastiera sia uguale a 50. Una possibile soluzione può essere quella di inserire un’altra istruzione if..else nel blocco di codice relativo all’else. Infatti, una volta stabilito che il numero inserito risulta “non maggiore di 50”, si hanno due possibilità: 1. 2.
il numero inserito è minore di 50; il numero inserito è uguale a 50.
Esempio Else_If ...................................................................................................................
Controllare se un numero inserito da tastiera è minore, maggiore o uguale a 50. Codice Else_If.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
118
#include using namespace std; //INIZIO int main () { //dichiara la variabile intera int numero; //chiedi numero cout << "\nInserisci un numero "; //leggi un numero cin >> numero; //controlla numero if (numero>50) { //è verificata la prima possibilità cout << "\nIl numero inserito è più grande di 50"; } else if (numero<50) //2a possibilità { //è verificata la seconda possibilità cout << "\nIl numero inserito è più piccolo di 50"; } else //3a possibilità
Unità didattica 7 - Istruzioni di selezione
27 28 29 30 31 32 33 34 35 36 37 38
{ //è verificata la terza possibilità cout << "\nIl numero inserito è uguale a 50"; } //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
Analisi del codice
L’istruzione if..else alla riga 16 è notevolmente cambiata e, tramite la nuova struttura, è stato possibile effettuare tutti i controlli che il problema richiedeva. Infatti, il primo controllo (riga 16) permette di informare l’utente, qualora sia verificato, che il numero inserito è maggiore di 50. Al contrario, il codice a partire dalla riga 21 consente di verificare, tramite l’inserimento di una nuova if..else , le altre due possibili situazioni, cioè se il numero inserito risulta inferiore a 50 o, tramite il costrutto else che inizia alla riga 26, se il numero inserito risulta sicuramente identico a 50. Si noti l’uso di parentesi graf fe dopo il costrutto else di riga 21: queste servono a racchiudere un blocco di codice composto da due o più istruzioni. In questo esempio non è necessario inserirle (anche se nessuno lo vieta), in quanto il blocco di codice relativo è composto da una sola istruzione. Le righe che vanno dalla 16 alla 31 si possono tranquillamente riscrivere come segue. ... //controlla numero if (numero>50) //è verificata la prima possibilità cout << "\nIl numero inserito è più grande di 50"; else if (numero<50) //2a possibilità //è verificata la seconda possibilità cout << "\nIl numero inserito è più piccolo di 50"; else //3a possibilità //è verificata la terza possibilità cout << "\nIl numero inserito è uguale a 50"; ...
L’importante è che tutto il codice risulti chiaro e semplice da leggere. ........................................................................................................................................... 119
Sezione 3
- Organizzazione degli algoritmi
7.4 Istruzione switch (selezione multipla) Lo schema organizzativo dell’alternativa a due vie non sempre risponde alle necessità che si possono incontrare nella stesura degli algoritmi. Per risolvere situazioni complesse da esprimere con la struttura di scelta è stato introdotto lo schema della scelta multipla. In pseudocodifica tale struttura può essere rappresentata nel modo seguente. SCEGLI variabile_di_controllo CASO lista valori_1: blocco istruzioni_1 ESCI; CASO lista valori_2: blocco istruzioni_2 ESCI; ... CASO lista valori_n: blocco istruzioni_n ESCI; ALTRIMENTI blocco istruzioni FINE SCEGLI La presenza della struttura di selezione multipla all’interno di un algoritmo determina, al momento dell’esecuzione del programma, le seguenti azioni:
se il valore delle variabili è presente in una delle liste di valori viene eseguito il blocco di istr uzioni corrispondente, fino alla prima istruzione ESCI; successivamente si procede con l’esecuzione della prima istruzione che segue la riga FINE SCEGLI; in caso contrario viene eseguito il blocco di istruzioni indicato dopo la riga ALTRIMENTI.
Se, per esempio, si deve associare a un numero compreso tra 1 e 7 il relativo giorno della settimana, utilizzando l’istruzione if dobbiamo utilizzare sette costrutti if..else, controllando sempre che il numero inserito non sia esterno ai limiti numerici imposti. Nel linguaggio C++ la scelta multipla è realizzata dall’istruzione switch, che ha la sintassi riportata di seguito. switch (variabile_di_controllo )
{ case valori1:
istruzioni1; break; case valori2: istruzioni2; break; ... case valorin:
istruzionin; break; default : istruzioni; } 120
Unità didattica 7 - Istruzioni di selezione
Dopo la parola chiave switch viene indicato il nome della variabile di controllo (detta anche variabile selettore ), di cui si deve controllare il valore per decidere quale strada seguire tra quelle possibili. Accanto ai valori previsti è consigliabile non dimenticare di scrivere le istruzioni da eseguire nel caso in cui la variabile non assuma nemmeno uno dei valori indicati, inserendo la parola chiave default; se tale etichetta non è presente e non vi è stata alcuna corrispondenza tra la variabile di controllo e le etichette case il controllo passa alla prima istruzione che segue il costrutto switch. È bene inserire sempre la clausola default nel costrutto switch in quanto, grazie a essa, si possono indicare istruzioni particolari nel caso in cui non esista alcuna corrispondenza tra la variabile di controllo e i valori presenti nei case. Viene generato un errore di compilazione se due o più etichette case fanno riferimento al medesimo insieme di valori. Per meglio chiarire la funzionalità del costrutto switch codifichiamo l’esempio della corrispondenza tra un numero inserito da tastiera e il relativo giorno della settimana. Esempio Settimana ............................................................................................................ Visualizzare il giorno della settimana corrispondente a un numero inserito da tastiera, compreso tra 1 e 7.
Codice Settimana.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#include using namespace std; //INIZIO int main () { //dichiara una variabile intera int numero; //chiedi un numero cout << "\nInserisci un numero intero (da 1 a 7): "; //leggi numero cin >> numero; switch (numero) { case 1: cout <<"\nE' Lunedì "; break; case 2: cout <<"\nE' Martedì "; break; case 3: cout <<"\nE' Mercoledì "; break; case 4: cout <<"\nE' Giovedì "; break; case 5: cout <<"\nE' Venerdì "; break; case 6: cout <<"\nE' Sabato"; break; case 7: cout <<"\nE' Domenica"; break; default: cout <<"\nNumero non corrispondente!!!"; break; } //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
121
Sezione 3 - Organizzazione degli algoritmi
Prova di esecuzione
Analisi del codice
La variabile numero diventa il selettore del costrutto switch, che tenta di stabilire una corrispondenza con le costanti appartenenti ai case. In questo esempio le costanti indicate nei case contemplano valori che vanno da 1 a 7 (dalla riga 17 alla riga 24); esse servono per verificare la corrispondenza con il numero inserito e, laddove tale corrispondenza compare, vengono eseguite tutte le istruzioni relative. Se la corrispondenza non è verificata entra in gioco il costrutto default (riga 24) che avverte, con un messaggio, che non esiste alcuna corrispondenza. ...........................................................................................................................................
7.5 Raggruppamento dei case Come si è avuto modo di studiare nel paragrafo precedente, l’utilizzo dell’istruzione switch risulta essere molto versatile quando si devono effettuare molte scelte in base ai valori che assume la variabile di controllo. Può capitare, a volte, che a un valore della variabile di controllo vengano associati più costrutti case. C++ prevede questa possibilità, tramite la sintassi indicata di seguito. switch (variabile_di_controllo ) { case valori1: case valori2: istruzioni1; break; case valori4: case valori5: istruzioni5; break; ... ... default: istruzioni; break; }
I valori dei due primi case, pur essendo differenti, sono stati raggruppati: ciò significa che, al verificarsi della corrispondenza di uno dei due valori con quello della variabile variabile_di_controllo, viene eseguito il medesimo codice. Lo stesso concetto vale per i case 4 e 5. Esempio PariDispari ........................................................................................................... Determinare se un numero inserito da tastiera, compreso nell’inter vallo che va da 1 a 10, è pari o dispari.
122
Unità didattica 7 - Istruzioni di selezione
Codice PariDispari.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
#include using namespace std; //INIZIO int main () { //dichiara una variabile intera int numero; //chiedi un numero cout << "\nInserisci un numero intero (da 1 a 10): "; //leggi numero cin >> numero; switch (numero) { //case che individuano i numeri dispari case 1: case 3: case 5: case 7: case 9: cout <<"\nNumero inserito dispari\n"; break; //case che individuano i numeri pari case 2: case 4: case 6: case 8: case 10: cout << "\nNumero inserito pari\n"; break; default: cout << "\nNumero esterno ai limiti!!!\n"; break; } //salta due righe cout << endl << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Prova di esecuzione
123
Sezione 3
- Organizzazione degli algoritmi
Analisi del codice
In questo semplice programma sono stati raggruppati i case che riguardano i numeri dispari, dalla riga 19 alla riga 23, e i case che riguardano i numeri pari, dalla riga 27 alla riga 31. In questo modo qualsiasi numero compreso nell’intervallo trova corrispondenza attraverso il gruppo di case di appartenenza; se il numero digitato non rientra nell’intervallo richiesto il controllo passa alla clausola default, alla riga 34, che avverte l’utente dell’errore commesso. La variabile di controllo dell’istruzione switch non può essere di tipo qualsiasi: sono gestiti tutti i tipi interi, il tipo carattere e gli enumeratori (che vedremo più avanti). Si ricorda che i tipi interi sono int, long e char, mentre una stringa è una sequenza di caratteri racchiusa tra virgolette. Se il tipo della variabile di controllo è diverso dai valori dei case si rende necessario un suo casting esplicito, allo scopo di consentirne la cor rispondenza. ...........................................................................................................................................
124
Unità didattica 7 - Istruzioni di selezione
Esercizi Unità didattica 7 Scrivere la sintassi del costrutto if..else . Nel seguente frammento di codice c’è un error e: individuare quale. ... if (num>Max); { cout << “\nIl numero inserito è maggiore di” << Max; }
Che cosa viene visualizzato utilizzando il seguente frammento di codice? ... int num = 5; if ((num>=10)&&(num<=20)) if (num % 2 == 0) cout << “\nIl numero è pari”; else cout << “\nIl numero è dispari”; else cout << “\nNumero non compreso nell’intervallo”; ...
Si vuole calcolare l’imponibile IVA e il totale di un importo inserito da tastiera. Se l’importo risulta minore o uguale a euro 150.00 l’IVA deve essere del 16%; se l’importo è superiore, l’IVA deve essere del 20%.
Scrivere il codice C++ per risolvere il problema seguente. In un piano cartesiano si conoscono le coordinate di un ver tice e la lunghezza del lato di un quadrato. Si consideri che i lati del quadrato sono paralleli agli assi e che il ver tice di cui si conoscono le coordinate è quello in basso a sinistra.
Scrivere il codice C++ per risolvere il pr oblema seguente: dati i coef ficienti di un'equazione di secondo grado calcolare, se esistono, le due soluzioni reali.
A che cosa serve l’istruzione switch? A che cosa servono i costrutti case? Perché bisogna inserire la parola chiave break alla fine dei case? A che cosa serve la clausola default? Perché è consigliabile inserire la clausola default? Scrivere un programma che associ a ogni mese dell’anno, inserito da tastiera e indicato da un numero intero da 1 a 12, il nome corrispondente.
Scrivere un programma che notifichi a quale dei quattro trimestri appar tiene un mese inserito da tastiera. 125
Sezione 3 - Organizzazione degli algoritmi
Esercizi Unità didattica 7 Date le coordinate di due punti sul piano cartesiano, determina il coefficiente angolare della retta passante per essi (controlla che la retta non sia verticale).
Dato in input un numero, controlla se è esterno all’intervallo [5;10]. Dato in input un numero intero compreso tra 0 e 15, s crivi la sua rappresentazione esadecimale. Dato in input un numero intero compreso tra 0 e 255, scrivi la s ua rappresentazione esadecimale (suggerimento: dividere il numero dato per 16 due volte: i due resti, pres i in ordine inverso, determinano la sequenza delle cifre esadecimali).
Dati in input tre numeri, scrivi il minimo. Dati in input tre numeri, indica il valore compreso tra il minimo e il massimo. Scrivere il codice C++ per risolvere il pr oblema seguente: dati i coef ficienti di un'equazione di secondo grado calcolare, se esistono, le due soluzioni reali.
126
Unità didattica
8
Istruzioni di ripetizione CHE COSA IMPARERAI A FARE $
Utilizzare le istruzioni di ripetizione nelle loro diverse forme
$
Sfruttare le istruzioni di controllo più complesse per la definizione di algoritmi complessi
$
Nidificare le strutture
CHE COSA DOVRAI STUDIARE $
Istruzione while
$
Istruzione do..while
$
Istruzione for
$
Modalità di interruzione
8 Istruzioni di ripetizione
Unità didattica
Nell’Unità didattica precedente sono state studiate tutte quelle istruzioni che, in qualche modo, possono modificare il flusso di programma. Spesso, però , un programmatore ha la necessità di ripetere un blocco di codice più volte. All’interno di una applicazione le istruzioni che consentono di ripetere un blocco di codice vengono chiamate istruzioni di ripetizione o iterative o, più grossolanamente, cicli. Queste istruzioni, insieme alle strutture di controllo, rivestono un’enorme importanza nella programmazione, quale che sia il linguaggio di codifica. Il C++ mette a disposizione tre tipi di istruzioni di ripetizione: while; do..while; for.
8.1
Istruzione while
L’istruzione while viene utilizzata per ripetere un blocco di codice fino a quando la condizione di controllo presente risulta non verificata ( false). È possibile (nel caso in cui l’istruzione di controllo risulti non verificata fin dalla sua prima valutazione) che le istruzioni che costituiscono il corpo del ciclo non vengono mai eseguite. La sua sintassi è riportata di seguito. while (condizione ) { blocco_di_istruzioni; }
L’istruzione while valuta la condizione tra parentesi, chiamata condizione di controllo e, se risulta verificata (true), vengono eseguite le istruzioni all’interno delle parentesi graffe. Una volta eseguite tutte le istruzioni, la condizione di controllo viene nuovamente valutata e, solo se risulta non verificata, il flusso del programma prosegue con l’istruzione successiva al blocco while , altrimenti vengono nuovamente eseguite le istruzioni al suo interno e così via. L’istruzione while realizza il costrutto in pseudocofica visto per la struttura di ripetizione precondizionale, che ha la forma seguente. MENTRE condizione blocco di istruzioni FINE MENTRE Facciamo un esempio. 128
Unità didattica 8 - Istruzioni di ripetizione
Esempio Tabellina1 ............................................................................................................ Scrivere un programma che consenta di calcolare la “tabellina” di un numero inserito da tastiera compreso tra 1 e 10. Per “tabellina” di un numero si intendono i suoi multipli da uno a dieci.
Pseudocodifica //input num //output num //variabile di lavoro i INIZIO i ←1 leggi (num) MENTRE i<= 10 scrivi (i, num, num * i) i←i+1 FINE MENTRE FINE
Codice Tabellina1.cpp 1 2
#include using namespace std;
3 4 5 6 7
//INIZIO int main () { //definisci il contatore i int i=1;
8 9 10 11
//dichiara una variabile intera. chiedi e leggi un numero int num; cout << "\nInserisci un numero intero (da 1 a 10): ";
12 13 14 15
cin >> num;
16 17
{
//ciclo while while(i<=10) cout <<"\n" << " " << i << " " << num << " " << num*i;
18 19 20 21 22 23
//incrementa il contatore ++i; } //salta due righe cout << endl << endl;
24 25
//arresta l'esecuzione del programma system ("pause");
26 27 28
//termina il programma return 0; }
129
Sezione 3 - Organizzazione degli algoritmi
Prova di esecuzione
Analisi del codice
Alla riga 7 è stata dichiarata e inizializzata una variabile di tipo intero, che viene usata come contatore relativo ai 10 numeri che compongono la tabellina. Dalla riga 10 alla riga 12 viene richiesto e acquisito un numero intero compreso da 1 a 10, di cui calcolare i valori della tabellina. Alla riga 15 inizia il ciclo while, che ha come condizione un’espressione che consente di controllare che il contatore risulti inferiore o uguale a 10. Il corpo del ciclo è costituito da due semplici istruzioni. La riga 17 serve per visualizzare il contatore, il numero inserito e il risultato del calcolo. Alla riga 19 viene incrementata la variabile contatore i. Il ciclo while si ripete fintanto che il contatore risulta inferiore o uguale a 10, come è stabilito dalla condizione. Nell’ultima iterazione, cioè quando il contatore ha assunto il valore 10, viene visualizzato l’ultimo risultato e viene incrementato il contatore stesso, che assume quindi il valore 11. La condizione, a questo punto, non è più verificata e il ciclo while termina la sua esecuzione, passando il controllo alla prima istruzione utile che lo segue. ...........................................................................................................................................
8.2
Tabulazione
Il programma visto nel paragrafo precedente produce risultati corretti, ma non li presenta in modo leggibile e ordinato: i numeri disposti in colonna sono disallineati, mentre dovrebbero essere incolonnati a destra, inoltre le colonne dei risultati sono prive di un’intestazione che chiarisca il significato dei numeri presentati in output. Per una prima soluzione del problema indicato si deve ricorrere agli strumenti di formattazione visti nel paragrafo 4.2, “ Output formattato”. Nell’esempio che segue useremo il metodo setw(n) , che imposta il numero di caratteri occupati dal risultato, anche detto “ampiezza”. Esempio Tabellina2 ............................................................................................................ Scrivere un programma che consenta di calcolare e di scrivere la “tabellina” di un numero inserito da tastiera, compreso tra 1 e 10. Presentare i risultati incolonnati a destra. Tabellina2.cpp 1 2
130
#include #include "iomanip"
Unità didattica 8 - Istruzioni di ripetizione
3 4 5 6 7
using namespace std; // INIZIO int main () { //definisci il contatore i
8 9 10
int i=1;
11 12 13 14 15
int num; cout << "\nInserisci un numero intero (da 1 a 10): "; cin >> num;
16 17 18 19
cout <<"\n"<
20 21 22 23
{
24 25 26 27 28
}
29 30 31
system ("pause"); //termina il programma return 0;
32
//chiedi e leggi il dato di input
//scrivi intestazione della tabellina
//ciclo while while(i<=10) cout <<"\n" << setw(5)<< i << setw(8) << num << setw(8)<< num*i; //incrementa il contatore ++i;
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma
}
Prova di esecuzione
Analisi del codice
Rispetto all’esempio precedente il codice presenta tre novità. 1. Alla
riga 2 viene richiamata la libreria iomanip, che contiene i metodi utili per la manipolazione dei dati di input/output. 2. Alla riga 21 l’istruzione di scrittura dei dati contiene il metodo setw(...) , che specifica il numero di caratteri che devono essere destinati alla visualizzazione di un valore: per ogni da131
Sezione 3 - Organizzazione degli algoritmi
to deve essere sempre richiamato il metodo setw() e non è possibile distribuirne l’effetto su più dati. Nel nostro esempio per la visualizzazione del contatore vengono riservati 6 caratteri, per quella degli altri due dati 8 caratteri. 3. Alla riga 16 la medesima operazione di impostazione dell’ampiezza di scrittura viene eseguita sulle stringhe di intestazione delle colonne, in modo tale che sia i dati numerici della tabella, sia le intestazioni risultino tutti incolonnati sulla destra. Si noti che la riga che contiene le intestazioni deve essere scritta una volta sola ed è, per tanto, posta all’esterno del ciclo while; al contrario, le istruzioni di scrittura a video dei risultati devono essere ripetute più volte, quindi sono poste all’interno del blocco di istr uzioni che è controllato dal ciclo while.
...........................................................................................................................................
8.3
Istruzione do..while
Con l’uso dell’istruzione while , se la condizione di controllo non risulta verificata, le istruzioni contenute nel ciclo non vengono eseguite. A volte però si ha la necessità che il blocco di codice interno al ciclo venga eseguito almeno una volta. C++ propone un’istruzione che consente una tale eventualità: il ciclo do..while. Di seguito la sua sintassi. do {
blocco_di_istruzioni } while (condizione );
L’istruzione do..while viene utilizzata per ripetere un blocco di codice fino a che la condizione di controllo presente alla fine del blocco di istruzioni risulta non verificata (false). Il ciclo do..while garantisce almeno una iterazione; ciò si evince dal fatto che la condizione di controllo è posta alla fine di tutte le istruzioni, dopo la parola chiave do. Infatti, prima vengono eseguite le istruzioni contenute nel blocco di codice, poi viene eseguito il test di controllo e, se risulta verificato, il ciclo si ripete, altrimenti si esce dal ciclo proseguendo con l’istruzione immediatamente successiva. In realtà, tale caratteristica non sempre risulta soddisfacente perché, di solito, le iterazioni devono essere eseguite solo nel caso in cui risulti immediatamente verificata la condizione. Nel ciclo do..while, le istruzioni vengono eseguite sempre almeno una volta, a prescindere dal risultato del test di controllo. Il costrutto do..while realizza la struttura che in pseudocodifica viene chiamata ripetizione postcondizionale, che è rappresentata dallo schema seguente. RIPETI blocco di istruzioni MENTRE condizione Esempio CalcolaMedia .....................................................................................................
Scrivere un programma che legga da tastiera la sequenza dei voti assegnati alle verifiche di uno studente in una certa materia e alla fine ne calcoli la media. Dopo l’acquisizione di ogni voto, il programma deve chiedere all’utente se l’elenco dei voti è finito: quando la risposta è positiva il ciclo termina e si procede al calcolo della media, mentre se la risposta è negativa vengono ripetute le operazioni di acquisizione di un voto. 132
Unità didattica 8 - Istruzioni di ripetizione
Pseudocodifica
INIZIO //input voto; risposta; //output media; //variabili di lavoro i=0 //contatore i somma ← 0 //totalizzatore RIPETI chiedi e leggi un voto somma = somma + voto //totalizza i voti i=i+1 //incrementa contatore chiedi se elenco terminato e leggi risposta MENTRE risposta = ‘n’ media = somma / i; //calcola media scrivi media FINE Codice
CalcolaMedia.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
#include using namespace std; //INIZIO int main () { //INPUT int voto; char risposta; //OUTPUT double media; //variabili di lavoro int i=0; double somma=0;
//contatore i //totalizzatore
do { //chiedi e leggi un voto cout << "\nInserisci un voto (da 1 a 10): "; cin >> voto; //totalizza i voti somma += voto; //incrementa contatore i++; //chiedi se elenco terminato e leggi risposta cout << "\nl'elenco dei voti è terminato (s/n)
";
133
Sezione 3
- Organizzazione degli algoritmi
31 32 33 34 35
cin >> risposta; } //controlla se ripetere il blocco while(risposta == 'n');
36 37 38
//calcola media media = somma / i; //scrivi media
39 40 41 42 43
cout << "\n\n media = " << media;
44 45 46 47
system ("pause"); //termina il programma return 0;
//salta due righe cout << endl << endl; //arresta l'esecuzione del programma
}
Prova di esecuzione
Analisi del codice
Alle righe 7 e 8 vengono definite le variabili utili per contenere i dati di input, ovvero quelli inseriti da tastiera. Alla riga 11 viene definita la variabile di output media. Le variabili di lavoro i e somma, definite alle righe 14 e 15, servono rispettivamente per totalizzare e per contare i voti. Il blocco di istruzioni compreso tra la riga 17 (do) e la riga 34 while ( (..) ) ha la funzione di richiedere un voto, sommare il voto nel totalizzatore somma , contare i voti e chiedere all’utente se l’elenco dei voti è terminato; il blocco viene ripetuto ogni volta che l’utente risponde “n”. La decisione se ripetere le istruzioni del blocco viene presa in base all’esito del test indicato alla riga 34: se risposta è “n” il test è verificato e l’esecuzione del programma riprende dalla riga 17, altrimenti il controllo dell’esecuzione passa all’istruzione successiva (dalla riga 35 in avanti). ...........................................................................................................................................
8.4
Istruzione for
Le strutture while e do..while risultano utili quando non si conosce a priori quante volte devono essere eseguite le istruzioni da ripetere. Se, invece, si conosce il numero di volte per cui deve essere ripetuto un gruppo di istruzioni, è meglio usare la struttura for. Le istruzioni da ripetere sono 134
Unità didattica 8 - Istruzioni di ripetizione
indicate dopo la parola for e sulla riga di for viene indicato il criterio per contare il numero di ripetizioni. Diversamente dal costrutto while, nel ciclo di tipo for la variabile contatore viene incrementata o decrementata a ogni ripetizione delle istruzioni al suo interno. In pseudocodifica la ripetizione enumerativa si indica con le parole indicate di seguito. PER contatore DA inizio A fine [con PASSO incremento] istruzioni FINE PER Tale struttura richiede che la variabile “contatore” venga impostata al valore di inizio e fa in modo che, ogni volta che vengono eseguite le istruzioni comprese tra PER e FINE PER, la variabile “contatore” venga incrementata di un valore uguale a “incremento”. La parte dell’istruzione compresa tra parentesi quadre può essere omessa: in tal caso l’incremento è da considerarsi uguale a 1. Nella maggior parte dei casi, la struttura PER si presenta in una forma semplificata rispetto a quella vista: per esempio, se si vuole ripetere un gruppo di istruzioni per 10 volte, basta scrivere: PER contatore DA 1 A 10 istruzioni RIPETI Esaminando il ciclo while del programma di esempio tabellina.cpp (e in particolare il codice relativo al ciclo while), si possono riscontrare alcuni elementi di base che ci portano alla definizione del ciclo for. //definisci il contatore i int i=1; .... //ciclo while while(i<=10) { cout <<"\n" << " " << i << " " << num << " " << num*i; //incrementa il contatore ++i; }
Gli elementi di base sono i seguenti: 1. dichiarazione e inizializzazione di un contatore (int i=1; ); ); 2. verifica condizionale del contatore (i<=num 3. istruzioni di manipolazione del dato (cout << ...; ); 4. incremento del contatore (i++; ).
Il C++, come tutti i linguaggi di alto livello, mette a disposizione un’istruzione che riunisce tre dei quattro punti precedenti in un unico costrutto: il ciclo for. La sua sintassi è riportata di seguito. ) for (inizializzazione; controllo; incremento {
blocco di istruzioni }
In cui ogni espressione che fa parte della condizione, che viene indicata tra parentesi tonde, deve essere separata obbligatoriamente da un punto e virgola. 135
Sezione 3 - Organizzazione degli algoritmi
Si ha che: inizializzazione rappresenta l’espressione in cui viene dichiarato e inizializzato
un con-
tatore, per esempio int i=1;; condizione rappresenta l’espressione in cui compare la condizione di controllo, per esempio i<=num;; incremento , infine, rappresenta l’espressione che effettua l’incremento del contatore, per esempio i++;. Volendo quindi riscrivere il ciclo utilizzando l’istr uzione for, il ciclo while precedente assume la forma seguente: for (int i=1; i<=num; i++) { fatt*=i; }
L’istruzione for viene utilizzata per ripetere un blocco di codice per un numero stabilito di volte.
Ecco spiegato nel dettaglio come funziona il ciclo for. In primo luogo esegue l’espressione di inizializzazione del contatore e lo fa, ovviamente, solo la prima volta; poi controlla la condizione e, se questa risulta verificata, vengono elaborate tutte le istruzioni del blocco di codice interno, altrimenti vengono eseguite le istruzioni successive al ciclo. Se la condizione di controllo risulta verificata, dopo che le istruzioni interne al ciclo sono state eseguite il flusso ritorna all’istruzione for , che incrementa il contatore ed esegue nuovamente il test di controllo. Tutto il processo si ripete fino a che la condizione di controllo non risulta più verificata. Esempio Fattoriale .............................................................................................................
Scrivere un programma che consenta di calcolare il fattoriale di un numero non maggiore di 10 inserito da tastiera.
Si tratta di moltiplicare la sequenza degli interi che vanno da 1 a un numero intero inserito da tastiera. Un simile calcolo viene effettuato mediante un ciclo che ripete la stessa operazione, che consiste nell’esecuzione di una moltiplicazione tante volte fino ad arrivare al numero i nserito. In matematica, questo genere di calcolo prende il nome di fattoriale. Per esempio, il fattoriale del numero 5 è 120 e si indica con 5!; il calcolo da eseguire è 1 * 2 * 3 * 4 * 5 = 120. Il calcolo del fattoriale viene effettuato moltiplicando tra loro il numero dato e tutti i numeri precedentemente forniti. Per esempio: il fattoriale di 6 (si scrive 6!) è 720 cioè, 1 * 2 * 3 * 4 * 5 * 6. Pseudocodifica
//input num //output fatt //variabili di lavoro i INIZIO 136
Unità didattica 8 - Istruzioni di ripetizione
fatt ← 1 scrivi (“\nInserisci un numero intero (Max 10) = ”) leggi (num) PER i DA 1 A num fatt ← fatt * i FINE PER scrivi (fatt) FINE Codice Fattoriale.cpp 1
#include
2 3 4 5
using namespace std; //INIZIO int main () {
6 7 8 9
//input int num; //output int fatt;
10 11 12
//chiedi e leggi il numero su cui calcolare il fattoriale cout << "\nInserisci un numero intero (Max 10) = ";
13 14
cin >> num;
15 16 17 18
//inizializzazione fatt = 1; //esegue il ciclo for for (int i=1; i<=num; i++)
19 20
{
21 22 23 24 25 26
}
27 28
cout << endl << endl; //arresta l'esecuzione del programma
29 30 31 32
system ("pause"); //termina il programma return 0;
fatt *= i;
//scrivi il risultato cout << "\nFattoriale = " << fatt; //salta due righe
}
Prova di esecuzione
137
Sezione 3
- Organizzazione degli algoritmi
Analisi del codice
Dopo aver dichiarato le variabili e dopo aver letto il numero da tastiera, alle righe 12 e 13, viene eseguito il ciclo for dove compaiono le espressioni che consentono l’iterazione (righe dalla 18 alla 21): infatti, subito dopo la parola for compare la dichiarazione e l’inizializzazione del contatore int i=1; , il controllo i<=num; e l’incremento del contatore i++. Il blocco di codice del ciclo contiene solo l’operazione di calcolo del fattoriale fatt *= i; , alla riga 20. A questo punto ci si potrebbe chiedere: “Perché usare un ciclo for piuttosto che un while?”. Le istruzioni iterative while, do e for consentono di creare cicli ciascuno con caratteristiche strutturali differenti, che si adeguano a situazioni specifiche. Quando in un programma si deve utilizzare un’istruzione di ripetizione e si è a conoscenza dell’esatto numero di cicli da ef fettuare, come nell’esempio appena visto, sarebbe meglio utilizzare un ciclo for; se, invece, il numero di iterazioni dipende da un valore che il programmatore non può stabilire a priori, come nel caso del programma CalcolaMedia.cpp (in cui la condizione di terminazione del ciclo poteva essere verificata anche dopo migliaia di inserimenti di voti), si dovrebbe usare un ciclo while; infine, se il blocco di codice da iterare deve essere eseguito almeno una volta, è conveniente utilizzare il ciclo do..while. Per concludere, possiamo dire che la scelta del tipo di ciclo da eseguire è strettamente correlata alla natura del problema e all’algoritmo utilizzato per risolverlo. ...........................................................................................................................................
138
Unità didattica 8 - Istruzioni di ripetizione
Esercizi Unità didattica 8 Dato il codice: int i = 0; do i++; while (i >= 5) cout << i;
quale valore viene scritto?
Dato il codice: int i = 0; while (i >= 5) { i++; } cout << i;
quale valore viene scritto?
Dato il codice: int i = 0; do i++; while (i < 5) cout << i;
quale valore viene scritto?
Dato il codice: int i = 0; while (i < 5) { i++; } cout << i;
quale valore viene scritto?
Dato il codice: int i = 0; do i++; cout << i; while (i >= 5)
quali valori vengono scritti?
139
Sezione 3 - Organizzazione degli algoritmi
Esercizi Unità didattica 8 Dato il codice: int i = 0; while (i >= 5) { i++; cout << i; }
quali valori vengono scritti?
Dato il codice: int i = 0; do i++; cout << i; while (i < 5)
quali valori vengono scritti?
Dato il codice: int i = 0; while (i < 5) { i++; cout << i; }
quali valori vengono scritti?
Dato un numero intero N, calcolare la somma dei quadrati degli interi minori o uguali a N. Dato un numero intero N, calcolare il fattoriale di N (fattoriale di N : N! = 1 * 2 * 3* . . . N). Contare i divisori di un numero intero (N è divisore di M, se M % N = 0). Determinare se un numero intero è primo (un numero è primo se nessun numero minore o uguale alla sua metà è divisore del numero dato (N è divisore di M, se M % N = 0).
Dato l'elenco dei voti di uno studente, calcolare la media dei voti. Dati in input il valore di un deposito bancario e il tasso di interesse annuo, calcolare gli interessi maturati dopo 5 anni.
Qual è la sintassi dell’istruzione while? Qual è la sintassi del ciclo do? 140
Unità didattica 8 - Istruzioni di ripetizione
Esercizi Unità didattica 8 Dato l'elenco dei voti di uno studente, calcolare la media dei voti. Si devono ripetere le seguenti istru-
zioni: leggi voto somma ← somma + numero Chiedi “elenco finito ?” leggi risposta
Data in input una serie di numeri, contare quelli pari e quelli dispari; comunicare anche la percentuale
sul totale dei pari e dei dispari. Si devono ripetere le seguenti istruzioni: leggi numero SE numero è pari ALLORA conta pari ALTRIMENTI conta dispari FINE SE Chiedi “serie finita ?” leggi risposta
Indicare la sintassi del ciclo for. È dato in input un elenco di N studenti (con N dato in input): per ogni studente si conoscono il nome, il
sesso e il voto in informatica. Calcolare la media dei voti di tutti gli studenti e la media dei voti dei maschi e delle femmine. Con i dati di input dell’esercizio precedente scrivere il nome dello studente che ha il voto più alto tra gli
studenti maschi, e il nome della studentessa che ha il voto più alto tra le studentesse.
141
Unità didattica
9
Le funzioni
CHE COSA IMPARERAI A FARE $
Organizzare in modo logico e razionale un programma suddiviso in funzioni
$
Utilizzare la sintassi per la dichiarazione di una funzione
$
Distinguere le variabili locali da quelle globali
$
Stabilire l’esatta collocazione delle variabili in ambiti funzionali
$
Utilizzare i valori di ritorno di una funzione
$
Impiegare funzioni che usufruiscono di parametri
$
Gestire i parametri per valore e per riferimento
CHE COSA DOVRAI STUDIARE $
Necessità delle funzioni
$
Definizione e uso di funzioni semplici
$ Ambito
delle variabili: variabili locali e globali
$
Funzioni: valori di ritorno
$
Sintassi per il passaggio di parametri
$
Passaggio di parametri per riferimento
$
Funzioni matematiche
Unità didattica
9 Le funzioni
9.1
Le funzioni
Questa Unità didattica esamina le caratteristiche delle funzioni C++, incluso il passaggio di parametri, la restituzione di un valore e la ricorsione. La forma generale di una funzione in C++ è la seguente: tipo_restituito nome_funzione (elenco_parametri ) { corpo_della_funzione }
dove: tipo_restituito : specifica
il tipo di dato restituito dalla funzione. Una funzione può restituire un qualsiasi tipo di dato, tranne un array. elenco_parametri : è un elenco di nomi di variabili separati da virgole e preceduti dai rispettivi tipi; tali variabili sono destinate a ricevere i valori degli argomenti nel momento in cui la funzione viene richiamata. Una funzione può anche non avere parametri, nel qual caso l’elenco dei parametri sarà vuoto; tuttavia, anche in mancanza di parametri, è richiesta la presenza delle parentesi. Nelle istruzioni di dichiarazione delle variabili è possibile dichiarare più variabili facenti capo a uno stesso tipo, inserendo l’elenco dei nomi di variabili separati da virgole; al contrario, tutti i parametri di una funzione devono essere dichiarati singolarmente e di ognuno si deve specificare sia il tipo sia il nome: questo significa che la dichiarazione dei parametri di una funzione ha la seguente forma generale: nome-funzione(tipo nomevarl, tipo nomevar2, ... , tipo nomevarN)
Per esempio, ecco un modo corretto e uno errato di dichiarare i parametri di una funzione. f(int i, int k, int j) f(int i, k, float j)
//corretta //errata
Nelle Unità didattiche precedenti è stata più volte incontrata la parola funzione , che è stato detto essere una porzione di codice che viene richiamata tramite il suo nome. Nonostante il C++ metta a disposizione tutte le funzionalità ad hoc per i nostri programmi, spesso si ha la necessità, data la natura specifica di un’applicazione, di dover scrivere nuove funzioni che più si adattano alle nostre esigenze. Il linguaggio non mette a disposizione certo, per esempio, una funzione che calcola il perimetro di un trapezio, ma, tramite l’uso delle istruzioni di calcolo, si può scrivere una funzione che effettui tutte le operazioni. Dopo aver visto i concetti principali della programmazione, si può passare a esaminare meglio che cos’è una funzione. 143
Sezione 3
- Organizzazione degli algoritmi
Una funzione è un blocco di codice autonomo che esegue un’elaborazione. Le funzioni sono fondamentali per due motivi: innanzitutto, suddividono il programma in parti più ridotte e lo rendono più comprensibile; in secondo luogo, permettono il riutilizzo di segmenti di codice in diversi programmi. Come sapete, quando si scrive il codice, si parte da un algoritmo di alto li vello e si continua a rifinirlo nel dettaglio finché non si ottiene il codice di un programma che esprima tutti gli algoritmi fino a comprendere quello di alto livello. Una funzione descrive una “riga” in uno di questi algoritmi, per esempio “apri un file”, “visualizza il testo sullo schermo”, “stampa un documento” e così via. Inoltre, una funzione può essere richiamata, all’interno di un programma, tutte le volte che è necessario senza ripetere ogni volta le istruzioni in essa contenute. Quindi, una volta scritta e definita una funzione, il codice al suo interno viene eseguito ogni volta che il flusso del programma incontra una sua invocazione. Definire una funzione significa creare e dotare di una nuova funzionalità il programma che si sta scri vendo. La sua definizione assume la seguente forma generale. intestazione funzione { blocco_di_codice_della_funzione }
9.2 La funzione più importante Nei programmi eseguiti finora, il codice è stato scritto seguendo la struttura generale di un programma C++. Prendiamo in considerazione il codice del primo programma di questo volume, Ciao.cpp . Ciao.cpp 1
#include "iostream"
2 3 4 5
using namespace std; //INIZIO int main () {
6 7 8 9 10 11 12
//scrivi il messaggio di saluto cout << "Ciao, mondo!" << endl; //arresta l'esecuzione del programma system ("pause"); //termina il programma return 0; }
Alla riga 4 c’è la dichiarazione del “contenitore” del nostro codice. Tale contenitore è una funzione e si chiama main. Il codice del programma è delimitato dalle parentesi graffe di riga 5 e riga 12. Alla riga 4 viene definita la funzione main. int main()
{ //scrivi il messaggio di saluto cout << "Ciao, mondo!" << endl; ...
144
Unità didattica 9 - Le funzioni
La funzione main è la funzione principale ed è obbligatorio che sia definita in ogni applicazione; essa costituisce il cosiddetto “ entry point” del programma, cioè il punto in cui inizia l’esecuzione del programma una volta compilato il codice. La sua intestazione è quella di una funzione standard:
restituisce il valore 0, quindi int; ha un nome univoco: main; non ha parametri di ingresso: ().
la funzione main() è una funzione speciale che ogni applicazione deve incorporare, ma se si vogliono suddividere logicamente le funzionalità del programma occorre scrivere nuove funzioni, da richiamarsi per eseguire i vari compiti. Si può ora modificare il programma Ciao.cpp , inserendo una nuova funzione che effettua la scrittura del messaggio di saluto. void saluto() { //scrivi il messaggio di saluto cout << "Ciao, mondo!" << endl; }
La funzione si chiama saluto , non restituisce alcun valore e non ha parametri. Poiché, in questo esempio, la funzione esegue una elaborazione, ma non restituisce alcun valore, il tipo di dato associato alla funzione è void. Il corpo è composto da una sola semplice istruzione di stampa.
Quando una funzione non restituisce alcun valore viene anche chiamata procedura e il valore restituito mancante è specificato mediante la parola chiave vod. A questo punto, una domanda è d’obbligo: dove va inserita la funzione saluto ? La funzione va inserita prima della funzione main.
Esempio Ciao1 ................................................................................................................... Ecco il codice della nuova versione del pr ogramma Ciao.cpp , che chiameremo Ciao1.cpp .
Codice Ciao1.cpp 1 2 3 4 5 6 7 8 9 10 11 12
#include "iostream" using namespace std; void saluto() { //scrivi il messaggio di saluto cout << "Ciao, mondo!" << endl; } //INIZIO int main () {
145
Sezione 3
13 14 15 16 17
- Organizzazione degli algoritmi
saluto(); //arresta l'esecuzione del programma system ("pause"); //termina il programma
18 19
return 0; }
Analisi del codice
Il programma, ora, è composto da due metodi: 1. 2.
la funzione principale main(); la funzione saluto() .
La funzione main() inizia alla riga 11 e termina alla riga 19, mentre alla riga 4 è stata dichiarata la nuova funzione saluto() , che termina alla riga 8. Alla riga 13 la funzione main() ospita una sola istruzione, che consente di richiamare (invocare ) la funzione saluto() , che provvede alla stampa del messaggio sul monitor. È evidente che il normale flusso di esecuzione del codice subisce un’alterazione, dovuta all’invocazione della funzione saluto() , che trasferisce il flusso alla riga 4, dove inizia il codice della funzione. Una volta terminata l’esecuzione del blocco di codice della funzione il flusso di esecuzione torna all’istruzione successiva all’invocazione della funzione, che in questo caso coincide con le istruzionei che terminano il programma. Nella figura è schematizzato il flusso di elaborazione del programma Ciao1.cpp : main
//INIZIO int main() { saluto(); //arresta l’esecuzione del programma system(”pause”); //termina il programma return 0; }
Invocazione funzione
funzione saluto()
void saluto() { //scrivi il messaggio di saluto cout << “Ciao, mondo!” << endl; }
Ritorna al main
........................................................................................................................................... 146
Unità didattica 9 - Le funzioni
9.3
Definizione delle funzioni
Il programma precedente è un banalissimo esempio sull’uso delle funzioni ma in realtà, in p rogrammi più complessi, le funzioni consentono di implementare una logica suddivisa nei vari compiti che il programma stesso deve svolgere. Ogni compito viene implementato tramite una funzione che viene invocata ogni volta che se ne ha bisogno e tutte le volte che lo si desidera. Un codice ben strutturato rappresenta il frutto di un’attenta analisi del problema da risolvere in fase di progettazione e, cosa più importante, rende il codice stesso più facile da leggere e agevolmente predisposto a eventuali modifiche e/o correzioni. È il momento, dunque, di scrivere un programma un po’ più complesso contenente nuove funzioni. Esempio Quadrato1........................................................................................................... Calcolare l’area del quadrato, dato il lato.
Il problema può essere suddiviso nei passi seguenti: 1. inserimento da tastiera della misura 2. calcolo del quadrato; 3. visualizzazione del risultato.
del lato;
Si può capire, dalla breve analisi effettuata, che il programma deve assolvere a tre “compiti”: lettura dei dati, calcolo del risultato e stampa del risultato. Questi lavori si prestano benissimo a essere realizzati con altrettante funzioni cui daremo, rispettivamente, i seguenti nomi: 1. leggiLato() ; 2. calcolaQuadr() ; 3. scriviQuadr() .
Codice Quadrato1.cpp 1
#include
2 3 4 5 6
using namespace std;
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
void leggiLato() { //chiedi e leggi lato cout << "\nInserisci la misura del lato "; cin >>lato; } void calcolaQuadr() { //calcola area quadrato area = lato * lato; } void scriviQuadr() { //scrivi area cout << "\nArea quadrato = " << area; }
147
Sezione 3
22 23 24 25 26
- Organizzazione degli algoritmi
//INIZIO int main () { //dichiara variabili lato e area
27 28 29
double lato; double area;
30 31 32 33 34
//invoca le funzioni leggiLato(); calcolaQuadr(); scriviQuadr();
35 36 37 38
//fine programma cout << "\n\nFine system ("pause"); return 0;
39
";
}
...........................................................................................................................................
La compilazione del codice indicato sopra, però, dà luogo a una discreta lista di errori; di seguito riportiamo a titolo di esempio che cosa appare nella finestra che elenca gli errori nel programma di editing Microsoft Visual C++ 2005 Express Edition.
Il programma, come c’è da aspettarsi, non viene compilato. Il codice scritto è sintatticamente corretto, eppure il compilatore genera degli errori dovuti, come si legge nella figura, al contesto operativo di due delle variabili utilizzate dal programma: infatti, l’errore segnalato è “identificatore non dichiarato”. Questo errore deriva dal fatto che la definizione delle variabili lato e area si trova dopo la definizione delle funzioni che utilizzano tali variabili. Si tratta di un errore connesso a un problema che tecnicamente viene definito “ambito delle variabili”, di cui parliamo nel prossimo paragrafo.
9.4
Ambito delle variabili
Tutti i programmi scritti nelle Unità didattiche precedenti comprendevano solo una funzione, la funzione main(), nella quale erano state dichiarate tutte le variabili di cui il programma aveva bisogno e, insieme a esse, tutto il codice dell’algoritmo che le utilizzava. Una volta eseguita l’ultima istruzione, il programma aveva termine. Tutte le variabili in esso dichiarate vengono, per così dire, distr utte 148
Unità didattica 9 - Le funzioni
automaticamente all’uscita dalla funzione, cioè viene perso ogni loro lor o riferimento, quindi le variabili main() vengono definite variabili locali in quanto sono state dichiarate appartenenti alla funzione main() e utilizzate nel corpo della funzione stessa. Con l’inserimento di nuove funzioni nel codice è necessario definire il campo d’azione (ambito ) di ciascuna variabile, per controllare in quali funzioni deve essere disponibile. Gli errori elencati nel tentativo di compilazione del programma Quadrato.cpp spiegano che il contesto in cui operano le variabili dichiarate è errato: infatti, nel codice tutte le variabili utilizzate nel programma sono state dichiarate all’interno della funzione main() main() , quindi sono locali a essa e in quanto tali possono essere utilizzate solo ed esclusivamente in tale ambito. Le altre funzioni presenti nel codice non possono essere a conoscenza della loro presenza, quindi richiamandone i nomi fanno riferimento a entità che si trovano al di fuori del loro ambito operati vo: ecco il motivo degli errori. A questo punto si rende ren de necessario necessari o cambiare l’ambito l’amb ito delle variabili vari abili utilizzat uti lizzatee dalle nuove funziofunz ioni, dotandole di un livello di visibilità più ampio: si rende necessario dar loro visibilità globale , af main possano utilizzarle. Per questo vengono dette variabi- finché anche le altre funzioni oltre a main li globali .
Le variabili globali vengono definite definite all’interno del contenitore del codice,non codice, non più all’interno della funzione main() (o altre funzioni). La modifica necessaria per evitare gli errori err ori di compilazione visti prima è illustrata nell’esempio che segue.
Esempio Quadrato_corrett Quadrato_corretto o .............. .............................. ................................ ................................ ............................... ............................... .................. Modificare il programma Quadrato.cpp in modo che, una volta compilato, non segnali alcun errore, come si vede nella figura della prova di esecuzione.
Codice Quadrato_corretto.cpp 1
#include
2
using namespace std;
3 4
//dichiara variabili lato e area
5
double lato;
6
double area;
7 8 9 10
void leggiLato() { //chiedi e leggi lato
11
cout << "\nInserisci la misura del lato ";
12
cin >>lato;
13
}
14 15
void calcolaQuadr()
16
{
17
//calcola area quadrato
18
area = lato * lato;
19
}
20
149 14 9
Sezione 3
- Organizzazione degli algoritmi
21
void scriviQuadr()
22
{
23
//scrivi area
24
cout << "\nArea quadrato = " << area;
25
}
26 27 28
//INIZIO int main ()
29 30
{ //invoca le funzioni
31 32
leggiLato(); calcolaQuadr();
33
scriviQuadr();
34 35
//fine programma
36
cout << "\n\nFine
37
system ("pause");
38
return 0;
39
";
}
Prova di esecuzione
Analisi del codice
Alle righe 5 e 6 sono state dichiarate tutte le variabili necessarie necessarie al programma. programma. Si tratta delle medesime variabili usate nel programma precedente ma, per fare in modo che le funzioni possano utilizzarle, sono state dichiarate al di fuori della funzione main(), quindi rese “visibili” ( globali ) a tutte le altre funzioni. Alla riga 8 compare la dichiarazione della della prima funzione che, non non restituendo alcun valore, è di tipo void ; si chiama leggiLato() e non ha alcun parametro. Il corpo di questa funzione contiene due istruzioni molto banali: la prima, alla riga 11, visualizza un messaggio di invito a inserire la misura del lato del quadrato; la seconda, alla riga 12, legge il numero inserito alla tastiera e lo assegna alla variabile globale lato . Le altre due funzioni sono definite alle righe 15 e 21. Infine, alla riga 28, c’è la già nota funzione main() (entry point ), ), alla quale è demandata la funzione di richiamare (invocare) le funzioni precedenti in sequenza, secondo la struttura che si è voluto dare al programma. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
L’inserimento delle funzioni all’interno del programma non segue alcun ordine particolare: possono essere collocate, per esempio, in ordine inverso rispetto a quello precedentemente presentato, ma le loro intestazioni devono sempre essere collocate prima della funzione main(). L’importante è che vengano invocate secondo la struttura logica stabilita dal programmatore. Nel blocco di codice di una funzione possono essere invocate tutte le funzioni esterne che si desidera, ma non possono essere dichiarate altre funzioni, ovvero una funzione non può contenere un’altra funzione.
150 15 0
Unità didattica 9 - Le funzioni
9.5 9. 5 Var aria iabi bili li lo loca cali li e gl glob obal alii È stato più volte accennato al concetto di variabile locale e globale: ora è il caso di approfondire e chiarire un problema di carattere formale che può essere riscontrato quando viene utilizzata una stessa variabile in ambito sia locale sia globale. Esempio Segno.................... Segno................................... ............................... ................................ ................................ ............................... ............................... .................. Creare una funzione che cambi il segno a una variabile intera.
Codice Segno.cpp 1 2 3 4
#include using namespace std;
5 6 7
{
8 9 10 11 12
}
void cambiaSegno () i n t n um ; n u m = - nu m ;
/ /v a r ia b i le lo c al e / / c a mb i a s e gn o a l l a v a r ia b i le l oc al e
//INIZIO int main () {
13 14 15 16
i n t n um = 12 5 ;
/ / v a ri a bi l e g l o ba l e
//invoca la funzione cambia segno cambiaSegno();
17 18 19 20
//scrivi num cout << "\nLa variabile num contiene " << num;
21 22 23 24
//fine programma cout << "\n\nFine system ("pause"); return 0;
25
";
}
Analisi del codice
Il programma non ha alcuna utilità pratica, se non quella di fare capire in che modo le due variabili dichiarate dichia rate alla alla riga 6 e alla riga 13 13 vengo vengono no gestite. gestite. Prova di esecuzione
151 15 1
Sezione 3 - Organizzazione degli algoritmi
Il C++, pur accertando la presenza di due variabili con lo stesso nome e tipo, ne ammette chiaramente l’uso, in quanto fanno parte di due ambiti completamente diversi: la variabile num definita alla riga 6 è stata dichiarata all’interno della funzione cambiaSegno() , quindi locale , mentre la va main, quindi glo- main riabile num definita alla riga 13 è stata dichiarata come come appartenente alla funzione quindi glo- bale rispetto bale rispetto a tale funzione. locale che, in questo esempio, “vieIl risultato dell’elaborazione è frutto dell’utilizzo della variabile locale che, ne nascosta” dal campo d’azione della variabile globale . .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
9.6
Valo lori ri di rit ritor orn no
Le funzioni consentono, come già è stato detto, di strutturare in maniera logica il programma, implementando singolarmente i vari algoritmi che insieme rappresentano la soluzione del problema. Le funzioni viste finora sono state utilizzate nella loro forma base, che non prevede la restituzione di valori al codice chiamante: infatti, nella loro intestazione sono state dichiarate void. In realtà, la possibilità di gestire un valore frutto dell’elaborazione di una funzione è molto più concreta di quanto si possa credere. Una funzione può restituire solo un unico valore. L’intestazione di una funzione che restituisce un valore al codice chiamante assume la seguente sintassi: tipo nome-funzione () { //corpo // corpo della funzione ; return variabile }
dove: tipo rappresenta
uno dei tipi di dato C++ validi e indica il tipo della variabile che la funzione restituisce al codice chiamante; return è la parola chiave dell’istruzione che restituisce un valore di quel tipo . Di seguito è riportato un esempio. Esempio Quadrato2 ................ ................................ ............................... ............................... ................................ ................................ ............................ ............ Quadrato2.cpp
152 15 2
1
#include
2 3
using namespace std;
4 5 6 7
//dichiara variabili lato e area double lato; double area;
8 9
void leggiLato() {
10 11
//chiedi e leggi lato cout << "\nInserisci la misura del lato ";
12 13 14 15 16
cin >>lato; } double calcolaQuadr() {
Unità didattica 9 - Le funzioni
17 18 19 20 21
//calcola quadrato return lato*lato; } void scriviQuadr()
22 23 24
{
25 26 27 28 29
}
//scrivi area cout << "\nArea quadrato = " << area;
//INIZIO int main () {
30 31 32 33
//invoca le funzioni leggiLato(); area = calcolaQuadr(); scriviQuadr();
34 35 36 37
//fine programma cout << "\n\nFine system ("pause");
38 39
";
return 0; }
Analisi del codice
Le modifiche significative sono due. La prima è alla riga 18, dove il calcolo del quadrato non viene assegnato alla variabile area ma viene indicato come valore da “consegnare” al programma principale: questo valore è associato alla funzione calcolaQuadr . La seconda è alla riga 32, in cui il risultato della funzione calcolaQuadr() viene assegnato alla variabile area . Si noti la differenza dif ferenza della sintassi tra le funzioni che non contengo l’istruzione return e quelle che la contengono: le prime, menzionate alle righe 31 e 33, da sole costituiscono un’istruzione, un’istr uzione, mentre le seconde sono utilizzate alla stregua di una variabile, in quanto restituiscono un valore, pertanto sono poste a destra dell’operatore di assegnazione. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
Per alcune funzioni l’inserimento dell’istruzione return non è indispensabile: se la funzione deve ese main, per essa non è richiesta l’istruguire un compito che non richiede la “consegna” di un risultato al main zione return. È il caso della della funzione funzione scriviQuadr(), che deve eseguire un’operazione di output. Invece, le funzioni che alla fine della loro esecuzione producono un risultato devono contenere l’istr uzione return. Rientra in questa categoria anche la funzione leggiLato.
9.7 9. 7 Pa Pass ssag aggi gio o dei dei para parame metr trii per per valo valore re L’inserimento delle funzioni, come si è visto, estende notevolmente la potenzialità di un’applicazione, strutturandone la logica in piccole porzioni di codice richiamabili dalla struttura str uttura principale. Ma il solo utilizzo delle funzioni che restituiscono r estituiscono un valore limita molto la progettazione di un programma robusto e di facile manutenzione. Il discorso si amplia qualora si abbia la necessità che una funzione possa elaborare valori che il codice chiamante fornisce alla funzione stessa. Ciò implica che alla funzione vengano comunicati i valori necessari al suo corretto funzionamento. 153 15 3
Sezione 3 - Organizzazione degli algoritmi
Le funzioni fin qui utilizzate non prevedevano parametri ma, a volte, si ha la necessità di dover comunicare loro dei valori (argomenti o parametri ) che vengono utilizzati dal codice della funzione stessa. Come si ricorderà, l’intestazione tipo di una funzione è la seguente: tipo-di-ritorno nome-funzione ([lista parametri ]) {
blocco di codice della funzione }
dove, oltre all’eventuale tipo di ritorno e al nome della funzione, tra parentesi tonde compare una lista (opzionale) di parametri; tale lista comprende una serie di coppie definite dal tipo e dal nome della variabile, definite localmente alla funzione in questo modo: ,...]) tipo-di-ritorno nome-funzione ([tipo1 var1, tipo2 var2 {
blocco di codice }
Ogni coppia è separata da una virgola. L’invocazione della funzione, dal canto suo, deve contenere l’esatta lista degli argomenti, dello stesso tipo, passati come parametri alla funzione, in questo modo: nome-funzione (variabile1, variabile2, ... );
Esempio AreaRettangolo ................................................................................................... Costruire una funzione che riceva come parametri la misura della base e dell’altezza di un rettangolo e ne restituisca l’area.
Codice AreaRettangolo.cpp 1 2 3 4
#include using namespace std;
5 6 7 8
{
9 10 11 12
154
double calcolaArea(double b, double h) //calcola area rettangolo return b*h; } //INIZIO int main () {
13 14 15 16 17
double base; double altezza; double area;
18 19 20
cout << "\nInserisci misura base "; cin >>base; //chiedi e leggi altezza
21 22 23 24
cout << "\nInserisci misura altezza "; cin >>altezza;
//chiedi e leggi base
//invoca le funzioni
Unità didattica 9 - Le funzioni
25 26 27 28 29
area = calcolaArea(base, altezza);
30 31 32
//fine programma cout << "\n\nFine system ("pause");
33 34
return 0;
//scrivi area cout << "\nArea rettangolo = " << area;
";
}
Prova di esecuzione
Analisi del codice
Alla riga 4 l’intestazione della funzione specifica due parametri: b e h. Questi parametri vengono considerati variabili locali alla funzione e ricevono dal programma principale i valori corrispondenti alle variabili globali base e altezza , indicati al momento dell’invocazione della funzione. Alla riga 25 viene invocata la funzione calcolaArea , e come argomenti vengono inserite le variabili globali base e altezza : i valori di tali argomenti vengono memorizzati nei parametri b e h locali alla funzione. ...........................................................................................................................................
Lo schema che riassume graficamente il passaggio dei parametri per valore a una funzione è riportato di seguito. area = calcolaArea(base, altezza);
base,altezza: argomenti della funzione
double calcolaArea(double b, double h); b,h: parametri della funzione
Gli argomenti passati come parametri alla funzione devono rispettare fedelmente sia il tipo di dato sia l’ordine di dichiarazione dell’intestazione della funzione, altrimenti il compilatore genera un errore perché non riesce a trovare la funzione da eseguire, non essendoci corrispondenza di tipo e di numero degli argomenti. Inoltre, nell’intestazione della funzione non è consentito assegnare valori ai parametri. Si ricordi che tutti i parametri passati per valore sono considerati locali alla funzione: al termine dell’elaborazione, dopo l’esecuzione dell’istruzione return, i loro valori non esisteranno più.
155
Sezione 3 - Organizzazione degli algoritmi
L’invocazione di una funzione determina l’assegnazione degli argomenti ai relativi parametri. L’assegnazione in questione è definita passaggio degli argomenti per valore, in quanto a ogni parametro viene assegnato il corrispondente valore dell’argomento. In altre parole, per ogni argomento viene generata un copia che viene assegnata al corrispondente parametro; qualsiasi eventuale modifica di tale copia determinata dall’esecuzione del codice del corpo della funzione non si rifletterà in alcun modo sugli argomenti originariamente passati alla funzione, come risulta chiaro dall’esempio che segue. Questo tipo di passaggio dei parametri consente dunque di rendere disponibile alla funzione un valore indispensabile per le elaborazioni che avvengono al suo interno, ma le cui “vicissitudini” non sono di nessun interesse per il programma chiamante. Esempio PerValore ............................................................................................................. Creare una funzione che incrementi il valore di due variabili intere passate alla funzione come parametri per valore.
Codice PerValore.cpp 1
#include
2
using namespace std;
3 4
void incrementa(int num1, int num2)
5
{
6
cout<<"\n\n\t--- Ingresso nella funzione ---\n";
7
//incrementa i valori
8
++num1;
9
++num2;
10
cout<<"I parametri sono stati incrementati valgono "<
11
cout << "\n\t--- Uscita dalla funzione ---";
12
}
13
//INIZIO
14
int main ()
15
{
16
int numero1 = 0;
17
int numero2 = 0;
18 19
cout<<"\n\nArgomenti prima dell'invocazione = "<
20 21
incrementa(numero1,numero2);
//invoca la funzione
22 23
cout<<"\n\nArgomenti dopo l'invocazione = "<
24 25
//fine programma
26
cout << "\n\nFine
27
system ("pause");
28
return 0;
29
156
}
";
Unità didattica 9 - Le funzioni
Prova di esecuzione
Analisi del codice
Il codice del programma non esegue un’elaborazione specifica, ma conferma ciò che è stato affermato in precedenza. Alle righe 16 e 17 sono state dichiarate due variabili, rispettivamente numero1 e numero2 di tipo int. Tali variabili sono state, inoltre, inizializzate a 0 e alla riga 21 sono di ventate gli argomenti dell’invocazione della funzione incrementa la quale, nella sua intestazione, ha come parametri due variabili di tipo int . Come si vede nella prova di esecuzione, il programma visualizza alcuni messaggi, che descrivono il flusso di elaborazione: si comincia con la visualizzazione dei valori degli argomenti prima dell’invocazione della funzione incrementa alla riga 19 della funzione main, cui fa seguito l’invocazione della funzione alla riga 21, che “dirotta” il flusso di elaborazione alla riga 4, dove inizia la definizione della funzione incrementa; in quest’ultima viene richiesto per prima cosa (riga 6) di scrivere a video un messaggio che avvisa l’utente che si è entrati nella funzione. Alle righe 8 e 9 avviene l’incremento dei parametri di ingresso; alla riga 10 viene visualizzato un messaggio di conferma dell’avvenuto incremento e infine, alla riga 11, viene visualizzato un messaggio che segnala l’uscita dalla funzione. Il flusso viene così restituito all’istruzione successiva a quella dell’invocazione della funzione (cioè alla riga 23), dove viene visualizzato il valore corrente delle variabili passate come argomenti dell’invocazione. Entrambi i valori sono rimasti inalterati, a conferma che la funzione incrementa non ha alterato i valori delle variabili numero1 e numero2. ...........................................................................................................................................
Il passaggio dei parametri per valore, in generale, va bene per moltissime applicazioni ma, spesso, si ha la necessità di dover utilizzare le funzioni per manipolare, cioè modificare, proprio i valori dei parametri passati a esse, affinché il codice chiamante possa utilizzarli per successive elaborazioni. La tecnica utilizzata per ottenere questo scopo viene detta passaggio dei parametri per riferimento , ed è l’argomento del prossimo paragrafo.
9.8 Passaggio dei parametri per riferimento Con il passaggio dei parametri per valore, come si è visto, gli argomenti dell’invocazione della funzione non vengono modificati. Si studia ora una tecnica che prevede la modifica dei parametri in ingresso alla funzione. L’intestazione della funzione tradizionale cambia e viene così modificata: tipo-di-ritorno nome-funzione ([tipo1 &var1, tipo2 &var2,...]) {
blocco di codice }
157
Sezione 3 - Organizzazione degli algoritmi
L’intestazione generale della funzione è rimasta quasi inalterata, tranne che per la modalità con cui viene passato il parametro. Il carattere “&” che precede il parametro indica che ogni eventuale modifica effettuata su quel parametro si riflette sull’argomento dell’invocazione.
Nel passaggio per riferimento la funzione riceve, infatti, l’indirizzo della locazione di memoria che contiene il valore da elaborare, sicché ogni “vicissitudine” del parametro provoca la modifica del valore della variabile originariamente passata alla funzione. L’invocazione della funzione non subisce alcun cambiamento, come si può vedere nella sintassi che segue. nome-funzione
(var1, var2 , ...);
Esempio PerRiferimento .....................................................................................................
Per una prova concreta, viene modificato il codice del programma perValore.cpp in modo che nella funzione incrementa i parametri vengano passati per riferimento e non più per valore. Il programma viene rinominato in PerRiferimento.cpp . Codice PerRiferimento.cpp 1
#include
2
using namespace std;
3 4
void incrementa(int &num1, int &num2)
5
{
6 7
cout<<"\n\n\t--- Ingresso nella funzione ---\n"; //incrementa i valori
8 9
++num1; ++num2;
10 11 12
cout<<"I parametri sono stati incrementati valgono "<
13
//INIZIO
14
int main ()
15 16
{ int numero1 = 0;
17
int numero2 = 0;
18 19 20
cout<<"\n\nArgomenti prima dell'invocazione = "<
21
incrementa(numero1,numero2);
22 23
cout<<"\n\nArgomenti dopo l'invocazione = "<
24 25
//fine programma
26 27
cout << "\n\nFine system ("pause");
28
return 0;
29
158
}
";
//invoca la funzione
Unità didattica 9 - Le funzioni
Prova di esecuzione
Analisi del codice La struttura del programma è pressoché identica a quella del precedente: è stata cambiata solo l’intestazione della funzione incrementa . Alla riga 4, nell’intestazione della funzione, compaiono ora i due parametri passati per riferimento, tramite l’operatore di indirizzamento &. Al momento dell’invocazione, alla riga 21, vengono passati alla funzione non più le copie dei valori degli argomenti ma un loro riferimento alla zona di memoria in cui sono stati definiti. La funzione riceve tali riferimenti e, dopo l’elaborazione, ne modifica il contenuto che, ovviamente, si riflette anche sugli argomenti dell’invocazione. ........................................................................................................................................... Di seguito un altro semplice esempio.
Esempio Scambia .............................................................................................................. Scrivere un programma che ef fettui lo scambio del contenuto di due variabili. Il programma prevede l’acquisizione dei due valori tramite la tastiera.
Codice Scambia.cpp 1 2 3
#include using namespace std;
4 5 6 7 8
//variabili globali int num1=0; int num2=0;
9 10 11 12 13 14 15 16 17 18
void leggiNum() { cout << "\nInserire il primo valore "; cin >> num1; cout << "\nInserire il secondo valore "; cin >> num2; } void scambia(int &n1, int &n2) {
159
Sezione 3
19 20 21 22 23
- Organizzazione degli algoritmi
int temp; //effettua lo scambio temp=num1; num1=num2; num2=temp;
24 25 26
}
27 28 29 30 31
int main () { leggiNum(); //acquisisci i valori cout << "\n\nArgomenti prima dell'invocazione = " << num1 << " " << num2; scambia(num1, num2); //invoca la funzione per lo scambio
//INIZIO
32 33 34 35
cout << "\nArgomenti dopo l'invocazione = " << num1 << " " << num2;
36 37 38
system ("pause"); return 0;
//fine programma cout << "\n\nFine
";
}
Prova di esecuzione
Analisi del codice
Il codice prevede due funzioni oltre a quella principale. La prima è LeggiNum() , alla riga 8, che viene invocata dalla funzione main alla riga 29 e consente la lettura da tastiera dei due valori che devono essere scambiati. La seconda è scambia, alla riga 17, che viene invocata dalla funzione main alla riga 31 ed effettua lo scambio dei valori con l’ausilio della variabile locale temp. Tale funzione riceve i paramentri per riferimento quindi, una num1 num2 volta usciti dalla funzione, i valori delle due variabili risultano argomenti effettivamente scambiati. Si può notare che gli argomenti dell’invocazione e i relativi parametri hanno nomi differenti, ma ciò non ha alcuna importanvalore1 valore2 za perché, essendo gli argomenti passati per riferimento, en- memoria trambi “puntano” alle locazioni di memoria delle rispettive variabili, come si può evincere dallo schema a destra. Quindi, ogni modifica dei parametri della funzione si riflette automaticamente sugli argomenti dell’invocazione.
parametri
n1
n2
........................................................................................................................................... 160
Unità didattica 9 - Le funzioni
9.9
Ricorsione
In C++, così come in diversi altri linguaggi, una funzione può richiamare se stessa. Quando, all'interno del corpo di una funzione, un'istruzione richiama la funzione stessa, tale funzione si dice ricorsiva.
La ricorsione è un processo che definisce qualcosa in termini di se stesso e in alcuni casi viene chiamata definizione circolare. In matematica è facile incontrare formule ricorsive. Prendiamo come esempo il già noto fattoriale di un numero intero: il fattoriale di un numero n è il prodotto di tutti i numeri interi da 1 a n. Il fattoriale di 3 è l × 2 × 3, ovvero 6. Il fattoriale di n si indica con n!. Preso un numero n possiamo scrivere: fattoriale(n) = n! = 1 × 2 × 3 × ... n ; oppure possiamo esprimere lo stesso concetto con una formula ricorsiva: fattoriale(n) = n! =
1 n × fattoriale(n – 1)
se n = 0 o n = 1 per ogni altro valore di n
Osservando la formula si vede che la condizione di arresto della ricorsione è n = 1 e che, nella parte a destra del segno di uguale, “ricorre” o “ricompare” l’invocazione della funzione che deve essere calcolata.
Esempio Fattoriale ............................................................................................................. Prendiamo l’esempio del calcolo del fattoriale mostrando due funzioni diverse, ma equivalenti: la prima presenta l’utilizzo di una struttura iterativa, la seconda sfrutta il concetto di ricorsione.
Codice Fattoriale.cpp 1 2
#include "iostream" using namespace std;
3 4 5 6
int fatt_iteraz(int n) { //variabile locale e inizializzazione
7 8
int fatt = 1; //esegui il ciclo for
9 10 11 12 13 14
}
15 16
int fatt_ricors (int n)
17 18
for (int i=1; i<=n; i++) { fatt = fatt * i; } return fatt;
{ //variabile locale
161
Sezione 3
- Organizzazione degli algoritmi
19
int fatt;
20
if (n == 1)
21
return 1;
22
//la funzione richiama se stessa
23
fatt = n*fatt_ricors(n-1);
24
return fatt;
25 26
}
27 28
//INIZIO int main ()
29 30
{ int num;
31
//chiedi e leggi il numero su cui calcolare il fattoriale
32 33
cout << "\nInserisci un numero intero (Max 10) = "; cin >> num;
34 35
//scrivi il risultato della funzione con iterazione
36
cout << "\n\nFattoriale per iterazione = " << fatt_iteraz (num);
37 38
//scrivi il risultato della funzione con ricorsione
39
cout << "\nFattoriale per ricorsione = " << fatt_ricors (num);
40 41 42
//fine programma cout << "\n\nFine
43 44
system ("pause"); return 0;
45
";
}
Prova di esecuzione
Analisi del codice
Il codice contiene due funzioni: la prima è fatt_iteraz(int n) , che calcola il fattoriale di n con un algoritmo iterativo ed è definita a partire dalla riga 4; la seconda è fatt_ricors(int n) , che è di tipo ricorsivo ed è definita a partire dalla riga 16. I valori restituiti dalle due funzioni sono scritti dalle istruzioni delle righe 36 e 39 (rispettivamente). La versione non ricorsiva della funzione dovrebbe risultare chiara: utilizza un ciclo for che va da 1 a n e moltiplica progressivamente ogni numero per il prodotto accumulato nella variabile fatt . 162
Unità didattica 9 - Le funzioni
L’operazione svolta dalla versione ricorsiva fatt_ricors() è un po’ più complessa. Quando fatt_ricors() viene richiamata con argomento uguale a 1, la funzione restituisce 1; in ogni altro caso la funzione restituisce il prodotto n * fatt_ricors(n-1), per valutare il quale viene nuovamente richiamata la funzione fatt_ricors() . Questo avviene finché n non diventa uguale a 1 e la funzione inizia a restituire un valore. A partire dal primo valore restituito dalla funzione, finalmente potranno essere calcolati tutti i valori “rimasti in sospeso” dalla chiamata ricorsiva, fino ad arrivare al risultato richiesto. È una specie di “gioco delle scatole cinesi”. Supponiamo di voler calcolare il fattoriale di 2. La prima chiamata a fatt_ricors() provoca una seconda chiamata ricorsiva con argomento uguale a 1, che restituisce il valore 1 che viene poi moltiplicato per 2 (il valore originale di n). Il risultato sarà quindi 2. Si può provare a seguire il procedere del calcolo del fattoriale per esempio del numero 3 inserendo istruzioni cout nelcodice della funzione fatt_ricors() , per verificare il livello raggiunto da ogni chiamata ed i vari risultati intermedi. Quando una funzione richiama se stessa sullo stack viene allocata una nuova serie di variabili locali e parametri, e il codice della funzione viene eseguito nuovamente dall’inizio utilizzando tali variabili. Una chiamata ricorsiva non crea una nuova copia della funzione: solo i valori su cui opera sono nuovi. Quando ogni istanza della funzione esegue il return le sue variabili locali e i suoi parametri vengono rimossi dallo stack e l’esecuzione riprende nel punto in cui la funzione aveva richiamato se stessa. La progettazione di una funzione ricorsiva deve essere condotta con molta attenzione. Infatti, se da una parte la ricorsione rende più efficiente e veloce il programma, dall’altra può rivelarsi una fonte di errori di esecuzione. Il più comune errore in cui si può incorrere utilizzando la ricorsione è “l’entrata in loop” del programma: può verificarsi, cioè, che l’esecuzione del programma “rimanga confinata” all’interno delle istruzioni della funzione ricorsiva, richiamandola “in eterno”. Le osservazioni riportate di seguito saranno sicuramente utili per evitare di progettare funzioni ricorsive “difettose”, oltre che per imparare a utilizzare al meglio questo utile strumento. Quando si scrivono funzioni ricorsive, da qualche parte si deve prevedere un’istruzione condizionale (per esempio un if) che faccia in modo che la funzione termini senza eseguire alcuna ricorsione. Se si omette tale istruzione condizionale, una volta entrati nel corpo della funzione non sarà più possibile uscirne. Durante lo sviluppo del programma si consiglia di utilizzare abbondantemente le istruzioni cout, in modo da sapere sempre cosa sta avvenendo e annullare l’esecuzione nel momento in cui si rileva un errore. 2. Spesso le routine ricorsive non riducono in modo significativo le dimensioni del codice né migliorano l’utilizzo della memoria, rispetto alla versione iterativa. Inoltre, le versioni ricorsive della maggior parte delle routine vengono eseguite in modo leggermente più lento rispetto alle loro equivalenti iterative, a causa del sovraccarico dovuto alla continua istanziazione delle funzioni. 3. Il vantaggio principale delle funzioni ricorsive consiste nella possibilità di creare versioni più chiare e semplici degli algoritmi. Per esempio, l’algoritmo QuickSort è piuttosto difficile da implementare in modo iterativo; inoltre, alcuni problemi, specialmente quelli di intelligenza ar tificiale, sono più adatti a soluzioni ricorsive; infine, spesso l’esecuzione ricorsiva risulta essere più lineare dell’analogo iterativo. 1.
...........................................................................................................................................
9.10 Funzioni matematiche
Oltre alle funzioni definite dal programmatore, per quanto riguarda le funzioni matematiche, C++ mette a disposizione la libreria che contiene un’ampia gamma di funzioni per le applicazioni di tipo scientifico. Ecco una breve descrizione delle funzioni più usate di . 163
Sezione 3
- Organizzazione degli algoritmi
Fanno parte di questa libreria tutte le funzioni trigonometriche e le funzioni di matematica analitica, che sono così immediatamente disponibili al programmatore. FUNZIONE
DESCRIZIONE
double sqrt(double x)
x
double pow(double x,double y)
y
double sin(double x) double cos(double x)
cosx
Coseno di x (x in radianti)
double tan(double x)
tanx
Tangente di x (x in radianti)
sen -1 x
Arco seno di x (x∈[-1,1])
double asin(double x)
-1
double acos (double x)
cos x
Arco coseno di x (x∈[-1,1])
double atan(double x)
tan -1 x
Arco tangente di x
double exp(double x)
ex
Esponenziale di x
log(x)
Logaritmo naturale di x (x > 0)
log10(x)
Logaritmo decimaledi x (x>0)
double log(double x) double log10(double x)
double ceil(double x)
Intero più piccolo > x
double floor(double x)
Intero più grande < x
double fabs(double x)
164
senx
Radice quadrata Elevamento a potenza Se x > 0, y può essere un valore qualsiasi Se x è 0, y deve essere > 0 Se x < 0, y deve essere un intero Seno di x (x in radianti)
|x|
Valore assoluto di x
Unità didattica 9 - Le funzioni
Esercizi Unità didattica 9 Descrivere la struttura dell’intestazione di una funzione. Scrivere una funzione che calcoli e scriva l’area di un trapezio. All’interno della funzione sono presenti l’acquisizione dei dati (base e altezza) e la scrittura dell’output (area).
Scrivere una funzione che calcoli e sc riva l’area di un trapezio e che abbia come parametri le basi e l’altezza.
Scrivere una funzione che acquisisca da tastiera i dati dell’esercizio precedente e stabilire l’esatta modalità di passaggio dei parametri.
Nella seguente porzione di codice compare un er rore: individuare quale. void Areaquadrato() { int area=0; area=lato*lato; return area; }
Nella seguente funzione, che calcola il quadrato di un numero intero, compare un errore: individuarlo e correggerlo. int Quad(int numero) { int quad = 0; quad=numero*numero; return numero; }
Scrivere una funzione che stabilisca l’aliquota IVA in relazione ai seguenti importi: importo fino a 100.000
IVA 20%; importo compreso tra 100.000 fino a 200.000 → IVA 18%; importo superiore a 200.000 → IVA 16%. →
Confermare la seguente af fermazione: “I parametri passati per riferimento forniscono alla funzione una copia esatta degli argomenti”.
Definire una funzione che, dato il prezzo di vendita di un prodotto e l’aliquota IVA, presenti il prezzo comprensivo di IVA.
Definire una funzione che, dato il pr ezzo di vendita di un prodotto comprensivo di IVA, restituisca il prezzo al netto dell’IVA e l’ammontare dell’IVA (Aliquota Iva costante = 20%).
Dato in input un numero N, definire una funzione che restituisca la somma dei numeri dispari minori o uguali a N.
Scrivere una funzione che abbia come parametri le coordinate di un punto sul piano car tesiano e che comunichi in output il valore della distanza del punto dall’origine degli assi. 165
Sezione 3 - Organizzazione degli algoritmi
Esercizi Unità didattica 9 Data l’altezza in cm e il peso in kg di una persona, definire una funzione che scriva il messaggio: “sottopeso” se altezza – 100 > peso; “normale” se altezza – 100 = peso; “soprappeso” se altezza – 100 < peso.
Scrivere una funzione che abbia come parametri le coordinate di un punto sul piano car tesiano e che restituisca il valore della distanza del punto dall’origine degli assi.
Scrivere una funzione che, acquisito in input un numero, calcoli e scriva in output il quadrato e la radice quadrata del numero (utilizzare le funzioni matematiche della libreria
).
Scrivere una funzione che, acquisito in input un numero, calcoli e restituisca al main come parametri il quadrato, il cubo, la radice quadrata e la radice cubica del numero (utilizzare le funzioni matematiche della libreria ).
Scrivere una funzione che abbia come parametri le coordinate cartesiane di due punti e che restituisca la distanza dei due punti.
Scrivere una funzione che abbia come parametri le coordinate cartesiane di due punti e che calcoli le coordinate del punto medio del segmento che unisce i due punti (scegliere il tipo di passaggio di parametri opportuno).
Dato un elenco di persone con l’indicazione del nome e dell’altezza, calcolare l’altezza media. Utilizzare una funzione per l’acquisizione dei dati di input.
Dato un elenco di persone con l’indicazione del nome e dell’altezza, indicare il nome del più alto e del più basso. Utilizzare una funzione per l’acquisizione dei dati di input.
166
Sezione
4
Strutture dei dati
†
Obiettivi generali
◊
Definire insiemi di dati numerabili
◊
Saper utilizzare schemi logici per organizzare insiemi complessi di dati
◊
Costruire tabelle di dati omogenei
◊
Manipolare stringhe di caratteri
◊
Raggruppare dati di tipo diverso
&
Questa sezione contiene
U.D.10 Enumerazioni e array U.D.11 Stringhe e strutture
Unità didattica
10
Enumerazioni e array CHE COSA IMP IMPARERAI ARERAI A FARE $
Definire dati di tipo enumerativo
$
Definire un array a una dimensione in C++
$
Caricare un vettore in memoria
$
Definire un vettore a dimensione variabile
$
Definire una matrice
$
Popolare una matrice
$ Visualizzar Visualizzare e
gli elementi di una matrice
CHE COSA DOVRAI STUDIARE $
Definizione di tipo enumerativo
$
Concetto di vettore
$
Definizione di vettore
$
Sintassi per la gestione di un vettore
$
Sintassi per la gestione delle matrici
10 Enumerazioni e array
Unità didattica
10.1 Introd Introduzion uzione e Finora abbiamo visto i tipi di dato più comuni e i più facili da utilizzare in C++, cioè i tipi predefiniti: int, double, bool ecc. In C++, però, si possono anche utilizzare tipi definiti dal programmatore. Tali nuovi tipi, per poter essere utilizzati, vanno inseriti nella dichiarazione delle variabili secondo la sintassi consueta: tipo vari variabil abile e ;
Tutti i dati che compaiono in un programma possiedono uno e un solo tipo, e possono essere di tipo semplice oppure aggregati in modo complesso. I tipi di dato semplici sono classificati secondo lo schema gerarchico riportato di seguito. semplici
reali
float
ordinali
enumerativi
double
int
predefiniti
char
bool
I tipi semplici possono essere float o double, oppure di tipo ordinale; i tipi ordinali possono essere definiti dal programmatore attraverso i tipi enumerativi, oppure possono appartenere ai tipi ordinali predefiniti int, bool, char. Il tipo ordinale si chiama così perché descrive un insieme finito e ordinato di valori, che possono essere associati a numeri interi positivi.
I tipi ordinali predefiniti e i tipi denza.
float
e
double
sono già stati presentati e utilizzati in prece-
10.2 Tipi enumerativi (enumerazioni) A volte una variabile variabile può assumere solo una serie di valori definiti all’interno di un insieme discreto di possibilità. Le enumerazioni sono molto comuni nella vita di tutti i giorni: per esempio, può essere un’enumerazione la lista dei controlli da eseguire sull’auto prima di affrontare un viaggio: freni, fari, gomme, olio, tergicristalli, carburante. 169 16 9
Sezione 4 - Strutture dei dati
Per definire in C++ un’enumerazione si usa la seguente sintassi: enum
nome_enumerazione {elenco_enumerazioni};
dove enum è la parola chiave che introduce intr oduce l’enumerazione. Riprendendo la lista di esempio proposta in precedenza, si può scrivere: enum controlli {freni, fari, gomme, olio, tergicristalli, carburante};
Il nome dell’enumerazione può essere utilizzato per dichiarare variabili di tale tipo, in maniera analoga alle dichiarazioni di tipo ti po viste in precedenza. Per esempio, potremo scrivere: controlli check;
La variabile check può assumere uno qualsiasi dei valori della lista dei controlli definita in precedenza. Per esempio, si può scrivere: check = gomme;
oppure if (check == fari) cout << ”controlla fari”;
Si deve ricordare che a ognuno dei valori di una variabile enumerativa corrisponde il numero d’ordine che esso occupa all’interno della definizione dell’enumerazione. Esempio Enumera................ Enumera............................... ............................... ................................ ................................ ............................... ............................... .................. Scrivere un programma che utilizza u tilizza una variabile di tipo controlli . Codice Enumera.cpp 1 2 3
#include using namespace std;
4 5 6 7 8
//INIZIO int main () { //definisci l'enumerazione enum controlli {freni, fari, gomme, olio, tergicristalli, carburante};
9 10 11 12
170 17 0
//definisci la variabile di tipo enumerativo controlli check;
13 14 15 16
//assegna un valore alla variabile check = gomme;
17 18
if (check == gomme) cout << "controlla fari";
19 20 21
//scrivi il numero d'ordine del valore della variabile enumerativa cout << "\nNumero d'ordine di gomme = " << check;
//esegui un confronto
Unità didattica 10 - Enumerazioni e array
22 23 24 25 26
//fine programma cout << "\n\nFine system ("pause"); return 0;
";
}
Prova di esecuzione
Analisi del codice
Alla riga 8 viene definito il tipo enumerativo enumerativo controlli . Alla riga 11 viene introdotta la variabile variabile check di tipo controlli e alla riga riga 14 le viene assegn assegnato ato un valore (scelto tra quelli definiti nell’enumerazione controlli ). Alla riga 17 viene eseguito un confronto. Anche se la variabile check contiene “gomme”, l’istruzione alla riga 20 non fa comparire in output la parola “gomme”, “gomme”, ma il suo numero di posizione (partendo da zero) all’interno della definizione dell’enumerazione. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
Fino a questo momento abbiamo visto come sia possibile definire e utilizzare in C++ tipi di dati che abbiamo definito “semplici”. Se ci soffermiamo, però, a considerare qualche esempio un po’ più complesso di quelli presentati fino a ora ci rendiamo rapidamente conto che non è affatto raro incontrare la necessità di gestire elenchi di dati. Pensiamo per esempio a un elenco di invitati a una festa, alla lista degli studenti di una classe o agli iscritti a una gara. In questi tre tr e esempi siamo di fronte a un dato (cognome e nome) sempre dello stesso tipo che si ripete più più volte. Nei prossimi paragrafi presenteremo le strutture messe a disposizione da C++ per gestire non più dati singoli, bensì strutture di dati che raggruppano in un’unica variabile dati diversi.
10. 0.3 3 Ti Tipo po ve vettto tore re Cominciamo a esaminare esaminare le strutture che contengono dati tutti dello stesso tipo: in generale, tali strutture prendono il nome di array e si distinguono in vettori come array a una dimensione e ma- trici come array a due dimensioni. La prima struttura che esaminiamo è la struttura di vettore che permette di raggruppare diversi dati dello stesso tipo e di associare un indice a ogni dato.
Un vettore è un insieme ordinato di oggetti oggetti omogenei, ovv ovvero ero appartenenti a un unico tipo. È possibile accedere ai vari oggetti del vettore utilizzando il loro indice numerico. Il numero di elementi contenuto nel vettore è indicato come dimensione dimensione.. 171 17 1
Sezione 4 - Strutture dei dati
Prendiamo, per esempio, l’elenco degli studenti di una classe, così come appare sul registro. 1
Abbiati Mario
2
Bonaldi Piera
3
Casati Luigi
4
Espo Es pos sit ito o Sa Salv lva ato tore re
..
. ..
..
. ..
24
Vivaldi Giu Giuseppe
A ogni studente è associato un numero di posizione e ogni numero individua uno studente. studente. In questo esempio, ogni elemento del vettore è di tipo stringa (per contenere il cognome e nome dello studente) e ogni elemento è individuato da un numero, detto indice. La dimensione del vettore è 24. Oppure consideriamo un quadrilatero irregolare in cui ogni lato ha una misura diversa. 4 3
2
1
Possiamo scrivere:
LATO
MISURA
1 2 3 4
15 8 7 16
Ogni elemento dell’esempio precedente è un dato di tipo numerico (la misura del lato) e per ogni misura è presente un indice che è il numero del lato; la dimensione è 4.
10.4 10 .4 Vet etto tori ri in C+ C++ + L’istruzione per definire un vettore ha il seguente formato: tipo nomeVettore [dimensione];
dove: tipo specifica
il tipo di dato comune a tutte le componenti; nomeVettore è il nome collettivo delle componenti del vettore; dimensione è il numero degli elementi contenuti nel vettore. Riprendendo gli esempi del paragrafo precedente, per definire il vettore che contiene i nomi dei 24 studenti di una classe possiamo scrivere: int dim = 24; string Studenti [dim];
172 17 2
Unità didattica 10 - Enumerazioni e array
Per definire le misure dei lati di un quadrilatero: int dim=4; int misure [dim];
È buona norma definire la dimensione del vettore come variabile (nel nostro caso: dim ): in questo modo eventuali variazioni della dimensione richiedono un solo intervento nel codice. Si ricordi che l’intervallo dei valori possibili per l’indice di un vettore parte da 0 e arr iva fino a dimensione –1.
In base a questa regola, nell’esempio dell’elenco degli studenti il primo studente (Abbiati Mario) è individuato dall’indice 0 e l’ultimo (Vivaldi Giuseppe) dall’indice 23. Quando si lavora con una variabile di tipo vettore, occorre sempre, all’interno del programma, indicare sia il nome della variabile sia l’indice che individua la componente del vettore che vogliamo trattare; per esempio, per assegnare al primo elemento del vettore la misura del primo lato, si scrive: misure[0] = 15
Si noti che l’indice va indicato racchiuso tra parentesi quadre dopo il nome della variabile. Per assegnare a ciascun lato la misura corrispondente, si scrive: misure[1] = 8 misure[2] = 7 misure[3] = 16
Dal punto di vista concettuale, possiamo pensare che in memoria è presente una struttura di questo tipo: VETTORE misure →
15
indice →
0
8 1
7
16
2
3
Alcuni esempi di istruzioni sugli elementi di un vettore
Sempre riferendosi alle misure dei lati di un quadrilatero, per scrivere la misura del terzo lato si utilizza la seguente istruzione: cout << “misura terzo lato = ” << misure[2];
Per sommare le misure del primo e del quarto lato, si utilizza la seguente istruzione: somma = misure[0]+ misure[3]
Spesso nasce l’esigenza di accedere a tutti gli elementi di un vettore o, come si usa dire, di “visitare” tutti gli elementi del vettore per poter eseguire una elaborazione su ciascuno di essi. In questo caso, torna molto utile ricorrere alla struttura della ripetizione con contatore: attraverso di essa si utilizza un indice che assume tutti i valori che vanno da 0 a dimensione –1 del vettore e che serve per riferirsi a ciascun elemento. Esempio Perimetro ............................................................................................................. Dopo avere definito e inizializzato un vettore che contenga le misure dei lati di un quadrilatero, visualizzare la misura del perimetro.
173
Sezione 4 - Strutture dei dati
Descrizione della soluzione
Le istruzioni per definire l’array e per assegnargli i valori iniziali sono già state viste in questo paragrafo. Per sommare tutti i lati del quadrato si definisca un ciclo for con l’indice che va da 0 a 3 e che permette di accedere a tutti e quattro i lati del quadrilatero, vale a dire a tutti e quattro gli elementi del vettore delle misure. Pseudocodifica
//struttura dati vettore delle misure dei lati //input //tutti i dati sono definiti all’interno del programma //output perimetro //variabili di lavoro i //indice per ciclo for INIZIO //inizializza il vettore delle misure misure[0] ← 15 misure[1] ← 8 misure[2] ← 7 misure[3] ← 16 //inizializza perimetro perimetro ← 0 //visita il vettore PER i DA 0 A dim – 1 perimetro = perimetro + misure[i] FINE PER
//somma le misure ad 1 ad 1
scrivi (Perimetro) FINE Codice
Perimetro.cpp 1 2 3 4
#include using namespace std;
5 6 7
int main () { //struttura del vettore
8 9 10 11 12 13 14
174
//INIZIO
const int dim=4; int misure[dim]; int i; int perim;
//contatore per ciclo for //dato di output
//inizializza il vettore misure
Unità didattica 10 - Enumerazioni e array
15 16 17 18 19
misure[0] misure[1] misure[2] misure[3]
20 21 22
//inizializza perimetro perim = 0;
23 24 25 26 27
//visita il vettore for(i=0; i
28 29 30 31
//scrivi perimetro cout<<"Perimetro = "<
32 33 34 35
//fine programma cout << "\n\nFine system ("pause"); return 0;
36
= = = =
15; 8; 7; 16;
//somm //s omma a le misu misure re ad 1 ad 1
";
}
Prova di esecuzione
Analisi del codice
Alla riga 8 viene indicata la dimensione del vettore, che in questo caso è costante, e alla riga 9 viene definita la struttura del vettore di nome misure e di dimensione dim . misure assegnando un valore a ogni componente. misure Alle righe da 15 15 a 18 viene inizia inizializzato lizzato il vettore vettore Alla riga 24 comincia la “visita” del vettore grazie al ciclo for (con l’indice che, a partire da 0, assume tutti i valori minori di dim ). ). Alla riga 26 la visita consiste consiste nel sommare sommare in perim ogni elemento del vettore misure. Le istruzioni di inizializzazione usate nell’esempio precedente possono essere sostituite da una scrittura più compatta. Nel codice Perimetro.cpp le righe da 15 a 18 ser servono vono per assegnare i valori iniziali iniziali alle componenti del vettore misure. Le riportiamo di seguito. misure[0] misure[1] misure[2] misure[3]
= = = =
15; 8; 7; 16;
L’operazione precedente può essere realizzata assegnando direttamente i valori delle componenti in fase di definizione del vettore. Quindi le sette righe di codice 7 8
const int dim=4; int misure[dim];
175 17 5
Sezione 4 - Strutture dei dati
. . . 14
//inizializza il vettore misure
15 15
misure[0] = 15; mis ure[ 1] = 8;
16
mis ure[ 2] = 7;
17 18
mis ure[ 3] = 16 ; . . .
possono essere sostituite dalla seguente scrittura più “veloce”: int [] misure = new int [] {15, 8, 7, 16};
Si noti che non è neppure necessario specificare la dimensione del vettore.
.............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
10.5 Car Carica icamen mento to di di un vet vettore tore in memo memoria ria Nel paragrafo precedente è stato illustrato un esempio di trattamento di un vettore, ipotizzando che i suoi valori fossero caricati all’interno del codice del programma. Nell’esempio che segue, viene illustrato come si può fare in modo che sia l’utente a caricare direttamente in un vettore i valori che desidera. Inizialmente, consideriamo il caso in cui la dimensione del vettore è nota a priori. Esempio arrayCarica ............... ............................... ................................ ............................... ............................... ................................ ........................... ...........
Consentire l’inserimento l’inser imento da tastiera dei valori di un vettore di interi con dimensione fissa = 5. Descrizione della soluzione
Dopo avere definito un vettore di 5 interi, si eseguono due “visite” sugli elementi del vettore: la prima contiene il blocco di istruzioni che permette di caricare i dati inseriti da tastiera in ciascun elemento del vettore; la seconda viene utilizzata per visualizzare i dati caricati. Pseudocodifica
//costanti dimensione = 5 //struttura dati vettore di 5 interi //input serie di 5 numeri //output vettore di 5 interi INIZIO PER ogni elemento del vettore richiedi dato carica dato FINE PER PER ogni elemento del vettore scrivi (elemento) FINE PER FINE 176 17 6
Unità didattica 10 - Enumerazioni e array
Codice arrayCarica.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
#include using namespace std; //INIZIO int main () { //definisci il vettore di 5 elementi const int dim = 5; //definisci la struttura dati int vett [dim]; //ciclo for x caricare vettore for (int i=0; i>vett[i]; } cout<<"\nContenuto del vettore: "; //visita il vettore for (int i=0; i
";
}
Prova di esecuzione
Analisi del codice
Alla riga 14 viene introdotto il ciclo ciclo for per il caricamento dei dati nel vettore. L’indice i è definito in modo “locale” rispetto all’istruzione for. 177 17 7
Sezione 4 - Strutture dei dati
Alla riga 18 il dato inserito da tastiera viene assegnato all’elemento vett con indice i, dove i, grazie al ciclo for, assume, di volta in volta, i valori che vanno da 0 a 4. Alla riga 24 la scrittura degli degli elementi del vettore viene realizzata con un secondo ciclo for. Alla riga 26 ogni elemento del vettore viene scritto sulla stessa riga dei precedenti. Solo alla fine del ciclo for, alla riga 28, si va a capo per scrivere una nuova riga di messaggio. .............................. .............. ................................ ................................ ................................ ............................... ............................... ................................ .............................. ..............
10.6 10. 6 Arr Array ay di di dime dimensi nsione one vari variabi abile le Allo scopo di fornire fornire all’utente strumenti flessibili, vediamo di perfezionare ulteriormente gli esempi fin qui visti, in modo da realizzare un programma che permetta all’utente di caricare un vettore la cui dimensione è definita dall’utente stesso. Nell’esempio del paragrafo precedente è stata data la possibilità di caricare i dati di un array da tastiera, ma la dimensione di mensione del vettore era definita come costante uguale a 5. Nulla vieta, però , di acquisire da tastiera la dimensione del vettore e di definire il vettore stesso solo dopo avere avuto a disposizione la dimensione scelta dall’utente. Esempio arrayV arrayVaria....................... aria....................................... ................................ ................................ ............................... ............................... ...................... ...... Permettere l’inserimento da tastiera dei valori di un vettore di interi. Anche la dimensione del vettore è acquisita da tastiera e deve essere inferiore a 20.
Descrizione della soluzione
L’algoritmo e il programma che viene presentato si discostano dal precedente soltanto perché la dimensione non è definita come costante, ma è letta da tastiera. Pseudocodifica
//costanti dimensione massima = 20 //struttura dati vettore di interi //input dimensione serie di n interi (n = dimensione) //output vettore di interi INIZIO leggi (dimensione) PER ogni elemento del vettore chiedi dato carica dato FINE PER PER ogni elemento del vettore scrivi (elemento) FINE PER FINE Codice arrayVaria.cpp 1 2
178 17 8
#include using namespace std;
Unità didattica 10 - Enumerazioni e array
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
//INIZIO int main () { const int MAX = 20; int dim; int i;
//dimensione massima per il vettore
//definisci il vettore int vett[MAX]; //acquisisci e controlla la dimensione do { cout<<"\nInserisci la dimensione (max = 20) " cin>>dim; } //controlla il rispetto dei limiti while (dim < 1 | dim > MAX); //salta una riga cout<<"\n"; for (i=0; i>vett[i]; } cout<<"\nContenuto del vettore: "; //visita il vettore for (i=0; i
";
}
Prova di esecuzione
179 17 9
Sezione 4 - Strutture dei dati
Analisi del codice
Alla riga 7 viene definita la costante intera MAX, che rappresenta la dimensione massima per il vettore. Tale dimensione è posta pari a 20, così come richiesto dal testo del problema. Alla riga 8 è definita la variabile dim , in cui viene memorizzata la dimensione effettiva del vettore inserita da tastiera. Alla riga 12 viene definito il vettore di dimensione MAX, di cui verranno utilizzate soltanto le prime dim posizioni. Alle righe 17 e 18 viene rispettivamente richiesto e acquisito il valore di dim ; le elaborazioni successive del vettore fanno riferimento solo a questo valore, come si può vedere alle righe 26 e 35. Alla riga 21 viene eseguito un controllo sul valore dim inserito da tastiera: soltanto se rientra tra i limiti imposti dal problema (è cioè compreso tra 1 e MAX) l’elaborazione può procedere. Alla riga 24 viene immessa a video una riga vuota, per separare le istruzioni di acquisizione della dimensione dalla fase di caricamento e stampa dei dati del vettore. Tale fase è identica a quella già vista nell’esempio ArrayCarica . ...........................................................................................................................................
10.7 Matrici
Nei paragrafi precedenti abbiamo visto come lavorare con array a una dimensione, vale a dire con array che hanno un unico indice di individuazione degli elementi che li compongono. È possibile, tuttavia, definire anche array a due o più dimensioni. Solitamente, gli array a una dimensione prendono il nome di vettori, mentre gli array a due dimensioni sono detti matrici.
Come già visto, un array a una dimensione è definito da un nome e da una dimensione, con una sintassi del tutto simile a quella riportata di seguito. const int dim = 4; int vettore[dim];
Se, per esempio, è dato il vettore: VETTORE Dati → Indice →
37
53
11
28
0
1
2
3
avremo che l’elemento Vettore[2] contiene il valore 11. Per definire una matrice, invece, occorre specificare due o più dimensioni. Una matrice è un insieme di dati dello stesso tipo organizzati in una griglia: ogni elemento che compone la matrice è individuato dall’indice di riga e dall’indice di colonna in cui l’elemento è posizionato.
In C++ la definizione della struttura di una matrice è analoga alla definizione di un array a una dimensione. Per esempio, se si scrive: int matrice[3][4]
180
Unità didattica 10 - Enumerazioni e array
s’intende che la struttura di nome matrice è composta da 3 righe e 4 colonne, come mostrato nell’esempio che segue. MATRICE
Righe
0
7
37
24
3
1
45
12
18
81
2
11
53
37
28
0
1
2
3
Colonne
Per elaborare un singolo elemento della matrice occorre specificare il numero di riga e il numero di colonna. Con riferimento alla matrice precedente, si può dire che 18 è contenuto nella cella individuata da matrice[1][2] .
Esempio MatriceCarica ..................................................................................................... Caricare i dati di una matrice di interi con numero di righe e numero di colonne non superiore a 20. Scrivere il contenuto della matrice.
Descrizione della soluzione Il limite di 20 è posto per non definire in memoria una struttura troppo ingombrante. Il programma è organizzato in tre parti. La prima parte serve per acquisire il numero di righe e il numero di colonne e per preparare la matrice. Quando viene acquisito il numero di righe, viene controllato che questo non ecceda il limite di 20: se ciò accade, il programma richiede di nuovo l’inserimento del dato e così succede per il numero delle colonne. La matrice è inizialmente definita come un array con dimensioni massime: solo dopo avere acquisito il numero di righe e di colonne vengono definite le sue dimensioni effettive. La seconda parte consiste nel caricamento dei dati nella matrice. Vengono utilizzati due indici: i per le righe e j per le colonne. Per il caricamento si consideri ogni riga della matrice come un vettore e si proceda come negli esempi del paragrafo precedente al caricamento del vettore attraverso una ripetizione enumerativa con l’indice j che va da 0 al numero di colonne – 1. Tale iterazione viene nidificata in una ripetizione più esterna che ripete l’operazione di acquisizione per tutte le righe della matrice, con l’indice i che varia da 0 al numero di righe – 1. Per la terza e ultima parte, la stampa della matrice, di nuovo si devono organizzare due cicli for uno interno all’altro: il più interno serve per “visitare” gli elementi di una riga mentre l’esterno serve per ripetere tale operazione su tutte le righe. Occorre avere l’accortezza di scrivere gli elementi di una stessa riga senza andare a capo e di andare a capo solo alla fine della riga della matrice, cioè alla fine del ciclo for più interno.
Pseudocodifica Costante MAX = 20 //struttura dati Matrice di interi //input Righe Colonne
//limite massimo per le dimensioni della matrice
181
Sezione 4 - Strutture dei dati
Serie di numeri per caricare la matrice //output Matrice //variabili di lavoro i indice di riga j indice di colonna INIZIO //acquisisci numero delle righe RIPETI leggi (righe) //controlla rispetto dei limiti MENTRE righe < 1 OR righe > 20 //acquisisci numero delle colonne RIPETI leggi (colonne) //controlla rispetto dei limiti MENTRE colonne < 1 OR colonne > 20 //carica dati nella matrice PER i DA 0 A righe – 1 PER j DA 0 A colonne – 1 chiedi dato carica dato FINE PER FINE PER //visita la matrice PER i DA 0 A righe – 1 PER j DA 0 A colonne – 1 scrivi (matrice [i, j]) FINE PER vai a capo FINE PER FINE Codice
MatriceCarica.cpp 1
#include
2 3 4 5
#include using namespace std;
6 7
int main () {
8 9 10 11 12 13
182
//INIZIO
const int MAX = 20;
//dimensione massima per la matrice
//definisci la matrice int mat[MAX][MAX]; int righe; int colonne;
Unità didattica 10 - Enumerazioni e array
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
int i; int j; //acquisisci numero delle righe do { cout<<"\nInserisci il numero righe (max="<>righe; } //controlla il rispetto dei limiti while (righe < 1 | righe > MAX); //acquisisci numero delle colonne do { cout<<"\nInserisci il numero colonne (max="<>colonne; } //controlla il rispetto dei limiti while (colonne < 1 | colonne > 20); cout<<"\n"; //carica dati nella matrice for (i=0; i>mat [i][j]; } } cout<<"\nContenuto della matrice: \n"; //visita la matrice for (i=0; i
";
} 183
Sezione 4 - Strutture dei dati
Prova di esecuzione
Analisi del codice
Alla riga 11 viene definita la struttura della matrice, che viene identificata con il nome mat e per la quale è specificato il numero delle righe e delle colonne. Alle righe 20 e 21 viene acquisito il numero di righe che compongono la matrice; le istruzioni di acquisizione sono inserite all’interno di una ripetizione che ser ve per controllare se il numero digitato rientra nei limiti imposti (da 1 a 20). Alle righe 29 e 30 viene definito un processo analogo per l’acquisizione del numero di colonne. Alla riga 38 inizia il caricamento dei dati nella matrice: il primo ciclo for serve per ripetere le operazioni sottostanti per tutte le righe. Alla riga 40 viene definito il ciclo for più interno, che permette di “muoversi” all’interno di una riga, e per ogni elemento della riga viene acquisito il dato da inserire come indicato alle righe 43 e 44. In particolare, alla riga 44, il dato letto da tastiera viene assegnato a una “casella” della matrice di nome mat individuata dai due indici i e j. Alla riga 50 inizia la scrittura dei dati della matrice: l’impostazione generale del codice successivo è analoga a quella precedente relativa al caricamento dei dati: la differenza principale è che, nel ciclo for più interno, l’istruzione di scrittura indicata alla riga 56 permette di scrivere i dati uno di seguito all’altro, mentre alla riga 59 è indicata l’istruzione per andare a capo. Tale istruzione non rientra nel ciclo for interno, ma fa parte del ciclo for esterno, così che essa venga eseguita solo alla fine di ogni riga e venga ripetuta per ogni riga. ........................................................................................................................................... Allo scopo di acquisire dimestichezza con i problemi relativi al trattamento delle matrici, viene presentato un secondo esempio. Esempio TotaleRighe.......................................................................................................... Caricare i dati di una matrice di interi con numero di righe e numero di colonne non superiore a 20. Scrivere il contenuto della matrice e indicare per ogni riga il totale dei valori in essa contenuti.
Descrizione della soluzione
La prima parte della richiesta del problema è identica a quella dell’esempio precedente; si tratta solo di intervenire nella parte relativa alla visualizzazione del contenuto della matrice e inserire in questa parte le istruzioni per comunicare i totali di riga. 184
Unità didattica 10 - Enumerazioni e array
Si definisca una variabile totRiga in cui accumulare i valori di ciascuna riga e, con riferimento al segmento di codice relativo alla scrittura della matrice, si inseriscano le istruzioni che ser vono per: 1. 2. 3.
inizializzare totRiga ; sommare in totRiga i valori di una riga della matrice; scrivere totRiga.
Pseudocodifica
Costante Max = 20 //limite massimo per le dimensioni della matrice //struttura dati matrice di interi //input Righe Colonne Serie di numeri per caricare la matrice //output matrice totaleRiga //variabili di lavoro i indice di riga j indice di colonna INIZIO .... //parte uguale all’esempio precedente .... //visita la matrice PER i DA 0 A righe – 1 totRiga ← 0 PER j DA 0 A colonne – 1 scrivi (matrice [i, j]) totRiga ← totRiga + mat [i, j] FINE PER scrivi (totRiga) e vai a capo FINE PER FINE Codice TotaleRighe.cpp 1 2
#include #include
3 4 5
using namespace std;
6 7 8 9 10 11 12
//INIZIO int main () { const int MAX = 20;
//dimensione massima per la matrice
//definisci la matrice int mat[MAX][MAX]; int righe;
185
Sezione 4 - Strutture dei dati
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 186
int int int int
colonne; i; j; totRiga;
//acquisisci numero delle righe do { cout<<"\nInserisci il numero righe (max="<>righe; } //controlla il rispetto dei limiti while (righe < 1 | righe > MAX); //acquisisci il numero delle colonne do { cout<<"\nInserisci il numero colonne (max="<>colonne; } //controlla il rispetto dei limiti while (colonne < 1 | colonne > MAX); //salta una riga cout<<"\n"; //carica la matrice for (i=0; i>mat [i][j]; } } cout<<"\nContenuto della matrice: \n"; cout<
Unità didattica 10 - Enumerazioni e array
68 69 70 71 72
cout<
73 74 75
//fine programma cout << "\n\nFine system ("pause");
76 77
return 0;
";
}
Prova di esecuzione
Analisi del codice Le righe 56 e 68 realizzano rispettivamente l’inizializzazione e la scrittura a video di totRiga ; poiché queste due operazioni devono essere fatte una volta sola per ogni riga, esse sono poste all’interno del ciclo for più grande, ma all’esterno del ciclo for più interno. Alla riga 64 è indicata l’istruzione per totalizzare i singoli valori di ogni elemento di ogni riga, pertanto essa è posta all’interno del ciclo for più interno. ...........................................................................................................................................
10.8 Passaggio di un vettore come parametro a una funzione Nell’Unità didattica 9 è stato affrontato l’argomento del passaggio dei parametri dal main alla funzione; in particolare, è stata spiegata la differenza tra passaggio per valore e per riferimento. I parametri presi in considerazione erano, tuttavia, dati di tipo semplice, non dati strutturati come un vettore o una matrice. Nel caso in cui i parametri da passare siano array occorre rispettare alcune particolarità sintattiche, che sono oggeto di questo paragrafo. Vediamo da subito un esempio. Esempio funzioniArray........................................................................................................ Acquisire da tastiera un vettore di dimensione non superiore a 20 righe e stamparlo. Per la realizzazione del programma utilizzare tre funzioni, i cui nomi sono chiediDim , leggi Vettore, scriviVettore .
187
Sezione 4 - Strutture dei dati
Lo sviluppo top-down del programma è illustrato nella figura a destra.
main
Chiedi dimensione
Leggi vettore
Codice
funzioniArray.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 188
#include using namespace std; const int MAX = 20; int dim; int i;
//dimensione massima per il vettore //dimensione acquisita da tastiera
//funzione per leggere la dimensione int chiediDim (int &d) { //acquisisci e controlla la dimensione do { //chiedi e leggi la dimensione cout<<"\nInserisci la dimensione "; cin>>d; } //controlla il rispetto dei limiti while (d < 1 | d > MAX); return d; } //funzione per leggere il vettore void leggiVettore (int v[], int d) { for (i=0; i>v[i]; } } //funzione per scrivere il vettore void scriviVettore (int v[], int d) { cout<<"\nContenuto del vettore: "; //visita il vettore for (i=0; i
Scrivi vettore
Unità didattica 10 - Enumerazioni e array
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
//INIZIO int main () { //definisci il vettore con dimensione MAX int vett[MAX]; //richiama la funzione per acquisire la dimensione del vettore dim = chiediDim (dim); //salta una riga cout<<"\n"; //richiama la funzione leggiVettore leggiVettore(vett,dim); //richiama la funzione scriviVettore scriviVettore(vett,dim); //fine programma cout << "\n\nFine system ("pause"); return 0;
";
}
Prova di esecuzione
Risulta del tutto uguale a quella dell’esempio ArrayVaria. Analisi del codice
Dalla riga 9 alla riga 21 è sviluppata la funzione chiediDim . Si noti che nell’intestazione della funzione (riga 9) il parametro d richiede il passaggio per riferimento (&d), quindi le variazioni del contenuto della variabile d hanno effetto sulla variabile globale dim , quando questa verrà passata alla funzione alla riga 53. Alla riga 59 viene richiamata la funzione leggiVettore , che è sviluppata dalla riga 24 alla riga 32 del codice. Si noti che alla riga 59 l’argomento vett non riporta né la dimensione né l’indicazione che si tratta di un’array, e che il parametro v[] presente nell’intestazione della funzione alla riga 24 si limita a “ricordare”, con la presenza delle parentesi quadre, che si tratta di un vettore e non di un dato elementare; inoltre v[] non è preceduto dal carattere & per richiederne il passaggio per riferimento, in quanto tale modalità di passaggio di parametri è in questo caso automatica. Analoghe considerazioni valgono per la funzione scriviVettore : in particolare, è da notare che il passaggio dei parametri per riferimento è indispensabile per la funzione leggiVettore , ma non per scriviVettore . Nel caso in cui si voglia lavorare con array a più di una dimensione i richiami alla funzione leggiVettore (che in questo caso si può chiamare leggiMatrice) mantengono la stessa sintassi indicata nell’esempio, con l’argomento privo di dimensioni, mentre nell’intestazione della funzione il parametro che si riferisce alla struttura multidimensionale DEVE contener e la specifica di tutte le dimensioni tranne la prima. Per esempio, se si vuole lavorare sulla matrice quadrata matr di ordine MAX, per richiamare la funzione leggiMatrice si deve scrivere l’istruzione che segue. leggiMatrice(matr, . . . );
mentre l’intestazione della funzione assume la forma indicata di seguito. void scriviMatrice (int m[][MAX], . . . )
........................................................................................................................................... 189
Sezione 4 - Strutture dei dati
Esercizi Unità didattica 10 Dopo aver caricato in memoria un vettore di interi con dimensione
d
(con
d
inserito da tastiera), calco-
lare la somma dei valori contenuti nel vettore.
Dopo aver caricato in memoria un vettore di interi con dimensione
d
(con d inserito da tastiera), azzera-
d
(con d inserito da tastiera), azzera-
d
(con d inserito da tastiera), azzera-
re il primo elemento del vettore.
Dopo aver caricato in memoria un vettore di interi con dimensione re l’ultimo elemento del vettore.
Dopo aver caricato in memoria un vettore di interi con dimensione re l’elemento di posto n, con n dato in input.
Dopo aver caricato in memoria un vettore di interi con dimensione
d
(con
d
inserito da tastiera), calco-
lare la media dei valori contenuti nel vettore.
Dopo aver caricato in memoria un vettore di interi con dimensione
d
Dopo aver caricato in memoria un vettore di interi con dimensione
d
Dopo aver caricato in memoria un vettore di interi con dimensione
d
Dopo aver caricato in memoria un vettore di interi con dimensione
d
(con
d
inserito da tastiera), scrive-
d
(con
d
inserito da tastiera), scrive-
d
(con d inserito da tastiera), creare
(con d inserito da tastiera), calcolare la media dei valori contenuti nel vettore. Successivamente scrivere gli elementi del vettore che hanno valore superiore alla media. (con d inserito da tastiera), calcolare la media dei valori contenuti nel vettore. Successivamente contare gli elementi del vettore che hanno valore superiore alla media. (con d inserito da tastiera), calcolare la media dei valori contenuti nel vettore. Successivamente creare un nuovo vettore che contenga gli elementi del vettore iniziale che hanno valore superiore alla media.
re gli elementi pari contenuti nel vettore.
Dopo aver caricato in memoria un vettore di interi con dimensione re gli elementi di posto pari contenuti nel vettore.
Dopo aver caricato in memoria un vettore di interi con dimensione
un nuovo vettore che contenga gli elementi pari del vettore iniziale.
Dopo aver caricato in memoria un vettore di interi con dimensione d (con d inserito da tastiera), inserire “in testa” al vettore un nuovo elemento. Scrivere il vettore iniziale e il vettore modificato. (Suggerimenti: poiché deve essere inserito un nuovo elemento il vettore deve esser e definito con dimensione pari a d + 1; “in testa” al vettore vuol dire al posto 0 del vettore; per fare spazio al nuovo elemento, i dati preesistenti devono essere spostati di un posto a destra).
Dopo aver caricato in memoria un vettore di interi con dimensione d (con d inserito da tastiera), inserire “in coda” al vettore un nuovo elemento. Scrivere il vettore iniziale e il vettore modificato. (Suggerimenti: poiché deve essere inserito un nuovo elemento il vettore deve essere definito con dimensione pari a d + 1; “In coda” al vettore vuol dire dopo l’ultimo elemento del vettore; per fare spazio al nuovo elemento, basta aumentare la dimensione iniziale). 190
Unità didattica 10 - Enumerazioni e array
Esercizi Unità didattica 10 Dopo aver caricato in memoria un vettore di interi con dimensione
d
Dopo aver caricato in memoria un vettore di interi con dimensione
d
(con d inserito da tastiera), eliminare l’ultimo elemento del vettore. Scrivere il vettore iniziale e il vettore modificato. (Suggerimenti: basta decrementare la dimensione del vettore). (con d inserito da tastiera), eliminare il primo elemento del vettore. Scrivere il vettore iniziale e il vettore modificato. (Suggerimenti: si tratta di spostare tutti gli elementi del vettore di un posto a sinistra e, successivamente, diminuire la dimensione del vettore).
Dopo aver caricato in memoria una matrice con dimensioni date in input non superiori a 20, scrivere l’elemento di posto [r,c] (con r e c inserite da tastiera).
Dopo aver caricato in memoria una matrice con dimensioni date in input non superiori a 20, scrivere gli elementi della riga k (con k dato in input).
Dopo aver caricato in memoria una matrice con dimensioni date in input non superiori a 20, scrivere gli elementi della colonna k (con k dato in input).
Dopo aver caricato in memoria una matrice con dimensioni date in input non superiori a 10, calcolare i totali di colonna.
Dopo aver caricato in memoria una matrice quadrata di or dine n (con n dato in input non superiore a 10), scrivere gli elementi che stanno sulla diagonale principale (per matrice quadrata di ordine n si intende una matrice con numero-righe = numero-colonne = n; gli elementi che stanno sulla diagonale principale hanno indice di riga e indice di colonna uguali).
191
Unità didattica
11
Stringhe e strutture CHE COSA IMPARERAI A FARE $
Definire una stringa
$
Concatenare più stringhe
$
Estrarre sottostringhe
$
Confronto lessicografico tra stringhe
$
Trattare le stringhe come array di caratteri
$
Dichiarare una struttura
$
Usare le strutture come un nuovo tipo di dato
CHE COSA DOVRAI STUDIARE $
Sintassi del metodo length
$
Operazione di somma tra stringhe
$
Sintassi per estrarre una sottostringa
$
Operazioni di confronto tra stringhe
$
Concetto di dato aggregato
$
Concetto di struttura e la sua sintassi
Unità didattica
1 1 .1
11 Stringhe e strutture
D efinizione di stringa
Insieme ai dati di tipo numerico, le stringhe rappresentano il tipo di dati più utilizzato nei programmi. Una stringa è una sequenza di caratteri, come “Hello”. In C++ le stringhe sono racchiuse tra virgolette doppie, che non sono considerate parte della stringa.
È possibile dichiarare variabili destinate a contenere stringhe, utilizzando istruzioni come quella che segue. string nome = “Giovanni”;
Il tipo string è un tipo standard di C++; per utilizzarlo è sufficiente includere il riferimento all’header file , utilizzando la direttiva riportata di seguito. #include
L’operazione di assegnazione permette di definire la stringa che deve essere contenuta da una variabile di tipo string , come illustra l’istruzione di esempio che segue. nome = “Carlo” ;
È anche possibile leggere una stringa da tastiera: il frammento di codice seguente mostra come. cout « “Inserisci il tuo nome =>
“;
cin » nome;
In questo caso nella variabile di tipo stringa viene memorizzata una sola parola: infatti, tenendo presente che le parole sono considerate dal C++ divise dai separatori (caratteri di spaziatura, di tabulazione, di a capo...), se l’utente digita “Mario Bianchi” in risposta al prompt, nella variabile nome viene memorizzata la sola parola “Mario”. Per leggere la stringa successiva si deve utilizzare una seconda istruzione di input. Questo vincolo complica la scrittura di un’istruzione di input che possa trattare correttamente le risposte di tipo stringa fornite dall’utente in risposta a una domanda: in alcuni casi è possibile che l’utente digiti solo il nome, in altri il nome seguito dal cognome, in altri ancora possono essere presenti le iniziali di un secondo nome. Questa situazione può essere risolta mediante il comando getline. L’istruzione getline(cin, nome);
legge tutti i caratteri digitati fino a che si preme INVIO e genera una stringa che contiene tutti i caratteri e che viene memorizzata nella variabile nome . 193
Sezione 4 - Strutture dei dati
Esempio LeggiNome .......................................................................................................... Chiedere il nome all’utente e memorizzarlo in una stringa.
Codice LeggiNome.cpp 1
#include
2
#include
3
using namespace std;
4 5
//INIZIO
6 7
int main () {
8
//definisci una variabile stringa
9 10
string nome;
11 12
//chiedi e leggi il nome cout << “\nInserisci il tuo nome =>
13
cin >> nome;
“;
14 15
//scrivi il contenuto della variabile nome
16
cout << “\nla variabile nome contiene “ << nome;
17 18 19
//fine programma cout << “\n\nFine
20 21
system (“pause”); return 0;
22
“;
}
Prova di esecuzione
Analisi del codice
Alla riga 2 viene incluso l’header file , che permette di richiamare i metodi in grado di operare sulle stringhe. Alla riga 13 viene letto da tastiera quanto ha digitato l’utente. Come si vede dalla prova di esecuzione, l’utente scrive sia il nome sia il cognome ma, poiché i due dati sono separati da uno spazio, il contenuto della variabile nome è soltanto “Mario”: infatti, quando alla riga 16 del programma viene scritto a video il contenuto della variabile nome , compare soltanto la stringa “Mario”. ........................................................................................................................................... Esempio NomeCompleto ................................................................................................... Chiedere il nome all’utente e memorizzare in una stringa tutti i caratteri digitati.
194
Unità didattica 11 - Stringhe e strutture
Codice NomeCompleto.cpp 1 2 3
#include #include using namespace std;
4 5 6 7 8
//INIZIO int main () { //definisci una variabile stringa
9 10 11 12
string nome;
13 14 15 16
getline(cin,nome); //scrivi il contenuto della variabile nome cout << “\nla variabile nome contiene “ << nome;
17 18
//fine programma
//chiedi e leggi tutta la risposta dell’utente cout << “\nInserisci il tuo nome => “;
19 20 21 22
cout << “\n\nFine system (“pause”); return 0;
“;
}
Prova di esecuzione
Analisi del codice
Come si vede dalla prova di esecuzione, il programma questa volta visualizza tutto ciò che è stato digitato dall’utente. Questo risultato dipende dalla riga 13, dove l’istr uzione di lettura permette la memorizzazione nella stringa di tutto il contenuto della riga inserita dopo il prompt “Inserisci il tuo nome =>”. ...........................................................................................................................................
1 1 .2
Lunghezza di una stringa
Nell’esempio appena visto la variabile nome contiene la stringa “Mario Bianchi”, che è composta da 12 caratteri compreso lo spazio. Il numero di caratteri di una stringa definisce la lunghezza della stringa.
Per esempio, la lunghezza della stringa “Mario Bianchi” è 13, mentre la lunghezza di “Hello, World!\n” è 14, dato che il carattere di escape “\n” è considerato unico. 195
Sezione 4 - Strutture dei dati
Si può calcolare la lunghezza di una stringa mediante la funzione length. A differenza della funzione getline , la funzione length è richiamata con la cosiddetta “notazione punto”: si deve per prima cosa scrivere il nome della stringa di cui si vuole calcolare la lunghezza, poi si inserisce un punto e infine il nome della funzione, seguiti da una coppia di parentesi. Un esempio è riportato di seguito. int n = nome.length();
Anticipando la terminologia che sarà trattata più avanti, nell’Unità didattica 12, relativa alla programmazione a oggetti, si precisa che length() è un metodo della classe string che si applica a un oggetto come nome. Esempio Lunghezza ............................................................................................................ Scrivere la lunghezza della risposta dell’utente a un prompt a video.
Codice Lunghezza.cpp 1
#include
2
#include
3
using namespace std;
4 5
//INIZIO
6
int main ()
7
{
8 9
//definisci una variabile stringa string nome;
10 11 12
//chiedi e leggi tutta la risposta dell’utente cout << “\nInserisci il tuo nome => “;
13 14
getline(cin,nome);
15
//scrivi il contenuto della variabile nome
16
cout << “\nLunghezza della risposta = “ << nome.length();
17 18
//fine programma
19
cout << “\n\nFine
20
system (“pause”);
21
return 0;
22
}
Prova di esecuzione
196
“;
Unità didattica 11 - Stringhe e strutture
Analisi del codice
L’unica differenza con l’esempio precedente è data dalla riga 16 dove, al posto della visualizzazione di quanto inserito dall’utente, viene scritta la lunghezza della stringa nome. ...........................................................................................................................................
1 1 .3
C oncatenazione ed estrazione
È possibile unire due stringhe utilizzando l’operatore “+” che, applicato alle stringhe, si limita ad accodare una stringa a un’altra stringa. L’operazione che permette di unire più stringhe per formarne una unica prende il nome di concatenazione. Esempio Concatena .......................................................................................................... Concatenare la stringa “in laboratorio” alla stringa “Benvenuti”, in modo da ottenere la frase “Benvenuti in laboratorio”.
Codice Concatena.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#include #include using namespace std; //INIZIO int main () { //dichiara due stringhe string stringa1 = “Benvenuti”; string stringa2 = “in laboratorio”; //concatena le stringhe stringa1 = stringa1 + stringa2; //scrivi il risultato cout << “\nStringa risultante = “ << stringa1; //fine programma cout << “\n\nFine system (“pause”); return 0;
“;
}
Prova di esecuzione
197
Sezione 4 - Strutture dei dati
Analisi del codice
Il codice mostra come concatenare due stringhe: infatti alla riga 13 , grazie all’operatore “+”, la stringa “in laboratorio” viene accodata alla stringa “Benvenuti”. Nonostante la compilazione del programma Concatena.cpp non rilevi errori, il risultato non è quello atteso: infatti, manca un carattere di separazione tra la prima stringa e la seconda. Per correggere questa “imperfezione”, la riga 13 deve essere modificata come segue: 13
stringa1 = stringa1 + “ “ + stringa2;
...........................................................................................................................................
Così come è possibile unire stringhe corte per formarne una lunga, si possono estrarre stringhe secondarie da una stringa iniziale più lunga. Per estrarre una stringa secondaria (detta anche sottostringa ) da una stringa principale si utilizza la funzione substr. La sintassi della funzione substr è s.substr(inizio,lung)
dove: è la stringa a cui viene applicata la funzione substr ; inizio è il numero del carattere della stringa da cui inizia l’estrazione della sottostringa (si tenga conto che la posizione del primo carattere ha indice 0); lung è la lunghezza della sottostringa da estrarre.
s
Esempio Estrai .................................................................................................................... Estrarre la prima parola del messaggio di saluto “Hello, world!”.
Codice Estrai.cpp
198
1 2 3
#include #include using namespace std;
4 5 6 7 8
//INIZIO int main () { //stringa di saluto
9 10 11
string stringa = “Hello, world!”;
12 13 14 15 16
string sottoStringa = stringa.substr(0,5);
17 18
//fine programma cout << “\n\nFine
//estrai la prima parola
//scrivi il risultato dell’estrazione cout << “\nStringa estratta = “ << sottoStringa;
“;
Unità didattica 11 - Stringhe e strutture
19 20 21
system (“pause”); return 0; }
Prova di esecuzione
Analisi del codice La parola da estrarre è “Hello”, che inizia alla posizione 0 della stringa ed è lunga 5 caratteri, quindi l’istruzionee alla riga 12 è substr(0,5) . ........................................................................................................................................... Esempio Iniziali .................................................................................................................. Dopo aver letto da console il nome e il cognome specificato dall’utente scriverne a video le iniziali, separate da un punto.
Codice Iniziali.cpp 1 2
#include #include
3 4
using namespace std;
5 6 7
//INIZIO int main () {
8
//dichiara le due stringhe nome e cognome
9 10 11
string nome; string cognome;
12 13 14
//chiedi e leggi il nome e il cognome cout << “Inserisci nome e cognome => “; cin >> nome >> cognome;
15 16 17 18
//estrai le lettere iniziali e concatenale string iniziali = nome.substr(0, 1) + “.” + cognome.substr(0, 1) + “.”;
19 20 21
//scrivi il risultato cout << “\nLe iniziali sono “ << iniziali;
22
//fine programma
23 24 25
cout << “\n\nFine system (“pause”); return 0;
26
“;
}
199
Sezione 4 - Strutture dei dati
Prova di esecuzione
Analisi del codice
L’istruzione da esaminare è alla riga 17: nella variabile di nome iniziali vengono concatenate le seguenti stringhe:
il primo carattere della variabile nome, ottenuto come nome.substr(0, 1); il carattere “.”; il primo carattere della variabile cognome, ottenuto come cognome.substr(0, un altro carattere “.”.
1);
...........................................................................................................................................
1 1 .4
C onfronti tra stringhe
Ogni istruzione if verifica una condizione, che in molti casi consiste nel confronto tra due valori: per esempio, si può verificare la condizione che “area < 10 e area >= 0”. I simboli “<” e “>=” sono definiti operatori relazionali. Il C++ dispone di sei operatori relazionali, riepilogati nella tabella che segue. O P E R A TO R E
D E S C R IZ IO N E
==
Uguale a
<
Minore di
>
Maggiore di
<=
Minore o uguale a
>=
Maggiore o uguale a
!=
Diverso
Come si può osservare, solo due degli operatori relazionali ( > e <) sono simili alle notazioni matematiche. L’operatore == può inizialmente confondere la maggior parte dei neofiti di C++, tuttavia in C++ l’operatore = ha già un significato specifico, dato che è l’operatore di assegnazione. L’operatore == indica invece la relazione di uguaglianza, come risulta evidente nelle due righe di codice che seguono. a = 5; if (a== 5)
//operazione di assegnazione //verifica se a è uguale a 5
È necessario ricordarsi di utilizzare l’operatore == all’interno delle operazioni di confronto. È anche possibile confrontare le stringhe. Per esempio, si può scrivere: if (name == “Harry”)
200
Unità didattica 11 - Stringhe e strutture
In C++ la differenza tra lettere maiuscole e minuscole è importante: per esempio, “Harry” e “HARRY” non sono la stessa stringa. Nel confronto tra stringhe, gli operatori <, <=, > e >= seguono le regole dell’ordinamento alfabetico. Se si scrive string name = “Tom”; if (name < “Dick”)
la condizione indicata dopo if risulta falsa perché, nell’ordinamento alfabetico, “Dick” viene prima (cioè è “minore”) di Tom. In realtà, l’ordine alfabetico utilizzato dal C++ è diverso da quello di un normale dizionario: il C++, infatti, è sensibile alle maiuscole e ordina i caratteri iniziando dai numeri, seguiti dai caratteri maiuscoli e da quelli minuscoli: per esempio, la cifra “1” viene prima del carattere “B”, che viene prima di “a”. Il carattere spazio viene prima di tutti gli altri caratteri. Volendo essere rigorosi si dovrebbe precisare che l’ordinamento dei caratteri dipende dal sistema operativo utilizzato: la maggioranza dei sistemi operativi per PC utilizza il cosiddetto codice ASCII ( American Standard Code for Information Interchange ) oppure una delle sue estensioni, i cui caratteri sono ordinati come è stato descritto. Quando si confrontano due stringhe, le lettere corrispondenti vengono confrontate finché termina una delle stringhe o si riscontra la prima differenza: se termina una delle stringhe, quella più lunga viene disposta dopo l’altra (cioè, risulta essere maggiore dell’altra), mentre se viene rilevata un’errata corrispondenza tra i caratteri si rende necessario confrontarli per determinare quale stringa è la successiva nella sequenza lessicale. Quest’ultimo processo viene definito confronto lessicografico . Se, per esempio, si confrontano le stringhe “auto” e “automa”, le prime quattro lettere corrispondono e si raggiunge il termine della prima stringa. Pertanto la stringa “auto” precede la stringa “automa”, secondo la convenzione di ordinamento spiegata sopra. Confrontiamo ora le stringhe “ autorità” e “automa”. Anche in questo caso le prime quattro lettere corrispondono ma, dal momento che la “r” viene dopo la “m”, la stringa “autorità” segue la parola “automa”. È possibile confrontare solo i numeri con i numeri e le stringhe con le stringhe. Il test indicato di seguito, per esempio, string name = “Harry”; if (name > 5) //Errore//
non è valido.
1 1 .5
C aratteri e stringhe C
Il C++ prevede che i singoli caratteri siano di tipo char. Nel linguaggio C, il precursore di C++, l’unico modo per implementare le stringhe consiste nel definire array di caratteri. Nel codice C++ è possibile distinguere le stringhe di tipo C, poiché sono definite come di tipo char* oppure come array (char[] ). Si ricordi, inoltre, che in C i singoli caratteri sono racchiusi tra virgolette semplici: per esempio, la stringa‘a’ identifica il carattere a mentre “a” corrisponde a una stringa che contiene il singolo carattere ‘a’. L’utilizzo di array di caratteri per definire le stringhe implica una complicazione significativa per i programmatori, che devono implementare manualmente lo spazio di memoria da riservare per queste sequenze: in C è comune l’errore di memorizzare una stringa in una variabile troppo piccola per contenerne tutti i caratteri. Non essendo possibile verificare questa eventualità, è prevedibile che il programmatore poco esperto possa sovrascrivere le aree di memoria destinate ad altre variabili. 201
Sezione 4 - Strutture dei dati
Le stringhe C++ standard gestiscono invece questa complicazione in modo completamente automatico. Nella maggior parte delle attività di programmazione, infatti, non è necessario ricorrere al tipo di dati char e si possono utilizzare stringhe di lunghezza unitaria per definire singoli caratteri.
1 1 .6
D ichiarazione di una struttura
Le strutture sono particolari tipi di dati che vengono definiti aggregati , cioè capaci di contenere tipi di dati diversi. Dichiarare un oggetto di tipo struttura significa avere a che fare con un nuovo tipo di dato.
La sintassi di una struttura fa uso della parola chiave
struct :
struct Nome_struttura
{ //membri della struttura
}
I membri della struttura altro non sono che variabili. Di solito le strutture vengono utilizzate per gestire quantità di dati non molto grandi e che occupano poca memoria. Per dichiarare una struttura si usa la stessa sintassi della dichiarazione di una variabile, dove al posto del tipo si inserisce il nome della struttura. Nome_struttura MiaStruttura;
Viene definita, ora, una semplice struttura e un breve programma di esempio. Esempio Struttura ............................................................................................................... Creare una struttura che consenta di memorizzare il nome e il cognome di uno studente.
Questa potrebbe essere la soluzione: struct Studente
{ string nome; string cognome; }
La struttura si chiama Studente e ha due membri di tipo string , che sono rispettivamente il nome e il cognome dello studente. La variabile Alunno di tipo Studente si dichiara come segue. Studente alunno;
Una volta dichiarata una variabile di tipo struct, si può accedere ai suoi membri utilizzando la cosiddetta notazione puntata, che prevede che il nome della variabile, per esempio alunno , sia seguito dal punto e dal nome della variabile membro a cui si deve fare riferimento. Nel caso dell’assegnazione di un valore al membro nome della variabile alunno , l’istruzione da utilizzare è la seguente. alunno.nome=”Pippo”;
202
Unità didattica 11 - Stringhe e strutture
Per assegnare, invece, il valore di una variabile membro a una variabile, si procede come di seguito. string nomealunno = alunno.nome;
Una struttura deve essere dichiarata prima del main e dopo le direttive using del programma. ...........................................................................................................................................
Esempio Struttura1 ........................................................................................................ Definire la struttura Studente che contiene un nome e un cognome; assegnare alle variabili membro i valori “Paolo”e “Rossi” e visualizzare il contenuto della struttura.
Codice Struttura1.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
#include #include using namespace std; struct Studente { //membri della struttura string nome; string cognome; }; //INIZIO int main () { //dichiara la variabile di tipo struct Studente alunno; //assegna i valori ai membri //della struttura alunno.nome="Paolo"; alunno.cognome="Rossi"; cout<<"\nNome dello studente: "; //visualizza i membri della struttura cout<<" "<
";
}
Prova di esecuzione
203
Sezione 4 - Strutture dei dati
Analisi del codice
Dalla riga 5 alla riga 10 c’è l’implementazione della struttura Studente . È stata dichiarata prima del main . La struttura contiene, rispettivamente alle righe 8 e 9, le dichiarazioni di due membri di tipo string , atti a contenere il nome e il cognome dello studente. Alla riga 16 viene dichiarata la variabile alunno di tipo Studente che viene utilizzata alle righe 20 e 21 per fare riferimento, con la notazione puntata, ai suoi membri assegnando loro valori dello stesso tipo, ossia string . Infine, alla riga 26, vengono visualizzati i valori così assegnati ai membri della struttura alunno , tramite la ormai consueta notazione. ...........................................................................................................................................
1 1 .7
M etodi costruttori
Una struttura può contenere tutti i tipi di dati e una particolare funzione. Esiste una funzione che può essere definita all’interno di una struttura e che ha la particolare caratteristica di impostare lo stato iniziale della struttura . Tale tipo di funzione viene definita costruttore, e il suo compito è inizializzare i membri della struttura.
È da premettere che i membri di una struttura non possono essere inizializzati al momento della definizione. Per esempio, la seguente struttura: public struct Studente { //membri della struttura public string nome=”Pippo”; public string cognome=”Inzaghi”; } ....
genera un errore in fase di compilazione quindi, per avere la sicurezza che i membri siano inizializzati, bisogna richiamare il costruttore al momento della creazione di una variabile di tipo struttura. Il costruttore deve avere obbligatoriamente lo stesso nome della struttura e avere tanti parametri (passati per valore) quanti sono i membri della struttura. Esempio di costruttore ....................................................................................................... .... struct Studente { //membri della struttura string nome; string cognome; //costruttore public Studente(string nome, string cognome) { //assegnazione dei parametri
204
Unità didattica 11 - Stringhe e strutture
//ai membri della struttura nome = nome; cognome = cognome; } }
........................................................................................................................................... Questo per quanto riguarda la definizione del costruttore all’interno della struttura; ora lo si deve utilizzare al momento della creazione della struttura all’interno del codice. Per la creazione dell’oggetto alunno si deve utilizzare l’istruzione riportata di seguito. ... Studente Alunno(“Pippo”,”Inzaghi”); ...
In questo modo si crea un nuovo oggetto di nome alunno e si richiama la sua funzione costruttore, che ne inizializza i membri.
Esempio Struttura2 ............................................................................................................. Modificare il codice di Struttura1.cpp per inserire il costruttore del tipo Studente .
Codice Struttura2.cpp 1
#include
2
#include
3
using namespace std;
4 5
struct Studente
6 7
{ //membri della struttura
8 9
string nome; string cognome;
10 11
//costruttore
12
Studente(string n, string c)
13 14
{ nome=n;
15
cognome=c;
16 17
} };
18 19
//INIZIO
20
int main ()
21
{
22 23
//creazione della variabile struct alunno Studente alunno("Paolo","Rossi");
24 25
cout<<"\nNome dello studente: "<
26 27
//fine programma
28
cout << "\n\nFine
";
205
Sezione 4 - Strutture dei dati
29
system ("pause");
30
return 0;
31
}
Il risultato è uguale a quello dell’esempio Struttura1. Analisi del codice
Alla riga 12 viene dichiarato il costruttore della struttura Studente ; la sua intestazione possiede due parametri passati per valore, dello stesso tipo dei membri, cioè string . Il codice al suo interno assegna i parametri ai rispettivi membri. Nel main, alla riga 23, c’è l’istruzione per la creazione dell’oggetto alunno di tipo Studente , che richiama automaticamente il costruttore della struttura con i relativi parametri. ...........................................................................................................................................
Lo studio delle strutture introduce alcuni concetti importantissimi che sono alla base delle classi. Infatti, le strutture sono molto simili alle classi tranne per il fatto che non possono ereditare membri da altre strutture. L’ereditarietà è un sistema che consente di espandere notevolmente le “funzionalità di una classe” ed è uno dei principi su cui si basa l’intero linguaggio C++. I concetti e gli argomenti che riguardano le classi verranno affrontati in maniera dettagliata nella prossima Sezione.
206
Unità didattica 11 - Stringhe e strutture
Esercizi Unità didattica 11 Scrivere un programma che acquisisca il nome dell’utente da tastiera e ne visualizzi ogni lettera su una riga differente.
Scrivere un programma che acquisisca il nome dell’utente da tastiera e visualizzi le lettere in ordine inverso.
Aggiungere un metodo al programma pr ecedente che visualizzi il nome dell’utente al contrario. Scrivere un programma che contenga una funzione che abbia come parametro una stringa inserita dall’utente e ne sostituisca tutte le occorrenze della lettera “i” con un “°”.
Come si può ricavare il primo carattere di una stringa? E l’ultimo? Come si elimina il primo carattere? E l’ultimo?
Considerare un programma C++ che contiene le due istruzioni di richiesta di input: string nome; string cognome; int age; cout << “Inserisci nome e cognome: “; cin >> nome >> cognome; cout << “ età : “; cin >> age;
Qual è il contenuto delle variabili nome, cognome ed età se l’utente digita gli input indicati di seguito? James Carter 56 Lyndon Johnson 49 Hodding Carter 3rd 44 Richard M. Nixon 62
Quali sono i valori delle espressioni seguenti? In ciascuna riga si consideri che: string s = “Hello” ; string t = “World” ;
s.substr(l, 2) s.length() + t.length()
Disegnare lettere sullo schermo. Si può riprodurre una grande lettera, per esempio “H”, nel modo indicato di seguito. *
*
*
*
***** *
*
*
*
207
Sezione 4 - Strutture dei dati
Esercizi Unità didattica 11 Questa lettera può essere definita come una costante stringa impostata come segue: const string LETTER_H = “* *\n* *\n*****\n*
*\n*
*\n”;
Si può effettuare la stessa operazione con le lettere E, L e O. Scrivere poi il messaggio disegnando le lettere appena definite come stringhe. H E L L O
Scrivere un programma che trasformi i nomi dei mesi nei numeri cor rispondenti. Trasformare la stringa “Rota Cesare” in “Rota dr. Cesare”. Controllare che la terza cifra di un CAP sia soltanto o “0” o “1”. Tra le coppie di stringhe seguenti, quale viene prima secondo l’ordine lessicografico?
“Tom”, “Dick” “Tom”, “Tornato” “church”, “Churchill” “operaio”, “operatore” “capo”, “capello” “C++”, “Car” “Tom”, “Tom” “cantuccini”, “canto” “tarallo”, “taralluccio”
Scrivere un programma che stampa la domanda “Vuoi continuare?” e legge un input dall’utente. Se l’input dell’utente è “s”, “S”, “Sì”, “sì”, “Si”, “si”, “OK”, “Ok”, “ok”, “Sicuramente” o “Perché no? “, visualizzare “OK.” Se l’input dell’utente è “N” o “No”, visualizzare “Fine lavoro”. Altrimenti visualizzare “Errore”.
Descrivere una struttura che possa contenere i dati relativi a una partita di calcio. Descrivere una struttura che possa contenere i dati relativi ai voti delle verifiche sia orali che scritte di un studente.
Descrivere una struttura che contenga i dati anagrafici di una persona. Aggiungere alla struttura dell’esercizio precedente i dati relativi a uno studente.
208
Sezione
5
Classi e oggetti
†
Obiettivi
◊
Risolvere un problema individuando gli oggetti e le loro interazioni
◊
Definire una classe attraverso i suoi dati e i suoi metodi
◊
Realizzare classi flessibili attraverso il polimorfismo
◊
Strutturare gerarchie di classi sfruttando l’erditarietà
&
Questa sezione contiene
U.D.12 Concetti generali U.D.13 Polimorfismo ed ereditarietà
Unità didattica
12
Concetti generali CHE COSA IMPARERAI A FARE $ Associare
dati e codice in un’unica struttura
$
Definire più metodi individuati da una sola intestazione
$
Estendere dati e metodi da una classe di tipo generale ad altre classi derivate
$
Utilizzare un metodo standard per descrivere una classe
CHE COSA DOVRAI STUDIARE $
Concetto generale di incapsulazione
$
Metodologia e sintassi per l’overloading
$
Concetto generale di polimorfismo
$
Definizione di derivazione e di ereditarietà
$
Terminologia e diagrammi utilizzati nella OOP
$
Sintassi di base per la dichiarazione degli oggetti
Unità didattica
12
Concetti generali 12.1 Introduzione alla OOP Con la sigla OOP si indica la metodologia per l’organizzazione dei programmi denominata pro- grammazione orientata agli oggetti (Object Oriented Programming ). In generale, un programma può essere organizzato in due modi: ponendo al centro il codice (“ciò che accade”) o ponendo al centro i dati (“gli attori interessati”). Utilizzando le tecniche della programmazione strutturata, i programmi vengono tipicamente organizzati intorno al codice. Questo approccio prevede che il codice operi sui dati. Per esempio, un programma scritto con un linguaggio di programmazione strutturato come il Pascal è definito dalle sue funzioni, le quali operano sui dati usati dal programma. I programmi a oggetti seguono un approccio opposto: infatti sono organizzati intorno ai dati e si basano sul fatto che sono questi a controllare l’accesso al codice. In un linguaggio a oggetti si definiscono i dati e le routine che sono autorizzate ad agire su tali dati. Pertanto, sono i dati a stabilire quali sono le operazioni che possono essere eseguite.
I linguaggi che consentono di attuare i principi della programmazione a oggetti hanno tre fattori in comune: l’incapsulazione,il polimorfismo e l’ereditarietà.
12.2 Incapsulazione In un linguaggio a oggetti, il codice e i dati possono essere raggruppati in modo da creare una sorta di “scatola nera”. Quando il codice e i dati vengono raggruppati in questo modo, si crea un oggetto. In altre parole, un oggetto è un “dispositivo” che supporta l’incapsulazione.
L’incapsulazione permette di associare ai dati del programma il codice autorizzato ad agire su tali dati. All’interno di un oggetto, il codice, i dati o entrambi possono essere definiti privati per tale oggetto oppure pubblici . Il codice o i dati privati sono noti e accessibili solo da parte degli elementi dell’oggetto stesso. Questo significa che il codice e i dati privati non risultano accessibili da parte di elementi del programma che si trovano all’esterno dell’oggetto. Se il codice o i dati sono pubblici, risulteranno accessibili anche da altre parti del programma che non sono definite all’interno dell’oggetto. Generalmente, le parti pubbliche di un oggetto sono utilizzate per fornire un’interfaccia controllata agli elementi privati dell’oggetto stesso. Un oggetto è, in tutto e per tutto, una variabile di un tipo definito dall’utente. Può sembrare strano pensare a un oggetto, che contiene codice e dati, come a una variabile; tuttavia, nella programmazione a oggetti avviene proprio questo. Ogni volta che si definisce un nuovo tipo di oggetto, si crea implicitamente un nuovo tipo di dato. Ogni specifica istanza di questo tipo è una variabile composta. Esempio Rettangolo1......................................................................................................... Definire una classe per gestire con oggetti che abbiano le caratteristiche di un rettangolo.
211
Sezione 5 - Classi e oggetti
Si può assegnare alla classe indicata dal problema il nome di Rettangolo . I dati essenziali di un rettangolo sono le misure della base e dell’altezza: base e altezza sono quindi i dati che necessariamente fanno parte della classe Rettangolo . Inoltre, è interessante conoscere di un rettangolo l’area e la misura del perimetro, quindi, alla classe Rettangolo associamo anche due metodi: uno per calcolare l’area e l’altro per calcolare il perimetro. Pertanto fanno parte della classe Rettangolo quattro membri: base altezza
dati
area perimetro
metodi o funzioni
...........................................................................................................................................
12.3 Polimorfismo
I linguaggi di programmazione a oggetti supportano il polimorfismo, che è caratterizzato dalla frase “un’interfaccia, più metodi”. In altri termini, vale l’affermazione riportata di seguito. Il polimorfismo consente a un’interfaccia di controllare l’accesso a una categoria generale di azioni.
La specifica azione selezionata è determinata dalla natura della situazione. Esempio Termostato........................................................................................................... Un esempio di polimor fismo tratto dal mondo reale è il termostato.
Una caratteristica tipica di tutti i termostati è che non serve conoscere il tipo di combustibile utilizzato (gas, petrolio, elettricità e così via): il termostato funziona sempre nello stesso modo. In questo caso, il termostato (che è l’interfaccia) è sempre lo stesso qualsiasi sia il tip o di combustibile (metodo) utilizzato. Per esempio, se si desidera raggiungere una temperatura di 20 gradi, si imposta il termostato a 20 gradi. Non è necessario sapere quale sia il meccanismo che fornisce il calore. ...........................................................................................................................................
Questo stesso principio si può applicare anche in programmazione: per esempio, un programma potrebbe definire tre diversi tipi di array: un array per i valori interi, uno per i caratteri e uno per valori in virgola mobile. Grazie al polimorfismo, è possibile creare solo due metodi ( carica() e scrivi() ), utilizzabili per i tre tipi di array. Nel programma vengono create tre diverse versioni di questi metodi, una per ogni tipo di array, ma il nome delle funzioni rimane lo stesso. Il compilatore seleziona automaticamente la funzione corretta sulla base del tipo dei dati memorizzati, pertanto l’interfaccia dell’array (ovvero le funzioni carica() e scrivi() ) rimane invariata, qualunque sia il tipo di array utilizzato. Naturalmente le singole versioni di queste funzioni definiscono implementazioni (metodi) specifiche per ciascun tipo di dati. Il polimorfismo aiuta a ridurre la complessità del programma consentendo di utilizzare la stessa interfaccia per accedere a una categoria generale di azioni. È compito del compilatore selezionare l’azione specifica (ovvero il metodo) da applicare in una determinata situazione. Il programmatore non deve più fare questa selezione in modo specifico, ma deve semplicemente ricordare e utilizzare l’interfaccia generale. 212
Unità didattica 12 - Concetti generali
12.4 Ereditarietà
L’ereditarietà è il processo grazie al quale un oggetto acquisisce le proprietà di un altro oggetto. Questo è un concetto fondamentale poiché chiama in causa il concetto di classificazione. Se si prova a riflettere, la maggior parte della conoscenza è resa più gestibile da classificazioni gerarchiche. Per esempio, una mela rossa Delicious appartiene alla classificazione “mela”, che a sua volta appartiene alla classe “frutta”, che a sua volta si trova nella classe più estesa “cibo”. Senza l’uso della classificazione, ogni oggetto dovrebbe essere definito esplicitamente con tutte le proprie caratteristiche. L’uso della classificazione consente di definire un oggetto sulla base delle qualità che lo rendono unico all’interno della propria classe. L’ereditarietà è lo strumento che permette di costruire nuove classi utilizzando quelle già realizzate.
È il meccanismo di ereditarietà a rendere possibile per un oggetto essere una specifica istanza di un caso più generale. Come si vedrà, l’ereditarietà è un importante aspetto della programmazione a oggetti. Esempio Auto e autocarro ................................................................................................. Definire la classe automobile e derivare la classe autocarro .
Un possibile diagramma per la classe automobile è riportato a lato:
Nella prima casella dello schema “Automobile”, è indicato il nome della classe, nella seconda casella sono indicati i dati e nella terza i metodi. La classe Autocarro può essere definita estendendo le proprietà e i comportamenti presenti nella classe Auto.
marca velocità colore numero porte livello carburante cilindrata parti accelera fermati fai il pieno
Autocarro
Automobile
marca velocità colore numero porte livello carburante cilindrata parti accelera fermati fai il pieno
Automobile
simbolo per l’ereditarietà
marca velocità colore numero porte livello carburante cilindrata tara portata massima parti accelera fermati fai il pieno carica scarica
I membri ereditati sono riportati in corsivo.
........................................................................................................................................... 213
Sezione 5 - Classi e oggetti
La classe che è stata derivata da un’altra usando l’ereditarietà viene detta sottoclasse. La classe generatrice di una sottoclasse prende il nome di sopraclasse. La relazione di ereditarietà permette di individuare una gerarchia di classi che può essere descritta graficamente usando un grafo ad albero. Esso è costituito da un grafo orientato i cui nodi sono rappresentati dalle classi, o meglio dai diagrammi delle classi, e gli archi individuano la presenza di una relazione di ereditarietà. Esempio Mezzi di trasporto ................................................................................................
Ipotizzare un grafico per la gerarchia delle classi relativa ai mezzi di trasporto. Mezzi di trasporto
A motore
Moto
Auto
Senza motore
Bus
Barca
Bici
Cavallo
Vela
Nell’esempio, la classe Moto è sottoclasse della classe dei veicoli A motore che a sua volta è sottoclasse di Mezzi di trasporto . In alto ci sono le classi più generiche che diventano più specializzate man mano si scende lungo la gerarchia. Nel diagramma sono state indicate le classi solo attraverso il loro nome, tralasciando l’elenco degli attributi e dei metodi.
...........................................................................................................................................
La sottoclasse eredita dalla sopraclasse tutti gli attributi e tutti i metodi, evitando di ripetere la descrizione degli elementi comuni nelle sottoclassi. L’ereditarietà consente di condividere le similarità tra le classi e di inserire le differenze. La nuova classe si differenzia dalla sopraclasse in due modi:
214
per estensione, quando la sottoclasse aggiunge nuovi attributi e metodi che si sommano a quelli ereditati, come nell’esempio della sottoclasse Autocarro che arricchisce di nuovi membri la sopraclasse Auto; per ridefinizione, quando la sottoclasse ridefinisce i metodi ereditati. In pratica, viene data un’implementazione diversa di un metodo. Si crea un nuovo metodo che ha lo stesso nome del metodo ereditato da una sopraclasse, ma con funzionalità diversa. Quando per un oggetto della sottoclasse viene invocato un metodo ridefinito, viene eseguito il nuovo codice e non quello che era stato ereditato.
Unità didattica 12 - Concetti generali
12.5 Introduzione alle classi Questo paragrafo introduce il concetto di classe che rappresenta la funzionalità più importante del linguaggio C++. Per avere un’idea immediata di che cosa sia una classe possiamo usare come esempio il fatto che tutti gli oggetti o esseri viventi, spesso, sono riconducibili a determinate categorie (per esempio: computer, automobili, piante, animali e così via). Queste categorie costituiscono le classi.
Una classe è una categoria o un gruppo di oggetti (con questo termine includiamo, per comodità, anche gli esseri viventi) che hanno attributi simili e comportamenti analoghi. In C++ per creare un oggetto si deve innanzitutto definire la sua forma generale utilizzando la parola chiave class. Una classe ha una sintassi simile a una struttura e può contenere al suo interno oltre che dati anche parte di codice (incapsulamento). Ecco un esempio.
Esempio ClasseRettangolo................................................................................................ Definire la classe Rettangolo che contiene al suo interno i dati relativi alla base b e all’altezza h e che racchiude al suo interno anche i due metodi utilizzati per il calcolo dell’ area e del perimetro .
Codice classeRettangolo.cpp 1
#include
2 3 4 5
using namespace std;
6 7 8 9
{
//definisci la classe class Rettangolo public: //definisci i dati membro della classe float b, h;
10 11
//definisci i due metodi
12 13
float area() {
14 15 16 17
return b*h; } float perimetro() {
18 19
}
20
//calcola l’area
//calcola il perimetro
return 2*(b+h); };
Analisi del codice Alla riga 5 viene introdotta, con la parola class, la classe Rettangolo . Alle righe 6 e 20 le parentesi graffe delimitano il contenuto della classe. Alla riga 9 vengono definiti i dati relativi alla classe Rettangolo. Poiché non è specificato alcun livello di protezione per l’accesso ai membri della classe si assume, per default, che b e h siano private ; in questo caso, per semplificare l’esempio successivo, la protezione è “public”. Le righe 12 e 16 sono le intestazioni dei due metodi della classe, area e perimetro .
........................................................................................................................................... 215
Sezione 5
- Classi e oggetti
Una classe può contenere parti private e pubbliche. In generale, tutti i membri definiti all’interno di una classe sono privati. I dati e i metodi private non sono visibili da alcun’altra funzione che non sia definita all’interno della classe. In assenza di indicazione (per default) si assume che il livello di protezione sia private . Questo è uno dei modi in cui si ottiene l’incapsulazione: l’accesso a determinati membri può essere controllato in modo rigido mantenendoli private . Nell’esempio precedente le variabili b e h possono essere utilizzate solo dai metodi definiti all’interno della classe. Al contrario, i metodi area() e perimetro() possono essere utilizzati in qualsiasi istruzione posta all’esterno della definizione della classe. Anche se questo non viene illustrato dall’esempio presentato, è possibile definire funzioni private che possono essere richiamate solo dai membri della classe. Per rendere pubblica (ovvero accessibile da altre parti del programma) una parte della classe, è necessario dichiararla esplicitamente come pubblica utilizzando la parola chiave public . Tutte le variabili e le funzioni definite dopo public possono essere utilizzate da tutte le altre funzioni del programma. Essenzialmente, la parte rimanente del programma accede a un oggetto utilizzando le sue funzioni pubbliche. Anche se è possibile avere variabili pubbliche, si deve in generale cercare di limitarne l’uso. Quindi si deve cercare di rendere tutti i dati privati e controllare l’accesso ai dati utilizzando funzioni pubbliche.
Si ricordi che una classe racchiude metodi e dati. Solo i metodi di una classe hanno accesso ai dati privati della classe in cui sono dichiarati. Perciò solo area() e perimetro() possono accedere a b e h.
12.6 Terminologia e rappresentazione grafica Per indicare i componenti di una classe esistono alcune varianti che possono creare qualche piccola confusione. La definizione e lo schema che seguono si propongono di fare chiarezza. Tutti gli elementi che compongono la definizione di una classe prendono il nome di membri. All’interno dei membri si distinguono i dati membro e le funzioni membro. Spesso, in luogo di dati membro e di funzioni membro vengono usati i termini più immediati di dati e metodi: anche nel seguito della trattazione verrà utilizzata prevalentemente quest’ultima dizione. Per la documentazione delle applicazioni informatiche può essere usato lo standard UML (Uni- fied Modeling Language , linguaggio unificato di modellazione). In tale standard si usano i termini attributi e operazioni corrispondenti, rispettivamente, a dati membro e funzioni membro . Facendo riferimento alla classe Rettangolo vista nell’esempio precedente, si può realizzare lo schema seguente.
216
ESEMPIO RETTANGOLO
1a DEFINIZIONE
2a DEFINIZIONE
UML
b, h, area(), perimetro()
membri
membri
membri
b, h
dati membro
dati
attributi
area(), perimetro()
funzioni membro
metodi
operazioni
Unità didattica 12 - Concetti generali
Lo standard UML definisce anche le modalità per rappresentare graficamente una classe. Una classe viene rappresentata da un rettangolo. Il nome della classe, per convenzione, è una parola con l’iniziale maiuscola e appare alla sommità del rettangolo. Se il nome della classe consiste di una parola composta, a sua volta, da più parole, allora viene utilizzata la notazione in cui tutte le iniziali di ogni parola sono scritte in maiuscolo. NomeClasse
Dopo il nome della classe vengono descritti i dati a essa appartenenti. Un dato rappresenta una proprietà di una classe; esso descrive un insieme di valori che la proprietà può avere. Una classe può avere zero o più dati. La lista dei dati di una classe viene separata graficamente dal nome della classe a cui appartiene tramite una linea orizzontale. Un dato il cui nome è costituito da una sola parola viene scritto sempre in caratteri minuscoli. Se, in vece, il nome del dato consiste di più parole (per esempio, informazioniCliente ) allora il nome del dato viene scritto unendo tutte le parole che ne costituiscono il nome stesso con la par ticolarità che la prima parola viene scritta in minuscolo mentre le successive hanno la loro prima lettera in maiuscolo. NomeClasse
tipo1 dato1 tipo2 dato2 = “valore default” ……
Come si vede nella figura precedente, è possibile specificare un tipo per ogni dato ( string , int, bool ecc.). È anche possibile specificare il valore di default che un dato può avere. Un metodo è un’azione che gli oggetti di una certa classe possono compiere. Analogamente al nome degli attributi, il nome di un metodo viene scritto con caratteri minuscoli. Anche qui, se il nome dell’operazione consiste di più parole, allora tali parole vengono unite tra di loro e ognuna di esse, eccetto la prima, viene scritta con il primo carattere maiuscolo. La lista dei metodi viene rappresentata graficamente sotto la lista degli attributi e separata da questa tramite una linea orizzontale. NomeClasse
tipo1 dato1 tipo2 dato2 = “valore default” …… tipo1 metodo1() tipo2 metodo2 (lista parametri)
Anche i metodi possono avere informazioni addizionali. Nelle parentesi che seguono il nome di un metodo, infatti, è possibile mostrare gli eventuali parametri necessari al metodo insieme al loro tipo. Infine, se il metodo rappresenta una funzione è necessario anche specificare il tipo restituito. 217
Sezione 5 - Classi e oggetti
12.7 Dichiarazione degli oggetti Dopo avere definito una classe, è possibile creare un oggetto descritto dalla classe semplicemente impiegando il nome stesso della classe. In pratica, il nome della classe diviene un nuovo specifìcatore di tipo. Per esempio, la seguente istruzione crea l’oggetto rett di tipo Rettangolo . Rettangolo rett;
Si noti l’analogia tra la dichiarazione di un oggetto e la dichiarazione di una variabile. Si consideri, per esempio, la variabile numero di tipo intero int numero;
Quando si dichiara un oggetto di una classe, si crea un’istanza di tale classe.
In questo caso, rett è un’istanza di Rettangolo . È anche possibile creare oggetti nel luogo stesso in cui viene definita la classe, specificandone il nome dopo la parentesi graffa di chiusura, esattamente come avviene nel caso delle strutture. Per ricapitolare: in C++ class crea un nuovo tipo di dati che può essere utilizzato per creare oggetti appartenenti alla classe.
Pertanto, un oggetto è un’istanza di una classe esattamente come altre variabili sono istanze, per esempio, del tipo int. In altre parole, una classe è un’astrazione logica, mentre un oggetto è reale (ovvero esiste all’interno della memoria del computer). Quando si fa riferimento a un membro di una classe da una parte di codice che non si trova all’interno della classe stessa, l’operazione deve essere eseguita sempre in congiunzione con un oggetto di tale classe. A tale scopo, si deve utilizzare il nome dell’oggetto seguito dall’operatore punto seguito a sua volta dal membro. Questa regola si applica quando si deve accedere sia a dati membro sia a funzioni membro. Per esempio, il frammento di codice seguente richiama il metodo area() per l’oggetto rett. Rettangolo rett;
// dichiara l’oggetto rett
. . . rett.area();
// richiama il metodo area()
All’interno di una classe, un metodo può richiamare un altro metodo oppure può fare riferimento direttamente ai dati senza utilizzare l’operatore punto (.). Il nome dell’oggetto e l’operatore punto (.) devono essere utilizzati solo quando l’accesso a un membro avviene da parte di codice che non appartiene alla classe. Siamo ora in grado di esaminare il primo esempio completo di programma OOP. Esempio Rettangolo2 ......................................................................................................... Scrivere l’area e il perimetro di un oggetto appar tenente alla classe Rettangolo .
Indicazioni per la soluzione
La classe Rettangolo è già stata definita nel paragrafo 12.5 “Introduzione alle classi”, nell’esempio ClasseRettangolo; resta da specificare come utilizzare i metodi relativi alla classe. 218
Unità didattica 12 - Concetti generali
Rettangolo2.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
#include using namespace std; //definisci la classe class Rettangolo { public: //definisci i dati membro della classe float b, h; //definisci i due metodi float area() //calcola l’area { return b*h; } float perimetro() //calcola il perimetro { return 2*(b+h); } }; //INIZIO int main () { //definisci l’oggetto rett Rettangolo rett; //inizializza le variabili b, h rett.b = 10; rett.h = 6; //scrivi l’area e il perimetro invocando i relativi metodi cout << "\nArea del rettangolo = " << rett.area(); cout << "\nPerimetro rettangolo = " << rett.perimetro(); //fine programma cout << "\n\nFine system ("pause"); return 0;
";
}
Prova di esecuzione
Analisi del codice
Alla riga 5 viene introdotta la classe Rettangolo , seguita dalla definizione dei suoi membri. Alla riga 9 sono definiti i dati membro della classe; per semplificare il primo esempio e per non introdurre 219
Sezione 5 - Classi e oggetti
ulteriori precisazioni di tipo teorico, i dati membro hanno livello di protezione public , anche se tale scelta risulta decisamente sconsigliabile. Alle righe 12 e 16 sono definiti i metodi legati alla classe Rettangolo . Alla riga 20 la parentesi graffa chiusa e il punto e virgola indicano la fine della definizione della classe. Alla riga 26 viene dichiarato l’oggetto rett che appartiene alla classe Rettangolo . Alle righe 29 e 30 è indicata l’inizializzazione delle variabili b e h che fanno parte dell’oggetto rett. Alle righe 33 e 34, all’interno delle operazioni di scrittura a video, è presente l’invocazione dei metodi area() e perimetro() . La scelta di assegnare ai dati membro b e h il livello di protezione public è da sconsigliare: infatti i dati membro di una classe non devono essere disponibili per le parti del programma esterne alla definizione della classe, vale a dire devono avere livello di protezione private. In questo modo si garantisce che solo i metodi definiti all’interno della classe possono inter venire sui dati della classe stessa.
...........................................................................................................................................
Viene presentato ora un esempio dove i dati della classe vengono definiti private . Esempio Rettangolo3 ......................................................................................................... Scrivere l’area e il perimetro di un oggetto appartenente alla classe Rettangolo . Le dimensioni del rettangolo sono acquisite da tastiera.
Indicazioni per la soluzione
Si riprenda l’esempio Rettangolo.cpp visto sopra e si aggiunga, all’interno della classe, un nuo vo metodo per acquisire la base e l’altezza. Rettangolo3.cpp 1 2 3 4
#include using namespace std;
5 6 7 8 9
class Rettangolo { //definisci i dati membro con protezione private float b, h;
10 11 12
public: //definisci i due metodi float area()
13 14 15 16 17
{
18 19 20 21 22 23 24
220
//definisci la classe
return b*h; } float perimetro() {
// calcola l’area
// calcola il perimetro
return 2*(b+h); } //leggi base e altezza da console void leggi() { //leggi base
Unità didattica 12 - Concetti generali
25 26 27 28 29
cout << "\nInserisci la base => "; cin >> b;
30 31 32
cin >> h;
//leggi altezza cout << "\nInserisci altezza => ";
return;
33 34 35 36 37
} };
38 39 40 41
{
//INIZIO int main () //definisci l’oggetto rett Rettangolo rett;
42 43 44 45
//invoca il metodo leggi rett.leggi();
46 47 48 49 50
cout << "\nArea del rettangolo = " << rett.area(); cout << "\nPerimetro rettangolo = " << rett.perimetro();
51 52 53
system ("pause"); return 0;
//scrivi l’area e il perimetro invocando i relativi metodi
//fine programma cout << "\n\nFine
";
}
Analisi del codice
Alla riga 8 vengono definiti i dati b e h; non è indicato il livello di protezione, quindi devono essere considerati con livello di protezione private . Alla riga 22 è stato inserito il nuovo metodo leggi, che acquisisce da tastiera la base e l’altezza. Il metodo è stato inserito all’interno della classe Rettangolo , ed è quindi abilitato a lavorare sui membri private (b e h) della classe. Alla riga 43 è invocato il metodo leggi, che acquisisce i dati relativi all’oggetto rett appartenente alla classe Rettangolo . Le righe da 40 a 52 contengono istruzioni esterne alla definizione della classe Rettangolo , pertanto in esse non compaiono le variabili b e h: queste possono essere manipolate solo all’interno dalla classe, dato che sono state definite private. ...........................................................................................................................................
221
Sezione 5 - Classi e oggetti
Esercizi Unità didattica 12 Definire i dati e i metodi di un oggetto video. Disegnare il diagramma della classe. Definire i dati e i metodi dell’oggetto lampada. Disegnare il diagramma della classe. Definire i dati e i metodi dell’oggetto hardisk. Disegnare il diagramma della classe. Definire i dati e i metodi dell’oggetto CPU. Disegnare il diagramma della classe. Definire i dati e i metodi dell’oggetto pompaBenzina. Disegnare il diagramma della classe. Definire i dati e i metodi dell’oggetto televisore. Disegnare il diagramma della classe. Definire i dati e i metodi dell’oggetto distributore_automatico. Disegnare il diagramma della classe. Per gli esercizi che seguono: disegna il diagramma della classe e realizza in C++ il programma che la utilizza.
Definire la classe punto del piano cartesiano con i metodi sposta_a_destra, sposta_a_sinistra, sposta_in_alto e sposta_in_basso.
Definire la classe verifica con i dati materia, data e voto e con il metodo cambiaVoto . Disegna il diagramma della classe e realizza in C++ il programma che utilizza tale classe.
Definire la classe data_di_nascita e realizza il metodo che restituisce l’età. Definire la classe NumeroReale con i metodi che restituiscono la parte intera e la par te decimale. Definire la classe Approssimazione con il dato valoreIniziale e con tre metodi: perDifetto perEccesso interoPiuVicino
Definire la classe Segmento individuata dalle coordinate sul piano cartesiano dei suoi vertici e che possiede i metodi per restituire la lunghezza del segmento e le coordinate del punto medio.
Indicare con un diagramma ad albero che sfr utta l’ereditarietà la gerarchia delle classi per le seguen- ti categorie :
Periferiche di un calcolatore; Elettrodomestici; Specie animali; Sport; Poligoni. Per ciascuno delle gerarchie indicate negli esercizi dal numero 14 al numero 18, individuare almeno un dato e un metodo comune a tutte le classi. 222
Unità didattica
13
Polimorfismo ed ereditarietà CHE COSA IMPARERAI A FARE $
Utilizzare i costruttori per definire un oggetto
$
Parametrizzare un costruttore
$
Individuare i membri static di una classe
$
Definire e gestire un array di oggetti
$
Utilizzare un unico identificatore per invocare metodi con funzioni simili
$
Definire classi polimorfe
$
Sfruttare la possibilità di derivare una classe da un’altra
CHE COSA DOVRAI STUDIARE $
Sintassi dei costruttori parametrizzati
$
Definizione di un membro static
$
Metodologia per la definizione di array di oggetti
$
Sintassi per sfruttare l’overloading dei metodi e dei costruttori
$
Procedimento per ereditare in una nuova classe i membri di una classe più generale
13
Unità didattica Polimorfismo ed ereditarietà
13.1 Costruttori
È molto comune che una parte di un oggetto debba essere inizializzata prima dell’uso. Per esempio, ripensando alla classe Rettangolo sviluppata nella precedente Unità didattica, per cominciare a la vorare con l’oggetto rett, devono essere inizializzate le dimensioni b e h. Questo si ottiene richiamando il metodo leggi() . Poiché capita molto spesso di dover inizializzare un oggetto, C++ consente di inizializzare gli oggetti al momento della creazione. Tale inizializzazione automatica è ottenuta grazie all’impiego di un costruttore. Un costruttore è un particolare metodo di una classe: porta lo stesso nome della classe e permette l’inizializzazione automatica dell’oggetto su cui si intende operare. Esempio Costruttore...........................................................................................................
Con riferimento al codice dell’esempio Rettangolo2 dell’Unità didattica 12, modificare il modulo leggi() in modo da farlo rientrare come costruttore nella classe Rettangolo . Codice Costruttore.cpp 1 2
#include using namespace std;
3 4
//definisci la classe
5 6 7 8 9 10
224
class Rettangolo { //definisci i dati membro con protezione private float b, h; public:
11 12
//definisci i due metodi float area() //calcola l’area
13 14 15 16
{
17 18
{
19 20
} //COSTRUTTORE
21 22 23 24 25
//inizializza base e altezza leggendo dati da console public: Rettangolo() { //leggi base cout << "\nInserisci la base => ";
return b*h; } float perimetro()
//calcola il perimetro
return 2*(b+h);
Unità didattica 13 - Polimorfismo ed ereditarietà
26 27 28 29 30
cin >> b; //leggi altezza cout << "\nInserisci altezza => "; cin >> h;
31 32 33
return; }
34
};
Analisi del codice Alla riga 22 è definito il costruttore Rettangolo() . Si noti che:
il costruttore è un metodo, quindi segue la relativa sintassi; l’identificatore della classe e quello del costruttore devono essere uguali; per il costruttore non viene specificato il tipo di dato restituito; poiché il costruttore deve essere invocato all’esterno della classe, è definito public .
........................................................................................................................................... Nel caso specifico dell’esempio precedente, il costruttore ha il compito di acquisire dati da tastiera, ma nella pratica la maggior parte dei costruttori non ha bisogno di input né di output: semplicemente, il metodo si occupa di eseguire varie inizializzazioni. Il costruttore di un oggetto viene richiamato automaticamente nel momento in cui deve essere creato l’oggetto. Questo significa che viene richiamato al momento della dichiarazione dell’oggetto. Se si è abituati a pensare che una dichiarazione sia un’istruzione passiva, occorre prepararsi a cambiare idea: in C++, una dichiarazione è un’istruzione che viene eseguita come qualunque altra. La distinzione non è puramente accademica: il codice eseguito per costruire un oggetto può essere anche molto pesante, pertanto, il costruttore di un oggetto viene richiamato una sola volta per ogni oggetto globale; nel caso di oggetti locali, il costruttore viene richiamato ogni volta che si incontra la dichiarazione di un nuovo oggetto. L’operazione complementare del costruttore è svolta dal distruttore.
In molte circostanze, un oggetto deve eseguire una o più azioni nel momento in cui finisce la propria esistenza. Gli oggetti locali vengono costruiti nel momento in cui si entra nel blocco ove si trovano e vengono distrutti all’uscita dal blocco; gli oggetti globali vengono distrutti nel momento in cui termina il programma. Quando viene distrutto un oggetto, viene automaticamente richiamato il relativo distruttore (se presente). Vi sono molti casi in cui è necessario utilizzare un distruttore. Per esempio, potrebbe essere necessario de-allocare la memoria precedentemente allocata dall’oggetto oppure potrebbe essere necessario chiudere un file aperto. In C++ è il distruttore a gestire gli eventi di disattivazione. Il distruttore ha lo stesso nome del costruttore ma è preceduto dal carattere ~.
Esempio Rettangolo4......................................................................................................... Modificare il codice dell’esempio Rettangolo2 dell’Unità didattica 12, inserendo il costruttore e il distruttore per la classe Rettangolo .
225
Sezione 5
- Classi e oggetti
Codice
Rettangolo4.cpp 1
#include
2
using namespace std;
3 4 5
//definisci la classe class Rettangolo
6 7
{ //definisci i dati membro con protezione private
8 9
float b, h;
10
public:
11 12
//definisci i due metodi float area() //calcola l’area
13 14
{
15 16
} float perimetro()
17
{
return b*h;
18
//calcola il perimetro
return 2*(b+h);
19
}
20 21
//COSTRUTTORE //inizializza base e altezza leggendo dati da console
22
public: Rettangolo()
23
{
24 25
//leggi base cout << "\nInserisci la base => ";
26
cin >> b;
27 28
//leggi altezza
29 30
cout << "\nInserisci altezza => "; cin >> h;
31 32
//segnala costruzione completa
33
cout << "\nCostruzione rett \n ";
34 35
return;
36 37
}
38 39
//DISTRUTTORE ~Rettangolo()
40
{
41 42
//segnala distruzione cout << "\n\nDistruzione rett \n";
43 44
} };
45 46
//INIZIO
47
int main ()
48
{
49
//definisci l’oggetto rett
50
Rettangolo rett;
51 52
226
//scrivi l’area e il perimetro invocando i relativi metodi
Unità didattica 13 - Polimorfismo ed ereditarietà
53 54 55 56 57
cout << "\nArea del rettangolo = " << rett.area(); cout << "\nPerimetro rettangolo = " << rett.perimetro();
58 59 60
//fine programma cout << "\n\nFine system ("pause");
61 62
return 0;
rett.~Rettangolo();
";
}
Prova di esecuzione
Analisi del codice La riga 50 risulta ora completamente comprensibile: l’oggetto rett viene creato con la struttura definita dalla classe Rettangolo , viene riservata l’area di memoria necessaria per contenerlo e viene invocato il metodo costruttore Rettangolo() che inizializza i dati membro definiti nella classe. Le righe 33 e 42, rispettivamente nei metodi costruttore e distruttore, sono state inserite per poter controllare il flusso di elaborazione, ma non influiscono sull’efficienza del programma. Può succedere, invece, che il costruttore sia vuoto. Alla riga 39 è presente il metodo distruttore. Si noti che anche i distruttori, come i costruttori, non restituiscono alcun valore. ...........................................................................................................................................
13.2 Costruttori parametrizzati I costruttori possono ricevere argomenti. Normalmente, questi argomenti aiutano a inizializzare un oggetto al momento della creazione. Per creare un costruttore parametrizzato, basta aggiungervi parametri così come si fa con qualsiasi altro metodo. Quando si definisce il corpo del costruttore si possono utilizzare i parametri per inizializzare l’oggetto. Esempio Persona................................................................................................................ Dichiarare una classe di nome Persona con i dati cognome e nome e con due metodi: uno per scrivere prima il cognome poi il nome, l’altro per scrivere prima il nome poi il cognome. Utilizzare un costruttore parametrizzato che consegni a un oggetto della classe i dati acquisiti da tastiera.
227
Sezione 5 - Classi e oggetti
Diagramma di classe Persona
nome cognome cognomeNome() nomeCognome()
Indicazioni per la soluzione
Definire un oggetto amico che appartiene alla classe Persona , acquisire il cognome e il nome da tastiera all’interno del main e utilizzare tali dati come parametri del metodo costruttore per inizializzare un nuovo oggetto di nome amico . Codice Persona.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
228
#include #include using namespace std; class Persona { //definisci i dati membro della classe con protezione private string n; //Nome string c; //Cognome string tutto; //concatena cognome e nome public: string cognomeNome() { tutto = c+" "+n; return tutto; } //concatena nome e cognome public: string nomeCognome() { tutto = n+" "+c; return tutto; } //COSTRUTTORE : inizializza c e n public: Persona(string co, string no) { c = co; n = no; } }; //INIZIO int main () { //dati di input
Unità didattica 13 - Polimorfismo ed ereditarietà
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
string cognome; string nome; //leggi il cognome cout << "\nInserisci cognome => "; cin >> cognome; //leggi nome cout << "\nInserisci nome => "; cin >> nome; //dichiara l'oggetto amico di classe Persona Persona amico(cognome,nome); //scrivi prima il cognome e poi il nome cout << "\nCognome e nome = " << amico.cognomeNome(); //scrivi prima il nome e poi il cognome cout << "\nNome e cognome = " << amico.nomeCognome(); //fine programma cout << "\n\nFine system ("pause"); return 0;
";
}
Prova di esecuzione
Analisi del codice
Alla riga 5 viene definita la classe Persona. Alle righe 8 e 9 sono definiti i dati membro della classe Persona. Alla righe 13 e 20 sono presenti i due metodi richiesti: entrambi concatenano le due stringhe, inseriscono tra esse uno spazio bianco. Alla riga 27 viene introdotto il metodo costruttore con i due parametri co e no che corrispondono ai dati cognome e nome indicati. Dalla riga 42 alla riga 47 vengono acquisiti da tastiera il cognome e il nome. Alla riga 50 viene invocato il costruttore che utilizza gli argomenti cognome e nome. La tabella seguente riassume lo scambio di dati tra il main e la classe Persona . MAIN
PARAMETRI DEL COSTRUTTORE
MEMBRI DELLA CLASSE
cognome
→
co
→
c
nome
→
no
→
n
........................................................................................................................................... 229
Sezione 5 - Classi e oggetti
I costruttori parametrizzati sono molto utili poiché evitano di dover eseguire una nuova chiamata di un metodo semplicemente per inizializzare una o più variabili in un oggetto: ogni chiamata di un metodo evitata rende il programma più efficiente.
13.3 Membri static di una classe I metodi e i dati che sono membri di una classe possono essere resi static . Questo paragrafo spiega cosa ciò significhi per ogni tipo di membro.
Quando la dichiarazione di una variabile membro è preceduta dalla parola chiave static si chiede al compilatore di creare una sola copia di tale variabile e di utilizzare tale copia per tutti gli oggetti della classe. A differenza dei comuni dati membro, non viene creata una singola copia di una variabile membro static per ogni oggetto. Indipendentemente dal numero di oggetti creati per una classe, esiste una sola copia dei dati membro static . Pertanto, tutti gli oggetti di tale classe utilizzano la stessa variabile. Tutte le variabili static vengono inizializzate a zero nel momento in cui viene creato il primo oggetto. Quando si dichiarano i dati membro static all’interno di una classe, non si definiscono tali dati. Questo significa che non si sta allocando spazio di memoria per tali dati (in C++, una dichiarazione descrive qualcosa e una definizione crea qualcosa). Si deve pertanto fornire una definizione globale per i dati membro static in un altro punto, all’esterno della classe. L’utilizzo di variabili dichiarate static comporta anche l’introduzione di un nuovo operatore, che permette di specificare il campo d’azione di una variabile.
L’operatore :: , chiamato operatore di risoluzione del campo di azione , specifica a quale classe appartiene un dato membro di tipo static o una funzione membro. Prima che una variabile dichiarata static possa essere utilizzata è necessario specificare l’ambito a cui la variabile appartiene, utilizzando la sintassi riportata di seguito. NomeClasse
:: variabile ;
Per comprendere l’uso e gli effetti dei dati membri static , si consideri il seguente esempio. Esempio Static.................................................................................................................... Definire una classe Esempio con due dati membro interi: a e b, con a dichiarato static . Dichiarare e costruire due oggetti con valori iniziali diversi. Verificare il comportamento di a e di b.
Diagramma di classe ClasseEsempio static a b mostra
230
Unità didattica 13 - Polimorfismo ed ereditarietà
Static.cpp 1 2 3
#include using namespace std;
4 5 6 7 8
//dichiara la classe ClasseEsempio class ClasseEsempio { //dichiara i dati membro della ClasseEsempio static int a;
9 10 11
int b;
12 13 14 15 16
public: void imposta (int i, int j) { a = i; b = j;
17 18 19 20
}
21 22 23 24
public: void mostra(int numOggetto) { cout << "\nIn oggetto " << numOggetto; cout << "\nvariabile statica a = " << a;
25 26 27 28 29 30 31 32 33 34 35 36 37
//inizializza a e b
//mostra su quale oggetto si sta lavorando //e il contenuto delle variabili
cout << "\nvariabile non st. b = " << b<<”\n”; } }; int ClasseEsempio :: a;
//definisci a
//INIZIO int main () { //dichiara due oggetti della classe ClasseEsempio ClasseEsempio oggetto1; ClasseEsempio oggetto2;
38 39 40
//inizializza a e b oggetto1.imposta(10,10);
41 42 43 44 45
//mostra il contenuto di oggetto1 oggetto1.mostra(1);
46 47 48 49
//inizializza a e b oggetto2.imposta(20,20); //mostra il contenuto di oggetto2 oggetto2.mostra(2);
50 51 52 53
//mostra il contenuto di oggetto1 oggetto1.mostra(1);
54
cout << "\n\nFine
//fine programma ";
231
Sezione 5 - Classi e oggetti
55 56 57
system ("pause"); return 0; }
Prova di esecuzione
Analisi del codice
Alla riga 29 viene utilizzato l’operatore ::, che segnala al compilatore che la variabile a appartiene alla classe ClasseEsempio o, in altre parole, che a è nel campo d’azione di ClasseEsempio . Alla riga 39 il costruttore di oggetto1 assegna ad a e b il valore 10 all’interno di oggetto1 (come evidenziato dal metodo mostra(): si vedano le prime tre righe dell’output creato dalla prova di esecuzione). Alla riga 45 il costruttore di oggetto2 assegna ad a e b il valore 20 all’interno di oggetto2 (come evidenziato dal metodo mostra(): si vedano le righe 4, 5 e 6 dell’output creato dalla prova). Alla riga 51 il metodo mostra() visualizza che anche in oggetto1 b ha assunto il valore 20, ma evidenzia che a in oggetto1 contiene ancora 10 perché “statico”, come indicato alla riga 8. ........................................................................................................................................... Un altro interessante uso di una variabile membro static consiste nel registrare il numero di oggetti esistenti per una determinata classe. Esempio Counter................................................................................................................ Definire più oggetti per ClasseEsempio e mostrarne il numero.
Diagramma della classe ClasseEsempio a b ClasseEsempio() mostra()
Codice Counter.cpp
232
1 2
#include using namespace std;
3 4 5
//dichiara la classe Contatore class Contatore
Unità didattica 13 - Polimorfismo ed ereditarietà
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
{ //definisci i dati membro della classe Contatore int a; int b; static int ctr; //contatore //COSTRUTTORE, inizializza a e b public: Contatore (int i, int j) { a = i; b = j; ctr++; } //DISTRUTTORE ~Contatore () { ctr--; } //mostra numero degli oggetti void mostra() { cout << "\nContatore oggetti = " << ctr; } }; int Contatore :: ctr;
//definisci contatore
//INIZIO int main () { //dichiara un oggetto di classe Contatore Contatore oggetto1(10,10); //mostra numero oggetti oggetto1.mostra(); //dichiara un oggetto di classe Contatore Contatore oggetto2(20,20); //mostra numero oggetti oggetto1.mostra(); //distruggi oggetto2 oggetto2.~Contatore(); //mostra numero oggetti oggetto1.mostra(); //fine programma cout << "\n\nFine system ("pause"); return 0;
";
}
233
Sezione 5 - Classi e oggetti
Prova di esecuzione
Analisi del codice
Alla riga 10 la variabile ctr è definita static . Alla riga 18 la variabile ctr viene incrementata; questa istruzione è all’interno del metodo costruttore, quindi ctr viene incrementata ogni volta che viene definito un oggetto. Alla riga 24 la variabile ctr viene decrementata; l’istruzione si trova all’interno del distruttore e, quindi, segnala che un oggetto è stato escluso dal conteggio. Alle righe 43 e 49 il metodo mostra() visualizza il numero di oggetti presenti. ........................................................................................................................................... L’impiego di variabili membro static dovrebbe consentire di eliminare la necessità di utilizzare variabili globali. Il problema derivante dall’uso di variabili globali in tecniche di programmazione a oggetti consiste nel fatto che quasi sempre esse violano il principio di incapsulazione.
13.4 Overloading Esaminiamo ora gli argomenti dell’overloading dei metodi e del polimorfismo. L’overloading dei metodi è uno degli aspetti fondamentali del linguaggio di programmazione C++. Infatti, esso non solo fornisce il supporto per il polimorfismo in fase di compilazione ma aggiunge al linguaggio flessibilità e comodità. Tra i metodi modificati tramite overloading, quelli più importanti sono i costruttori.
L’overloading consiste nell’impiegare lo stesso nome per due o più metodi. Il “segreto” dell’overloading è il fatto che ogni ridefinizione del metodo deve utilizzare parametri di tipo differente oppure in numero differente. Grazie a queste diversità, il compilatore è in grado di scegliere quale metodo utilizzare in una determinata situazione. Esempio ValoreAss .............................................................................................................
Calcolare il valore assoluto di un numero indipendentemente dal suo tipo. Indicazioni per la soluzione
Per ottenere il valore assoluto di un numero, basta cambiare il suo segno quando è negativo. Si devono costruire diversi metodi che restituiscono il valore assoluto in funzione del tipo di dato che ad essi viene passato dal programma chiamante: pertanto, si deve avere a disposizione un metodo per il valore assoluto di un intero, di un intero lungo, di un double e così via. Sfruttando le proprietà dell’overloading, tutti i diversi metodi possono avere il medesimo nome, in modo che nel programma principale si possa utilizzare un unico identificatore senza preoccuparsi del tipo di dato su cui lavorare. 234
Unità didattica 13 - Polimorfismo ed ereditarietà
Codice ValoreAss.cpp 1
#include
2
using namespace std;
3 4
class ValoreAssoluto
5 6
{ //restituisci il valore assoluto di un intero
7 8
public: int abs(int intero)
9 10
{ if (intero<0)
11
{
12
intero = -intero;
13
}
14
return intero;
15
}
16
//restituisci il valore assoluto di un double
17
double abs(double doppio)
18
{
19 20
if (doppio<0) {
21 22
doppio = -doppio; }
23 24
return doppio; }
25 26
//restituisci il valore assoluto di un intero lungo
27
long abs(long lungo)
28
{
29
if (lungo<0)
30
{
31
lungo = -lungo;
32
}
33 34
return lungo; }
35 36
};
37 38
//INIZIO int main ()
39 40
{
41
//definisci l'oggetto calcola della classe ValoreAssoluto ValoreAssoluto calcola;
42 43
//scrivi il valore assoluto di un intero
44
//richiamando il metodo abs di calcola
45
cout << "\n" << calcola.abs(-10);
46 47
//scrivi il valore assoluto di un double
48
//richiamando il metodo abs di calcola
49 50
cout << "\n" << calcola.abs(-11.1);
51 52
//scrivi il valore assoluto di un int lungo //richiamando il metodo abs di calcola
235
Sezione 5
- Classi e oggetti
53 54 55 56 57
cout << "\n" << calcola.abs(-10L);
58 59
return 0;
//fine programma cout << "\n\nFine system ("pause");
";
}
Prova di esecuzione
Analisi del codice
Alla riga 4 è definita la classe ValoreAssoluto per contenere tre versioni diverse del metodo abs che ha la funzione di restituire il valore assoluto di un numero. Alle righe 8, 17 e 27 sono presenti le intestazioni di tre metodi diversi, tutti con il nome uguale: il primo è in grado di operare su un parametro intero e restituisce un valore intero, il secondo svolge la medesima funzione, ma su valori double e il terzo su valori di tipo intero lungo. Alle righe 45, 49 e 53 viene richiamato il metodo abs con tre parametri diversi: rispettivamente per un intero, per un double e per un intero lungo. All’interno della classe ValoreAssoluto sono definiti tre metodi diversi; all’interno del main si richiama uno dei metodi definiti nella classe ValoreAssoluto utilizzando sempre il medesimo nome. L’esempio precedente cita solo tre tipi di dato, ma il pr ogramma può essere esteso per comprendere tutti i tipi di dato numerici previsti da C++. ...........................................................................................................................................
Come si è detto, la caratteristica principale dell’overloading dei metodi è il fatto che questi devono differire per quanto riguarda il tipo e/o il numero dei parametri. Dunque due metodi non possono differire solo per il tipo di dato restituito. Per esempio, ecco un modo errato per eseguire l’overloading: se per il metodo mioMetodo() si scrive: int mioMetodo(int 1); float mioMetodo(int 1);
si compie un errore perché le due intestazioni differiscono solo per il tipo del valore restituito.
13.5 Polimorfismo I costruttori possono essere modificati tramite overloading e, nella pratica comune, questo avviene molto spesso. I motivi principali che spingono a utilizzare il polimorfismo dei costruttori sono la maggiore flessibilità che può essere data alla definizione di una classe e la possibilità di creare oggetti tra loro simili appartenenti alla medesima classe. Spesso si crea una classe per la quale esistono due o più modi per costruire un oggetto: in tal caso, è opportuno fornire una funzione costruttore modificata tramite overloading per entrambi questi metodi. 236
Unità didattica 13 - Polimorfismo ed ereditarietà
Questa è una regola fondamentale in quanto, se si cerca di creare un oggetto per il quale non esiste un costruttore, viene prodotto un errore in fase di compilazione. Offrendo un costruttore per ognuna delle modalità in cui un utilizzatore della classe può voler costruire un oggetto, si aumenta la flessibilità della classe. L’utente è libero di scegliere il modo migliore per costruire un oggetto in una determinata circostanza. Il polimorfismo trova la sua realizzazione nell’uso di metodi e di costruttori modificati tramite overloading.
Esempio Retta .................................................................................................................... L’equazione di una retta sul piano cartesiano può essere definita in due modalità: attraverso la sua equazione in forma esplicita con una equazione del tipo: ax + by + c = 0
oppure utilizzando la forma implicita con un’equazione del tipo: y = mx + q
Nel primo caso, la retta è individuata da tre parametri: a, b, c ; nel secondo, dai parametri m , q. Definire la classe retta con due costruttori, uno per la forma esplicita e uno per la forma implicita. Aggiungere alla classe Retta i metodi: per scrivere l’equazione della retta in forma esplicita (si ricordi che m = -a/b e che q = –c/b ); mostraIntersezioni() che visualizza le coordinate dei punti di intersezione della retta con gli assi cartesiani. mostraImplicita()
Diagramma della classe Retta
a, b, c // coefficienti retta in forma esplicita m, q // coefficienti retta in forma implicita Retta(a, b, c) //costruttore retta esplicita Retta(m, q) //costruttore retta implicita mostraImplicita() mostraIntersezioni()
Codice Retta.cpp 1 2 3
#include using namespace std;
4 5
//definisci la classe class Retta
237
Sezione 5
6
- Classi e oggetti
{
7
double a, b, c;
//coefficienti retta in forma esplicita
8
double m, q;
//coefficienti retta in forma implicita
9 10
public:
11
//COSTRUTTORE della retta in forma esplicita
12 13
Retta(double c1, double c2, double c3) {
14 15
a = c1; b = c2;
16 17
c = c3; m = -a/b;
18
q = -c/b;
19 20
}
21
//COSTRUTTORE della retta in forma implicita
22
Retta(double c1, double c2)
23
{
24
m = c1;
25
q = c2;
26
}
27 28 29
//scrivi la retta in forma implicita void mostraImplicita()
30 31
{
32
}
33 34
//scrivi le coordinate delle intersezioni con gli assi cartesiani
35 36
void mostraIntersezioni() {
cout << "\nY = " << m << "X + " << q << "\n";
37
cout << "\nIntersezioni asse X: (" << -q/m << "," << 0 << ")";
38
cout << "\nIntersezioni asse Y: (" << 0 << "," << q << ")";
39 40
} };
41 42
//INIZIO
43
int main ()
44 45
{ double c1, c2, c3;
//coefficienti della retta
46 47
//acquisisci i coefficienti della retta in forma esplicita
48
cout << "\nInserisci a ";
49 50
cin >> c1; cout << "\nInserisci b ";
51
cin >> c2;
52
cout << "\nInserisci c ";
53
cin >> c3;
54 55
//dichiara l'oggetto esplicita della classe Retta
56
Retta esplicita(c1,c2,c3);
57
238
58 59
//invoca il metodo per visualizzare la retta //anche in forma implicita
60
esplicita.mostraImplicita();
Unità didattica 13 - Polimorfismo ed ereditarietà
61 62 63 64 65
//invoca il metodo per visualizzare le intersezioni con gli assi esplicita.mostraIntersezioni(); //acquisisci i coefficienti della retta in forma esplicita
66 67 68
cout << "\n\nInserisci m "; cin >> c1; cout << "\nInserisci q ";
69 70 71 72 73
cin >> c2;
74 75 76 77
//invoca il metodo per visualizzare le intersezioni con gli assi implicita.mostraIntersezioni();
78 79 80 81
cout << "\n\nFine system ("pause"); return 0;
//definisci l'oggetto implicita della classe Retta Retta implicita(c1,c2);
//fine programma ";
}
Prova di esecuzione
Osservazioni sulla prova di esecuzione Il programma inizia con la richiesta dei valori dei coefficienti a, b, c (righe da 48 a 53). Successivamente, visualizza l’equazione della retta in forma implicita (riga 60) e le coordinate dei punti di intersezione con gli assi cartesiani (riga 63). In un secondo momento vengono richiesti i coefficienti m e q di una nuova retta, questa volta in forma implicita (righe 66 e 68); l’utente può inserire i valori di m e q leggendoli dall’equazione della retta precedente. Il calcolo delle coordinate dei punti di intersezione dà risultati identici ai precedenti per l’ovvia ragione che viene usato sempre lo stesso metodo definito una volta per tutte all’interno della classe. Analisi del codice Alla riga 5 viene definita la classe Retta . Le righe 7 e 8 descrivono i dati membro della classe e hanno protezione private . La riga 12 è l’intestazione del costruttore della classe con tre para239
Sezione 5 - Classi e oggetti
metri (a, b, c); tale costruttore è in overload con il costruttore definito alla riga 22, il quale contiene solo due parametri m ( e q) ed è in alternativa con il costruttore precedente. Sarà il compilatore a scegliere il costruttore opportuno in base al numero dei parametri specificati al momento modella definizione di un oggetto appartenente alla classe. Alla riga 29 viene dichiarato il metodo straImplicita(), che scrive l’equazione della retta. Alla riga 35 viene dichiarato il metodo mostraIntersezioni(), che scrive le coordinate dei punti di intersezione della retta con gli assi cartesiani. Alla riga 56 viene dichiarato e definito l’oggetto esplicita che appartiene alla classe Retta: il suo costruttore utilizza tre parametri. Alle righe 60 e 63 vengono invocati dall’oggetto esplicita i metodi mostraImplicita() e mostraIntersezioni(). Alla riga 72 viene dichiarato e definito l’oggetto implicita che appartiene alla classe Retta: il suo costruttore utilizza due parametri. Alla riga 75 viene invocato dall’oggetto implicita il metodo mostraIntersezioni(). ........................................................................................................................................... Nell’esempio precedente lo scopo dell’overloading del costruttore di Retta è quello di rendere più flessibile e semplice il suo uso. Tale maggiore flessibilità e facilità d’uso è par ticolarmente importante quando si creano librerie di classi che possono ess ere utilizzate anche da altri programmatori.
13.6 Ereditarietà L’ereditarietà costituisce una delle pietre angolari della programmazione orientata agli oggetti, in quanto consente di creare classificazioni gerarchiche. Utilizzando l’ereditarietà, è possibile creare una classe generale che definisce le caratteristiche comuni a una serie di oggetti correlati. Tale classe può, in seguito, essere ereditata da una o più classi, ognuna delle quali aggiunge alla classe ereditata solo elementi specifici.
Per conservare la terminologia standard di C++, la classe ereditata viene chiamata classe base. La classe che “riceve” l’eredità è detta classe derivata. A sua volta, una classe derivata può fungere da classe base per un’altra classe derivata: in questo modo, è possibile riprodurre a più livelli il meccanismo di ereditarietà. Il supporto dell’ereditarietà del C++ è ricco e flessibile. Quando una classe ne eredita un’altra, i membri della classe base divengono anche membri della classe derivata. Per definire una classe derivata da un’altra si utilizza la sintassi riportata di seguito. class
Nome-classe-derivata : Nome-classe-base
{ corpo_della_classe
}
Esempio Ordine1................................................................................................................
Una società commerciale vende un unico prodotto che viene descritto con la sigla “pc AMD quick”. Per il controllo e la gestione degli ordini ricevuti dalla società, si devono definire tre classi: Ordine , Spedizione , Fattura . Definire la classe Ordine attraverso gli attributi: descrizione , quantità , prezzo , dataOrdine , indirizzoCliente. Definire il suo costruttore e il metodo per visualizzare gli attributi. Derivare dalla classe Ordine la classe Spedizione aggiungendo l’attributo dataSpedizione. Derivare da quest’ultima la classe Fattura aggiungendo il metodo per il calcolo del totale della fattura. 240
Unità didattica 13 - Polimorfismo ed ereditarietà
Indicazioni per la soluzione Il problema verrà diviso in due parti: la prima riguarda la definizione della classe Ordine e la deri vazione della classe Spedizione; la seconda la derivazione della classe Fattura. La prima parte consiste nel definire la classe Ordine: tale classe è composta dai dati membro richiesti dal problema: descrizione , quantità , prezzo, dataOrdine , indirizzoCliente e possiede tre metodi: due costruttori in overload e showOrdine . Il metodo showOrdine mostra l’importo dell’ordine; i due costruttori hanno funzioni diverse: uno è vuoto e privo di parametri e serve per l’operazione di derivazione, l’altro, con parametri, imposta i dati iniziali.
Diagramma delle classi Ordine descrizione quantità prezzo dataOrdine indirizzoCliente costruttore showOrdine
Spedizione dataSpedizione showSpedizione
Codice Ordine1.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include #include using namespace std; //descrivi la classe Ordine class Ordine { string descrizione; int quantità; double prezzo; string dataOrdine; string nomeCliente; //metodo per la scrittura del contenuto di Ordine public: void showOrdine() { cout << "\nOrdine"; cout << "\nArticolo venduto " << descrizione;
241
Sezione 5
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
242
- Classi e oggetti
cout cout cout cout cout
<< << << << <<
"\nQuantità venduta " << quantità; "\nPrezzo di vendita " << prezzo; "\nData ordine " << dataOrdine; "\nImporto " << quantità*prezzo; "\nCliente " << nomeCliente;
} //costruttore indispensabile per la derivazione Ordine() { //metodo vuoto e senza parametri } //costruttore effettivo per l'impostazione dei dati Ordine(string d, int q, double p, string dO, string nC) { descrizione = d; quantità = q; prezzo = p; dataOrdine = dO; nomeCliente = nC; } }; //descrivi la classe Spedizione class Spedizione : Ordine { string dataSpedizione; public: Spedizione(string dS) { dataSpedizione = dS; } //scrivi i dati di spedizione void showSpedizione() { cout << "\nData spedizione " << dataSpedizione; } }; //INIZIO int main { string int q; double string string
() d; p; dO; nC;
//descrizione dell'articolo ordinato //quantità ordinata //prezzo articolo //data Ordine //nome Cliente
//acquisisci i dati di Ordine cout << "\nInserisci Descrizione cout << "\nInserisci Quantità cout << "\nInserisci Prezzo
"; cin >> d; "; cin >> q; "; cin >> p;
Unità didattica 13 - Polimorfismo ed ereditarietà
76 77
cout << "\nInserisci data ordine "; cin >> dO; cout << "\nInserisci nome cliente "; cin >> nC;
78 79 80 81
//dichiara e definisci l'oggeto ord Ordine ord (d, q, p, dO, nC);
82 83 84 85
//scrivi il contenuto di ord ord.showOrdine(); string dS;
86 87 88 89 90
//data di spedizione
//acquisisci la data di spedizione cout << "\n\nInserisci data spedizione "; cin >> dS; //dichiara e definisci l'oggetto sped
91 92 93
//derivato da ord Spedizione sped(dS);
94 95 96 97 98
//scrivi il contenuto di sped sped.showSpedizione();
99 100 101
//fine programma cout << "\n\nFine
";
system ("pause"); return 0; }
Prova di esecuzione
Analisi del codice
Alla riga 6 inizia la descrizione della classe Ordine secondo le indicazioni del diagramma della classe visto in precedenza. Alla riga 28 è presente un costruttore vuoto in overload: tale costruttore non ha parametri e non contiene istruzioni all’interno del corpo, ma deve comunque essere presente per rendere possibile la derivazione della classe Spedizione dalla classe Ordine . 243
Sezione 5 - Classi e oggetti
Alla riga 34 è definito il costruttore effettivo della classe Ordine : ha la funzione di assegnare un valore ai dati che appartengono alla classe. Alla riga 45 inizia la descrizione della classe Spedizione e, attraverso il carattere “ :” viene specificato che Spedizione è derivata da Ordine . Dalla riga 47 alla riga 59 è presente il corpo della classe Spedizione : è molto semplice, ma va ricordato che i suoi membri costituiscono una estensione dei membri della classe Ordine . Dalla riga 73 alla riga 77 si trovano le istruzioni necessarie all’acquisizione dei dati per istanziare, attraverso il costruttore della classe, un nuovo oggetto della classe Ordine . Alla riga 80 viene creato l’oggetto ord appartenente alla classe Ordine . Alla riga 92 viene creato l’oggetto sped appartenente a Spedizione . Va sottolineato il fatto che la riga 92 del codice invoca implicitamente il costruttore Ordine() . Nella classe Ordine sono definiti in overloading due costruttori; in par ticolare, quello che viene invocato al momento della creazione dell’istanza della classe derivata è il costr uttore che è privo di parametri. La conseguenza di tutto ciò è che i dati membro di Ordine vengono inizializzati una seconda volta facendo in modo che i dati numerici risultino nulli e i dati stringa diventino vuoti. Nell’esempio precedente non si nota questo fatto, ma risulta evidente quando si vogliono utilizzare i dati della classe base. Vedremo nel prossimo esempio come ovviare a quanto sopra. Per semplicità e per ridurre le righe di codice, le date sono s tate definite come stringhe compiendo, così, una piccola scorrettezza di programmazione.
...........................................................................................................................................
Viene affrontata ora la seconda parte del problema della gestione degli ordini: quello di produrre i dati relativi alla fattura che la società deve emettere a fronte dell’ordine spedito. Per tale problema, ci limitiamo a calcolare il totale della fattura. Esempio Ordine2................................................................................................................ Calcolare il totale della fattura relativa a un ordine ricevuto.
Indicazioni per la soluzione
Il calcolo del totale della fattura è abbastanza semplice: basta calcolare il prodotto della quantità ordinata per il prezzo unitario e aggiungere l’IVA. Si ipotizzi che l’IVA sia costante al 20%. La fattura è definita come una nuova classe che aggiunge il dato del totale da pagare ai dati relativi all’ordine e alla spedizione. In altri termini, definiamo la classe Fattura come derivata da Spedizione che, a sua volta, deriva da Ordine . Da quanto detto si ottiene il seguente diagramma. Diagramma delle classi
Ordine
descrizione quantità prezzo dataOrdine indirizzoCliente ordine() showOrdine getImporto
Spedizione
dataSpedizione spedizione() showSpedizione Fattura
totaleFattura
244
Unità didattica 13 - Polimorfismo ed ereditarietà
Codice
Ordine2.cpp 1 2
#include #include
3
using namespace std;
4 5
const int iva = 20;
6 7
//descrivi la classe Ordine
8
class Ordine
9 10
{ string descrizione;
11 12
static int quantità; static double prezzo;
13
string dataOrdine;
14 15
string nomeCliente;
16 17
//metodo per la scrittura del contenuto di Ordine public:
18 19
void showOrdine() {
20
cout << "\nOrdine";
21 22
cout << "\nArticolo venduto " << descrizione; cout << "\nQuantità venduta " << quantità;
23 24
cout << "\nPrezzo di vendita " << prezzo; cout << "\nData ordine " << dataOrdine;
25
cout << "\nImporto
" << quantità*prezzo;
26 27
cout << "\nCliente
" << nomeCliente;
}
28 29
//costruttore indispensabile per la derivazione
30
Ordine()
31 32
{
33 34
}
35
//costruttore effettivo per l'impostazione dei dati
36 37
Ordine(string d, int q, double p, string dO, string nC) {
//metodo vuoto e senza parametri
38 39
descrizione = d; quantità = q;
40
prezzo = p;
41 42
dataOrdine = dO; nomeCliente = nC;
43 44
}
45
//restituisci l'importo dell'ordine
46 47
double getImporto() {
48 49 50
return quantità*prezzo; } };
51
245
- Classi e oggetti
Sezione 5
52
//descrivi la classe Spedizione
53
class Spedizione : public Ordine
54
{
55
string dataSpedizione;
56 57
public:
58
Spedizione(string dS)
59
{
60
dataSpedizione = dS;
61
}
62 63
//scrivi i dati di spedizione
64
void showSpedizione()
65
{
66
cout << "\nData spedizione " << dataSpedizione;
67
}
68 69
//costruttore indispensabile per la derivazione di Fattura
70
Spedizione()
71
{
72
//metodo vuoto e senza parametri
73 74
} };
75 76 77
class Fattura : public Spedizione
78
{
79
public:
80
double totaleFatt()
81
{
82
const int iva = 20;
83 84
//aggiungi l'IVA all'importo dell'ordine
85
return getImporto()*(100+iva)/100;
86 87
} };
88 89
double Ordine :: prezzo;
90
int Ordine :: quantità;
91 92
//INIZIO
93
int main ()
94
{
95
string d;
//descrizione dell'articolo ordinato
96
int q;
//quantità ordinata
97
double p;
//prezzo articolo
98
string dO;
//data Ordine
99
string nC;
//nome Cliente
100
246
101
//acquisisci i dati di Ordine
102
cout << "\nInserisci Descrizione
103
cout << "\nInserisci Quantità
104
cout << "\nInserisci Prezzo
"; cin >> p;
105
cout << "\nInserisci data ordine
"; cin >> dO;
"; cin >> d; "; cin >> q;
Unità didattica 13 - Polimorfismo ed ereditarietà
106
cout << "\nInserisci nome cliente "; cin >> nC;
107 108
//Dichiara e definisci l'oggeto ord
109
Ordine ord (d, q, p, dO, nC);
110 111
//scrivi il contenuto di ord
112
ord.showOrdine();
113 114
string dS;
//data di spedizione
115 116
//acquisisci data di spedizione
117
cout << "\n\nInserisci data spedizione "; cin >> dS;
118 119
//dichiara e definisci l'oggetto sped
120
//derivato da ord
121
Spedizione sped(dS);
122 123
//scrivi il contenuto di sped
124
sped.showSpedizione();
125 126
//dichiara e definisci l'oggetto fattura
127
//derivato da sped
128
Fattura fatt;
129 130
//scrivi il totale della fattura
131
//invocando il metodo totaleFattura
132
cout << "\nTotale fattura = " << fatt.totaleFatt();
133 134
//fine programma
135
cout << "\n\nFine
136
system ("pause");
137
return 0;
138
";
}
Prova di esecuzione
247
Sezione 5
- Classi e oggetti
Analisi del codice
Sono state segnalate in grassetto soltanto le parti nuove rispetto al listato Ordine.cpp. Alle righe 11 e 12 i dati quantità e prezzo vengono definiti static per evitare che i costruttori definiti alle righe 30 e 70 inizializzino i valori assegnati ai dati della classe Ordine. Dalla riga 46 alla riga 49 viene definito il nuovo metodo che restituisce l’importo dell’ordine. Alla riga 70 viene inserito il costruttore di Spedizione che, come già detto, ha la sola funzione di consentire la derivazione della classe Fattura dalla classe Spedizione. Alla riga 77 inizia la descrizione della classe Fattura derivata da Spedizione. Alla riga 80 troviamo il metodo che restituisce il totale della fattura: tale metodo appartiene alla classe Fattura e utilizza il metodo getImporto appartenente alla classe Ordine. Alle righe 89 e 90 viene utilizzato l’operatore ::, che segnala al compilatore che le variabili prezzo e quantità appartengono alla classe Ordine o, in altre parole, che le variabili sono nel campo di azione della classe Ordine. Dalla riga 93 alla riga 127 il main non presenta novità rispetto a quanto visto nell’esempio Ordine. La riga 128 dichiara e definisce l’oggetto fatt, appartenente alla nuova classe Fattura, sul quale, alla riga 132, viene invocato il metodo totaleFattura() . ...........................................................................................................................................
248
Unità didattica 13 - Polimorfismo ed ereditarietà
Esercizi Unità didattica 13 Per gli esercizi da 1 a 4 definire il costruttore.
Definire la classe punto del piano cartesiano con i metodi sposta_a_destra, sposta_a_sinistra, sposta_in_alto e sposta_in_basso.
Definire la classe verifica con i dati materia, data e voto e con il metodo cambiaVoto. Disegna il diagramma della classe e realizza in C++ il programma che utilizza tale classe.
Definire la classe data_di_nascita e realizza il metodo che restituisce l’età. Definire la classe NumeroReale con i metodi che restituiscono la parte intera e la par te decimale. Definire la classe Approssimazione con il dato valoreIniziale e con i seguenti tre metodi: perDifetto perEccesso interoPiuVicino
Definire la classe Segmento individuata dalle coordinate sul piano cartesiano dei suoi vertici e che possiede i metodi per restituire la lunghezza del segmento e le coordinate del punto medio.
Definire la classe ArticoloInVendita con i dati prezzoDiVendita (in euro) e descrizione. Dopo, definire i due oggetti: Articolo1 (30, “maglione”) Articolo2 (11, “camicia”) Definire anche una variabile static per rappresentare la percentuale di un possibile sconto sul prezzo di vendita.
Definire una classe che permetta la conversione da euro in lire e viceversa (si approssimi il fattore di conversione a 2000. Si definiscano due costruttori: il primo con il parametro long (per le lire) il secondo con il parametro float (per gli euro).
Definire una classe che, per la misura degli angoli, permetta la conversione da gradi in radianti: si definiscano due costruttori: uno per la misura in gradi che ha come parametri tre interi (gradi, minuti, secondi) e uno per la misura in radianti che ha un unico parametro di tipo float.
Definire una classe Pesata che contiene i seguenti dati: nome, altezza (in metri) e peso (in kg) e che possegga il metodo che valuta se una persona è normolinea. Una persona è normolinea se vale la relazione 23 < peso / altezza 2 < 27.
Definire per la classe Retta di equazione y = mx + q, i due membri funzione traslazione_lungo_l’asse_x e traslazione_lungo_l’asse_y. Derivare la classe Parabola di equazione y =
ax2 + bx + c, che eredita i metodi per le traslazioni.
Data una parabola y = ax2 + bx + c, definire una classe Parabola utilizzando i suoi tre coefficienti a, b, c. Crea il suo costruttore e i metodi ascissaDelVertice(), ordinataDelVertice() e concavità().
249
Sezione 5 - Classi e oggetti
Esercizi Unità didattica 13 Definire la classe Motore attraverso gli attributi cilindrata , marca, valvole. Definire l'intestazione dei metodi avvia(), accelera() e interrompi() senza implementarli. Derivare la classe MotoreDiesel da Motore e ridefinire il metodo avvia().
Definire la classe IndirizzoGenerico con il metodo getIndirizzo() , che restituisce una stringa vuota. Definire la classe IndirizzoEmail come classe derivata da IndirizzoGenerico , con l'attributo account. Ridefinire il metodo getIndirizzo() per restituire il valore di account.
Definire la classe IndirizzoWeb come classe derivata di IndirizzoGenerico dell’esercizio precedente. IndirizzoWeb possiede l'attributo URL. Ridefinire il metodo getIndirizzo() per restituire l’URL.
Definire la classe IndirizzoFisico come classe derivata da IndirizzoGenerico. IndirizzoFisico contiene gli attributi via, numero, cap, località. Ridefinisci il metodo getIndirizzo()
per restituire un'unica stringa che raccolga via, numero, cap e località .
Definire una classe Triangolo attraverso le misure a, b, c dei suoi tre lati. Definire il suo costruttore e il metodo per calcolare il perimetro. Derivare da Triangolo la classe TriangoloIsoscele e definire il suo costruttore. Derivare da Triangololsoscele la classe TriangoloEquilatero e definire il suo costruttore.
Data una parabola y = ax 2 + bx + c, definire una classe Parabola utilizzando i suoi tre coefficienti a, b, c. Crea il suo costruttore e i metodi ascissaDelVertice() , ordinataDelVertice() e concavità().
250
Sezione
6
Operare con gli archivi
†
Obiettivi generali
◊
Riconoscere l’importanza dell’archiviazione dei dati
◊
Organizzare i file di dati in modo funzionale
◊
Comprendere la necessità degli archivi di dati nelle applicazioni gestionali
◊
&
Conoscere le caratteristiche dei file di testo
Questo modulo contiene
U.D.14 U.D.15
Archivi File di testo
Unità didattica
14
Archivi
CHE COSA IMPARERAI A FARE $
Riconoscere gli elementi fondamentali della definizione di un archivio
$
Definire la struttura di un archivio di dati
$
Descrivere gli aspetti fondamentali delle operazioni di I/O
$
Distinguere le caratteristiche fondamentali delle diverse organizzazioni dei file
$
Distinguere le caratteristiche fondamentali dei diversi tipi di accesso
CHE COSA DOVRAI STUDIARE $
Definizione di archivio
$
Definizione di record
$
Operazioni fondamentali sugli archivi
$
I/O standard e su memoria di massa
$
Tipi di archivio
$
Tipi di accesso
Unità didattica
14 Archivi
I computer sono parte integrante della nostra vita da diversi anni. La loro integrazione con le nostre attività quotidiane è talmente vasta e radicata che ormai non ci rendiamo nemmeno più conto della loro presenza e di come contribuiscano, nel bene e nel male, alla nostra esistenza. Siamo circondati dai computer praticamente in ogni momento della giornata. Infatti, pensare ai computer solo come alle macchine che siamo abituati a vedere sui nostri tavoli al lavoro o nei laboratori delle scuole e delle università, è piuttosto restrittivo: gran parte degli oggetti che ci circondano possiedono al loro interno un piccolo processore. Nella sola automobile sono presenti, per esempio, diversi computer: la centralina del motore, la centralina degli airbag, la centralina del condizionatore, la centralina degli impianti audio ecc. Oltre ai computer come entità fisiche (l’hardware) anche i loro programmi (il software) sono parte integrante della nostra vita: la radiosveglia che ogni mattina interrompe i nostri sogni, infatti, è un piccolo computer con un piccolo programma che si occupa di far scattare la suoneria all’ora prestabilita. Gran parte dei programmi esistenti non si limitano però alla semplice gestione di una radiosveglia. Pensiamo, per esempio, ai programmi che gestiscono l’anagrafe di un Comune, oppure, più semplicemente, a un programma che tenga aggiornato l’elenco dei CD e dei DVD in nostro possesso. Una funzione comune a tutti i programmi di gestione consiste nella memorizzazione, e in seguito nell’elaborazione, di grandi quantità di dati. Questi dati vengono raggruppati in archivi, che generalmente sono gestiti attraverso un linguaggio sviluppato ad hoc per interagire con essi, in grado di gestire insiemi di archivi tra lor o integrati. Gli archivi e il loro trattamento sono l’argomento di questa Unità didattica. 14.1 Definizione di archivio
Una delle prime necessità con cui l’informatica ha dovuto confrontarsi è stata la memorizzazione dei dati su un supporto da dove fosse possibile recuperarli in un secondo tempo. Si definisce archivio l’insieme dei dati che vengono salvati su un supporto. Un archivio viene anche chiamato generalmente file, che è la parola corrispondente in lingua inglese.
È stata di proposito fornita una definizione di archivio molto generale, che può dare adito a dubbi e sollevare domande. Vediamo quindi di approfondire il concetto. Nel mondo reale ci è ben chiaro, per esperienza diretta, in che cosa consista un archivio. In qualsiasi ufficio ci sia capitato di passare, per esempio la segreteria della scuola, avremo sicuramente avuto modo di vedere la classica cassettiera in metallo, che contiene le schede personali degli studenti. Pertanto, un archivio può essere visto e pensato come una struttura metallica con i cassetti. Negli stessi uffici avremmo però potuto osservare, con la stessa probabilità, uno scaffale pieno di grossi raccoglitori ad anelli con delle etichette sul dorso. In questo caso, i raccoglitori sono archivi di documenti. Il concetto di archivio in sé è, pertanto, un concetto astratto, che può essere adattato a una serie di oggetti fisici anche molto diversi l’uno dall’altro. Ciò che accomuna tutti gli archivi è lo scopo per cui sono creati: mantenere raccolte le informazioni. 253
Sezione 6 - Operare con gli archivi
Da un punto di vista informatico, un archivio può essere semplicemente pensato come un comunissimo file.
Un file è l’unità logica di memorizzazione dei dati all’interno di un supporto informatico. Per supporto si intende un disco fisso, un floppy, un CD-ROM, un DVD oppure ogni altra forma di memorizzazione possibile, presente o futura. Come impareremo nel seguito della trattazione, un singolo file non costituisce una raccolta di dati sufficiente per risolvere un problema di tipo gestionale, ma inizieremo da questo concetto per affrontare il tema della definizione e della strutturazione di una base di dati.
14.2 Dati Ora che abbiamo definito un archivio in ambito informatico, possiamo esaminare gli elementi che può contenere. Abbiamo infatti visto come sia possibile, in generale, pensare a un archivio come a un contenitore di dati. Il concetto di dato è un concetto astratto esattamente come quello di archivio. In pratica, in che cosa consistono i dati che “salviamo” in un archivio?
Si può definire dato una qualsiasi informazione che si vuole registrare o memorizzare all’interno di un computer. Ancora una volta, la definizione lascia il campo aperto a molte considerazioni, essendo il concetto molto generale. Vediamo quindi qualche esempio. Esempio Data.....................................................................................................................
Scrivere un programma che si limita a salvare su un file la data del giorno in cui è stato eseguito l’ultima volta. In questo caso, il dato da salvare è la data di sistema e l’archivio consiste in un singolo file di testo, dove ci si limita a scrivere la data. ...........................................................................................................................................
L’utilità di un tale programma può sembrare piuttosto limitata. Tuttavia non dobbiamo dimenticare che ogni programma, per quanto piccolo e “stupido”, può essere par te di un sistema che diventa molto complesso, se visto nel suo insieme. Quando si accede a una macchina Unix, per esempio, il sistema indica sul terminale quando l’utente si è inter facciato con il sistema l’ultima volta. Esempio Diario ...................................................................................................................
Scrivere un programma per gestire un diario di bordo. È necessario salvare, per ogni annotazione, la data, l’ora e il luogo. Il programma deve poter tenere traccia delle ultime annotazioni. In questo caso il dato da salvare non è più semplice, ma consiste in diversi “pezzi” di informazione. Anche in questo caso, tuttavia, possiamo limitarci a salvare le varie parti come se si trattasse di comunissimo testo, separandole tra loro con un carattere di separazione (generalmente la virgola). L’archivio, ancora una volta, consiste di un singolo file di testo. ........................................................................................................................................... 254
Unità didattica 14 - Archivi
Esempio Studenti................................................................................................................ Scrivere un programma per gestire l’elenco degli studenti di una scuola. Il programma deve essere in grado di associare un elenco di studenti con la classe e la sezione a cui essi appartengono.
A prima vista, questo caso sembra più semplice dei precedenti. In realtà è leggermente più complesso, poiché a una singola informazione (la classe e la sezione, che consideriamo un unico dato), deve venire associato un elenco più o meno numeroso di nomi (in realtà, di nomi e cognomi che, per semplicità, possiamo pensare come un unico dato). La soluzione più semplice consiste nel salvare più file, uno per ogni classe, nominandoli con il nome della classe e della sezione (per esempio, 3A). All’interno di ogni singolo file, i nomi degli alunni saranno scritti in formato testuale, separando i nomi dai cognomi con un semplice spazio (o con un altro carattere separatore). In questo caso, quindi, l’archivio è in realtà suddiviso in molti file (uno per ogni classe dell’istituto), e i dati in esso contenuti sono banale testo che, eventualmente, comprende un carattere separatore. ........................................................................................................................................... Ora che sappiamo in che cosa consistono i dati, possiamo approfondire ulteriormente il discorso.
14.3 Definizione di record Come abbiamo visto negli esempi del paragrafo precedente, quando il problema da risolvere diventa appena più complesso, aumenta anche la complessità dei dati e dell’archivio che li contiene. Spesso le informazioni che devono essere salvate sono suddivise logicamente in più parti.
L’unità macroscopica delle informazioni prende il nome di record. Un singolo record può essere suddiviso logicamente in più parti. Nell’esempio “Diario” del paragrafo precedente, il singolo record è costituito dall’insieme di informazioni che sono logicamente raggruppate tra loro, ovvero:
data; ora; luogo; annotazione.
Dal punto di vista grafico, un record può essere pensato come la riga di una tabella: Data
Ora
Luogo
Annotazione
Chiaramente, anche le singole unità di informazione che formano un record hanno un nome.
Le parti logiche in cui viene suddiviso un singolo record prendono il nome di campi. Normalmente, il nome del campo deriva dal tipo di informazione che è associata al campo stesso. Risulta piuttosto comodo poter fare riferimento ai singoli campi assegnando loro un nome: nell’esempio “Diario”, i nomi dei campi potrebbero essere proprio “Data”, “Ora”, “Luogo” e “Annotazione”.
255
Sezione 6 - Operare con gli archivi
Esempio Studenti e classi ..................................................................................................
Alla luce di quanto appreso in questo paragrafo, trovare un’altra soluzione per il problema dell’esempio “Studenti” visto nel paragrafo precedente.
Poiché ora siamo in grado di gestire singoli record, possiamo riformulare la soluzione nel modo descritto di seguito. Per ogni iscritto all’istituto conosciamo il nome, il cognome, la classe e la sezione. Il record in questo caso è (schematicamente) il seguente: Sezione
Classe
Nome
Cognome
A questo punto, l’archivio potrà consistere in un unico file, che contiene i record sopra descritti, con i singoli campi separati da un carattere separatore, come per esempio il punto e virgola. ...........................................................................................................................................
Ora che abbiamo imparato un po’ di teoria, possiamo passare alla pratica per gestire queste strutture di dati.
14.4 Operazioni fondamentali sugli archivi Finora ci siamo limitati a pensare a come strutturare i nostri archivi e i nostri record. Tuttavia è necessario pensare anche a come gestire gli archivi e i record nella pratica. Esistono diverse operazioni standard che occorre prevedere per poter gestire i dati; esse possono essere riassunte schematicamente in questo modo:
apertura dell’archivio; lettura dei record; scrittura dei record; modifica dei record (compresa la cancellazione); chiusura dell’archivio.
Queste operazioni possono sembrare piuttosto banali, ma ancora una volta una riflessione più attenta potrebbe portarci a riclassificare il problema. Consideriamo, per esempio, la semplice apertura dell’archivio. L’operazione di apertura dell’archi vio può cambiare a seconda del tipo e del numero di file che si stanno aprendo. In generale, l’operazione è abbastanza semplice e consiste in una chiamata alla funzione di sistema di apertura del file. Per completezza, scriviamo una funzione che apre un file il cui nome viene passato come parametro. Esempio Apertura e chiusura di un file .............................................................................. 1 2 3 4 5 6
//INIZIO
7 8
int main () {
9 10 11 12 13
256
#include #include #include using namespace std;
. . . . . . . . //apre il file dati ofstream dati("dati.txt");
Unità didattica 14 - Archivi
14 15 16 17
//controlla esito apertura if(!dati) { cout << "ERRORE apertura file"; . . . . . . . . } . . . . . . . .
31
dati.close(); . . . . . . . . }
Analisi del codice
Alla riga 2 è stata aggiunto l’header , che permette la gestione di file. Alla riga 12 viene eseguita l’apertura del file, in particolare va specificato che il file di nome dati è definito come oggetto della classe ofstream (che è una classe derivata da fstream ). In questo caso il prefisso “ of” indica che il file è aperto in scrittura. Alla riga 15 viene controllato il buon esito dell’operazione di apertura. Mentre l’operazione di apertura crea un collegamento tra il file specificato nel programma e il file presente nel file system, l’operazione di chiusura (indicata alla riga 31) disattiva tale collegamento e non permette più ulteriori operazioni sul file. ........................................................................................................................................... Al momento dell’apertura di un file devono essere specificati due nomi: il nome logico a cui farà riferimento il programma e il nome fisico, che è quello utilizzato dal file system del sistema operativo. Nell’esempio precedente il nome logico del file è dati, mentre il suo nome fisico è dati.txt . Ciò significa che le operazioni sul file dati specificate nel programma vengono fisicamente eseguite sul file dati.txt .
14.5 I/O standard e su memoria di massa Concettualmente, la lettura e la scrittura dei dati in un archivio sono operazioni semplici. Siamo abituati a leggere e a scrivere fin da bambini, e pertanto i meccanismi di astrazione e i processi mentali che mettiamo in atto per leggere e scrivere ci sono naturali. Chiaramente, non è così quando si tratta di insegnare a una macchina a effettuare le stesse operazioni. Come vedremo nel seguito della trattazione, la semplice operazione di lettura presuppone un’organizzazione dei dati all’interno dell’archivio. Per il momento, limitiamoci a considerare le operazioni di lettura e scrittura nella loro forma più generale.
Si dice operazione di lettura (o di input) l’operazione che permette di trasferire un dato o un insieme di dati da una unità periferica nella memoria RAM; si dice operazione di scrittura (o di output) il trasferimento di un dato o di un insieme di dati dalla memoria RAM alla periferica. 257
Sezione 6
- Operare con gli archivi
L’interazione tra utente e calcolatore avviene principalmente attraverso un terminale composto da una tastiera e un video. Queste due periferiche costituiscono le periferiche standard per le operazioni di input e di output: in particolare, la tastiera viene detta unità di standard input , mentre il video è l’unità di standard output . Ciò significa che le istruzioni di lettura e scrittura dei dati possono avere due formati: uno senza indicazione specifica della periferica usata (e in questo caso si intende che vengono utilizzate le unità standard); l’altro con l’indicazione della risorsa (nome file e/o periferica) che viene utilizzata. Per documentare le risorse che vengono impiegate all’interno di un’applicazione informatica si possono usare i simboli descritti nella tabella sottostante. RISORSA
SIMBOLO
NOTE
Video
Vuole rappresentare un tubo catodico visto di profilo.
Tastiera
Memoria centrale
Memoria di massa
Vuole rappresentare un diskpack.
Stampante
Vuole rappresentare un foglio di carta strappato a metà.
Programma
Flusso dei dati
Utilizzando i simboli descritti sopra, lo standard input e lo standard output sono descritti graficamente nello schema sottostante. Output
Input
Le operazioni di scrittura su memoria di massa e di lettura da memoria di massa sono simboleggiate dagli schemi riportati di seguito. 258
Unità didattica 14 - Archivi
Output / scrivi
Input / leggi
Normalmente, nella descrizione delle applicazioni viene indicato anche il programma, mentre il terminale viene rappresentato con il simbolo sottostante.
Se, per esempio, si vuole schematizzare un programma che acquisisce dati da tastiera e li registra su un disco magnetico, ci si dovrà affidare al disegno che segue. Registra.exe
Dati.dat
Dove “Registra.exe” è il nome del programma e “Dati.dat” è il nome del file creato dal programma.
14.6 Tipi di archivio Ora abbiamo una visione più chiara di che cosa sia e di come si possa gestire un archivio. Possiamo quindi concentrarci su come è fatto. Se pensiamo agli esempi riportati in questa Unità didattica, possiamo già capire che gli archivi possono essere di tipo diverso. Il primo tipo di archivio che ci viene in mente, per esempio, è il semplice file di testo. In un file di testo le informazioni vengono salvate usando solamente i caratteri standard ASCII 127 (tipicamente), e di norma costituiscono un file che può essere letto con un qualsiasi editor di testi, come per esempio il BloccoNote di Windows. Anche se questo tipo di archivio può sembrarci banale e di scarsa utilità, non dobbiamo dimenticare che la maggior parte dei programmi esistenti utilizza proprio questo tipo di archivio per i parametri di configurazione. Risulta infatti piuttosto semplice scrivere un programma che si modifica in maniera dinamica leggendo i dati da un file di configurazione ogni volta che viene attivato. In ambiente Windows, i file di configurazione hanno solitamente estensione .ini e si trovano nella directory del sistema. Possono essere divisi per sezioni, ma contengono tutte le informazioni necessarie per configurare un programma. Anche se ultimamente si preferisce usar e il registro di configurazione del sistema al posto dei file di inizializzazione, in alcuni programmi l’elenco dei file utilizzati più di recente si trova nel file di configurazione.
Proseguendo nella lettura degli esempi proposti in questa Unità didattica, possiamo notare come i dati vengano spesso strutturati sotto forma di record. In questo caso, come viene organizzato l’archivio? Una metodologia piuttosto semplice per risolvere i problemi consiste nel rifarsi a un caso precedente di cui si conosce già la soluzione. In questo caso, possiamo rifarci nuovamente al file di testo. Si tratta solamente di decidere come suddividere i dati che fanno parte dei record. Tipicamente, si sceglie un carattere separatore e lo si utilizza per dividere fisicamente i dati. 259
Sezione 6
- Operare con gli archivi
Il formato di file CSV (Comma Separated Values , cioè valori separati da virgola), universalmente adottato, prevede proprio che i campi del record vengano separati dal carattere “,” (virgola).
Quando si sviluppano programmi ad hoc, tuttavia, non sempre si adottano gli standard di sviluppo internazionali, ma si impiega il carattere meno utilizzato dall’applicazione (per esempio “#”). La metodologia appena illustrata non è certamente l’unica quando si vuole gestire un file di record, ma essa offre una certa facilità di implementazione e di gestione. Per questo motivo è largamente utilizzata nelle applicazioni create ad hoc, mentre per le basi di dati commerciali (Oracle, SQLServer, MySQL) si utilizzano altre tecniche più complesse. L’illustrazione di queste ultime esula dagli scopi di questo libro, e pertanto si rimanda il lettore alla letteratura specializzata. Un ultimo metodo per salvare i dati in un archivio consiste nel salvataggio a byte. In questo caso, i singoli record vengono scritti sull’archivio come un flusso continuo. Poiché la quantità di byte occupata dal singolo record e dai campi che lo compongono è nota a priori, l’estrazione dei dati avviene tenendo conto della lunghezza fissa dei record.
14.7 Tipi di accesso È possibile accedere a un file in diversi modi, fra cui i principali sono:
il sequenziale; il diretto.
Si accede a un file in modo sequenziale se tutte le operazioni di lettura e di scrittura possono avvenire solo scorrendo il file dall’inizio fino al punto in cui vogliamo leggere o scrivere i dati.
In altre parole, supponiamo che un database contenga un milione di record, e i l nostro obiettivo sia leggere l’ultimo record contenuto nell’archivio. Non c’è alcun modo per posizionarsi sull’ultimo record se non quello di leggere preventivamente tutti i 999.999 record che lo precedono. Chiaramente, un file ad accesso sequenziale non è molto utile nel caso in cui si vogliano salvare molti dati, a causa della pesantezza nella sua elaborazione (è sempre necessario partire dal primo record e scorrerli tutti). Solitamente, gli archivi di tipo sequenziale sono file di testo dove vengono salvati pochi dati utili per l’esecuzione di un programma. In questo caso, poiché il programma, per funzionare, deve leggere tutte le impostazioni di configurazione, diventa agevole per il programmatore utilizzare un file ad accesso sequenziale, che è molto semplice da gestire. Si accede a un file in modo diretto se tutte le operazioni di lettura e di scrittura possono avvenire in modo puntuale sul singolo record, indipendentemente dalla sua posizione all’interno dell’archivio.
Supponiamo sempre di avere un archivio con un milione di record al suo interno e che il nostro obiettivo sia leggere l’ultimo record. Poiché questa volta l’archivio è ad accesso diretto, possiamo posizionarci subito sull’ultimo record e leggerlo senza prima scorrere tutti gli altri record. Questo tipo di accesso è molto utile nel caso in cui si stia trattando un archivio con dati strutturati organizzati a record, in quanto, a parte l’apertura del file, non c’è alcuna operazione preventiva da compiere, e quindi il tempo di accesso a un singolo dato si abbassa. 260
Unità didattica 14 - Archivi
Esercizi Unità didattica 14 Le parti logiche in cui viene suddiviso un singolo record prendono il nome di ...................................... Elencare le operazioni fondamentali sui file. Nel primo schema della figura che segue la freccia indicare un’operazione di ..................................... nel secondo un’operazione di .........................................................................................................
Indicare i tipi di organizzazione dei file. Completare la definizione: Un ........................... è l’unità logica di memorizzazione dei dati all’interno di un supporto informatico. Per supporto si intende un disco fisso, un floppy, un CD-ROM, un DVD oppure ogni .............................
Completare la definizione: Si può definire ...................................... qualsiasi informazione che si vuole registrare o memorizzare all’interno di un computer.
Completare la definizione: Si dice operazione di .............................. l’operazione che permette di trasferire un dato o un insieme di dati da una unità periferica nella memoria RAM. Si dice operazione di ....................................... il trasferimento di un dato o di un insieme di dati dalla memoria RAM alla periferica.
Che cosa si intende per unità standard di input e per unità standard di output? Leggere il seguente schema:
Registra.exe
Dati.dat
Completare la definizione: Si accede a un file in modo ............................................ se tutte le operazioni di lettura e di scrittura possono avvenire solo scorrendo il file dall’inizio fino al punto in cui vogliamo leggere o scrivere i dati.
Completare la definizione: Si accede a un file in modo ............................................ se tutte le operazioni di lettura e di scrittura possono avvenire in modo puntuale sul singolo r ecord, indipendentemente dalla sua posizione all’interno dell’archivio.
261
Unità didattica
15
File di testo
CHE COSA IMPARERAI A FARE $
Definizione degli elementi fondamentali per la creazione e la lettura di un file di testo e per l’accodamento di una riga
CHE COSA DOVRAI STUDIARE $
Concetto di creazione di un file di testo
$
Descrizione delle istruzioni
$
Concetto di lettura di un file di testo
$
Descrizione delle istruzioni
$
Concetto di accodamento
$
Descrizione delle istruzioni
Unità didattica
15
File di testo
15.1 Creazione di un file di testo Vi sono tre tipi di file: file di input, file di output e file di input/output. Per creare un file di input lo si deve dichiarare come oggetto della classe ifstream ; per creare un file di output lo si deve dichiarare come oggetto della classe ofstream ; i file su cui devono essere eseguite operazioni sia di input che di output devono essere dichiarati come oggetti di classe fstream . Il frammento di codice che segue, per esempio, crea un file di input di nome in, un file di output di nome out e un file su cui è possibile eseguire operazioni sia di input sia di output di nome io. ifstream in; ofstream out; fstream io;
//input //output //input e output
Esempio CreaDati..............................................................................................................
Registrare in un file sequenziale una serie di numeri inseriti da tastiera. Indicazioni per la soluzione
Per ogni dato devono essere eseguite le seguenti operazioni:
leggi un numero da tastiera; scrivi un numero nell’archivio di dati.
Poiché il problema non specifica quanti sono i numeri da inserire, per ogni inserimento si deve ripetere anche la domanda se l’elenco è finito. Si deve, quindi, realizzare una ripetizione che contenga le seguenti istruzioni:
leggi un numero da tastiera; scrivi il numero nell’archivio di dati; chiedi “elenco finito ? (s/n)”; leggi la risposta.
Queste quattro operazioni devono essere eseguite almeno una volta e devono essere ripetute per ogni numero “mentre” l’elenco non è finito, ovvero quando la risposta alla domanda “elenco finito?” è uguale a “n”. Pseudocodifica
INIZIO Apri il file in scrittura SE operazione NON ha avuto successo ALLORA termina programma FINE SE 263
Sezione 6
- Operare con gli archivi
RIPETI chiedi e leggi un numero da tastiera scrivi un numero nell’archivio di dati chiedi “elenco finito ? (s/n)” leggi la risposta MENTRE risposta = “n” Chiudi file FINE Flusso dei dati
CreaDati.exe
dati.txt
Codice
CreaDati.cpp 1
#include
2 3
#include #include
4 5
using namespace std;
6 7
//INIZIO int main ()
8
{
9 10
double numero; string risp;
11 12
//apri file dati in scrittura
13
ofstream dati("dati.txt");
14 15
if(!dati)
16
{
//se l'operazione non ha avuto successo
17
//termina programma
18 19
cout << "ERRORE apertura file"; //fine programma
20 21
cout << "\n\nFine system ("pause");
22 23
";
return 1 ; }
24 25 26
do {
//RIPETI
27
//chiedi e leggi un dato da tastiera
28
cout << "Inserisci un numero: ";
29 30
cin >> numero;
31
//scrivi un dato nell’archivio di dati
32
dati << numero << "\n" ;
33 34
264
//chiedi se elenco finito
Unità didattica 15 - File di testo
35
cout << "Elenco finito? (s/n) ";
36
cin >> risp;
37
}
38 39
while (risp == "n");
//MENTRE elenco NON finito
40 41 42
//chiudi file dati.close();
43 44
//fine programma
45 46
cout << "\n\nFine system ("pause");
47
return 0;
48
";
}
Analisi del codice
Alla riga 2 viene richiamata la libreria , che consente la gestione dei file di testo. Alla riga 13 l’operazione di apertura del file permette di creare il collegamento tra il nome dati utilizzato nel programma e il file fisico dati.txt , utilizzato dal file system. Il file dati è creato come oggetto della classe ofstream , che deriva da fstream e che serve per la gestione dei file in output. Alla riga 15 viene controllato il buon esito dell’operazione di apertura, e in caso negativo il programma viene interrotto. La riga 32 serve per scrivere sul file di dati il numero letto da tastiera. Balza agli occhi l’analogia con le operazioni di scrittura a video: nello specifico, al posto della funzione cout troviamo il nome del file aperto alla riga 13. Prova di esecuzione
Per verificare i buon esito della prova di esecuzione si può leggere il contenuto del file dati.txt , utilizzando il programma Blocco note di Windows.
265
Sezione 6 - Operare con gli archivi
L’operazione di apertura contiene il nome fisico del file dati.txt, ma non è indicato il percorso di tale file. La conseguenza è che il file di testo viene creato all’interno della cartella in cui si trova il programma; se si vuole creare il file in un’altra cartella si deve indicare, oltre al nome, anche il percorso: per esempio “C:\archivi\dati.txt” .
...........................................................................................................................................
15.2 Lettura di un file di testo Esempio LeggiDati ............................................................................................................ Visualizzare il contenuto del file creato dall’esecuzione dell’esempio “CreaDati”.
Indicazioni per la soluzione
Su ogni riga del file dati.txt devono essere eseguite le operazioni che seguono: leggi riga; visualizza riga.
Tali operazioni devono essere ripetute fintantoché non si raggiunge la fine delle righe che compongono il file.
Per segnalare che è stata raggiunta la fine del file si deve utilizzare il metodo eof() (“eof” sta per End of File ). Tale metodo restituisce un valore booleano: true se è stato fatto un tentativo di lettura oltre la fine del file, false altrimenti. Analisi dei dati
Input File dati.txt Output Serie di numeri Flusso dei dati
dati.txt
Pseudocodifica
INIZIO apri il file dati.txt in lettura SE il file non esiste termina il programma leggi un numero dal file FINE SE MENTRE il file non è finito scrivi un numero a video leggi un numero dal file 266
LeggiDati.exe
Unità didattica 15 - File di testo
RIPETI chiudi il file FINE Nella pseudocodifica si nota che la prima operazione di lettura è stata posta al di fuori della struttura di ripetizione MENTRE-RIPETI (si dice che viene effettuata una “lettura fuori giro”), mentre le altre istruzioni di lettura (compresa l’ultima) si trovano all’interno della struttura di ripetizione. La scelta del costrutto MENTRE-RIPETI porta ad eseguire un’operazione di lettura in più, quello del carattere di fine file che fa restituire true al metodo eof(); inoltre, dopo tale lettura non si deve eseguire alcuna elaborazione. La lettura iniziale (fuori giro) è necessaria per fare iniziare il ciclo di ripetizione: infatti, se il file non è vuoto risulta dati.eof() = false. Codice
LeggiDati.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
#include #include #include using namespace std; //INIZIO int main () { double numero; //apri il file in lettura ifstream dati("dati.txt"); if(!dati) //SE il file non esiste { //termina programma cout << "ERRORE apertura file"; //fine programma cout << "\n\nFine "; system ("pause"); return 1 ; } //leggi il primo numero del file dati >> numero; while (!dati.eof()) //MENTRE il file non è finito { //scrivi il numero letto a video cout << numero << "\n"; //leggi un numero dati >> numero; } //RIPETI //chiudi il file dati.close(); //fine programma cout << "\nFine "; system ("pause"); return 0; }
267
Sezione 6
- Operare con gli archivi
Prova di esecuzione
Analisi del codice
L’apertura del file in lettura indicata alla riga 11 utilizza la classe ifstream . Come si vede nella pseudocodifica, l’istruzione di lettura dal file è presente in due punti: alla riga 24 (lettura fuori giro) e alla riga 31 (all’interno del ciclo di ripetizione). In queste due istruzioni si nota come l’operatore >> è utilizzabile per leggere un flusso di dati, proveniente dalla tastiera o da un file di input. ...........................................................................................................................................
15.3 Accodamento Oltre alle istruzioni di apertura dei file viste nei due esempi precedenti (in output o in input), esiste una terza modalità, più accurata delle precedenti, che permette di specificare in maniera più dettagliata il criterio di apertura di un file.
La modalità con cui viene aperto un file può essere specificata mediante i flag di modalità di apertura, i cui valori possono essere scelti tra quelli dell’enumerazione openmode , che è un membro pubblico della classe base degli stream ios_base . Eccoli: ios_base::app: il file viene aperto posizionandosi in fondo in modo permanente,
cioè i nuovi dati vengono inseriti sempre in fondo al file (modalità “Append”); ios_base::ate: il file viene aperto posizionandosi inizialmente in fondo (modalità “At The End”); ios_base::binary : il file viene aperto in modo binario, cioè trattandone il contenuto come puri dati; ios_base::in : il file viene aperto in lettura (modalità “Input”); ios_base::out : il file viene aperto in scrittura (modalità “Output”); ios_base::trunc : il file viene aperto cancellandone l’eventuale contenuto esistente, creandolo se non esiste. Si ricorda che, per default, un file viene aperto posizionandosi sul primo carattere e trattandone il contenuto come testo, producendo una segnalazione di errore nel caso si stia tentando di aprire un file non esistente. La tabella che segue presenta qualche esempio chiarificatore. INVECE DI
268
È POSSIBILE SCRIVERE
ifstream dati(“dati.txt”);
fstream dati; dati.open(“dati.txt”, ios::in);
ofstream dati(“dati.txt”);
fstream dati; dati.open(“dati.txt”, ios::out);
Unità didattica 15 - File di testo
Particolare interesse riveste la modalità ios_open::app , che permette di accodare (append, in inglese) nuovi dati a un file già esistente.
Esempio Accoda................................................................................................................ Aggiungere altri numeri al file dati.txt, senza cancellare i dati preesistenti.
Codice
Accoda.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
#include #include #include using namespace std; //INIZIO int main () { double numero; string risp; //apri file dati in append fstream dati; dati.open(“dati.txt”, ios::out|ios::app); if(!dati) //se l'operazione non ha avuto successo { //termina programma cout << "ERRORE apertura file"; //fine programma cout << "\n\nFine "; system ("pause"); return 1 ; } do {
//RIPETI //chiedi e leggi dato da tastiera cout << "Inserisci un numero: "; cin >> numero; //scrivi dato su archivio dati dati << numero << "\n" ; //chiedi elenco finito cout << "Elenco finito? (s/n) "; cin >> risp;
} while (risp == "n");
//MENTRE elenco NON finito
//chiudi file dati.close(); //fine programma
269
Sezione 6
46 47 48 49
- Operare con gli archivi
cout << "\n\nFine system ("pause"); return 0;
";
}
Prova di esecuzione
Anche in questo caso il buon esito del programma può essere facilmente verificato mediante la lettura dei dati contenuti nel file dati.txt , aprendolo con Blocco note.
Analisi del codice
Rispetto all’esempio creaDati.cpp cambia l’istruzione alla riga 13 che da 13
ifstream dati("dati.txt");
è stata modificata nelle due istruzioni indicate di seguito. 13 14
fstream dati; dati.open(“Dati.txt”, ios::out|ios::app);
Come si vede, la riga 13 definisce l’oggetto dati come di classe fstream , la riga 14 specifica che il file deve essere aperto in scrittura (modalità ios::out ) e con l’obbligo di inserire i nuovi dati in fondo al file (modalità ios::app ). Per l’apertura di un file possono essere specificate diverse modalità, combinate tra di loro dall’operatore di OR bit a bit (indicato nel codice con il simbolo “|”): ogni flag differisce infatti dagli altri per un solo bit. ...........................................................................................................................................
270
Unità didattica 15 - File di testo
Esercizi Unità didattica 15 Le modalità di apertura di un file sono: ............................................................................................ Le modalità di apertura di un file di testo in C++ sono: ..................................................................... L’operazione di apertura di un file in C++ associa al file fisico uno ..................................................... La sintassi di apertura di un file di testo in C++ è: ............................................................................ La sintassi di scrittura di una riga in un file di testo in C++ è: ............................................................ Trascrivere in un file di testo le prime due terzine della
Divina Commedia .
Disegnare lo schema del flusso dei dati del programma dell’esercizio precedente. Creare un file di testo per archiviare una lista di nomi con relativo numero telefonico; il nome e il numero telefonico devono essere separati da un punto e vir gola.
Aggiungere un nuovo nome e relativo numero telefonico al file creato nell’esercizio precedente. Trascrivere in un file di testo i dati di una matrice di tre righe e quattro colonne: ogni riga della matrice occupa una riga del file, e i numeri sono separati da una vir gola.
Che cosa significa eof? ................................................................................................................. A che cosa serve la funzione eof?
Indica che il programma legge anche la fine del file Rileva quando è stata raggiunta la fine di un file Controlla che il file abbia una fine Aggiunge un segnale di fine al file
Che tipo di valore restituisce la funzione
eof?
Intero Singola precisione Booleano Stringa
Quale struttura di controllo contiene l’algoritmo usato per leggere tutte le righe di un file di testo?
Selezione Selezione multipla Ripetizione precondizionale Ripetizione postcondizionale Ripetizione enumerativa
L’istruzione C++ per leggere una riga da un file di testo ha la seguente sintassi: ................................ 271
Sezione 6 - Operare con gli archivi
Esercizi Unità didattica 15 Indicare la sintassi della funzione
eof
in C++.
Trascrivere in un file di testo le prime due terzine della
Divina Commedia .
Successivamente, con un al-
tro programma, visualizzare le terzine.
Disegnare lo schema del flusso dei dati dei programmi dell’esercizio precedente. Creare un file di testo per archiviare una lista di nomi con relativo numero telefonico; il nome e il numero telefonico devono essere separati da un punto e virgola. Success ivamente, con un altro programma, visualizzare il contenuto del file senza mostrare il punto e virgola inserito come separatore.
Creare un file di testo per archiviare una lista di nomi con relativo numero telefonico; il nome e il numero telefonico devono essere separati da un punto e virgola. Success ivamente, con un altro programma, mostrare il numero telefonico di un nome inserito da tastiera.
Creare un file di testo dove sono archiviati i cognomi e i nomi degli studenti di una classe. Success ivamente, mostrare cognome e nome degli studenti (possono essere più di uno) che hanno cognome uguale a un cognome inserito da tastiera.
272
Sezione
7
Le eccezioni
†
Obiettivi
◊
Costruire programmi robusti
◊
Prevenire gli errori di risposta dell’utente
◊
Tenere sotto controllo le anomalie del sistema operativo
&
Questa sezione contiene
U.D.16
Gestione delle eccezioni
Unità didattica
16
Gestione delle eccezioni
CHE COSA IMPARERAI A FARE $ Valutare
gli errori che un’applicazione può generare
$
Scrivere codice per la gestione degli errori
$
Controllare la fine di un programma con anomalie
CHE COSA DOVRAI STUDIARE $
Sintassi per la gestione degli errori
$
Sintassi del costrutto “try..catch”
16
Unità didattica Gestione delle eccezioni
16.1 Concetto di anomalia Questa Unità didattica presenta uno dei meccanismi offerti dal linguaggio C++ per gestire gli errori, le anomalie e ogni altro imprevisto che si può verificare durante l’esecuzione di un programma. Abbiamo visto che durante la compilazione di un programma una delle funzioni del compilatore consiste nel controllare che la sintassi e la grammatica di quanto è stato scritto nel codice siano esatte. Osserviamo cosa succede se proviamo a compilare il codice dell’esempio riportato di seguito. Esempio ErroreDivisione .................................................................................................... ErroreDivisione.cpp 1 2 3 4
#include using namespace std; //INIZIO int main ()
5 6 7 8
{ int dividendo; int divisore;
9 10 11 12
//chiedi e leggi dividendo cout << "Inserisci il valore del dividendo "; cin >> dividendo;
13 14 15 16
//chiedi e leggi divisore cout << "Inserisci il valore del divisore cin >> divisore;
17 18 19 20
//dichiara la variabile per il risultato int divisione;
21 22 23 24 25
divisione=dividendo/divisore;
";
//esegui l’operazione
//scrivi il risultato cout << "\n Divisione = " << divisione << endl;
26 27 28
//fine programma cout << "\nFine "; system ("pause");
29 30
return 0; }
Il compilatore non trova nulla da segnalare, quindi il programma risulta corretto e, se i dati inseriti sono validi, si ottiene quanto riportato nella prova di esecuzione che segue. 275
Sezione 7 - Le eccezioni
Prova di esecuzione
Nel programma, però, non è presente alcun controllo sulla validità dei dati e, se l’utente inserisce 0 come valore per il divisore, succede quanto riportato nella figura che segue.
Come si può vedere, si è verificata un’anomalia che produce un arresto del programma e non permette di procedere nell’esecuzione dell’operazione. ...........................................................................................................................................
Un’anomalia del codice consiste in una situazione ingestibile e rilevabile solo durante l’esecuzione del programma. Le anomalie sono quasi sempre dovute a errori di progettazione o di scrittura del codice: solo la progettazione e la stesura attenta possono evitarci di incorrere in anomalie del codice. Un modo per difendersi dalle anomalie consiste nella gestione degli errori.
Per codice di gestione degli errori si intende quella parte di codice che si occupa di gestire l’eventualità che si verifichino errori a run-time (cioè in fase di esecuzione). Nel caso visto sopra della divisione per zero, la gestione degli errori può essere realizzata modificando il codice ErroreDivisione.cpp come indicato di seguito. 21 22 23
276
if (divisore !=0) { //esegui l’operazione
Unità didattica 16 - Gestione delle eccezioni
24 25 26 27 28
divisione=dividendo/divisore;
}
29 31 32
else { cout << "\n Divisione impossibile " << endl;
33 34 35 36 37
} //fine programma . . . . . .
//scrivi il risultato cout << "\n Divisione = " << divisione << endl;
}
Il fatto che il codice controlli il valore della variabile divisore fa parte di una buona procedura di gestione degli errori, perché previene le anomalie di esecuzione.
16.2 Eccezioni Quanto abbiamo detto fino a questo momento sulla gestione degli errori vale in qualsiasi linguaggio di programmazione. Il fatto che le procedure per la gestione degli errori non siano standardizzate fa sì che ognuno di noi adotti un proprio stile di programmazione, anche se in generale è opportuno seguire uno schema o un’indicazione di massima per la stesura del codice; negli anni, per favorire la creazione di uno standard, è stato introdotto il concetto di “eccezione”.
Un’eccezione è un’anomalia nell’esecuzione del codice e la gestione delle eccezioni consiste in un meccanismo standard definito a livello del linguaggio che permette di controllare e risolvere le anomalie. La gestione delle eccezioni consiste in una sintassi standard che definisce le direttive di massima per il trattamento delle anomalie di esecuzione, lasciando tuttavia il programmatore libero di realizzare il codice per la loro gestione nel modo che ritiene più opportuno. La gestione delle eccezioni non costituisce quindi, di per sé, la gestione degli errori, bensì fornisce un metodo per realizzare quest’ultima.
Quando il codice è in esecuzione e si verifica una anomalia, viene lanciata (throw , in inglese) automaticamente un’eccezione, per gestire la situazione attraverso un codice di controllo che cattura (catch) l’eccezione e che agisce di conseguenza. È anche possibile generare esplicitamente un’eccezione. La prima cosa da definire per gestire le eccezioni è il blocco try.
Un blocco try consiste nella parola chiave try seguita da un blocco di codice racchiuso da parentesi graffe, la cui esecuzione viene controllata dal gestore delle eccezioni. Vediamo come riscrivere il codice dell’esempio precedente, alla luce di quanto ora detto. Per farlo abbiamo bisogno, però, di affrontare il concetto di generazione delle eccezioni. La generazione di un’eccezione avviene attraverso il comando throw , che provoca l’uscita dal blocco di programmazione in cui tale parola chiave si trova e trasferisce il controllo al gestore dell’eccezione. 277
Sezione 7 - Le eccezioni
Il comando throw riceve in input un solo argomento, che può essere di qualsiasi tipo. In base al tipo dell’eccezione generata il gestore sarà diverso. Vediamo un codice d’esempio chiarificatore. Esempio Eccezione ............................................................................................................
Riscrivere il programma “ErroreDivisione” in modo da catturare e gestire l’anomalia “divisione per zero”. Codice Eccezione.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 10 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
278
#include using namespace std; //INIZIO int main () { int dividendo; int divisore; //chiedi e leggi il dividendo cout << "Inserisci il valore del dividendo "; cin >> dividendo; //chiedi e leggi il divisore cout << "Inserisci il valore del divisore cin >> divisore;
";
//dichiara la variabile per il risultato int divisione; try { //controlla divisore if (divisore == 0) { throw 100; } //esegui la divisione divisione = dividendo/divisore; //scrivi il risultato cout << "\n Divisione = " << divisione << endl; } catch (int codice) { cout << "Codice errore = "<< codice << endl; cout << "programma interrotto "; } //fine programma cout << "\n\nFine system ("pause"); return 0; }
";
Unità didattica 16 - Gestione delle eccezioni
Prove di esecuzione
Analisi del codice Dalla riga 20 alla riga 33 si sviluppa il blocco try. La riga 23 controlla se il divisore è uguale a zero e, in caso affermativo, viene subito lanciato il gestore dell’eccezione, attraverso l’istruzione indicata alla riga 25 e, inoltre, le righe da 27 a 32 successive all’istruzione throw vengono ignorate. Se il divisore è diverso da zero l’istruzione di riga 25 non viene eseguita e il programma procede con le istruzioni alle righe 29 e 32, che eseguono il calcolo e la scrittura a video del risultato della divisione. Dalla riga 35 alla riga 39 si sviluppa il blocco catch , la cui intestazione è simile a quella di una funzione. Il parametro codice contiene il codice di errore specificato dopo il comando throw (in questo caso, il valore “100”). Sia nel caso in cui non sia stato riscontrato alcun errore, sia nel caso in cui sia stato eseguito il blocco catch , il controllo passa all’istruzione contenuta nella riga 42. ........................................................................................................................................... All’interno di un blocco try possono essere analizzate diverse situazioni di errore e, quindi, possono essere presenti più istruzioni throw . Per ogni valore associato a throw deve essere poi presente un blocco di istruzioni corrispondente, in grado di gestire l’eccezione generata.
Il gestore dell’eccezione è un blocco di codice che viene eseguito solo quando il blocco try a esso associato genera un’eccezione. Il blocco di gestione delle eccezioni è caratterizzato da una o più parole chiave catch, ciascuna seguita dal proprio blocco di programmazione; ogni parola chiave catch deve, perciò, essere seguita da una coppia di parentesi graffe. Esempio ErroriDiversi..........................................................................................................
È dato un vettore di 10 interi, già presente in memoria. Scrivere un pro gramma che richieda all’utente l’indice di un elemento del vettore e restituisca il logaritmo naturale dell’elemento corrispondente del vettore individuato. Indicazioni per la soluzione Le anomalie che il programma deve individuare sono almeno tre: 1. 2. 3.
l’indice specificato dall’utente è negativo; l’indice è maggiore di 10; il valore dell’elemento corrispondente del vettore è negativo. 279
Sezione 7 - Le eccezioni
Le prime due anomalie sono legate a quanto specifica l’utente, mentre la terza dipende dal contenuto del vettore: in tutti e tre i casi la situazione genera errori di esecuzione, quindi deve essere prodotto un programma che controlli ciò di cui non si conoscono gli effetti. Codice
ErroriDiversi.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
280
#include #include using namespace std; //INIZIO int main () { int indice; //definisce dimensione const int Dim = 10; //definizione e inizializzazione vettore double vett [Dim]={1,-8,-3,7,-6,6,13,-8,9,10}; try { //Chiedi e leggi indice cout << "indica il numero di indice dell’elemento richiesto "; cin >> indice; //controlla indice if (indice < 0) throw 1; // anomalia 1 codice errore di tipo int if (indice > Dim) throw 2; // anomalia 2 codice errore di tipo int //scrivi l’elemento individuato dall’indice cout << "\nElemento di posto " << indice << " = " << vett[indice]; //controlla se elemento è positivo if (vett[indice] <= 0) throw 0.5; // anomalia 3 errore di tipo double //se tutto ok scrive logaritmo cout << "\n\nLogaritmo = " << log(vett[indice]); } catch (int codice) // gestisce codici errore di tipo int { switch (codice) { case 1: cout<< "\nErrore = indice negativo";break; case 2: cout<< "\nErrore = indice fuori limite massimo"; } } catch (double err) // gestisce codici errore di tipo double { cout << "\n\nLogaritmo impossibile "; }
Unità didattica 16 - Gestione delle eccezioni
49 50 51 52 53
//fine programma cout << "\n\nFine system ("pause"); return 0;
";
}
Prove di esecuzione
In questa prima prova il dato inserito è corretto e l’elemento del vettore è positivo, pertanto non si rilevano anomalie e l’esecuzione del programma va a buon fine.
L’utente ha digitato in questo caso un indice negativo.
L’utente ha digitato, questa volta, un indice maggiore di 10.
L’argomento del logaritmo è negativo. Analisi del codice
Alla riga 14 è definito il vettore a cui il programma fa riferimento. Alle righe 19 e 20 viene richiesto l’indice dell’elemento del vettore di cui si vuole calcolare il logaritmo; subito dopo vengono verificate le condizioni di anomalia. 281
Sezione 7 - Le eccezioni
Alla riga 22 viene individuata l’anomalia riguardante l’indice negativo e generato il codice del corrispondente gestore di eccezione. Analogamente alla riga 23: viene individuata l’anomalia riguardante l’indice superiore a 10 e generato il corrispondente codice di gestione dell’eccezione. Da ultimo, prima di calcolare il logaritmo, viene verificato che l’argomento del logaritmo sia un numero positivo; in caso contrario, alla riga 29 viene generato il nuovo codice di eccezione, pari a 0.5. Si noti che i primi due codici delle anomalie sono di tipo intero e il terzo è di tipo double. Alla riga 35 il primo gestore di eccezioni, introdotto dalla parola chiave catch, ha come parametro una variabile di tipo intero, quindi raccoglie le anomalie individuate dai codici 1 e 2. All’interno del blocco di istruzioni compreso tra la riga 36 e la 42 i due valori interi del codice vengono considerati e trattati singolarmente. L’altro gestore di eccezioni si trova alla riga 44, ha come parametro una variabile di tipo double e gestisce l’anomalia di esecuzione segnalata con il codice 0.5. ...........................................................................................................................................
282
Unità didattica 16 - Gestione delle eccezioni
Esercizi Unità didattica 16 Completare:
“Un’anomalia del codice consiste in una .................................................................. e rilevabile solo durante l’esecuzione del programma”. Completare:
“Le anomalie sono quasi sempre dovute a ....................................................... Solo la progettazione e la stesura attenta possono evitarci di incorrere in anomalie del codice. Un modo per difendersi dalle anomalie consiste nella gestione degli errori”. Completare:
“La gestione degli errori è quella parte di codice che gestisce l’eventualità di un errore a .................... ..................................................................................................................................................”. Completare:
“In C++ si attiva un’eccezione con la parola chiave ............................................................ si cattura l’eccezione con la parola chiave ...................................................................................................”. Completare:
“La parola chiave try è seguita da ..............................................................................................”. Scrivere gli elementi della successione S = 2i con i = 1, 2, ....... n. Catturare l’eccezione i > 30. Se S è
definito intero, quali valori assume S per i > 30? Dati in input due numeri
e y, calcolare pow(x,y) . x e y siano definiti double. Catturare le eccezioni che si generano quando non vengono rispettate le seguenti condizioni:
x
se x > 0, y può essere un valore qualsiasi; se x = 0, y deve essere > 0; se x < 0, y deve essere intero.
Calcolare log(x), con x dato in input. Controllare che x sia > 0 e gestire l’eccezione. Calcolare sqrt(x), con x dato in input. Controllare che Dati in input due interi
a
Dati in input due reali
a
x
sia > = 0 e gestire l’eccezione.
e b, calcolare a*b. Gestire la situazione anomala che si genera quando a*b risulta maggiore del massimo consentito per gli interi. e b, calcolare a*b. Gestire la situazione anomala che si genera quando sulta esterno agli inter valli previsti per i dati di tipo double.
a*b
ri-
Dati in input due reali a e b, calcolare il rapporto a/b. Gestire in modo distinto le due situazioni anomale a!=0 , b=0
e a=0, b=0.
È dato un vettore di 10 interi già definito all’interno del programma. Viene richiesto all’utente l’indice del-
l’elemento desiderato e il programma restituisce l’intero corrispondente presente nel vettore. Catturare l’eccezione “out of range”.
283
A
Appendice Riepilogo degli operatori z
= x - y ;
significa
z = (x - y) ; perché – ha precedenza rispetto a =. Gli operatori di prefìsso unario e quelli di assegnazione associano da destra a sinistra. Tutti gli altri operatori associano da sinistra a destra. Per esempio:
x - y - z significa
(x - y) - z perché l’operatore – associa da sinistra a destra, mentre
x = y
=
z
significa
x = (y = z) perché l’operatore = associa da destra a sinistra. OPERATORE
DESCRIZIONE
OPERATORE
DESCRIZIONE
:: . -> [] () ++ — ! ~ + (unario) - (unario) * (unario) & (unario) new delete sizeof
Risoluzione della classe Accesso a membro Deriferimento e accesso a membro
Divisione o divisione tra interi Resto intero della divisione Addizione
(tipo) .* ->*
Cast Accesso al puntatore a membro Deriferimento e accesso a puntatore a membro Moltiplicazione
/ % + << >> < <= > >= == != & ^ | && || ? : = += -= *= /= %= &= |= ^= ,
*
Vettore o indice di array Chiamata di funzione Incremento Decremento NOT booleano NOT bitwise Positivo Negativo Deriferimento di puntatore Indirizzo di variabile Allocazione dell’heap Rilascio dell’heap Dimensione di una variabile o tipo
Sottrazione Output (o shift bitwise) Input (o shift bitwise) Minore di Minore di o uguale a Maggiore di Maggiore di o uguale a Uguale a Diverso da AND di cortocircuito XOR di cortocircuito OR di cortocircuito AND booleano OR booleano Alternativa Assegnazione Operatori combinati con assegnazione Sequenza di espressioni 285
B
Appendice Sequenze di caratteri escape Queste sequenze di escape possono essere utilizzate nelle stringhe (per esempio “\n”) e nei tipi char (per esempio ‘\‘‘ ). SEQUENZA DI ESCAPE
286
DESCRIZIONE
\n
Nuova riga
\r
Invio a capo
\t
Tabulazione
\v
Tabulazione ver ticale
\b
Backspace
\f
Nuova pagina (formfeed)
\a
Avviso
\\
Barra retroversa
\”
Virgolette doppie
\’
Virgolette singole
\?
Punto interrogativo
\xh1h2
Carattere specificato in forma esadecimale
\oo1o2
Carattere specificato in forma ottale
Indice analitico
A accesso ai file 260 ai membri di una classe 202 algoritmo 92, 94-96 allocare 42 analisi del problema 91 anomalia 276 append 269 archivio 253-254 argomenti 96
B base di un sistema di numerazione 9 bit 11 blocco try 277 byte 22 booleani 42, 48
C campi 255 caratteri di escape 62 caratteristica 29 case sensitive 42 casting 48 esplicito 46 cicli 128 classe 215 base 240 derivata 240 di problemi 94 metodi 216 codice ASCII 22-24 del calcolatore 22 di gestione degli errori 276 macchina 22 numerico decimale 21 UNICODE 24, 45 codifica 21 comando 96 commenti 38 compilatore 36
compilazione 36 errori di 37 complemento a due 25-28 concatenazione di stringhe 197 confronto lessicografico di stringhe 201 conversione dalla base 2 alla base 10 12 dalla base 10 alla base 2 12 costanti 49, 96 letterali 99 costruttore 204, 224 CSV (Comma Separated Values ) 260
D dati 96, 254 -255 di input 95 di output 95 membro 216 definizione circolare 161 dichiarazione di variabili 40 dimensione di un vettore 171 distruttore 225
E eccezioni 277-282 cattura delle 277 editor di testi 35 emissione (output) 95 entry point 145 eof() 266 ereditarietà 213-214
F file 253-254 d’intestazione 36 eseguibile 36 sorgente 36 FINE (termine di un algoritmo) 108 flag di modalità di apertura di un file 268 forma normalizzata di un numero 29
funzione 144 membro 216
G gestione delle eccezioni 277 gestore dell’eccezione 279
I immissione (input) 95 incapsulazione 211 indice di un vettore 171 inizializzazione di una variabile 98 INIZIO (di un algoritmo) 108 input 95, 99, 257 istanza di una classe 218 istruzioni 93, 96 catch 277 cin 55 class 218 di assegnamento 97 di lettura (di input) 99 di scrittura (di output) 99 di ripetizione, o iterative 128-138 for 136 if 113-114 while 128-130
L librerie 36 linker 36 lunghezza di una stringa 195-197
M mantissa 29 matrici 180-187 membri di una classe 216 MENTRE ... FINE MENTRE 108 metodo della virgola mobile 29
N notazione puntata 202
287