Sei sulla pagina 1di 240

p001-004_CeC_ 26-09-2007 16:06 Pagina 1

Piero Gallo Fabio Salerno

Il linguaggio
C++
Dalla programmazione imperativa
alla programmazione a oggetti

Minerva Italica
p001-004_CeC_ 26-09-2007 16:06 Pagina 2

© 2004 by Minerva Italica, Milano


Edumond Le Monnier S.p.A. Il Sistema Qualità di Edumond Le Monnier S.p.A.
Tutti i diritti riservati è certificato da BVQI
secondo la Norma UNI EN ISO 9001:2000
www.pianetascuola.it (Vision 2000) per le attività di:
www.minervaitalica.it progettazione, realizzazione
e commercializzazione di testi scolastici,
Prima edizione: gennaio 2004 dizionari e supporti.

ISBN 88-298-2328-7

Edizioni:
2 3 4 5 6 7 8 9 10
2004 2005 2006 2007 2008

Questo volume è stato stampato presso


Cartoedit - Città di Castello (Pg)

Stampato in Italia - Printed in Italy

Supervisione: Francarturo Bertolotti

Progetto grafico e copertina: Silvano Colombo


Redazione: Lorenzo Plebani, Giovanni Malafarina
Coordinamento tecnico: Daniele Sculati
p001-004_CeC_ 26-09-2007 16:06 Pagina 3

Presentazione

Il volume descrive il linguaggio di programmazione C++ partendo dal paradigma


imperativo e introduce gradualmente il paradigma ad oggetti. La proverbiale effica-
cia ed efficienza del C++ procedurale viene così ad integrarsi e ad estendersi agli
oggetti del C++. In un unico volume, in definitiva, si ottiene un risultato dalla doppia
valenza: trattare due paradigmi distinti e allo stesso tempo impostare un comune
metodo didattico, grazie al quale il lettore, al termine del percorso formativo, potrà
affrontare lo studio di un qualsiasi progetto reale applicativo secondo i canoni di uno
dei due paradigmi trattati, oppure secondo un canone ibrido.
Il C++ procedurale, diffusissimo e utilizzato nei più disparati contesti, viene così
integrato e arricchito delle funzionalità tipiche del mondo degli oggetti, consentendo
al lettore di avvicinarsi in modo graduale a concetti, anche complessi, tipici del
mondo della programmazione ad oggetti, quali incapsulamento, polimorfismo, eredi-
tarietà. La grande duttilità e portabilità di questi due paradigmi consente inoltre di
poter scrivere applicazioni per diverse piattaforme hardware-software e routine dalla
eccezionale efficienza esecutiva.
L’impianto didattico si basa sull’offerta di esempi dalla complessità crescente.
Essi permettono di acquisire quelle conoscenze di base sulle strutture di controllo e
sulle strutture dati comuni ai due linguaggi, per poi consentire di spaziare su argo-
menti quali i famosi puntatori e le classi di oggetti, indispensabili per produrre pro-
grammi di livello professionale.

Le caratteristiche didattiche del volume


La metodologia adottata nel volume fa sì che lo studente veda immediatamente i
risultati dei propri sforzi, trovando motivazione nello studio e, contemporaneamen-
te, familiarizzi, quasi senza accorgersene, con concetti tipici dei linguaggi di pro-
grammazione.
Il volume è articolato in moduli, ciascuno preceduto dalla dichiarazione dei prere-
quisiti, degli obiettivi, delle conoscenze da apprendere e delle competenze da acquisi-
re. Risulta pertanto facile per il docente programmare l’insegnamento e per l’alunno
progettare l’apprendimento.
I singoli moduli sono strutturati in unità didattiche, anch’esse precedute dalla
dichiarazione dei prerequisiti, degli obiettivi, delle conoscenze da apprendere e delle
competenze da acquisire. Le singole unità sono chiuse da verifiche finali, sia di tipo
cognitivo (Verifica delle conoscenze) sia di tipo operativo (Verifica delle competenze),
molte delle quali contengono anche numerosi esercizi svolti, quasi sempre forniti di
una breve analisi del processo risolutivo.

Il sito Web
Per offrire uno strumento didattico sempre aggiornato e vivo e che risponda alle
nuove esigenze formative richieste dal mondo del lavoro, abbiamo pensato che il
volume dovesse essere strutturato in modo che la parte cartacea fosse solo una com-
ponente. Il volume si completa, infatti, con un sito Web. Tra i due esiste una stretta
relazione. Scopo del sito è da un lato quello di sostituire la guida del docente, dal-
l’altro – e soprattutto – quello di costituire un punto di riferimento sia per il docente

3
p001-004_CeC_ 26-09-2007 16:06 Pagina 4

sia per il discente. Anche gli alunni, infatti, possono, collegandosi al sito, fornire mate-
riale didattico, spunti, osservazioni ed anche formulare proposte innovative, pro-
porre nuove tematiche di studio, ecc.
Il sito Web è raggiungibile all’indirizzo:
www.minervaitalica.it/laboratorio_informatica/
Il sito è relativo a tutti i tomi della collana “Laboratorio di informatica”. Nella sezio-
ne appositamente dedicata al presente volume, esso offre:
• un’area che consente il download dei codici presenti nel volume. Per ogni unità
didattica, infatti, i codici relativi agli esercizi sviluppati possono essere scaricati in
vari formati (.zip, .html, .pdf, ecc.), selezionando la sigla ad essi corrispondente,
appositamente indicata nelle pagine del volume (per mezzo di rimandi, come quel-
lo mostrato nell’esempio seguente: “vedi sito: CPP.A.02.C”);
• un’area che offre ulteriori esercizi svolti rispetto a quelli riportati nel testo,
anch’essi scaricabili in vari formati;
• link utili, che rimandano a siti Web significativi, relativi ai linguaggi trattati nel
volume;
• un’area dedicata a appendici e a sezioni di approfondimento e di aggiornamento;
• un’area dedicata alla guida del docente, che offre percorsi didattico-formativi e
proposte di prove di verifica (pratiche, aperte, semistrutturate, strutturate).

Il sito sarà aggiornato periodicamente: potrà, pertanto, prevedere nuove aree qui
non menzionate.

Ringraziamenti
I meriti del lavoro devono essere condivisi tra tutti coloro che ci hanno aiutato,
consigliato, o dei quali abbiamo consultato i testi. In modo particolare, gli autori rin-
graziano il prof. Ilario Pagliara, docente universitario di Fondamenti di Informatica,
che ha dato un valore aggiunto a molte unità e ha collaborato all’impostazione strut-
turale e contenutistica dell’opera, e la prof.ssa Marialina Pietramala, ordinario di
Informatica industriale, per l’attento e puntuale lavoro di rilettura critica delle bozze
e per i preziosi suggerimenti.
Gli autori

4
p005-008_CeC 26-09-2007 16:07 Pagina 5

Indice generale

MODULO A: Fondamenti del linguaggio C++

Prerequisiti 9 6.2 Le costanti 24


Obiettivi 9 6.2.1 Le costanti numeriche intere 24
Conoscenze da apprendere 9 6.2.2 Le costanti numeriche decimali
Competenze da acquisire 9 o in virgola mobile 24
6.2.3 I caratteri 25
6.2.4 I caratteri speciali 25
UNITÀ DIDATTICA A1 Introduzione ai linguaggi C e C++ 10
6.2.5 Le costanti stringa 26
Prerequisiti 10 6.3 Come dichiarare variabili e costanti 26
Obiettivi 10 6.4 I tipi di dato fondamentali 27
Conoscenze da apprendere 10 6.4.1 Il tipo int 28
Competenze da acquisire 10 6.4.2 Il tipo char 28
6.4.3 I tipi float e double 29
1 Generalità sul linguaggio C 10 6.4.4 Il tipo bool 30
2 Le principali caratteristiche del linguaggio C 11 7 Gli operatori 30
3 Il linguaggio C++ 11 7.1 L’operatore di assegnamento 30
4 I miglioramenti rispetto al C 12 7.2 Gli operatori aritmetici 31
5 Descrizione sintattica dei linguaggi di programmazione 12 7.3 Gli operatori relazionali 32
6 Gli elementi di un programma C++ 13 7.4 Gli operatori logici 32
6.1 Il programma principale (funzione main) 14 7.5 Gli operatori sui bit 33
6.2 La preelaborazione del codice sorgente: 7.6 Un operatore particolare: l’operatore sizeof 35
le direttive al preprocessore 15 7.7 Gli operatori speciali , e ? 36
6.3 Le funzioni per la gestione dell’input/output 16 8 La conversione di tipo 36
7 Realizziamo il nostro primo programma C++ 16 8.1 La conversione implicita 36
8 Dal codice sorgente al codice eseguibile 17 8.1.1 Conversione tra tipi numerici interi 36
9 Come creare e compilare un progetto in C++ 17 8.1.2 Conversione tra tipi numerici in virgola
mobile 37
Verifica di fine unità 20 8.2 La conversione esplicita 37
Verifica delle conoscenze 20 8.2.1 Il casting esplicito 37
Verifica delle competenze 20 8.2.2 Metodi di conversione 38

Verifica di fine unità 39


UNITÀ DIDATTICA A2 Elementi lessicali ed espressioni 21 Verifica delle conoscenze 39
Verifica delle competenze 39
Prerequisiti 21
Obiettivi 21
Conoscenze da apprendere 21 UNITÀ DIDATTICA A3 Istruzioni e strutture di controllo 41
Competenze da acquisire 21
Prerequisiti 41
Obiettivi 41
1 L’alfabeto del linguaggio C++ 21
Conoscenze da apprendere 41
2 Le regole lessicali 22
Competenze da acquisire 41
3 I commenti 22
4 Gli identificatori 23 1 Le istruzioni di input/output 41
5 Le espressioni 23 2 Le istruzioni di salto 42
6 Gli operandi 24 3 Le istruzioni condizionali (o di selezione) 43
6.1 Variabili e costanti 24 3.1 L’istruzione if..else 43

5
p005-008_CeC 26-09-2007 16:07 Pagina 6

3.2 L’istruzione switch 45 10.1 Variabili locali dichiarate static 70


4 Le istruzioni iterative 47 10.2 Variabili globali dichiarate static 71
4.1 I costrutti while e do..while 47 11 Le variabili automatic e register 72
4.2 Il costrutto for 49
4.3 Un esercizio con i cicli while e for: Verifica di fine unità 73
la successione di Fibonacci 50 Verifica delle conoscenze 73
4.4 Le istruzioni break e continue 51 Verifica delle competenze 73
4.5 Le iterazioni infinite 52 Esercizio svolto 74

Verifica di fine unità 53


Verifica delle conoscenze 53 UNITÀ DIDATTICA A5 Tipi di dato strutturati 75
Verifica delle competenze 53
Prerequisiti 75
Obiettivi 75
Conoscenze da apprendere 75
UNITÀ DIDATTICA A4 Funzioni, moduli software
Competenze da acquisire 75
e file header 56

Prerequisiti 56 1 Gli array 75


Obiettivi 56 2 La dichiarazione di un vettore 76
Conoscenze da apprendere 56 3 L’inizializzazione di un vettore 76
Competenze da acquisire 56 4 Come passare un vettore a una funzione 78
5 Le stringhe 79
1 Tipi di sottoprogrammi: procedure e funzioni 56 6 Le funzioni che operano sulle stringhe 82
2 Costruzione e uso di una funzione 57 7 Gli array bidimensionali e multidimensionali 83
3 Le regole di visibilità: variabili locali e globali 58 8 Le strutture 85
4 Il passaggio dei parametri 60 9 Gli array di strutture: le tabelle 87
5 Moduli software, linkage e variabili extern 62 10 Le unioni 88
6 File header e information hiding 65 11 Le enumerazioni 89
7 Alcuni importanti file header 67
8 La direttiva #define 67 Verifica di fine unità 90
9 Librerie di funzioni e compilazione condizionata 68 Verifica delle conoscenze 90
10 Le variabili static 70 Verifica delle competenze 90

MODULO B: Allocazione dinamica della memoria

Prerequisiti 93 Verifica delle competenze 105


Obiettivi 93 Esercizio svolto 105
Conoscenze da apprendere 93
Competenze da acquisire 93

UNITÀ DIDATTICA B1 Puntatori e reference 94


UNITÀ DIDATTICA B2 Allocazione e deallocazione
Prerequisiti 94 dinamica della memoria 106
Obiettivi 94
Prerequisiti 106
Conoscenze da apprendere 94
Obiettivi 106
Competenze da acquisire 94
Conoscenze da apprendere 106
Competenze da acquisire 106
1 I puntatori 94
2 L’algebra dei puntatori 96 1 Stack e heap 106
3 Puntatori e passaggio di parametri 98 2 L’allocazione dinamica della memoria 107
4 Puntatori e array 99 3 La deallocazione dinamica 108
5 Puntatori e stringhe 101 4 Un esempio di allocazione dinamica: il tipo di dato
6 Puntatori e strutture 102 astratto Pila 110
7 Gli argomenti della funzione main( ) 103
Verifica di fine unità 112
Verifica di fine unità 105 Verifica delle conoscenze 112
Verifica delle conoscenze 105 Verifica delle competenze 112

6
p005-008_CeC 26-09-2007 16:07 Pagina 7

MODULO C: C++ e gli oggetti

Prerequisiti 113 3 Assegnazione di oggetti di una stessa classe 142


Obiettivi 113 4 Puntatori a oggetti e passaggio di oggetti come
Conoscenze da apprendere 113 parametri 143
Competenze da acquisire 113 4.1 Passaggio per valore 144
4.2 Passaggio per indirizzo tramite puntatore 145
4.3 Passaggio per reference di un oggetto 145
UNITÀ DIDATTICA C1 Iniziamo a lavorare con gli oggetti 114
5 L’operatore delete 146
Prerequisiti 114 6 Il puntatore all’oggetto corrente: this 146
Obiettivi 114 7 La classe Pila 147
Conoscenze da apprendere 114 7.1 Le specifiche della classe Pila 147
Competenze da acquisire 114 7.2 Implementazione della classe Pila con
1 Classi di oggetti 114 allocazione dinamica 147
2 Dichiarazione e implementazione delle funzioni 8 Classi nello stesso file o in file separati 149
membro di una classe 116 9 Variabili e funzioni membro di classe 150
3 Visibilità di variabili e funzioni membro: 10 Funzioni amiche di una classe 153
incapsulamento e information hiding 118 10.1 Funzioni amiche di più classi 154
4 Variabili locali e variabili membro 121 11 Classi amiche 155
5 Relazione tra classi, struct e union 121
6 Creazione di oggetti, funzioni costruttore e distruttore 122 Verifica di fine unità 156
6.1 La dichiarazione di un oggetto 122 Verifica delle conoscenze 156
6.2 Allocazione con chiamata esplicita Verifica delle competenze 156
del costruttore 122 Esercizio svolto 156
6.3 Dichiarazione e allocazione con chiamata
esplicita al costruttore 123
UNITÀ DIDATTICA C3 Ereditarietà e polimorfismo 157
6.4 Dichiarazione di più funzioni costruttore 123
6.5 Forma abbreviata di istanziazione 126 Prerequisiti 157
7 Funzioni membro distruttore 126 Obiettivi 157
8 Ciclo di vita di un oggetto 126 Conoscenze da apprendere 157
9 Interazione tra oggetti 127
9.1 Invocazione di una funzione membro 127 1 Ereditarietà, classi e sottoclassi 157
9.2 Accesso alle variabili membro 127 2 Ereditarietà singola 158
10 Creiamo un’applicazione completa: la classe 2.1 Un altro esempio di ereditarietà 162
Automobile 128 2.2 Costruttori, distruttori ed ereditarietà 162
11 Passaggio di oggetti come parametri e valori 2.3 Costruttori delle sottoclassi con parametri 163
di ritorno 130 2.4 Chiamata della funzione membro
12 Un confronto tra la programmazione imperativa della superclasse 165
e quella ad oggetti 133 3 Funzioni virtuali 165
12.1 Analisi e progettazione a oggetti per il nostro 4 Funzioni e classi astratte 166
esempio 133 4.1 Un esempio di classi astratte 166
12.2 Confronto del codice a oggetti con quello 5 Polimorfismo 168
imperativo 135 5.1 In che cosa consiste il binding dinamico? 168
6 Ereditarietà multipla 171
Verifica di fine unità 137 6.1 Il problema dell’ambiguità nell’ereditarietà
Verifica delle conoscenze 137 multipla 172
Verifica delle competenze 137 6.2 Un’altra ambiguità risolta a tempo
Esercizio svolto 138 di compilazione 174

UNITÀ DIDATTICA C2 Allocazione dinamica, Verifica di fine unità 175


oggetti e puntatori 139 Verifica delle conoscenze 175
Verifica delle competenze 175
Prerequisiti 139 Esercizi svolti 175
Obiettivi 139
Conoscenze da apprendere 139
Competenze da acquisire 139 UNITÀ DIDATTICA C4 Overloading di operatori e classi
generiche 177
1 Allocazione dinamica di oggetti in memoria 139
1.1 L’operatore new 140 Prerequisiti 177
1.2 Accesso alle variabili e alle funzioni membro 141 Obiettivi 177
2 Puntatori e oggetti 141 Conoscenze da apprendere 177

7
p005-008_CeC 26-09-2007 16:07 Pagina 8

1 Overloading di operatori 177 1 Gli stream 189


1.1 Definire una nuova operazione: le funzioni 2 Stream e file 190
operatore 178 3 Le classi per gestire i file 191
1.2 Overloading di operatori binari 178 4 Flussi sequenziali 193
1.3 Overloading di operatori unari prefissi 180 4.1 Leggere e scrivere flussi sequenziali di byte 193
1.4 Overloading di operatori unari postfissi 181 4.2 Leggere e scrivere flussi sequenziali di oggetti 194
1.5 Overloading dell’operatore di assegnazione 181 4.3 Leggere e scrivere flussi sequenziali di testo 196
1.6 Overloading di operatori relazionali 183 5 Manipolatori per la formattazione dell’output 198
2 Funzioni e classi generiche 184 6 Interazione con i database 198
2.1 Funzioni generiche 184 6.1 Basi di dati, DBMS e linguaggi SQL 198
2.2 Classi generiche 185 6.2 ODBC e ADO 199
6.3 Esempio di visualizzazione dei dati
Verifica di fine unità 188 di una tabella mediante ODBC 201
Verifica delle conoscenze 188 6.4 Esempio di inserimento e modifica dei dati
Verifica delle competenze 188 di una tabella mediante ODBC 205

UNITÀ DIDATTICA C5 Flussi e database 189 Verifica di fine unità 206


Verifica delle conoscenze 206
Prerequisiti 189 Verifica delle competenze 206
Obiettivi 189
Conoscenze da apprendere 189

MODULO D: Ambiente di programmazione visuale per C++

Prerequisiti 207 1 Creare la prima applicazione visuale:


Obiettivi 207 uso di pulsanti e caselle di testo 218
Conoscenze da apprendere 207 1.1 Creare un nuovo progetto 219
Competenze da acquisire 207 1.2 Creare la struttura dell’applicazione visuale
con AppWizard 219
1.3 La prima compilazione e il primo linking
UNITÀ DIDATTICA D1 Interfaccia grafica ed eventi
dei file dell’applicazione 220
in Visual C++ 208
1.4 La prima esecuzione dell’applicazione 220
Prerequisiti 208 1.5 Modificare l’interfaccia dell’applicazione 221
Obiettivi 208 1.5.1 Aggiungere una casella di testo 221
Conoscenze da apprendere 208 1.5.2 Aggiungere un pulsante 222
Competenze da acquisire 208 1.6 Gestire l’evento clic sul pulsante 222
1.7 Verifica dell’applicazione appena modificata 224
1 La Microsoft Foundation Class 208 1.8 Salvare e chiudere il progetto 225
2 La programmazione visuale 210 2 Utilizziamo le finestre di dialogo predefinite 225
3 L’ambiente Visual C++ 210 3 Utilizzare GUI e classi definite dall’utente 226
4 I modelli di una applicazione 211 3.1 Includere classi definite dall’utente 227
4.1 Classi e oggetti grafici 212 3.2 Associamo l’evento al pulsante 227
4.2 I controlli 212 4 Utilizzare caselle di controllo e pulsanti di opzione 228
5 Gestione degli eventi in Visual C++ 214 5 Finestre definite dal programmatore e caselle
5.1 Un modello di gestione degli eventi 214 combinate 229
5.2 Come creare un gestore per un evento 215 6 Finestre di uso comune 231
5.3 Raggruppare gli eventi 216 7 Lavoriamo con i menu 231
7.1 Creiamo un’applicazione con menu e finestre 232
Verifica di fine unità 217
7.2 Colleghiamo il menu alla funzione di gestione 234
Verifica delle conoscenze 217
8 Strumenti visuali per interagire con i database 235
Verifica delle competenze 217
Verifica di fine unità 238
Verifica delle conoscenze 238
UNITÀ DIDATTICA D2 Creare applicazioni visuali Verifica delle competenze 238
in Visual C++ 218

Prerequisiti 218
Obiettivi 218
Conoscenze da apprendere 218
Competenze da acquisire 218 INDICE ANALITICO 239

8
p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 9

Fondamenti Modulo A
del
linguaggio C++
• U.D. A1.
Introduzione ai linguaggi C e C++
• U.D. A2.
Elementi lessicali ed espressioni
• U.D. A3.
Istruzioni e strutture di controllo
• U.D. A4.
Funzioni, moduli software
e file header
• U.D. A5. Tipi di dato strutturati

Prerequisiti
● Variabili e costanti
● Algoritmi
● Costrutti fondamentali della programmazione strutturata
● Metodologia top-down
● Caratteristiche dei linguaggi compilati

Obiettivi
● Costruire i primi programmi C++ privilegiando la fase di analisi e la metodologia implementativa
top-down
● Comprendere le peculiarità dei tipi di dato e, in particolare, dei dati strutturati
● Riuscire a scrivere correttamente le istruzioni C++ rispettando la struttura del programma

Conoscenze da apprendere
● Conoscere la struttura di un programma C++
● Conoscere i tipi di dato utilizzati dal linguaggio C++
● Conoscere i costrutti fondamentali

Competenze da acquisire
● Saper classificare le istruzioni del linguaggio C++
● Saper implementare correttamente i costrutti fondamentali della programmazione strutturata
● Saper realizzare semplici programmi C++
● Riuscire a sviluppare software che manipoli dati strutturati

9
p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 10

Unità didattica

A1 Introduzione
ai linguaggi C e C++

Prerequisiti ● Variabili e costanti


● Algoritmi

Obiettivi ● Familiarizzare con le istruzioni elementari del linguaggio C++


● Comprendere le caratteristiche di base dei linguaggi di medio-basso livello
● Compilare ed eseguire un programma C++

Conoscenze ● Conoscere le caratteristiche generali dei linguaggi C e C++


da apprendere ● Conoscere la struttura di un programma C++
● Conoscere gli strumenti per iniziare il lavoro di programmazione
● Conoscere la sintassi delle prime istruzioni C++ e il metalinguaggio utilizzato per
la loro rappresentazione

Competenze ● Saper realizzare semplici programmi C++


da acquisire ● Saper utilizzare un ambiente di sviluppo integrato
● Saper utilizzare le prime istruzioni di output

1 Generalità sul linguaggio C


Il C è un linguaggio di programmazione procedurale di alto livello. Progettato e rea-
lizzato da Dennis Ritchie nel 1972, fu sviluppato presso gli AT&T Bell Laboratories
allo scopo di ottenere un linguaggio di alto livello per l’implementazione dei sistemi
operativi.
Il linguaggio C fu utilizzato per la prima volta in modo estensivo per la riscrittura
del codice del sistema operativo UNIX dal linguaggio Assembler; inevitabilmente,
molte delle sue caratteristiche derivano dalle specifiche necessità legate alla realiz-
zazione di un sistema operativo in generale, e di UNIX in particolare. Benché stretta-
mente legato a quest’ultimo sistema, il C può essere impiegato con i principali siste-
mi operativi oggi disponibili.
Steve Johnson, nella metà degli anni Settanta, scrisse un compilatore C trasportabi-
le su sistemi diversi dal PDP-11: da allora il C cominciò a essere utilizzato come lin-
guaggio in altri sistemi operativi, come MS-DOS e CPM/80.
Nel 1978 fu pubblicato The C Programming Language di Brian Kernighan e Dennis
Ritchie (K&R): la pubblicazione servì da definizione ufficiale del linguaggio, anche se
già dalla distribuzione della versione V7 erano presenti alcune caratteristiche assen-
ti in K&R.
Nella metà degli anni Ottanta il comitato X3JI1 dell’ANSI (American National Stan-
dards Institute) sviluppò uno standard per il linguaggio C che aggiungeva importanti
elementi e ufficializzava molte caratteristiche presenti nei diversi compilatori realiz-

10 Modulo A: Fondamenti del linguaggio C++


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 11

zati successivamente alla pubblicazione di K&R. Da allora l’ANSI C cominciò a esse-


re considerata la versione “ufficiale” del linguaggio in sostituzione a quella di K&R.

2 Le principali caratteristiche del linguaggio C


Le caratteristiche peculiari del linguaggio C possono essere riassunte nel seguen-
te elenco.
• È un linguaggio general purpose, ossia può essere impiegato per codificare pro-
getti software di natura diversa, da quelli real time a quelli che operano su basi di
dati, da quelli tecnico-scientifici sino alla realizzazione di moduli per il sistema ope-
rativo.
• È un linguaggio strutturato di tipo imperativo (o procedurale).
• È un linguaggio case sensitive, ossia opera una distinzione tra lettere minuscole e
lettere maiuscole. Ad esempio, la variabile PIPPO è diversa dalle variabili Pippo e
pippo.
• È un linguaggio compilato. Il problema dei linguaggi imperativi compilati è che so-
no legati alla piattaforma su cui vengono compilati: se compiliamo un programma
sotto UNIX, quel programma potrà essere eseguito esclusivamente su sistemi ope-
rativi UNIX.
• Nella versione ANSI, è facilmente portabile e perciò multipiattaforma; può infatti
essere compilato su un’ampia gamma di computer.
• Pur essendo un linguaggio di alto livello (in quanto possiede strutture ad alto livel-
lo) e nonostante possa definirsi potente ed efficiente, il C è un linguaggio relativa-
mente di basso livello, particolarmente per quanto riguarda la rappresentazione
dei dati.
• Produce programmi efficienti. Essendo nato per implementare sistemi operativi, il
C evita anche, ove possibile, qualsiasi operazione che possa determinare un deca-
dimento del livello di efficienza del programma eseguibile.

3 Il linguaggio C++
Intorno alla prima metà degli anni Ottanta fu sviluppata un’estensione del linguag-
gio C chiamata C++. Tale potenziamento si pone come sovrainsieme del linguaggio C
mantenendone le caratteristiche fondamentali e aggiungendovi concetti propri dei
linguaggi object oriented. Ufficialmente, il C++ nasce nel 1983 a opera del danese
Bjarne Stroustrup, che nel 1979 viene in contatto con gli ideatori del C e di UNIX nei
laboratori della AT&T Bell, presso i quali rimane in seguito come ricercatore.
Piuttosto che sviluppare un nuovo linguaggio, Stroustrup scelse la strada di man-
tenere il C come linguaggio base, al fine sia di non perdere i vantaggi offerti da que-
st’ultimo in termini di portabilità ed efficienza, sia di conservare la compatibilità con
l’elevato numero di programmi C già allora in circolazione, sia – infine – di mantene-
re l’integrità di molte librerie C e l’uso degli strumenti per esso sviluppati.
È per questo motivo che nel C++ si ritrovano tutti i punti di forza del C descritti nel
paragrafo precedente. In particolare, il C++:
• conserva una compatibilità quasi assoluta (qualcosa è diverso) con il C, da cui ere-
dita la sintassi e la semantica per tutti i costrutti comuni, oltre alla notevole flessi-
bilità e potenza;
• estende le caratteristiche del C, rimediando almeno in parte alle carenze del suo
predecessore (che manca soprattutto di un buon sistema dei tipi);
• presenta anche degli aspetti negativi (come ogni linguaggio), in parte ereditati dal C;
• consente di esportare facilmente le applicazioni verso altri sistemi.

Unità didattica A1: Introduzione ai linguaggi C e C++ 11


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 12

Grazie alle potenzialità descritte, il C++ rappresenta un linguaggio autonomo in-


dipendente dal C, venendosi a posizionare tra i linguaggi di medio livello. In parti-
colare, l’introduzione di costrutti quali i template e le classi rende il C++ un linguag-
gio multiparadigma (con particolare predilezione per il paradigma a oggetti e la pro-
grammazione generica).
È proprio a causa di questa fusione dei paradigmi procedurale e object oriented che
nasce il nome C++. L’idea è dovuta a Rick Mascitti e il carattere speciale ++ (che si
legge plus plus) è un operatore del linguaggio C, la cui funzione è di incrementare di
un’unità il valore di una variabile. In tal senso, il C++ rappresenta la versione del lin-
guaggio C incrementata con i costruttori della programmazione orientata agli oggetti.

4 I miglioramenti rispetto al C
L’elenco che segue indica i miglioramenti di lieve entità (che non hanno nulla a che
fare con gli oggetti) apportati dal linguaggio C++ rispetto al C.
• Commenti. Il C++ introduce il delimitatore di “commento sino a fine riga” //. Viene
però conservato l’uso dei delimitatori /* e */.
• Nome delle enumerazioni. Il nome delle enumerazioni è un nome di tipo. Questa
possibilità semplifica la notazione poiché non richiede l’uso del qualificatore enum
davanti al nome di un tipo enumerativo.
• Nomi delle classi. Il nome di una classe è un nome di tipo. Questo costrutto non
esiste in C. In C++ non è necessario usare il qualificatore class davanti ai nomi del-
le classi.
• Dichiarazioni di blocchi. Il C++ consente l’uso di dichiarazioni all’interno dei bloc-
chi e dopo le istruzioni di codice. Questa possibilità permette di dichiarare un iden-
tificatore più vicino al punto in cui esso viene utilizzato per la prima volta. È perfi-
no possibile dichiarare l’indice di un ciclo all’interno del ciclo stesso.
• Operatore di qualifica della visibilità. L’operatore di qualifica della visibilità ::
(detto anche operatore di scope) è un nuovo operatore utilizzato per risolvere i
conflitti di nome. Ad esempio, se una funzione ha una dichiarazione locale della va-
riabile vettore ed esiste una variabile globale vettore, il qualificatore ::vettore con-
sente di accedere alla variabile globale anche dall’interno del campo di visibilità
della variabile locale. Non è possibile l’operazione inversa.
• Lo specificatore const. Lo specificatore const consente di bloccare il valore di
un’entità all’interno del suo campo di visibilità. È anche possibile bloccare i dati in-
dirizzati da una variabile puntatore, l’indirizzo di un puntatore e perfino l’indirizzo
del puntatore e dei dati puntati.
• Overloading delle funzioni. In C++, più funzioni possono utilizzare lo stesso nome;
la distinzione si basa sul numero e sul tipo dei parametri.
• Valori standard per i parametri delle funzioni. È possibile richiamare una funzio-
ne utilizzando un numero di parametri inferiore rispetto a quelli dichiarati. I para-
metri mancanti assumeranno valori standard.
• Gli operatori new e delete. Gli operatori new e delete lasciano al programmatore il
controllo dell’allocazione e della deallocazione della memoria dello heap.

5 Descrizione sintattica
dei linguaggi di programmazione
Per descrivere la sintassi di un linguaggio di programmazione ci si serve general-
mente di un formalismo molto diffuso: la BNF (Backus-Naur Form o Forma Normale di
Backus).
Al fine di rendere lo studio semplice ed efficace, nel corso della nostra trattazione
impiegheremo solo alcuni simboli propri della BNF e pochissime regole. In particolare:

12 Modulo A: Fondamenti del linguaggio C++


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 13

• le parole chiave del linguaggio saranno scritte in grassetto;


• le parole racchiuse tra le parentesi angolari (< >) rappresentano le categorie sin-
tattiche, ossia elementi generali del linguaggio che, nei vari programmi, saranno so-
stituiti con opportune occorrenze. Ad esempio:
A=B

<NomeVariabile> = <Espressione> A = C + 2*D

C=0

• i blocchi racchiusi tra parentesi quadre ([ ]) indicano l’opzionalità, ossia il fatto


che tali blocchi possono anche non essere presenti. Ad esempio:
int <NomeVariabile> [= <ValoreIniziale>]

• i blocchi racchiusi tra parentesi graffe { } indicano la possibilità di ripetizione. Ad


esempio:
int <NomeVariabile> {,<NomeVariabile>}

• indica che subito dopo il metodo è possibile inserire una lista opzionale di para-
metri separati da una virgola;
• il simbolo | ha il significato di OR. Ciò significa che all’interno di una lista di paro-
le chiave separate da questo simbolo occorre sceglierne soltanto una. Ad esempio,
in base alla seguente sintassi:
int | float <NomeVariabile>

• la variabile di nome <NomeVariabile> dovrà essere preceduta dalla parola chiave


int oppure da float.

6 Gli elementi di un programma C++


Come abbiamo detto, il C++ è un linguaggio derivato dal C, che conserva con que-
sto una pressoché completa compatibilità. Sarà dunque necessario, per poter muo-
vere i primi passi verso la programmazione in C++, procedere alla conoscenza di quei
concetti e di quegli elementi del linguaggio C che sono alla base della programma-
zione in C++. Alla fine della lettura di questa Unità didattica dovreste essere in grado
di scrivere un programma breve ma funzionante.
La struttura generale di un programma C++ è rappresentata in figura A1.1.
✦ Figura A1.1
Struttura di un
Facoltativo Direttive al preprocessore programma C++.

Dichiarazione di variabili globali


Facoltativo
e/o dei prototipi delle funzioni
Programma principale
Obbligatorio
(funzione main)

Facoltativo Funzione 1

Facoltativo Funzione 2
...

Facoltativo Funzione N

Unità didattica A1: Introduzione ai linguaggi C e C++ 13


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 14

6.1 Il programma principale (funzione main)


Ogni programma C++ è formato da chiamate a funzioni. La funzione main è la fun-
zione principale di un programma codificato in C++ e come tale deve essere sempre
presente, poiché è la prima a essere richiamata dal compilatore (ossia è la prima a
essere lanciata in esecuzione) e l’ultima a terminare alla fine del programma. A que-
sta funzione è assegnato il compito di gestire le chiamate delle altre funzioni, occu-
pandosi del controllo generale dell’attività del codice.
La sintassi della funzione main è la seguente:
[<TipoRestituito>] main([<ElencoParametri>])
{
<Istruzione1>;
<Istruzione2>;
...
<IstruzioneN>;
}

dove:
• <TipoRestituito> indica il tipo di dato restituito dalla funzione il quale, in mancan-
za di esplicita dichiarazione (cioè per default), è un valore intero (int);
• <ElencoParametri> rappresenta l’insieme dei parametri che la funzione accetta in
ingresso (i parametri saranno trattati approfonditamente in seguito). Nel caso in
cui la funzione main non abbia parametri in ingresso occorre comunque far segui-
re la parola main da una coppia di parentesi tonde.
Come si evince dalla sintassi, quando il compilatore richiama la funzione main ese-
gue il gruppo di istruzioni racchiuse all’interno della coppia di parentesi graffe { } che
definiscono il corpo del programma principale. Dal punto di vista tecnico, le paren-
tesi graffe permettono di incapsulare istruzioni composte. Tali istruzioni possono
consistere nella definizione del corpo di una funzione (come accade nel nostro caso
per la funzione main) oppure nel riunire più linee di codice che dipendono dalla stes-
sa istruzione di controllo, come nel caso in cui diverse istruzioni vengono eseguite a
seconda del risultato vero/falso del test di un’istruzione.
Detto questo, appare evidente che il più semplice programma C++ che si possa
scrivere è il seguente:

main ( )
{
}

Questo programma non esegue nulla, perché tra le parentesi graffe non è racchiu-
sa alcuna istruzione. In fase di compilazione il compilatore, pur riscontrando l’as-
senza di istruzioni e non rilevando quindi alcun errore, invia un avvertimento all’u-
tente, poiché non è stato specificato il tipo di dato che la funzione deve restituire:
sappiamo che in questo caso il valore restituito sarà di tipo intero. Per evitare tale
avvertimento avremmo dovuto scrivere:

int main( )
{
return 0;
}

Con l’istruzione return la funzione restituisce il valore (o l’espressione) indicato.


Naturalmente, tale valore deve essere dello stesso tipo della funzione. L’istruzione re-
turn deve essere l’istruzione conclusiva del corpo della funzione poiché provoca l’u-
scita dalla funzione stessa. Per tale motivo eventuali istruzioni inserite di seguito sa-
ranno ignorate dal compilatore.

14 Modulo A: Fondamenti del linguaggio C++


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 15

Nel caso in cui non si voglia usare l’istruzione return occorre dichiarare esplicita-
mente che la funzione non restituisce alcun risultato. Per far questo occorre far pre-
cedere il nome main dalla parola chiave void:

void main( )
{
}

I vecchi compilatori non standard spesso lasciavano ampia libertà circa la dichia-
razione della funzione main: alcuni consentivano di dichiararla void ma ora, a norma
di standard, main deve avere tipo int e se nel corpo della funzione non viene inserita
esplicitamente un’istruzione return il compilatore inserisce automaticamente una
istruzione return 0;.
Avete notato come l’istruzione return sia stata scritta un po’ più spostata a destra
rispetto alle parentesi graffe? Le istruzioni vengono rientrate per indicare la loro di-
pendenza, cioè per evidenziare che l’istruzione è contenuta in un’altra. È buona abi-
tudine curare questi incolonnamenti poiché, così facendo, si garantisce leggibilità al
codice. La tecnica di questi incolonnamenti è conosciuta come indentazione.
Sempre riferendoci all’istruzione return, si può notare che ogni istruzione C++ ter-
mina con un punto e virgola (;).
Su una singola riga è possibile scrivere più istruzioni, ma è buona norma scriver-
ne solo una al fine di migliorare la leggibilità del codice.

6.2 La preelaborazione del codice sorgente: le direttive


al preprocessore
Il linguaggio C++ è un linguaggio compilato. Come sappiamo, i compilatori sono
programmi che accettano in ingresso un programma scritto in linguaggio di alto li-
vello (detto programma sorgente) e lo traducono interamente in un programma in
linguaggio macchina (detto programma oggetto): a traduzione ultimata, il program-
ma potrà essere eseguito. Lo scopo di questo processo è quindi di tradurre il pro-
gramma originale (codice sorgente) in uno semanticamente equivalente, ma esegui-
bile su una certa macchina.
A differenza di altri linguaggi, la compilazione in C++ prevede una prima fase di
preelaborazione, detta precompilazione, svolta da un apposito modulo denominato
preprocessore.
Normalmente il preprocessore è incluso nel compilatore del linguaggio oppure è ri-
chiamato dal traduttore in modo assolutamente trasparente al programmatore. In fa-
se di preelaborazione non viene eseguita alcuna azione di traduzione del codice, vie-
ne soltanto effettuata una preparazione del programma sorgente per la successiva
compilazione.
Le istruzioni di dichiarazione per il preprocessore vengono chiamate dichiarative
di precompilazione o direttive al preprocessore e iniziano con il carattere # segui-
to dalla parola chiave include o define e dalla dichiarazione della direttiva. Della di-
rettiva #define ci occuperemo dettagliatamente nelle successive Unità didattiche.
Concentriamo per ora l’attenzione su #include.
La direttiva #include consente di inserire nel file sorgente la libreria standard in-
dicata dopo la direttiva stessa. Una libreria è un insieme organizzato di programmi
utilizzati frequentemente nelle applicazioni e memorizzati su un dispositivo di me-
moria di massa.
La direttiva va racchiusa all’interno di parentesi angolari o di doppi apici. Ad esem-
pio, le direttive:

#include <iostream.h>
#include "iostream.h"

Unità didattica A1: Introduzione ai linguaggi C e C++ 15


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 16

indicano al compilatore di includere e compilare insieme al programma il file


iostream.h che contiene i prototipi delle funzioni standard di input/output (I/O) del
C++, richiamabili poi da un qualsiasi punto del programma. Racchiudendo la diretti-
va tra parentesi angolari (< >), il file verrà cercato nelle apposite cartelle definite dal
compilatore, mentre indicandola tra i doppi apici (" ") il file verrà dapprima ricerca-
to nella cartella corrente. L’estensione .h del file sta a indicare che i file in questione
sono file di intestazione o file di header. Sottolineiamo, infine, che le dichiarazioni
di inclusione #include non terminano con il punto e virgola.

6.3 Le funzioni per la gestione dell’input/output


Per realizzare i nostri primi programmi in C++ utilizzeremo una forma semplificata
delle funzioni per l’input e per l’output.
In particolare, per scrivere il valore di un’espressione o di una costante stringa sul-
l’unità di output predefinita – in genere il monitor – utilizzeremo la funzione cout (cout
è abbreviazione di console output e rappresenta il flusso standard di output), seguita
dall’operatore <<.
Il simbolo << è detto operatore di output: esso inserisce oggetti nell’output indicato
alla sua sinistra, che normalmente è il monitor del computer. Se diversi oggetti vengo-
no inviati verso cout, essi procedono in fila, uno dopo l’altro, mentre vengono immessi
nel flusso, e sono visualizzati nell’ordine sul monitor. La sintassi è la seguente:
cout << <Espressione> | <"Stringa"> {<< <Espressione> | <"Stringa">};

Ad esempio, l’istruzione:
cout << "Ciao a tutti";

visualizza sul video la stringa Ciao a tutti.


Se, invece, si desidera accettare i dati inseriti dall’unità di input predefinita, in ge-
nere la tastiera, si utilizzerà la funzione cin (cin è abbreviazione di console input e
rappresenta il flusso standard di input), seguita dall’operatore >>. Il simbolo >> è
detto operatore di input. Se diversi oggetti vengono inviati da cin, essi procedono in
fila, uno dopo l’altro, mentre vengono immessi nel flusso. La sintassi è la seguente:
cin >> <NomeVariabile> {>> <NomeVariabile>};

Ad esempio, l’istruzione:
cin >> Numero;

consente di accogliere all’interno della variabile Numero il valore digitato dalla ta-
stiera. Vedremo in seguito che le variabili dovranno essere dapprima opportuna-
mente dichiarate.

7 Realizziamo il nostro primo programma C++


Con queste conoscenze iniziali siamo in grado di scrivere il nostro primo pro-
gramma C++. Come è tradizione nei manuali di linguaggi di programmazione, il primo
programma che realizzeremo è quello che visualizza sul video la frase Ciao mondo!.
Il codice C++ è il seguente:
#include <iostream.h>

int main( )
{
cout << "Ciao mondo!";
return 0;
}

16 Modulo A: Fondamenti del linguaggio C++


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 17

che dopo essere stato digitato, compilato e linkato visualizza sul video il seguente ri-
sultato:

8 Dal codice sorgente al codice eseguibile


Vediamo, ora, come digitare, compilare e linkare un programma. I passi da esegui-
re per creare un file eseguibile sono i seguenti.
1) Scrittura del programma sorgente e memorizzazione dello stesso all’interno di un
file con estensione .c, .cpp, .cp.
2) Compilazione del programma sorgente e ottenimento del programma oggetto me-
morizzato in un file caratterizzato dall’estensione .obj. Il compilatore, durante la
fase di traduzione del codice sorgente in codice oggetto, verifica la correttezza for-
male del codice sorgente e segnala la presenza di errori che impediscono la crea-
zione del file oggetto (con un messaggio error), oppure invia un warning quando
rileva errori che consentono comunque di creare il file oggetto (i warning sono av-
vertimenti di situazioni anomale che potrebbero creare problemi in esecuzione).
3) Linking del programma oggetto con le librerie standard del linguaggio dichiarate
nel codice sorgente a opera di un particolare programma chiamato linker. Il risul-
tato sarà il programma eseguibile memorizzato in un file caratterizzato dall’esten-
sione .exe.
Supponiamo di voler eseguire il nostro programma descritto nel paragrafo prece-
dente, immaginando di editarlo e memorizzarlo nel file Ese1.cpp. Schematicamente,
le varie fasi possono essere riassunte come in figura A1.2.
✦ Figura A1.2
Editore Schematizzazione
di del processo di
testi compilazione
File di di un programma C++.
File di
intestazione
iostream.h libreria

File File
Preprocessore Compilatore Linker
sorgente eseguibile
Ese1.cpp Ese1.exe

File
oggetto
Ese1.obj

9 Come creare e compilare un progetto in C++


Il processo di creazione e compilazione di un progetto C++ dipende strettamente
dal tipo di computer e di sistema operativo che si utilizza.
Come abbiamo già detto, possiamo compilare ed eseguire programmi con il C++ in
qualsiasi piattaforma. Una volta decisa la piattaforma (Linux, Windows ecc.), occor-
rerà scegliere l’ambiente di sviluppo. Per poter utilizzare sia il linguaggio C sia il lin-
guaggio C++ si può utilizzare agevolmente l’ambiente di sviluppo integrato Visual
C++ (che fa parte della suite Microsoft Visual Studio).

Unità didattica A1: Introduzione ai linguaggi C e C++ 17


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 18

Per compilare, linkare ed eseguire i nostri programmi C++ abbiamo due possibilità:
1) utilizzare l’ambiente a interfaccia grafica di Visual C++ usando appositi pulsanti
per la compilazione, il linking e l’esecuzione;
2) utilizzare l’ambiente testuale del sistema operativo MS-DOS utilizzando appositi
comandi per compilare, linkare ed eseguire i programmi da digitare nella finestra
del “Prompt dei comandi”.
Proviamo a scrivere il nostro programma Ese1.cpp utilizzando Microsoft Visual
C++. Dopo aver lanciato Visual C++, la videata che appare sul monitor è la seguente:

Occorre a questo punto creare un nuovo progetto. Allo scopo, procediamo nel se-
guente modo.
• Dal menu File selezioniamo la voce New...:

• Viene visualizzata la finestra di dialogo


New, nella quale occorre selezionare la
scheda Projects. Nella casella Location
stabiliamo la cartella in cui salvare il file,
nella casella Project name inseriamo il no-
me del progetto (nel nostro caso Proget-
to1). A questo punto selezioniamo nella li-
sta a sinistra la voce Win32 Console Ap-
plication. Terminata la digitazione, faccia-
mo clic sul pulsante OK. Il tutto è riporta-
to nella figura a lato.

18 Modulo A: Fondamenti del linguaggio C++


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 19

Verrà ora richiesto il tipo di progetto da creare: possiamo scegliere An empty project,
ossia un progetto vuoto. Dopo un clic sul pulsante Finish, il progetto risulterà creato.
Occorre ora aggiungere a esso uno o più file, che conterranno il codice C++, uno dei
quali (e soltanto uno) deve contenere la funzione main. Si procede nel seguente modo:
• dal menu File si seleziona New...;
• verrà visualizzata nuovamente la finestra New;
• si seleziona la scheda Files;
• si seleziona la casella di controllo Add to project;
• si seleziona nella lista a sinistra la voce C++ Source File se si
vuole creare un file con estensione .cpp oppure la voce C/C++
Header File se si vuole creare un file con estensione .h;
• si digita all’interno della casella File name il nome del file (nel
nostro caso Ese1);
• si fa clic sul pulsante OK (l’intera procedura è riportata nella
figura a lato);
• nell’area di lavoro di destra nella finestra successiva, si edita il
file creato aggiungendo tutte le istruzioni C++ necessarie:

• si salva il file stesso utilizzando l’opzione Save del menu File oppure facendo clic
sull’apposita icona presente sulla barra degli strumenti standard;
• si procede in questo modo per tutti i file che si intende aggiungere al progetto.

Dopo aver inserito tutti i file necessari al progetto si deve procedere alla compila-
zione per poter poi eseguire il progetto stesso. Per compilare l’intero progetto apria-
mo il menu Build e selezioniamo il comando Build <NomeProgramma>.exe.
Nella finestra dei messaggi in basso apparirà il risultato della compilazione. Se non
ci sono stati errori, verrà visualizzata la frase: 0 Errors, 0 Warnings. In caso contrario,
il compilatore indicherà le righe di codice in cui sono stati riscontrati dei problemi e
avviserà il programmatore se non è stato possibile creare l’eseguibile. Quando non
vengono più riscontrati errori è possibile eseguire il progetto. Sarà sufficiente aprire
il menu Build e selezionare la voce Execute <NomeProgramma>.exe.

Unità didattica A1: Introduzione ai linguaggi C e C++ 19


p009-020_CeC_Unità A1 26-09-2007 16:27 Pagina 20

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Chi ha progettato e implementato il linguaggio C?


2. Il C è un linguaggio compilato o interpretato?
3. Quali sono le caratteristiche del linguaggio C?
4. Chi ha progettato e implementato il linguaggio C++?
5. Che cosa significa che il linguaggio C++ è un linguaggio multiparadigma?
6. Che cos’è la Forma Normale di Backus?
7. All’interno di un programma C++, quale elemento non può mai mancare?
8. Da che cosa è delimitato il corpo del programma principale?
9. All’interno della funzione main è sempre necessario inserire l’istruzione return?
10. Qual è la funzione svolta dalla parola chiave void?
11. Che cos’è il programma sorgente? E il programma oggetto?
12. Che cos’è il preprocessore?
13. Che cosa sono le direttive al preprocessore?
14. All’interno di un programma C++, tutte le istruzioni devono terminare con un punto e virgola?
15. In C++, qual è il flusso standard di output? E quello di input?
16. Qual è la differenza tra error e warning ?
17. Quali estensioni caratterizzano un file che contiene un programma in C++?
18. Che cos’è il linker ?

Verifica delle competenze


■ SVOLGI IL SEGUENTE ESERCIZIO

Trovare gli errori presenti all’interno dei seguenti frammenti di codice.

a) c)
#include <iostream.h>
#include <iostream.h>
void main( )
int main
{
{
return(1+2);
}
}

b) d)
#include <iostream.h>;
#include <iostream.h> void main( );
void main( ) {
cout << "Ciao a tutti"; cout << "Ciao a tutti";
}

20 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 21

Unità didattica

Elementi lessicali
ed espressioni A2
● Compilare ed eseguire un’applicazione C++ Prerequisiti

● Scrivere espressioni in C++ Obiettivi

● Conoscere la sintassi delle espressioni, in particolare quali sono gli operandi Conoscenze
e gli operatori disponibili da apprendere
● Conoscere la precedenza tra gli operatori
● Conoscere i tipi primitivi disponibili
● Comprendere il concetto di casting tra tipi primitivi

● Saper scrivere espressioni con operandi e operatori Competenze


● Saper attribuire a variabili e costanti tipi appropriati per le esigenze del problema da acquisire
● Saper effettuare conversioni di tipo implicite ed esplicite tra tipi primitivi
● Saper riconoscere e applicare la precedenza tra operatori

1 L’alfabeto del linguaggio C++


L’alfabeto del linguaggio C++ è formato da:

1) le lettere minuscole e maiuscole dell’alfabeto inglese (dalla a fino alla z):


a b c d e f g h i j k l m n o p q r s t u v w y z
A B C D E F G H I J K L M N O P Q R S T U V X Y Z

2) le dieci cifre decimali (dallo 0 al 9):


0 1 2 3 4 5 6 7 8 9

3) i seguenti caratteri speciali:


• i simboli di punteggiatura . , : ;
• i segni matematici + − * / = % < >
• i simboli spazio # $ & ! ^ | \ ' " _ ? ~
• le parentesi [ ] ( ) { }

4) i simboli non grafici (o non stampabili), come quelli di:


• nuova linea (new line);
• nuova pagina (form feed);
• spazio indietro (backspace);
• tabulazione orizzontale.

Unità didattica A2: Elementi lessicali ed espressioni 21


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 22

2 Le regole lessicali
Il processo di compilazione è suddiviso in più fasi, ciascuna volta all’acquisizione
di opportune informazioni necessarie alla fase successiva. La prima di queste fasi è
nota come analisi lessicale e ha il compito di riconoscere gli elementi costitutivi del
linguaggio sorgente, individuandone anche la categoria lessicale. Ogni linguaggio
prevede un certo numero di categorie lessicali; in C++ possiamo distinguere le se-
guenti categorie:
• commenti;
• identificatori (creati dal programmatore);
• parole riservate (consultabili sul sito di riferimento);
• costanti letterali;
• segni di punteggiatura e operatori.

3 I commenti
I commenti in un programma C++, come in qualsiasi altro linguaggio, hanno valore
soltanto per il programmatore e vengono ignorati dal compilatore.
Nel linguaggio C++ è possibile inserire i commenti in due modi diversi.
1) Secondo lo stile C, ovvero racchiudendoli tra i simboli /* e */, ad esempio:
/* Questo è un commento in stile C */

Questo metodo, detto commento a blocchi, permette di commentare più linee di


codice senza dover ripetere a ogni linea i simboli del commento stesso.
2) Secondo lo stile proprio del C++, ovvero facendoli precedere dal simbolo //. In que-
sto modo il commento si estende solo fino al termine della linea. Ad esempio:
// Questo è un commento in stile C++

Nel primo caso è considerato commento tutto quello che è racchiuso entro /* e */;
il commento, quindi, si può anche trovare in mezzo al codice. Analizziamo il seguen-
te esempio:
int main( )
{
int x = 10; /* Questo e' un commento diviso
su tre righe.
Questa e' l’ultima riga di commento */
x = 100 /* Questo commento e' valido */ + 5;
return 0;
}

Nel secondo caso, proprio del C++, è invece considerato commento tutto ciò che
segue // sino alla fine della linea. Ne consegue che non è possibile inserire un com-
mento di questo tipo in mezzo al codice o dividerlo su più righe (a meno che anche
l’altra riga non cominci con //). Vediamo un esempio:
int main( )
{
int x = 10; // Questo e' un commento valido
x = 100 // Attenzione! La riga successiva produce un errore
QUESTA RIGA NON E’ UN COMMENTO: NON E’ PRECEDUTA DA //
return 0;
}

22 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 23

Diversi ambienti di sviluppo (come ad esempio Microsoft Visual C++) evidenziano


i commenti in verde per permettere al programmatore di distinguere meglio tra il co-
dice e i commenti stessi.

4 Gli identificatori
Gli identificatori sono i nomi definiti dal programmatore per riferirsi a sei diverse
categorie di oggetti:
• variabili;
• costanti simboliche;
• etichette;
• tipi definiti dal programmatore;
• funzioni;
• macro.
Gli identificatori C++ devono iniziare con una lettera o con carattere di underscore _.
Possono essere seguiti da un numero qualsiasi di lettere, cifre o underscore; viene
fatta distinzione tra lettere maiuscole e lettere minuscole (abbiamo già detto che il
C++ è un linguaggio case sensitive). Tutti gli identificatori presenti in un programma
devono essere diversi tra loro, indipendentemente dalla categoria cui appartengono.
Gli identificatori non possono iniziare con una cifra numerica e non possono contenere
spazi. Ad esempio, non sono identificatori validi:
2biciclette
primo numero

mentre sono validi i seguenti:


j
MIN
max
eta_studente
_secondo_nome

Sebbene il linguaggio non preveda un limite alla lunghezza massima di un identifi-


catore, è praticamente impossibile non prescrivere un limite al numero di caratteri
considerati significativi, per cui ogni compilatore distingue gli identificatori in base
a un certo numero di caratteri iniziali tralasciando i restanti; il numero di caratteri
considerati significativi varia comunque da sistema a sistema, anche se lo standard
ANSI C++ ha stabilito che sono validi i primi 1024 caratteri di un identificatore.

5 Le espressioni
In C++ un’espressione è una combinazione di operatori e operandi che definisce
un’elaborazione e dà origine a un valore. A elaborazione terminata, l’espressione re-
stituisce un risultato il cui tipo di dato determina il tipo dell’espressione stessa.
Gli operandi sono combinazioni di: costanti, variabili semplici e strutturate, chia-
mate di funzioni o, ancora, altre espressioni. Rappresentano, pertanto, i valori che
vengono manipolati nell’espressione.
Gli operatori sono simboli che specificano come devono essere manipolati gli ope-
randi dell’espressione.
La valutazione di un’espressione viene effettuata eseguendo le operazioni indicate
dagli operatori sui loro operandi secondo le regole di precedenza degli operatori ti-
piche dell’aritmetica.

Unità didattica A2: Elementi lessicali ed espressioni 23


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 24

6 Gli operandi
Nello studio degli operandi, particolare importanza assumono le variabili, le co-
stanti e il tipo che può essere loro assegnato.

6.1 Variabili e costanti


Nei paragrafi precedenti abbiamo visto che l’identificatore serve per individuare
univocamente un dato. Ricordiamo che è buona norma utilizzare nomi che consen-
tano di richiamarne immediatamente il significato.
In funzione della possibilità di cambiamento del loro valore durante l’elaborazio-
ne, i dati si classificano in:
• variabili: sono quelli il cui valore può variare nel corso dell’elaborazione;
• costanti: sono quelli il cui valore non cambia.

6.2 Le costanti
All’interno delle espressioni è possibile inserire direttamente dei valori che sono
detti costanti o valori letterali. Tali elementi rappresentano quantità esplicite e co-
me tali non vanno dichiarati. Una generica costante può essere:
• un numero intero (positivo o negativo);
• un numero intero decimale in virgola mobile (in notazione scientifica o in quella
standard);
• un carattere singolo;
• una stringa (sequenza di caratteri), che può terminare con:
– un valore nullo;
– un carattere speciale.

6.2.1 Le costanti numeriche intere


Nel linguaggio C++ una costante intera si può rappresentare in sistemi di numera-
zione diversi. In particolare:
• una costante intera decimale è scritta utilizzando una sequenza di numeri decimali
(da 0 a 9 senza virgola). Ad esempio, 85 è una costante numerica decimale;
• una costante intera ottale è scritta utilizzando una sequenza di numeri ottali (da 0 a
7) preceduta dal carattere 0. Ad esempio, 042 rappresenta una costante intera ottale;
• una costante intera esadecimale è scritta utilizzando una sequenza di simboli esa-
decimali (0, 1, 2, …, 9, A, B, C, D, E, F) preceduta dalla coppia di caratteri 0x oppu-
re 0X. Ad esempio: 0XA, 0X18f sono costanti intere esadecimali. Come si evince
dall’ultimo esempio (0X18f), le lettere dalla A alla F possono anche essere scritte
in minuscolo.

6.2.2 Le costanti numeriche decimali o in virgola mobile


Le costanti numeriche decimali o in virgola mobile rappresentano un numero rea-
le che può essere scritto in forma decimale oppure mediante la notazione scientifica.
Per quanto riguarda la forma decimale, le costanti si compongono di due parti: quel-
la intera e quella decimale, separate dal punto (si utilizza infatti la notazione anglo-
sassone, che prevede il punto al posto della virgola).
I compilatori riconoscono una costante come numerica decimale soltanto se viene
rappresentata con la seguente configurazione, che riportiamo secondo le regole del-
la BNF:
[<Segno>][<ParteIntera>][.<ParteDecimale>][E[<Segno>]<Esponente>]

dove la lettera E indica la base 10.

24 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 25

Ad esempio, le seguenti costanti sono corrette:


0 // 0 in virgola mobile
480E+4 // 480 * 10000 (10 elevato a 4)
.14e−2 // 0.0014
−3.5e+3 // −3500.0

mentre non sono riconosciute dal compilatore le seguenti costanti:


.58E E25

6.2.3 I caratteri
Le costanti di tipo carattere sono singoli simboli racchiusi tra singoli apici, ad
esempio: 'b', '!', '3'.
In C++, come in tutti i linguaggi di programmazione, il numero 8 è diverso dal sim-
bolo '8' che, invece, è un carattere:

numero
8 ≠ '8' carattere

I caratteri sono generalmente memorizzati come codici ASCII a 8 bit (dipende co-
munque dall’implementazione). Esiste anche la possibilità di definire caratteri estesi
di tipo wchar_t (wide character type) che sono memorizzati come codici Unicode a
16 bit.

6.2.4 I caratteri speciali


All’interno delle istruzioni C++ è possibile inserire caratteri speciali non stampabi-
li. Ad esempio, per scrivere sul monitor una stringa con un a-capo si usa una sequen-
za speciale di caratteri che inizia con una barra rovesciata \, in inglese backslash,
definita sequenza di escape: il primo carattere permette di sfuggire dalla normale
rappresentazione, il secondo specifica quale carattere non stampabile si desidera
produrre (ovviamente utilizzando un carattere stampabile).
Un esempio di carattere speciale è l’apice (') che spesso sostituisce l’apostrofo: in
quest’ultimo utilizzo va indicato come carattere speciale facendolo precedere da
una barra rovesciata.
In generale, quindi, utilizzeremo le sequenze di escape per rappresentare caratte-
ri non stampabili, numeri ottali, numeri esadecimali e caratteri che non possono
essere scritti direttamente.
La tabella seguente riporta le sequenze di escape definite dal C++.

SEQUENZA DI ESCAPE DESCRIZIONE

\n Nuova riga (new line)


\t Tabulazione (tab)
\a Segnale sonoro (alert)
\f Nuova pagina (form feed)
\r Ritorno a capo della stessa riga (ritorno carrello o carriage return)
\v Tabulazione verticale
\b Una battuta indietro (backspace)
\\ Barra rovesciata (backslash)
\' Apice singolo
\" Doppi apici
\? Punto di domanda
\0 Fine stringa
\<CifraOttale> Numero ottale
\<CifraEsadecimale> Numero esadecimale

Unità didattica A2: Elementi lessicali ed espressioni 25


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 26

Facciamo alcuni esempi. Riprendiamo il programma C++ che abbiamo realizzato


nell’Unità didattica precedente. Per visualizzare sul video la frase Ciao mondo! ab-
biamo utilizzato la seguente istruzione:
cout << "Ciao mondo!";

Se vogliamo che il cursore sul monitor ritorni a capo dopo aver visualizzato la frase,
dobbiamo aggiungere in coda alla frase la sequenza di escape \n. Avremo così:
cout << "Ciao mondo! \n";

Analogo risultato può essere ottenuto utilizzando il manipolatore di output endl


(end line), al posto della sequenza di escape, indirizzandola alla console output.
L’istruzione sarà, pertanto, la seguente:
cout << "Ciao mondo!" << endl;

6.2.5 Le costanti stringa


Una costante stringa (o stringa letterale) è composta da una sequenza di caratte-
ri terminata con la sequenza di escape \0 e racchiusa all’interno di una coppia di
doppi apici. Un carattere è un qualsiasi componente di un insieme predefinito di ca-
ratteri, o alfabeto. Molti computer impiegano l’insieme di caratteri ASCII (American
Standard Code for Information Interchange) o Unicode. Ad esempio:
"Ciao mondo!"

è una costante stringa formata da dodici caratteri (compreso il carattere speciale e


lo spazio, che è anch’esso un carattere). Il carattere speciale \0 viene inserito auto-
maticamente dal compilatore (e come tale può non essere specificato esplicitamen-
te dal programmatore) nel momento in cui riconosce una costante stringa all’interno
del programma sorgente.
Nel caso in cui si scrivano due stringhe in sequenza, il compilatore C++ le conca-
tena automaticamente. Ad esempio, la seguente istruzione:
cout << "Ciao" " mondo!";

produce lo stesso output dell’istruzione:


cout << "Ciao mondo!";

Si noti la differenza esistente fra le costanti stringa e le costanti carattere, che ven-
gono rappresentate utilizzando un apice singolo all’inizio e alla fine del simbolo del
carattere. Ad esempio, 'A' è diverso da "A": il primo, infatti, rappresenta il carattere
A, il secondo la stringa formata dal solo simbolo A.

6.3 Come dichiarare variabili e costanti


La dichiarazione di una variabile in C++ avviene in base alla seguente sintassi:
[<Qualificatore>] <Tipo> <NomeVariabile> [= <ValoreIniziale>]
{, <NomeVariabile> [= <ValoreIniziale>]};

dove:
• <Qualificatore> può assumere i valori signed (con segno), unsigned (senza segno),
short (piccolo) e long (lungo) rispettivamente per trasformare un tipo in uno con
segno o in uno senza segno e diminuire o aumentare il numero di byte riservati al
tipo di dato. I valori, tuttavia, non sono liberamente applicabili a tutti i tipi: vedre-
mo che short si applica solo a int, signed e unsigned solo a char e int, e infine long so-
lo a int e double;
• <Tipo> rappresenta il tipo da assegnare alla variabile e sarà oggetto di studio del
prossimo paragrafo.

26 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 27

Ad esempio:

int X, a, A, b, B, c;
char Pippo;
unsigned int Tappo = 10;

sono dichiarazioni di variabili valide in C++. In dettaglio, con la prima vengono di-
chiarate sei variabili intere, con la seconda viene dichiarata la variabile Pippo di tipo
carattere e con la terza viene dichiarata la variabile Tappo di tipo intero senza segno,
alla quale è assegnato il valore iniziale 10.
Non bisogna mai dimenticare che il C++ è case sensitive, cioè distingue le lettere
maiuscole da quelle minuscole. Si noti, infine, il punto e virgola che segue sempre
ogni dichiarazione.

Le costanti possono essere dichiarate in due modi: uno proprio del C++, tramite la
parola chiave const, e uno ereditato dal linguaggio C, tramite la direttiva #define. Le
sintassi sono le seguenti:

const <Tipo> <NomeCostante> = <Valore> {,<NomeCostante> = <Valore>};


#define <NOMECOSTANTE> <Valore>

La seconda istruzione, che come si può notare non termina con il punto e virgola,
ordina al preprocessore di sostituire <Valore> a qualsiasi occorrenza dell’identifica-
tore <NOMECOSTANTE>. Per rimanere fedeli a una tipica convenzione utilizzata dai
programmatori C/C++, le costanti dichiarate con la direttiva #define saranno caratte-
rizzate da un identificatore scritto totalmente in maiuscolo.
Ad esempio, sono istruzioni corrette:

const int Sconto1 = 5, Sconto2 = 20;


#define IVA 20

6.4 I tipi di dato fondamentali


In un linguaggio di programmazione i tipi rappresentano le categorie di informa-
zioni che tale linguaggio consente di manipolare.
I tipi di dato del C++ si classificano in primitivi e strutturati, secondo il seguente
schema:

Tipi di dato

Primitivi semplici Strutturati

bool array

char strutture (struct)

int union

float

double

enum

Unità didattica A2: Elementi lessicali ed espressioni 27


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 28

Attenzione: tutti i tipi fondamentali devono essere scritti rigorosamente in minu-


scolo, altrimenti causano errori in compilazione: pertanto, Int è diverso da int.

6.4.1 Il tipo int


L’insieme del tipo int è rappresentato dal sottoinsieme dei numeri interi relativi
compresi tra un valore minimo negativo e un valore massimo positivo.
Il linguaggio C++ considera tre insiemi di numeri interi, cui corrispondono tre di-
versi tipi. Vediamo il loro insieme base nel caso in cui il compilatore riservi 2 e 4 by-
te in memoria (la precisione offerta da ciascun tipo dipende dall’implementazione):
• il tipo short int comprende tutti i valori interi relativi contenuti nell’intervallo da
–32768 a +32767 e ogni valore necessita di due byte in memoria per poter essere
rappresentato;
• il tipo long int comprende tutti i valori interi relativi contenuti nell’intervallo da
–2147483648 a +2147483647 e ogni valore necessita di quattro byte in memoria per
poter essere rappresentato;
• il tipo int nella rappresentazione su 2 byte (16 bit) coincide con short int, in quel-
la su 4 byte coincide con long int.
Per utilizzare di numeri interi senza segno è necessario utilizzare un qualificato-
re. Ad esempio, per creare un tipo di dato che rappresenta un numero intero senza
segno occorre far precedere le parole riservate int, short int e long int dal qualifica-
tore unsigned. I numeri interi così creati assumeranno valori da 0 a un valore massi-
mo. Ad esempio, se il tipo int comprende valori interi appartenenti all’intervallo
–32768 ÷ +32767, il tipo unsigned int comprenderà valori appartenenti all’intervallo
0 ÷ 65535.
In generale, avendo n bit a disposizione per un tipo intero, l’intervallo dell’insieme
dei valori senza segno andrà da 0 a (2n – 1).
Utilizzando il qualificatore signed prima della parola riservata int non produrremo
nuovi tipi ma solo quelli definiti all’inizio del paragrafo, poiché il tipo int è già dota-
to di segno.
Le operazioni consentite sul tipo intero sono:

OPERAZIONE OPERATORE

Addizione +
Sottrazione –
Moltiplicazione *
Divisione intera /
Resto della divisione tra interi (modulo) %
Assegnamento =
Incremento ++
Decremento ––

Analizziamo le seguenti dichiarazioni di variabili intere:

int somma, prodotto;


long int potenza;
unsigned short differenza;

L’ultima dichiarazione non prevede il tipo ma solo il qualificatore. In sua assenza il


compilatore, per default, attribuirà il tipo int.

6.4.2 Il tipo char


L’insieme del tipo char è formato dai numeri interi che, sulla base dello specifico
codice alfanumerico utilizzato dal computer, sono utilizzati per rappresentare i ca-
ratteri.

28 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 29

Il tipo char, quindi, è impiegato per la rappresentazione di piccoli interi (dunque


su di esso si possono eseguire le comuni operazioni aritmetiche previste per il tipo
int) e singoli caratteri. Come tale, viene utilizzato per rappresentare interi all’interno
di un intervallo più piccolo di quello previsto per il tipo int. Per questo motivo è
possibile (anche se non tanto sensato!) procedere alla moltiplicazione o alla divisio-
ne di un carattere per un numero o, ancora, addizionare una lettera a un intero. Fac-
ciamo un esempio. Analizziamo il seguente frammento di codice:
char car1='X', car2;
car2 = car1 + 32;
cout << car1 << car2 << endl;

Abbiamo aggiunto alla variabile car1 il valore 32 ottenendo un nuovo carattere. Il


risultato a video è il seguente:

I valori che possono essere assegnati a una variabile di tipo char possono essere:
• un carattere (quindi un simbolo racchiuso tra due singoli apici);
• un numero intero che rappresenta il codice numerico di un carattere.
Accanto a char troviamo il tipo wchar_t, già introdotto, che è utilizzato per me-
morizzare caratteri non rappresentabili con il tipo char (ad esempio i caratteri Uni-
code).
Proviamo a realizzare un programma che visualizzi la frase “ciao mondo” serven-
doci di variabili dichiarate di tipo char.

Esempio 1
Proviamo a scrivere la frase Ciao mondo

#include <iostream.h>
int main( )
{
char a=99,b=105,c=97,d=111,e=32,f=109,g=111,h=110,i=100,j=111;
cout << a << b << c << d << e << f << g << h << i << j << endl;
return 0;
}

Una volta eseguito, il programma produrrà il seguente effetto:

Per quanto riguarda le operazioni definite sul tipo char ricordiamo che, essendo
questo un sottoinsieme del tipo int, possono essere utilizzate tutte le operazioni de-
finite sul tipo int.

6.4.3 I tipi float e double


L’insieme dei tipi float e double costituisce un sottoinsieme dei numeri reali. La rap-
presentazione interna è in virgola mobile (floating point) in precisione singola (float)
e doppia (double).
Il tipo float necessita di 4 byte per poter essere rappresentato. Il numero di cifre
decimali significative è pari a 7 e il range di valori ammessi va da 10–39 a 10+38.

Unità didattica A2: Elementi lessicali ed espressioni 29


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 30

Il tipo double, invece, necessita di 8 byte per poter essere rappresentato. Il numero
di cifre decimali significative è pari a 16 e il range di valori ammessi va da 10–309 a 10+308.

6.4.4 Il tipo bool


Il tipo bool è utilizzato per rappresentare i valori di verità (true e false). Su di esso
sono definite essenzialmente le abituali operazioni logiche (cioè && per l’AND, || per
l’OR, ! per il NOT). È molto importante prestare attenzione alla differenza che inter-
corre fra queste ultime e le corrispondenti operazioni su bit (rispettivamente &, |, ~).
Un esempio di dichiarazione di una variabile booleana è la seguente:
bool trovato = false;

7 Gli operatori
Un operatore è un simbolo che “opera” su una o più espressioni, producendo un
valore che può essere assegnato a una variabile. Tutti gli operatori producono sem-
pre un risultato e alcuni possono anche modificare uno degli operandi. Alcuni ope-
ratori sono rappresentati simbolicamente con un singolo carattere, altri con due ca-
ratteri senza spazi separatori. Se non esplicitamente evidenziato, tutti gli operatori
operano su tipi primitivi.
Gli operatori si dividono in:
• operatori di assegnamento;
• operatori aritmetici;
• operatori relazionali;
• operatori logici;
• operatori sui bit;
• operatori speciali.

7.1 L’operatore di assegnamento


L’operatore di assegnamento è il carattere “=”. Il suo uso è diverso da quello uti-
lizzato in matematica, poiché serve ad assegnare un valore e non a effettuare con-
fronti, per i quali in C++ si usa l’operatore == (attenzione a non inserire spazi tra i due
caratteri “=”). Con l’operatore di assegnamento si valuta l’espressione alla sua destra
e il risultato viene assegnato alla variabile alla sua sinistra. Il valore assegnato è il ri-
sultato dell’operazione.
Ad esempio, supponiamo di avere la variabile A con valore 6; con l’istruzione:
X = 3*A;

alla variabile X viene assegnato il risultato di 3*A, cioè 18, quindi il risultato dell’o-
perazione di assegnamento (X = 3*A) è proprio 18.
Esaminiamo ora un particolare. L’assegnazione stessa è un’espressione con un va-
lore (il valore dell’espressione X = 3*A è 18), che può venire usato in un’altra asse-
gnazione.
Ad esempio l’istruzione:
Y = (X = 3*A);

contiene una assegnazione multipla: dapprima viene assegnato il valore 3*A alla va-
riabile X e poi si assegna il risultato ottenuto alla variabile Y.
Se si ha l’istruzione:
Y = X;

il valore di X viene “copiato” in Y: le due variabili rimangono completamente indi-


pendenti, cioè la modifica di una delle due non influenza l’altra. Ciò è vero solo se X

30 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 31

e Y sono variabili di tipo primitivo. Il comportamento di questo operatore nel caso


di operandi non primitivi verrà considerato successivamente.

7.2 Gli operatori aritmetici


Gli operatori aritmetici si dividono in:
1) operatori unari, che hanno la forma:
<Operando><Operatore> (postfissi)
oppure
<Operatore><Operando> (prefissi)

2) operatori binari, che hanno la forma:


<Operando1><Operatore><Operando2>

Gli operatori unari aritmetici si applicano a un solo operando e ne modificano il


valore. Si distinguono i seguenti:
• incremento: ++, ad esempio:
P++ // equivale a P = P+1

• decremento: − −, ad esempio:
P−− // equivale a P = P−1

Gli operatori unari aritmetici possono essere posti prima dell’operando (prefissi)
o dopo l’operando (postfissi) e il loro valore varia secondo questa posizione: l’ope-
ratore prefisso modifica l’operando prima di utilizzarne il valore, mentre l’operatore
postfisso modifica l’operando dopo averne utilizzato il valore. Ad esempio, con:
X = 10;
Y = X++; /* postfisso: si assegna prima il valore della variabile X
alla variabile Y e poi si incrementa la variabile X */

risulta Y = 10 e X = 11. Invece con:


X = 10;
Y = ++X; /* prefisso: prima si incrementa la variabile X e poi
si assegna il valore alla variabile Y */

risulta Y = 11 e X = 11.

Gli operatori binari aritmetici si applicano a due operandi e non ne cambiano il


valore, ma memorizzano il risultato. In C++ si utilizzano i seguenti operatori binari:
OPERATORE SIMBOLO RISULTATO

Addizione + Addiziona due operandi o concatena due stringhe.


Sottrazione – Sottrae il secondo operando dal primo.
Moltiplicazione * Moltiplica i due operandi.
Divisione / Divide il primo operando per il secondo. L’operatore di divisione, applicato
a variabili di tipo intero, produce un risultato troncato della parte decimale.
Resto (modulo) % Fornisce il resto della divisione tra due operandi interi. Non è possibile uti-
lizzare l’operatore di calcolo del modulo quando gli operandi sono in virgo-
la mobile. Se si divide il modulo per zero, si avrà un’eccezione nell’esecu-
zione: se l’operazione eccede il limite inferiore (underflow) il risultato sarà
zero, se eccede il limite superiore (overflow) si avrà un’approssimazione.

Unità didattica A2: Elementi lessicali ed espressioni 31


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 32

L’operatore di assegnamento può anche essere compattato, cioè abbinato a un


operatore aritmetico. In genere le espressioni del tipo:
<NomeVariabile> = <NomeVariabile> <Operatore> <Espressione>

possono essere scritte nella seguente forma abbreviata:


<NomeVariabile><Operatore> = <Espressione>

cioè si ha la seguente tabella:

SCRITTURA COMPATTA SCRITTURA EQUIVALENTE

X += Y X=X+Y
X –= Y X=X–Y
X *= Y X=X*Y
X /= Y X=X/Y
X %= Y X=X%Y

7.3 Gli operatori relazionali


Gli operatori relazionali richiedono operandi di tipo primitivo e producono sempre
un risultato booleano. Essi sono:

> maggiore
>= maggiore o uguale
< minore
<= minore o uguale
== uguale
!= diverso

Gli operatori relazionali restituiscono il valore logico true se la relazione è verifi-


cata, altrimenti restituiscono il valore false.

7.4 Gli operatori logici


Gli operatori logici richiedono come operandi delle espressioni booleane e produ-
cono un risultato booleano. Essi sono:

|| OR
&& AND
! NOT

Gli operatori && e || non valutano l’operando destro se non è necessario al risul-
tato, ovvero il risultato dell’operatore è indipendente dal valore dell’operando de-
stro. Ad esempio:

(3>1) || (x>7)
non viene valutato

Le tabelle di verità degli operatori logici sono:

X Y X&&Y X||Y !X

false false false false true


false true false true true
true false false true false
true true true true false

32 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 33

7.5 Gli operatori sui bit


In un programma C++, il nome delle variabili può essere messo in relazione con un
indirizzo di memoria che conterrà il valore della variabile.
Tale valore è memorizzato con una sequenza di bit che, sulla base del tipo di dato
della variabile, è decodificato dal compilatore in un numero intero, un numero reale,
un carattere ecc. Gli operatori finora introdotti agivano sul valore delle variabili (sui
bit) solo dopo averlo decodificato in ciò che rappresentava (un intero, un reale, un
carattere ecc.).
Gli operatori sui bit agiscono direttamente sul valore delle variabili senza decodi-
ficarlo; operano, quindi, sulle rappresentazioni binarie dei loro operandi e possono
essere applicati esclusivamente a dati di tipo intero o carattere, fornendo come risul-
tato un numero intero. In questo tipo di operazioni, il valore 1 di un bit corrisponde
al valore booleano true e, conseguentemente, il valore 0 corrisponde al valore boo-
leano false.
Gli operatori sui bit sono:
& AND bit a bit
| OR bit a bit
^ XOR bit a bit
~ Complemento a 1
>> Shift a destra con segno; preserva il segno nella rappresentazione in com-
plemento a 2 dell’operando, quindi inserisce 1 nelle posizioni se il primo
bit è 1 (numero negativo), inserisce 0 nelle posizioni se il primo bit è 0
(numero positivo)
<< Shift a sinistra con riempimento di zeri
Facciamo un esempio.
Consideriamo il seguente programma.

Esempio 2
Gli operatori sui bit (vedi sito: CPP.A2.01.C)

#include <iostream.h>
int main( )
{
unsigned short int Z, X=1, Y=2;
cout << "X = " << X << " Y= " << Y << '\n';
Z = X & Y; // operatore &
cout << "Z = X & Y = " << Z << '\n';
Z = X | Y; // operatore |
cout << "Z = X | Y = " << Z << '\n';
Z = Y ^ 2; // operatore ^
cout << "Z = Y ^ 4 = " << Z << '\n';
Z = Y | 2; // operatore |
cout << "Z = Y | 4 = " << Z << '\n';
Z = ~X; // operatore ~
cout << "Z = ~ X = " << Z << '\n';
return 0;
}

Compilandolo ed eseguendolo otteniamo a video il seguente risultato:

Unità didattica A2: Elementi lessicali ed espressioni 33


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 34

Stiamo considerando variabili di tipo unsigned short int. Sappiamo che tali operan-
di hanno una rappresentazione a 16 bit (2 byte). Avremo allora nel codice preceden-
te, nel caso dell’istruzione relativa all’operatore | :

rappresentazione di X 00000000 0000001


rappresentazione di Y 00000000 0000010
risultato finale X | Y 00000000 0000011 che corrisponde al numero intero 3

Gli operatori di shift effettuano uno spostamento di tutti i bit dell’operando sinistro
verso destra (>>) o verso sinistra (<< ) di un numero di posizioni specificato dall’ope-
rando destro. I nuovi bit che si creano vengono impostati a 1 o a 0 a seconda del segno
dell’operando, mentre i bit che escono (a causa dello spostamento) vengono persi.

Ad esempio, supponendo che il contenuto dell’ultimo byte della variabile X sia pari a
10010011, l’espressione:
X >> 2

fornisce come risultato 11100100 poiché l’operatore >> propaga il bit di segno nelle
prime due posizioni iniziali:

rappresentazione iniziale

1 0 0 1 0 0 1 1

1 1 1 0 0 1 0 0 perso perso

>> propaga il bit di segno rappresentazione finale

Facciamo un esempio considerando, per semplicità, sempre rappresentazioni a 8


bit. Valutiamo l’istruzione:
Y = 3 | (1 << 2);

Avremo:
rappresentazione di 3 00000000 0000011
rappresentazione di 1<<2 00000000 0000100
risultato finale 3 | (1<<2) 00000000 0000111 che corrisponde a 7
Le operazioni di shift sono utili per decodificare l’input di un dispositivo esterno,
quale un convertitore analogico/digitale, e consentono anche operazioni velocissime
tra interi in quanto uno scorrimento a destra divide un numero per 2 e uno scorri-
mento a sinistra lo moltiplica per 2.

L’operatore di complemento a 1, invece, inverte lo stato dei bit, per cui ogni 1 sarà
tramutato in 0 e viceversa.
Naturalmente due operazioni di complemento successive sullo stesso numero pro-
ducono il numero originale.
Dalla teoria sappiamo che:
• lo shift a sinistra di n posizioni equivale a moltiplicare per 2n: x<<n equivale a x⋅2n;
• lo shift verso destra di n posizioni equivale a dividere per 2n: x>>n equivale a x/2n.
Compilando ed eseguendo il seguente programma:

34 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 35

Esempio 3
Gli operatori di shift (vedi sito: CPP.A2.02.C)

#include <iostream.h>
int main( )
{
unsigned short int X=1;
X = (X<< 2);
cout << "dopo lo shift a sinistra X = " << X << '\n';
X = (X >> 2);
cout << "dopo lo shift a destra X = " << X << '\n';
return 0;
}

otteniamo a video:

7.6 Un operatore particolare: l’operatore sizeof


L’operatore sizeof restituisce la dimensione in byte della memoria occupata da un
tipo di dato o da una determinata variabile definita nel programma. La sua sintassi è:
sizeof(<TipoDato>);
sizeof(<NomeVariabile>);

anche se in realtà al posto di <NomeVariabile> è possibile sostituire un’espressione


qualsiasi (anche un casting, che vedremo successivamente).
Il codice che segue è un esempio di utilizzo di sizeof ma anche un pratico modo
per vedere le dimensioni dei tipi primitivi del C++.

Esempio 4
Utilizzo di sizeof (vedi sito: CPP.A2.03.C)

#include <iostream.h>
int main( )
{
char C;
cout << "sizeof(char) = " <<'\t' <<'\t'<< '\t' << sizeof(C) << '\n';
unsigned short int USI;
cout << "sizeof(unsigned short int) = " <<'\t'<< sizeof(USI) << '\n';
unsigned int UI;
cout << "sizeof(unsigned int) = " <<'\t' <<'\t'<< sizeof(UI) << '\n';
int I;
cout << "sizeof(int) = " <<'\t' <<'\t'<<'\t' << sizeof(I) << '\n';
cout << "sizeof(float) = " <<'\t' <<'\t'<< sizeof(float) << '\n';
cout << "sizeof(double) = " <<'\t' <<'\t'<< sizeof(double) << '\n';
cout << "sizeof(long double) = " <<'\t' <<'\t'<< sizeof(long double) << '\n';
return 0;
}

Il risultato a video sarà il seguente:

Come si vede, è possibile specificare come parametro di sizeof anche solo il nome
del tipo primitivo. Il risultato del codice precedente potrebbe fornire risultati diffe-

Unità didattica A2: Elementi lessicali ed espressioni 35


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 36

renti se mandato in esecuzione su piattaforme diverse (nel nostro caso la piattafor-


ma utilizzata è Microsoft Visual Studio 6.0 su sistema Windows 32 bit).

7.7 Gli operatori speciali , e ?


L’operatore virgola “,” è un operatore binario e associativo da sinistra verso de-
stra. La sua sintassi è:
<Espressione1> , <Espressione2> {,<EspressioneN>}

Esso fornisce come risultato il valore dell’ultima espressione riportata a destra del-
l’ultima virgola, ottenuto utilizzando i risultati parziali delle espressioni di sinistra.
Ad esempio, il seguente frammento di codice:
int a, b, c;
c = (a=50, b=10, a-b);
cout << c << endl;

fornisce come risultato 40, perché assegna alla variabile c il valore 40 ottenuto ese-
guendo l’intera espressione (a=50 , b=10, a–b) valutata partendo da sinistra.
L’operatore ? è un operatore ternario, avente come sintassi (attenzione ai due punti):
<EspressioneLogica> ? <Espressione1> : <Espressione2>

La semantica di questo operatore non è molto complicata. <EspressioneLogica> è


un’espressione booleana che deve essere valutata; se <EspressioneLogica> è vera,
viene valutata <Espressione1> e il suo risultato è il risultato dell’operatore ?; se
<EspressioneLogica> è falsa viene valutata <Espressione2> e il suo risultato è il ri-
sultato dell’operatore ?.
Analizziamo la seguente istruzione:
Y = ((X>0) ? 5:10);

La variabile Y assumerà valore 5 se X è un numero positivo, assumerà valore 10 se


X è negativo o nullo. Precisiamo che le parentesi sono state inserite solo per mag-
giore chiarezza, pertanto possono essere tranquillamente omesse.

8 La conversione di tipo
Il linguaggio C++, così come molti altri linguaggi compilati, utilizza solo variabili
che siano state precedentemente dichiarate, e ciò per due motivi:
• ottimizzare l’uso della memoria;
• aumentare l’affidabilità.

Il tipo di una variabile è quindi statico, cioè non può essere modificato in fase di
esecuzione, a differenza di altri linguaggi in cui si utilizza un controllo di tipo lasco,
per cui non esiste una sezione di dichiarazione di variabili: il tipo viene assegnato au-
tomaticamente in base al valore assegnato alle variabili. Pur avendo un controllo dei
tipi statico, in C++ la conversione di tipo può avvenire in due modi:
1) conversione implicita;
2) conversione esplicita.

8.1 La conversione implicita


8.1.1 Conversione tra tipi numerici interi
In un’espressione C++ è possibile utilizzare i diversi tipi interi (short, int, char, long)
messi a disposizione dal linguaggio: il risultato verrà convertito implicitamente dal
compilatore. Consideriamo, ad esempio, il seguente frammento di codice:

36 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 37

unsigned short int SI = 175;


long int LI = 1;
unsigned short int Ris = SI*LI;
cout << "Cast implicito da short a int , Ris = " << Ris << '\n';

SI*LI produce un long poiché long è il tipo più capiente. Tale valore viene memo-
rizzato in Ris che, essendo di tipo short int, causa un troncamento implicito di tale va-
lore. Proviamo a capire cosa accadrebbe se alla variabile LI venisse assegnato il va-
lore 1 000000 (fuori dal range degli short).

8.1.2 Conversione tra tipi numerici in virgola mobile


Consideriamo le seguenti istruzioni di assegnamento:
float X = 12.45;
double Y;
Y = X;

L’ultimo assegnamento non provoca errori ed è quindi consentito. Vediamo di ca-


pire perché. Se il tipo di arrivo è più “grande” del tipo di partenza, cioè con maggio-
re dimensione per memorizzare maggiore informazione, avviene una conversione
esplicita di tipo. Si può quindi trattare automaticamente uno short come un int, o un
int come un long. Analogamente, si può trattare un float come un double. In questi ca-
si, infatti, il tipo di arrivo è più grande e offre maggiore precisione: non vi è, pertan-
to, perdita di informazione: si dice allora che il tipo di partenza è stato promosso al
tipo di arrivo. Viceversa, se il tipo di arrivo è più “piccolo” del tipo di partenza, av-
viene sempre una conversione esplicita di tipo, ma con perdita di informazione. I
due processi sono schematizzati in figura A2.1.

da a
short
Promozione: Perdita:
conversione implicita conversione implicita
int ✦ Figura A2.1 Schema
senza perdita con perdita relativo a promozione
di informazione di informazione e perdita di dati in
a long da seguito a conversioni
implicite.

8.2 La conversione esplicita


La conversione esplicita può avvenire effettuando un casting esplicito o invocan-
do espliciti metodi di conversione.

8.2.1 Il casting esplicito


Se il tipo di arrivo è più “piccolo” del tipo di partenza è possibile utilizzare il mec-
canismo del casting esplicito per effettuare una conversione esplicita di tipo. Tale
meccanismo consente al programmatore di indicare esplicitamente la conversione di
tipo che intende realizzare. Vediamo come si applica il casting esplicito per la con-
versione di tipi primitivi. La sintassi è la seguente:
(<NomeTipo>) <ValoreDaConvertire>

dove <NomeTipo> è il nome del tipo di arrivo e <ValoreDaConvertire> è il tipo di par-


tenza. Supponiamo, ad esempio, di voler convertire un float in un intero. Scriveremo:
int Y;
float X = 15.45;
Y = (int) X;

e avremo come risultato la perdita delle cifre decimali, cioè Y varrà 15. Attenzione: in
questo caso vi sarebbe stato comunque un casting implicito.

Unità didattica A2: Elementi lessicali ed espressioni 37


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 38

Il C++ permette la conversione di un float in intero, però la trasformazione provo-


ca ovviamente una perdita di informazione. Bisogna utilizzare un casting tutte le vol-
te che nella conversione si può verificare la perdita di informazione. Nella tabella che
segue vengono riassunti alcuni casting consentiti, con il relativo effetto finale.

TIPO DI ARRIVO TIPO DI PARTENZA EFFETTO

char int (32 bit) Perdita 24 bit più significativi


char short int Perdita 8 bit più significativi
short int (16 bit) int (32 bit) Perdita 16 bit più significativi
int float double Perdita parte decimale
float int long Nessuna perdita
double long Perdita di informazioni
double float Perdita di precisione, il risultato
viene arrotondato

8.2.2 Metodi di conversione


Un altro modo per convertire esplicitamente un operando di tipo numerico consi-
ste nel far ricorso a metodi espliciti di conversione che hanno la seguente sintassi:
<NomeTipo>(<Operando>)

dove <NomeTipo> è il nome di un qualsiasi tipo primitivo, ad esempio:


• int(<Operando>) per convertire l’operando dal suo tipo di partenza al tipo intero;
• double(<Operando>) per convertire l’operando dal tipo di partenza al double.
Applichiamoli nel seguente frammento di codice:
int X;
float Y = 80.56;
double Z;
X = int(Y); // perdita di informazione
Z = double(Y); // promozione di tipo

Riassumiamo in un esempio conclusivo tutti i tipi di conversione finora analizzati.

Esempio 5
Conversione implicita ed esplicita (vedi sito: CPP.A2.04.C)

#include <iostream.h>
int main( )
{
int Eta=18;
float Altezza=180.50;
cout << "Eta = " << Eta << " Altezza = " << Altezza << '\n';
// casting implicito
double Y= Altezza;
cout << "Cast implicito da double a float, Y= " << Y << '\n';
//casting esplicito
int Q= (int) Altezza;
cout << "Cast esplicito da float a int, Q= " << Q << '\n';
//invocazione di metodi di conversione esplicita
int P = int(Altezza);
cout << "Utilizzo di metodi di conversione da double a int P= " << P << '\n';
// conversione implicita da int a double
double Z = 5.0;
Z=P+Z;
cout << "Somma di un int con un double Z = " << Z << '\n';
return 0;
}

38 Modulo A: Fondamenti del linguaggio C++


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 39

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Come si può inserire un commento in un 10. Qual è la differenza tra la dichiarazione di una
programma C++? costante effettuata con const e una svolta con
2. Come deve iniziare un identificatore in C++? #define?
3. Quali dei seguenti sono identificatori validi in C++? 11. Come agiscono gli operatori sui bit?
a) 125abc 12. In che cosa consiste la conversione di tipo?
b) _ciao_ 13. Quanti metodi di conversioni esistono in C++?
c) misura altezza 14. Che cosa si intende con casting?
d) MIsuraALTezza 15. Quale tipo primitivo può contenere numeri interi
compresi tra –32768 e +32767?
e) a
a) int
4. Sono considerati validi dal C++ gli identificatori che
iniziano con un doppio underscore _ _? b) char
5. Come viene scritta una costante intera ottale? E c) short
una intera esadecimale? d) long
6. 1 è diverso da ‘1’? Se sì, perché? 16. Quale tipo primitivo può contenere numeri compresi
7. Che cosa sono le sequenze di escape? tra –128 e +127?
8. Qual è la funzione svolta dai qualificatori signed e a) long
unsigned? b) float
9. Come vengono dichiarate le variabili in un c) double
programma C++? d) short

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Scrivere le scritture compatte equivalenti alle seguenti istruzioni:

X = X + Y;
X = X – Y;
X = X * Y;
X = X / Y;
X = X % Y;

2. Trovare gli errori presenti nel seguente frammento di codice.

int main( )
{
float a, b; /* questo è un commento
corretto.
a = 100 // questo commento è valido // + 5;
b = 10 // questo commento è valido
return 0;
}

Unità didattica A2: Elementi lessicali ed espressioni 39


p021-040_CeC_Unità A2 26-09-2007 16:29 Pagina 40

Verifica
di fine unità

3. Qual è l’output prodotto dal seguente programma (vedi sito: CPP.A2.05.C)?

#include <iostream.h>
int main( )
{
int A = 10, B = 3;
cout << A << " + " << B << " = " << (A + B) << endl;
cout << A << " - " << B << " = " << (A - B) << endl;
cout << " - " << B << " = " << (-B) << endl;
cout << A << " * " << B << " = " << (A * B) << endl;
cout << A << " / " << B << " = " << (A / B) << endl;
cout << A << " % " << B << " = " << (A % B) << endl;
return 0;
}

4. Qual è l’output prodotto dal seguente programma (vedi sito: CPP.A2.06.C)?

#include <iostream.h>
int main( )
{
int A = 10, B = 3;
cout << "A = " << A << ", B = " << B << endl;
++A;
−−B;
cout << "A = " << A << ", B = " << B << endl;
A++;
B−−;
cout << "A = " << A << ", B = " << B << endl;
return 0;
}

5. Qual è il risultato fornito dalle seguenti espressioni?

!(1 || 0)
!(1 || 1 && 0)
!((1 || 0) && 0)

6. Relativamente all’operazione di casting, completare i commenti del seguente frammento di codice riportando
l’output fornito dalle istruzioni:

int a = 10, b = 20;


float c;

c = a/b; // Si ottiene c = _____________

c = (float) a/b; // a e' riqualificato come ___________ e si ottiene


// c = _________________

c = (float) (a/b); // viene riqualificato ____________ e si ottiene


// ancora c = ______________

40 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 41

Unità didattica

Istruzioni
e strutture di controllo A3
● Concetto di variabile e di costante Prerequisiti
● Generalità sui tipi di dati
● Costrutti fondamentali della programmazione strutturata
● Saper risolvere semplici problemi tramite algoritmi
● Saper compilare ed eseguire un’applicazione C++

● Acquisire le conoscenze e le competenze di base necessarie a scrivere programmi Obiettivi


con strutture di controllo e istruzioni di input-output

● Conoscere le varie istruzioni C++ e pervenire a una loro classificazione Conoscenze


● Conoscere i costrutti del linguaggio C++ da apprendere
● Conoscere differenze e peculiarità delle istruzioni iterative

● Saper gestire l’input/output dei dati Competenze


● Saper applicare correttamente i vari costrutti del linguaggio C++ da acquisire
● Saper scegliere i tipi di dati più idonei per giungere alla soluzione di un problema
● Saper scegliere il tipo di iterazione più appropriata alle esigenze di un problema

1 Le istruzioni di input/output
Nel corso delle precedenti Unità didattiche abbiamo spesso utilizzato le istruzioni
di output servendoci dell’istruzione cout. Abbiamo anche visto che per accettare i
dati inseriti dall’unità di input predefinita, in genere la tastiera, occorre utilizzare la
funzione cin (cin è abbreviazione di console input) seguita dall’operatore >>. Il sim-
bolo >> è detto operatore di input. Se diversi oggetti vengono inviati da cin, essi pro-
cedono in fila, uno dopo l’altro, mentre vengono immessi nel flusso. La sintassi pro-
posta era la seguente:
cin >> <NomeVariabile> {>> <NomeVariabile> };

Proviamo ora a svolgere un semplice esercizio in modo da applicare l’istruzione di


input appena vista: calcolare l’area di un triangolo dati in input il valore della base e
dell’altezza.

Unità didattica A3: Istruzioni e strutture di controllo 41


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 42

Esempio 1
Calcolo dell’area di un triangolo (vedi sito: CPP.A3.01.C)

#include <iostream.h>
int main( )
{
float Base, Altezza, Area;
cout << "Inserire la misura della base ";
cin >> Base; // viene richiesto un valore da tastiera per la variabile Base
cout << "Inserire la misura dell’altezza ";
cin >> Altezza; // viene richiesto un valore da tastiera per la variabile Altezza
Area = (Base * Altezza) / 2;
cout << "L’area del triangolo misura " << Area << endl;
return 0;
}

Supponendo di fornire i valori 10 e 20 per le misure della base e dell’altezza, il ri-


sultato sarà il seguente:

Come si evince dall’output, nell’istruzione cin non è necessario utilizzare il carat-


tere non stampabile \n né il manipolatore endl: una volta effettuato l’input da tastie-
ra, il compilatore andrà automaticamente alla riga successiva.

2 Le istruzioni di salto
Le istruzioni di salto sono particolari istruzioni che modificano il flusso dell’ela-
borazione indipendentemente dal verificarsi di una specifica condizione (ossia mo-
dificano il flusso in modo incondizionato).
Il C++ prevede quattro istruzioni di salto, rispettivamente: break, continue, return e
goto.
Le istruzioni break e continue (che saranno trattate in dettaglio nel paragrafo 4.4)
sono utilizzate per modificare il flusso delle istruzioni di selezione e iterative (si ve-
da più avanti), mentre l’istruzione return è utilizzata per terminare l’esecuzione di
una funzione (si faccia riferimento alle Unità didattiche successive).
La tanto criticata istruzione goto è impiegata per eseguire salti incondizionati. La
cattiva fama di questa istruzione deriva dal fatto che il suo uso tende a rendere og-
gettivamente incomprensibile un programma; tuttavia, in determinate circostanze
(in particolare nelle applicazioni real-time) in cui la priorità assoluta è data alle pre-
stazioni, l’uso del goto consente di ridurre notevolmente i tempi. Comunque, quando
possibile, è sempre meglio evitare di ricorrervi.
L’istruzione goto prevede che l’istruzione alla quale saltare sia etichettata attra-
verso un identificatore.
La sintassi della dichiarazione dell’etichetta è la seguente:
<Etichetta> : <Istruzione>;

Il salto a un’istruzione si opera secondo la sintassi:


goto <Etichetta>;

42 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 43

Ad esempio:
if (Voto == 10) goto ESITO;
...
ESITO : cout << "Hai raggiunto il massimo dei voti!";

3 Le istruzioni condizionali (o di selezione)


Il C++ utilizza quattro istruzioni condizionali principali: if, if..else, ? e switch. L’i-
struzione ?, per la sua estrema semplicità, è già stata esaminata nell’Unità didattica
precedente. Prima di affrontare lo studio delle altre istruzioni di selezione è dovero-
so introdurre il concetto di blocco.
Le istruzioni condizionali consentono di eseguire in modo selettivo una singola ri-
ga di codice o una serie di righe di codice (che viene detta blocco di codice). Quan-
do all’istruzione condizionale è associata una sola riga di codice non è necessario
racchiudere l’istruzione da eseguire fra parentesi graffe. Se invece sono associate più
righe di codice, le stesse (ossia il blocco) dovranno essere racchiuse fra parentesi
graffe. Volendo, è comunque possibile utilizzare tali parentesi anche quando l’istru-
zione da eseguire è soltanto una: il compilatore non segnalerà alcun errore.

3.1 L’istruzione if..else


La struttura alternativa viene implementata in C++ attraverso l’istruzione if..else.
È un’istruzione molto intuitiva se si considera che le parole inglesi if ed else corri-
spondono rispettivamente alle italiane “se” e “altrimenti”. Secondo i canoni della pro-
grammazione strutturata, la struttura alternativa può presentarsi con o senza il ramo
altrimenti. In C++ la sintassi da seguire per codificare le due forme è la seguente:
if (<Condizione>)
[{]
<Istruzione/i da eseguire se la condizione è vera>;
[}]

if (<Condizione>)
[{]
<Istruzione/i da eseguire se la condizione è vera>;
[}]
else
[{]
<Istruzione/i da eseguire se la condizione è falsa>;
[}]

Nel caso in cui sia necessario condizionare anche le istruzioni rette da else (le co-
siddette if in cascata) si adopererà la seguente sintassi:

if (<Condizione1>)
[{]
<Istruzione/i da eseguire solo se la Condizione1 è vera>;
[}]
else if (<Condizione2>)
[{]
<Istruzione/i da eseguire se la Condizione1 è falsa e la Condizione2 è vera>;
[}]

Risolviamo a titolo di esempio un semplice problema: dato in input un numero N,


stabilire se è divisibile per 3. Il codice C++ è il seguente:

Unità didattica A3: Istruzioni e strutture di controllo 43


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 44

Esempio 2
Stabilire se un numero è divisibile per 3 (vedi sito: CPP.A3.02.C)

#include <iostream.h>
int main( )
{
int N;
cout << "Inserire un numero ";
cin >> N;
if ((N % 3) == 0)
cout << "Il numero inserito e' divisibile per 3" << endl;
else
cout << "Il numero inserito non e' divisibile per 3" << endl;
return 0;
}

All’interno di un’istruzione if è possibile inserire qualsiasi tipo di istruzione, anche


una nuova istruzione if: si parla, in tal caso, di if annidate o nidificate. Quando si uti-
lizzano più istruzioni if annidate, occorre sempre essere sicuri dell’azione else che
verrà associata a una determinata if. Proviamo a comprendere cosa accade nel se-
guente esempio:

if (Voto < 10)


if (Voto < 5) cout << "Sei totalmente insufficiente! \n";
else cout << " Forza! Ricomincia a studiare! \n";

L’assenza di indentazione e aver scritto più istruzioni su una stessa riga non rende
il codice particolarmente leggibile. In queste circostanze è facile che il programma-
tore poco esperto possa confonderne il flusso di esecuzione, non comprendendo a
quale istruzione if fa riferimento l’istruzione else finale.
Il principio base, in queste situazioni, stabilisce che un’istruzione else è sempre ri-
ferita all’ultima istruzione if presente nel codice. È utile non confondersi troppo le
idee con questo principio, poiché ci si potrebbe imbattere in casi in cui compaiono
tante istruzioni if..else nidificate, con la conseguenza che la lettura del codice sareb-
be davvero difficile per chiunque. Per rendere il codice leggibile è perciò buona nor-
ma far sempre uso di una corretta indentazione e, ove necessario, delle parentesi
graffe. L’esempio precedente, indentato e corredato di parentesi, diviene:
if (Voto < 10)
{
if (Voto < 5)
{
cout << "Sei totalmente insufficiente! \n";
}
else
{
cout << "Forza! Ricomincia a studiare! \n";
}
}

che è senz’altro più comprensibile. Adesso, infatti, diventa chiaro che l’istruzione el-
se è riferita all’istruzione if (voto < 5).
A titolo di esempio, riportiamo un esercizio che ingloba al suo interno il concetto
di casting esaminato nell’Unità didattica precedente e il costrutto if. Il seguente pro-
gramma accetta in input un carattere scritto in maiuscolo e lo converte in minusco-
lo e viceversa, sfruttando il fatto che nella codifica ASCII gli insiemi di caratteri mi-
nuscoli e maiuscoli sono rappresentati come numeri interi consecutivi.

44 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 45

Esempio 3
Conversione minuscolo/maiuscolo e viceversa (vedi sito: CPP.A3.03.C)

#include <iostream.h>
char C;
int main( )
{
cout << "Inserisci un carattere ";
cin >> C;
if (C >= 'a' && C <= 'z')
{
cout << "Il carattere e' in minuscolo " << endl;
cout << "Il maiuscolo e' " << (char) ('A' + C - 'a') << endl;
}
else if (C >= 'A' && C <= 'Z')
{
cout << "Il carattere e' in maiuscolo " << endl;
cout << "Il minuscolo e' " << (char) ('a' + C - 'A') << endl;
}
else cout << "Non hai inserito un carattere valido " << endl;
return 0;
}

3.2 L’istruzione switch


L’istruzione di selezione multipla è implementata in C++ con l’istruzione switch. Ta-
le istruzione permette di evitare noiose sequenze di if in cascata e risulta particolar-
mente utile quando in un programma si deve dare all’utente la possibilità di sceglie-
re tra più opzioni. La sintassi di switch è:

switch (<EspressioneIntera>)
{
case <ValoreCostante1>:
<Istruzioni1>;
[break;]
case <ValoreCostante2>:
<Istruzioni2>;
[break;]
...
[default:
<Istruzioni>;]
}

Il flusso logico in esecuzione relativo al costrutto precedente può essere così sin-
teticamente descritto:
• si valuta il valore dell’espressione intera passata come parametro all’istruzione
switch;
• l’esecuzione del programma viene rimandata al blocco in cui il parametro dell’i-
struzione case ha lo stesso valore di quello dell’istruzione switch;
• se il blocco individuato termina con un’istruzione break allora il programma esce
dal costrutto switch, altrimenti vengono eseguiti anche i blocchi successivi finché
non è individuata un’istruzione break oppure non si raggiunge l’ultimo blocco del-
lo switch;
• se nessun blocco corrisponde a un valore uguale a quello dell’istruzione switch, al-
lora viene eseguito il blocco default, se presente.
È importante sottolineare che le istruzioni presenti nel blocco di un’istruzione ca-
se non devono essere racchiuse tra parentesi graffe.

Unità didattica A3: Istruzioni e strutture di controllo 45


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 46

Vediamo un frammento di codice utile per comprendere meglio quanto detto:


....
GiorniRimasti = 0;
cout << "Inserire il numero corrispondente al mese ";
cin >> NumMese >> endl;
switch (NumMese)
{
case 1: // Gennaio
GiorniRimasti += 31;
...
case 10: // Ottobre
GiorniRimasti += 31;
case 11: // Novembre
GiorniRimasti += 30;
case 12: // Dicembre
GiorniRimasti += 31;
}
...

Nell’esempio, viene valutato il valore della variabile NumMese passata come para-
metro a switch. A seconda del valore che tale variabile assume viene poi eseguito il
relativo blocco di istruzioni. Se il numero del mese è 10, cioè ottobre, viene ovvia-
mente eseguito il blocco corrispondente, ma poiché manca l’istruzione break alla fine
di tale blocco, il programma continua a eseguire anche le istruzioni del blocco suc-
cessivo (quello con il numero del mese pari a 11) e quello ancora dopo (quello con il
numero del mese pari a 12) memorizzando nella variabile GiorniRimasti il valore 92,
ossia il numero di giorni rimasti sino alla fine dell’anno. Questo accade perché il C++
(proprio come il C) prevede il fall-through automatico tra le clausole dello switch: il
controllo passa da una clausola case alla successiva (default compreso) anche quan-
do la clausola viene eseguita. Per evitare ciò è sufficiente terminare le clausole con
break in modo che, alla fine dell’esecuzione della clausola, termini anche lo switch.
Cerchiamo ora di comprendere l’utilità dell’istruzione break modificando l’esem-
pio precedente. Supponiamo che il problema da risolvere sia il seguente: fornito in
input un numero corrispondente a un mese, visualizzare tale mese.

...
cout << "Inserire il numero corrispondente al mese ";
cin >> NumMese >> endl;
switch (NumMese)
{
case 1: // Gennaio
cout << "Gennaio" << endl;
break;
...
case 10: // Ottobre
cout << "Ottobre" << endl;
break;
case 11: // Novembre
cout << "Novembre" << endl;
break;
case 12: // Dicembre
cout << "Dicembre" << endl;
break;
default:
cout << "Numero non valido" << endl;
...
}
...

46 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 47

L’esecuzione di questo frammento di codice, nell’ipotesi che l’utente digiti il nume-


ro 10, fornirà in output la stringa “Ottobre” e, incontrando l’istruzione break, uscirà
dal costrutto switch.
Se l’utente digita un valore non compreso nell’intervallo 1 ÷ 12 verrà eseguita l’istru-
zione default, che visualizzerà il messaggio Numero non valido.

4 Le istruzioni iterative
Nella risoluzione dei problemi capita molto spesso di dover ripetere molte volte
una serie di istruzioni. È evidente, quindi, la necessità di disporre di una struttura in
grado di consentire la ripetizione di una sezione di codice per un numero finito di vol-
te: la struttura iterativa o ciclica.
Il linguaggio C++ dispone di tre tipi di istruzioni iterative: while, do..while, for.
Ognuno di essi ha il proprio specifico utilizzo.

4.1 I costrutti while e do..while


I costrutti while e do..while consentono l’esecuzione ripetuta di una sequenza di
istruzioni in base al valore di verità di una condizione.
Vediamo innanzitutto la sintassi di while:
while (<Condizione>)
<Istruzione>;

Al solito, <Istruzione> indica un’istruzione singola o una sequenza di istruzioni (un


blocco); in quest’ultimo caso va racchiusa tra parentesi graffe. Ciononostante, può
essere utile racchiudere tra parentesi <Istruzione> anche se è una sola.
La semantica del costrutto while è la seguente: si valuta <Condizione> e se essa ri-
sulta vera (true) si esegue <Istruzione>, poi si ripete il tutto; l’istruzione termina
quando la valutazione di <Condizione> fornisce il risultato false.
Vediamo subito un semplice esempio che consentirà di comprendere l’utilizzo del
costrutto while. Il problema che risolviamo è il seguente: dati in input N numeri, cal-
colare la loro somma.

Esempio 4
Somma di N numeri (vedi sito: CPP.A3.04.C)

#include <iostream.h>
int main( )
{
int Num;
int K = 1;
int SommaNumeri = 0;
cout << "Inserire un numero positivo ";
cin >> Num;
while (K <= Num)
{
SommaNumeri += K++;
}
cout <<"La somma dei numeri da 1 a " << Num << " e' " << SommaNumeri << endl;
return 0;
}

Analizziamo ancora un altro esercizio che contiene alcuni cicli while annidati: dati
in input N numeri interi, scomporli in fattori primi.

Unità didattica A3: Istruzioni e strutture di controllo 47


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 48

Esempio 5
Scomposizione in fattori primi (vedi sito: CPP.A3.05.C)

#include <iostream.h>
int main( )
{
int Count;
char Risposta;
bool Continuo = true;
unsigned long N,D;
while (Continuo) // significa "mentre Continuo assume valore vero"
{
cout << "Inserisci un numero intero ";
cin >> N;
D = 2;
while (N > 1)
{
while (N%D != 0) D++; // trovo i vari divisori primi di N partendo dal più piccolo
N /= D;
Count = 1;
while (N%D == 0) // trovo quante volte il divisore attuale divide N
{
N /= D;
Count++;
}
cout << "\n\t"<< D << " elevato a "<< Count << endl;
}
cout << "\n" << "Vuoi continuare? (s/n)";
cin >> Risposta;
if (Risposta == 'n')
Continuo = false;
}
return 0;
}

Esaminiamo ora l’altro costrutto, do..while; la sua sintassi è:


do
<Istruzione>;
while (<Condizione>);

Ancora una volta, <Istruzione> indica un’istruzione singola o una sequenza di istru-
zioni (in tal caso va racchiusa tra parentesi graffe).
Il do..while differisce dall’istruzione while in quanto prima si esegue <Istruzione> e
poi si valuta <Condizione>: se essa è vera (true) si riesegue il corpo, altrimenti l’i-
struzione termina. È evidente che il corpo del ciclo do..while viene sempre eseguito
almeno una volta.
Un classico utilizzo di una simile iterazione (detta iterazione postcondizionale) è
quello relativo al controllo di validità dei dati di ingresso. Analizziamo il seguente
programma, che continuerà a richiedere in input un numero sino a quando non si in-
serisce un valore compreso tra 10 e 20.

Esempio 6
Controllo della validità dei dati in ingresso (vedi sito: CPP.A3.06.C)

#include <iostream.h>
int main( )
{
int N;
do
{
cout << "Inserire un valore intero compreso tra 10 e 20: ";

48 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 49

cin >> N;
}
while (N < 10 || N > 20);
cout << "Il numero inserito e' " << N << endl;
return 0;
}

4.2 Il costrutto for


L’istruzione for viene utilizzata tradizionalmente per codificare la cosiddetta ripe-
tizione enumerativa o ripetizione con contatore: istruzioni cicliche, cioè, che devo-
no essere ripetute un numero prestabilito di volte. Come i più esperti sapranno, il ci-
clo for rappresenta una specializzazione del ciclo while: tuttavia nel linguaggio C++ la
differenza tra for e while è così sottile che i due costrutti possono essere liberamen-
te scambiati tra loro.
La sintassi dell’istruzione for è la seguente:
for ( [<InizializzazioneContatore>] ; [<Condizione>] ; [<IncrementoContatore>])
<Istruzione>;

dove:
• <InizializzazioneContatore> può essere una espressione che inizializza le variabili
del ciclo o una dichiarazione di variabili (in quest’ultimo caso le variabili dichiara-
te hanno visibilità limitata a tutto il ciclo. Della visibilità ci occuperemo dettaglia-
tamente nella successiva Unità didattica);
• <Condizione> è una qualsiasi espressione booleana. Il corpo del ciclo for sarà ese-
guito fintantoché la condizione si mantiene vera;
• <IncrementoContatore> è usualmente una istruzione di incremento o di decremen-
to del contatore da eseguire dopo ogni iterazione, ma può anche essere un’istru-
zione che comprende altre manipolazioni sulla variabile;
• <Istruzione>, come nei precedenti cicli esaminati, indica un’istruzione singola o una
sequenza di istruzioni che, in tal caso, va racchiusa tra parentesi graffe.
Come si evince dalla sintassi, i primi tre elementi appena descritti sono opzionali;
in particolare, se <Condizione> non viene specificata si assume che essa sia sempre
verificata.
Occorre prestare molta attenzione ai punto e virgola presenti nell’istruzione, poi-
ché in assenza di qualche elemento è tuttavia obbligatorio inserire tali punto e vir-
gola tra le informazioni non dichiarate.
Facciamo subito un semplice esempio: supponiamo di voler ottenere la somma di
cinque numeri interi immessi dall’utente. Il codice è il seguente:

Esempio 7
Somma di cinque numeri dati in input (vedi sito: CPP.A3.07.C)

#include <iostream.h>
int main( )
{
int I, Numero, Somma = 0;
for (I = 1; I <= 5; I++)
{
cout << "Inserire il " << I << "^ numero ";
cin >> Numero;
Somma += Numero;
}
cout << "\nLa somma e' " << Somma << endl;
return 0;
}

Unità didattica A3: Istruzioni e strutture di controllo 49


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 50

Il programma, quando incontra l’istruzione for, assegna il valore 1 alla variabile I


(la prima espressione del for), quindi controlla se il valore di I è inferiore a 5 (la se-
conda espressione): poiché l’espressione risulta vera, vengono eseguite le istruzioni
inserite nel corpo del ciclo (l’input del numero e l’aggiornamento della variabile Som-
ma). Terminate le istruzioni che compongono il ciclo si esegue l’aggiornamento di I
così come risulta dalla terza espressione contenuta nel for, si ripete il controllo con-
tenuto nella seconda espressione e si continua come prima finché il valore di I non
rende falsa la condizione.
Questo modo di agire del ciclo for è quello comune a tutti i cicli di questo tipo mes-
si a disposizione dai compilatori dei diversi linguaggi di programmazione. Il linguag-
gio C++ offre delle opzioni che espandono abbondantemente le potenzialità del ciclo
for generalizzandolo in maniera tale da inquadrarlo, come detto, quale caso partico-
lare del ciclo while. Il costrutto for, infatti, rappresenta l’istruzione iterativa univer-
sale poiché, a differenza di quanto accade in altri linguaggi di programmazione, con-
sente di implementare sia iterazioni definite sia istruzioni indefinite. Il programma
per la somma di cinque numeri positivi scritto in precedenza potrebbe, ad esempio,
venire riscritto nel seguente modo, che lo rende più generale e molto più potente:

Esempio 8
Somma di una sequenza di numeri dati in input chiusa dallo 0
(vedi sito: CPP.A3.08.C)
#include <iostream.h>
int main( )
{
int Numero, Somma;
cout << "\nInserire un intero positivo (0 per terminare): ";
cin >> Numero;
for (Somma = 0; Numero;)
{
Somma += Numero;
cout << "\nInserire un intero positivo (0 per terminare): ";
cin >> Numero;
}
cout << "\nLa somma e' " << Somma << endl;
return 0;
}

Il ciclo esegue l’azzeramento della variabile Somma (che verrà effettuato una sola
volta) e, subito dopo, controlla se il valore di Numero è diverso da zero: nel caso in
cui ciò sia vero verranno eseguite le istruzioni del ciclo. Terminate le istruzioni,
poiché manca la terza espressione del for, viene ripetuto il controllo su Numero.
L’inizializzazione della variabile Somma avrebbe potuto essere svolta fuori dal ciclo
for: in tal caso sarebbe mancata anche la prima espressione.
Poiché, nel linguaggio C++, ogni ciclo while può essere codificato utilizzando un ci-
clo for e viceversa, è bene tenere presente che la scelta del tipo di codifica da effet-
tuare va sempre fatta in modo da ottenere la massima chiarezza e leggibilità del pro-
gramma.

4.3 Un esercizio con i cicli while e for: la successione di Fibonacci


I numeri della successione di Fibonacci sono definiti dalla seguente equazione di
ricorrenza:
FIB(N) = 1 per N = 0 FIB(N) = 1 per N = 1
FIB(N) = FIB(N–1) + FIB(N–2) per N > 1
Pertanto i primi numeri di Fibonacci sono 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, …

50 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 51

Proviamo a realizzare un programma in C++ che, dato in input un numero maggiore


di 2 restituisca il corrispondente numero di Fibonacci. A titolo di esempio, conside-
riamo la sequenza poc’anzi riportata; il numero di Fibonacci di N = 5 è 8 o, meglio, il
sesto numero di Fibonacci è 8, il numero di Fibonacci per N = 2 è 2, ossia il terzo nume-
ro di Fibonacci è 2 ecc.

Esempio 9
Calcolo dei numeri di Fibonacci (vedi sito: CPP.A3.9.C)

#include <iostream.h>
int N, FibonacciA, FibonacciB;
char Continuo;

int main( )
{
do
{
do
{
cout << "\nInserisci un numero >=2 ";
cin >> N;
}
while (N < 2);
FibonacciA = 0;
FibonacciB = 1;
for (int I = 1; I < N; I++)
{
FibonacciB += FibonacciA;
FibonacciA = FibonacciB - FibonacciA;
}
cout << "\nIl Numero di Fibonacci di " << N << " e' " << FibonacciB << endl;
cout << "\nContinui [s/n]? ";
cin >> Continuo;
}
while (Continuo != 'n' && Continuo != 'N');
return 0;
}

4.4 Le istruzioni break e continue


Le istruzioni break e continue consentono di ottenere un maggiore controllo sui ci-
cli. Nessuna delle due istruzioni accetta argomenti.
L’istruzione break, già esaminata a proposito dell’istruzione switch, può essere uti-
lizzata anche dentro un ciclo causandone la terminazione, ossia “il salto” di tutte le
istruzioni rimanenti del gruppo di istruzioni nel ciclo e la prosecuzione con l’istru-
zione successiva alla fine del ciclo stesso.
A differenza dell’istruzione break, l’istruzione continue può essere utilizzata solo
dentro un ciclo e causa l’interruzione della corrente esecuzione del corpo del ciclo;
a differenza di break, quindi, il controllo non viene passato all’istruzione successiva
al ciclo ma all’inizio di questo, per eseguire una nuova iterazione.
Esaminiamo un piccolo programma che contiene l’istruzione continue, il cui scopo
è di stampare il quadrato dei numeri pari minori di 100.

Esempio 10
Un esempio dell’istruzione continue (vedi sito: CPP.A3.10.C)

#include <iostream.h>
int main( )
{

Unità didattica A3: Istruzioni e strutture di controllo 51


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 52

int I;
for (I = 0; I < 100; I++)
{
if (I % 2)
continue;
cout << "Il quadrato di " << I << " e' " << I * I << endl;
}
return 0;
}

4.5 Le iterazioni infinite


Per definizione, un ciclo deve terminare dopo l’esecuzione di un numero finito di
passi. Affinché ciò possa avvenire è necessario che a un certo punto le istruzioni ese-
guite nel ciclo facciano assumere all’espressione condizionale il valore booleano ne-
cessario a provocarne l’uscita (false nel caso di iterazione precondizionale e post-
condizionale).
Talvolta può essere utile scrivere un ciclo infinito: nel caso di iterazione precondi-
zionale sarà sufficiente porre l’espressione condizionale sempre a true. Ad esempio:
...
while (1)
{
...
}
...

Il valore 1, essendo diverso da 0, è interpretato come true, pertanto il corpo del ci-
clo sarà eseguito all’infinito a meno che non si inserisca al suo interno un’istruzione
break, return o goto che trasferirà il controllo al di fuori del ciclo.
Analizziamo il seguente frammento di codice: il ciclo in esso contenuto verrà ripe-
tuto fino a quando non viene digitato un dato corretto.
...
while (1)
{
cout << "\nConfermi? [s/n] ";
cin >> Risposta;
if (Risposta == 's' || Risposta == 'n') // risposta corretta
break;
else // e' stato commesso un errore
cout << "\a"; // emette un beep
}
...

Anche con l’istruzione for è possibile realizzare un ciclo infinito, semplicemente


eliminando i parametri dell’istruzione e lasciando solo i punto e virgola.

52 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 53

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Come vengono classificate le istruzioni in C++? 12. In un costrutto switch, è sempre necessaria la
2. L’istruzione iterativa è un’istruzione semplice o presenza dell’istruzione break? Perché?
strutturata? 13. Qual è la differenza tra i cicli while e do...while?
3. Qual è l’operatore di input? 14. Quale costrutto iterativo del C++ implementa
4. A che cosa serve l’istruzione goto? l’iterazione precondizionale?
5. Qual è la funzione svolta dalle istruzioni di salto? 15. Quale costrutto iterativo del C++ implementa
l’iterazione postcondizionale?
6. Che cosa sono le etichette?
16. Il linguaggio C++ mette a disposizione opzioni
7. Perché l’istruzione goto è così tanto criticata?
che espandono abbondantemente le potenzialità
8. Che cos’è un blocco? del ciclo for. Spiegare il significato di tale
9. Che cosa si intende con if in cascata? E con if affermazione.
annidate? 17. A che cosa servono le istruzioni break e continue?
10. È corretto dire che l’istruzione switch è Quali sono le differenze tra di esse, in termini
un’istruzione di selezione? Se sì, perché? operativi?
11. Il C++ (proprio come il C) prevede il fall-through 18. Come si realizza in C++ un’iterazione infinita?
automatico tra le clausole dello switch. Qual è il
significato di tale affermazione?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Il seguente programma C++ non utilizza l’istruzione if in maniera ottimale; inoltre presenta degli errori e non è
scritto secondo le regole di una corretta indentazione. Interpretare il programma e provare a impostarlo in
maniera più efficiente.

#include <iostream.h>
int K;

int main( )
{
cout << "Inserisci un numero ";
cin >> k;
if (k==5)
cout << "Numero uguale a cinque " << endl;
k++;
cout << "Il nuovo valore di k e' " << k << endl;

cout << "Inserisci un numero ";


cin >> k;

if (k==5) {
cout << "Numero uguale a cinque " << endl;
k++;
}
cout << "Il nuovo valore di k e' " << k << endl;
return 0;
}

Unità didattica A3: Istruzioni e strutture di controllo 53


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 54

Verifica
di fine unità

2. Il seguente programma C++ presenta degli errori e non è scritto secondo le regole di una corretta indentazione.
Interpretare il programma e provare a impostarlo in maniera corretta.

#include <iostream.h>
int a;

int main
{
cout << "Inserisci un numero ";
cin >> x;
if (x=3);
cout << "Numero uguale a tre " << endln;
}

3. Il seguente programma C++ presenta degli errori e non è scritto secondo le regole di una corretta indentazione.
Interpretare il programma e provare a impostarlo in maniera corretta.

#include <iostream.h>;
int h;

int main( )
{
cout << "Inserisci un numero ";
cin >>x;

if (h=3);
cout << "Numero uguale a tre "<<endl;
x++;
else cout << "Numero diverso da tre "<<endl;
return
}

4. Quale valore assume la variabile Z alla fine del programma?

#include <iostream.h>
char c;
int Z = 0;

int main( )
{
for (c = 'z'; c < 'a'; c++)
Z++;
cout << Z << endl;
return 0;
}

Esercizi sul costrutto sequenza


5. Realizzare un programma C++ che permetta di inserire un orario espresso in ore, minuti e secondi e visualizzare
un numero che esprime il numero complessivo di secondi.
6. Realizzare un programma C++ che consenta di inserire due numeri e, attraverso l’utilizzo di un menu, esegua una
delle quattro operazioni aritmetiche in base alla scelta effettuata.
7. Si vuole automatizzare il calcolo delle ore lavorative settimanali da retribuire a ciascun dipendente di una ditta.
Realizzare un programma C++ che, dati in input l’ora di entrata e l’ora di uscita riportate nel cartellino personale,
calcoli il totale delle ore da retribuire.

54 Modulo A: Fondamenti del linguaggio C++


p041-055_CeC_Unità A3 26-09-2007 16:30 Pagina 55

Verifica
di fine unità

8. Realizzare un programma C++ che determini il 16. Per aumentare il numero di visitatori di una mostra,
numero di scatti effettuati da un utente telefonico e si decide di far pagare il biglietto d’ingresso
l’ammontare della sua bolletta. Vengono forniti in differenziato in base all’età. Precisamente:
input i seguenti dati:
• il nome dell’utente; ETÀ PREZZO DEL BIGLIETTO
• il numero di scatti emersi dalla lettura della
Inferiore a 5 anni gratuito
bolletta precedente;
• il numero di scatti emersi dalla lettura della Fino a 10 anni €1
bolletta attuale; Da 11 a 17 anni € 1,50
• il costo dello scatto. Da 18 a 26 anni €2
Si ricorda, inoltre, che per determinare il valore Oltre 26 anni €3
della bolletta occorre aggiungere un canone fisso il
cui importo viene anch’esso fornito in input. Realizzare un programma C++ che, data in input
l’età del visitatore, visualizzi l’importo del biglietto
da pagare.
Esercizi sul costrutto selezione 17. Realizzare un programma C++ in grado di leggere
un numero composto da una sola cifra e di fornire in
9. Scrivere un programma C++ che, dato in input un
output la sua rappresentazione in lettere.
numero, stabilisca se è pari o dispari.
18. Realizzare un programma C++ che, ricevendo in
10. Scrivere un programma C++ che, dati in input tre
ingresso il nome di un mese, sia in grado di indicare
numeri, determini:
il numero dei giorni che lo compongono in un anno
• il maggiore;
specificato.
• il minore;
• la differenza tra il maggiore e il minore.
11. Scrivere un programma C++ che calcoli le radici di
Esercizi sul costrutto iterazione
un’equazione di secondo grado.
19. Realizzare un programma C++ che calcoli il
12. Calcolare la somma spesa da un cliente in un
quadrato dei primi N numeri naturali. Per risolvere
negozio di abbigliamento tenendo conto delle
il problema servirsi della seguente regola: il
seguenti condizioni:
quadrato di un numero x diverso da zero è uguale
• per spese inferiori a € 50, sconto 10%;
alla somma dei primi x numeri dispari. Ad esempio,
• per spese inferiori a € 100 ma superiori a € 50,
il quadrato di 5 è dato da 1 + 3 + 5 + 7 + 9 = 25.
sconto 20%;
• per spese superiori o uguali a € 100, sconto 30%. 20. Scrivere un programma C++ che, servendosi
esclusivamente delle operazioni di addizione e
13. Scrivere un programma C++ che calcoli la radice
sottrazione, calcoli il prodotto e il quoziente di due
dell’equazione di primo grado ax = b dati in input i
numeri positivi X e Y.
coefficienti a e b.
21. Realizzare un programma C++ che, dato in input un
14. In un condominio si decide di calcolare una tassa
numero intero, visualizzi la somma di tutti i numeri
una tantum rispetto alle dimensioni
naturali dispari minori o uguali a esso.
dell’appartamento, espresse in metri quadri, in
ragione di € K per ogni metro quadro. All’importo 22. Realizzare un programma C++ che, dato in input un
così calcolato viene aggiunta una quota fissa di € X numero intero positivo, restituisca il maggiore
e una percentuale del T%. Scrivere un programma numero naturale per cui esso è divisibile.
C++ che, dati in input i valori di K, X e T determini 23. Realizzare un programma C++ che, dato in input un
l’ammontare della tassa. numero intero maggiore di zero, ne visualizzi tutti i
15. Sullo stipendio dei dipendenti di una ditta viene divisori.
applicata una trattenuta fiscale in base alla 24. Realizzare un programma C++ che, dato in input
seguente tabella: un numero intero maggiore di zero, stabilisca se è
Scaglione 1: Trattenuta 5%; perfetto (un numero è perfetto se la somma dei
suoi fattori incluso 1 ed escluso se stesso è pari
Scaglione 2: Trattenuta 10%;
al numero stesso).
Scaglione 3: Trattenuta 15%;
25. Realizzare un programma C++ che, dato in input un
Scaglione 4: Trattenuta 25%; numero intero maggiore di zero, stabilisca se è
Scaglione 5: Trattenuta 35%; primo.
Realizzare un programma C++ che, dato in input lo 26. Realizzare un programma C++ che, dato in input un
stipendio e lo scaglione di appartenenza di un numero N intero maggiore di zero, calcoli la somma
dipendente, calcoli la trattenuta da versare. dei numeri compresi tra 1 e N.

Unità didattica A3: Istruzioni e strutture di controllo 55


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 56

Unità didattica

A4 Funzioni, moduli software


e file header

Prerequisiti ● Saper compilare ed eseguire un’applicazione C++


● Strutture di controllo

Obiettivi ● Riuscire a risolvere problemi complessi scomponendoli in sottoproblemi


attraverso affinamenti successivi
● Cogliere le differenze salienti fra tipi di funzioni
● Comprendere l’utilità dei moduli
● Riuscire a dichiarare le variabili in modo da garantire la visibilità desiderata

Conoscenze ● Conoscere gli ambienti e le regole di visibilità delle variabili


da apprendere ● Conoscere le tecniche per implementare i sottoprogrammi
● Conoscere le tecniche per effettuare il passaggio di parametri
● Conoscere le tecniche ricorsive
● Conoscere le metodologie per memorizzare un progetto software su più file

Competenze ● Saper scrivere programmi composti da funzioni


da acquisire ● Individuare i problemi che necessitano di routine di tipo void e no
● Saper costruire funzioni ricorsive in modo coerente
● Saper costruire e utilizzare file header
● Saper scomporre e memorizzare un programma in diversi file

1 Tipi di sottoprogrammi: procedure e funzioni


Come ogni moderno linguaggio, il C++ consente di dichiarare sottoprogrammi che
possono essere invocati nel corso dell’esecuzione di una sequenza di istruzioni a
partire da una sequenza principale (il corpo del programma).
In genere si distinguono due tipi di sottoprogrammi.
1) Le funzioni: sono sottoprogrammi che restituiscono al programma chiamante un
valore. La chiamata a una funzione produce al ritorno un valore che, ad esempio,
potrà venire assegnato a una variabile. Le funzioni vengono utilizzate principal-
mente a destra del segno di assegnazione.
2) Le procedure: sono sottoprogrammi che non restituiscono alcun valore; si occu-
pano di una fase dell’elaborazione.
Nel linguaggio C++ la componente principale dei codici è costituita da funzioni. Per
poter simulare le procedure (che non restituiscono alcun valore) è stato introdotto
il tipo void (cui si è già accennato nella prima Unità didattica di questo Modulo trat-
tando della funzione main). Ricordiamo, comunque, che il tipo void o tipo indefinito
è utilizzato dal C++ tutte le volte che il valore di ritorno di una funzione non deve es-
sere preso in considerazione. Insomma, nel linguaggio C++ le procedure sono funzio-
ni che restituiscono un void.

56 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 57

2 Costruzione e uso di una funzione


Possiamo riassumere nelle seguenti tre fasi il processo di costruzione e uso di una
funzione.
1) La definizione della funzione, cioè la scrittura dell’elenco delle operazioni da es-
sa svolte. La definizione specifica nell’ordine il tipo di valore restituito, il nome scel-
to dal programmatore (valgono le stesse regole viste per l’assegnazione dei nomi al-
le variabili) e, infine, l’elenco dei parametri. All’interno del corpo della funzione ven-
gono inserite le dichiarazioni delle variabili della funzione e le istruzioni. La sintassi
generale è quindi la seguente:
[<TipoRestituito>] <NomeFunzione> ([<ElencoParametri>])
{
<Istruzioni>
[return [(]<Risultato>[)]]
}

Come si evince dalla sintassi, la specifica del <TipoRestituito> è opzionale: si assu-


me, in sua assenza, che il tipo restituito sia int sebbene, per motivi di comprensibilità,
sia sempre opportuno dichiarare esplicitamente il tipo.
Nel caso in cui si desideri implementare una procedura occorre specificare come
<TipoRestituito> il tipo void.
Anche <ElencoParametri>, che tratteremo nei prossimi paragrafi, è opzionale: in
sua assenza occorre comunque far seguire il nome della funzione da una coppia di
parentesi tonde ( ). Nel caso in cui sia presente, cioè se la funzione riceve parametri,
sarà necessario specificare il tipo, seguito dal nome del parametro, proprio come ab-
biamo fatto per dichiarare una variabile. Anche il tipo dei parametri può essere int,
float ecc. Se a una funzione si passano più parametri, questi dovranno essere sepa-
rati da una virgola.
Le definizioni di funzioni possono essere scritte in qualsiasi punto del programma:
verranno mandate in esecuzione in seguito alla chiamata, perciò non ha alcuna im-
portanza dove sono fisicamente allocate.
Lo standard ANSI ha però fissato delle convenzioni secondo le quali è bene codifi-
care le definizioni delle funzioni dopo il main. In fondo, il main stesso altro non è che
una speciale funzione, eseguita per prima.
Fra le istruzioni contenute nella definizione della funzione assume una particolare
importanza l’istruzione return, impiegata per restituire un valore al chiamante. La
sintassi dell’istruzione prevede di specificare dopo la parola chiave return un valore
costante o una variabile o un’espressione compatibile con il tipo restituito dalla fun-
zione. Ad esempio:
return 2; // Restituisce al chiamante il valore 2
return X; // Restituisce al chiamante il valore contenuto nella variabile X
return (A+B); // Restituisce al chiamante il risultato dell’espressione specificata
return; // Ritorna al chiamante senza restituire alcun valore

Non è importante che le definizioni di tutte le funzioni usate in un programma se-


guano l’ordine con cui sono chiamate benché, per motivi di chiarezza e leggibilità, è
opportuno che sia così e che si segua l’ordine specificato prima. In ogni caso la fun-
zione con il nome main è eseguita per prima, in qualunque posto sia messa, e le fun-
zioni sono eseguite nell’ordine in cui vengono chiamate.
2) La dichiarazione del prototipo della funzione. Si tratta di ripetere all’inizio del
programma, prima della definizione della funzione main, l’intestazione delle funzioni
definite dopo. In pratica, si riscrive quanto specificato nella prima riga della defini-
zione della funzione includendo però un punto e virgola alla fine.
I prototipi sono stati introdotti per consentire al compilatore un maggiore control-

Unità didattica A4: Funzioni, moduli software e file header 57


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 58

lo sui tipi di parametri: conoscendoli in anticipo, infatti, all’atto della chiamata è pos-
sibile stabilire se i parametri passati sono congruenti con quelli attesi.
Nella costruzione di programmi complessi capita di utilizzare molte funzioni. In
questo caso le funzioni sono raccolte in librerie e i rispettivi prototipi sono rag-
gruppati nei file di intestazione (o file header). Gli header sono file di testo che han-
no estensione .h.
3) La chiamata della funzione. Consiste semplicemente nell’indicare, nel punto in
cui occorre utilizzare l’elaborazione fornita dalla funzione, il nome della funzione
stessa accompagnato dall’elenco dei parametri da trasmettere. Il programma chia-
mante può utilizzare il valore restituito dalla funzione: in tal caso il nome della fun-
zione figurerà, ad esempio, in un’espressione. Il chiamante può trascurare il valore
restituito anche se non è di tipo void: è sufficiente utilizzare la funzione senza asse-
gnare il valore da questa restituito.
Vediamo ora un esempio che mostra un prototipo e una funzione in un program-
ma: dati in input due numeri interi, fornire in output il loro prodotto.
#include <iostream.h>
double moltiplica (int X, int Y); // questo e' il prototipo della funzione

int main( )
{
int X, Y;
cout << "Inserire un numero";
cin >> X;
cout << "Inserire un altro numero";
cin >> Y;
cout << "Il prodotto e' " << moltiplica(X, Y) << endl; // chiamata della funzione
return 0;
}
double moltiplica (int X, int Y) // questa e' l’intestazione della funzione
{ // questo e' l’inizio del corpo della funzione
return X * Y;
} // questa e' la fine del corpo della funzione

Può non essere superfluo far notare che in questo programma la funzione è total-
mente inutile e può solo moltiplicare interi! L’abbiamo inserita unicamente per mo-
strarvi come si costruisce una funzione.

3 Le regole di visibilità: variabili locali e globali


Una variabile locale è visibile dal punto immediatamente successivo alla dichia-
razione (e prima dell’eventuale inizializzazione) sino alla fine del blocco di istruzioni
in cui è inserita (ricordiamo che un blocco di istruzioni è sempre racchiuso in una
coppia di parentesi graffe). Ciò significa che tale dichiarazione non è visibile all’e-
sterno di quel blocco, mentre è visibile negli eventuali blocchi annidati all’interno di
quello in cui la variabile è stata dichiarata.
Per quanto riguarda i sottoprogrammi, la variabile locale è quella dichiarata al suo
interno. Gli altri sottoprogrammi, anche se chiamati, non hanno accesso a essa: la va-
riabile locale è definita nel sottoprogramma e solo lì è utilizzabile. Se viene chiamato
un sottoprogramma, le variabili del chiamante sono mascherate e riprenderanno a
essere visibili quando il chiamato terminerà e si tornerà al chiamante. L’ambiente del
chiamante (ossia l’insieme delle variabili con i rispettivi valori) a questo punto verrà
ripristinato esattamente com’era prima della chiamata.
Una variabile globale è una variabile che non è stata dichiarata all’interno di alcun
blocco di elaborazione o di alcun sottoprogramma. Come tale, è visibile da tutti i bloc-

58 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 59

chi e da tutti i sottoprogrammi, nel senso che tutti i sottoprogrammi possono utiliz-
zarla e modificarla. In altri termini, le variabili globali sono “patrimonio comune”.
In conclusione, possiamo definire le seguenti regole di visibilità o scope rules per
determinare il campo di visibilità degli “oggetti” locali e globali di un programma:
• le variabili globali sono accessibili a tutti i sottoprogrammi;
• una variabile dichiarata in un sottoprogramma ha significato ed è visibile solo in
quel sottoprogramma;
• una variabile non può essere usata se non è stata dichiarata;
• una variabile dichiarata dentro un blocco è visibile in quel blocco e in tutti i bloc-
chi annidati;
• quando si dichiara una variabile locale con lo stesso nome di una variabile globa-
le, un riferimento a tale nome si riferirà alla variabile locale (si dice che la località
ha il sopravvento sulla globalità).
È da sottolineare che anche le variabili dichiarate all’interno della funzione main
sono variabili locali e hanno perciò visibilità solo all’interno del main. Mettiamo in ri-
salto, inoltre, che una variabile globale è visibile nel solo file sorgente in cui è defini-
ta, dalla definizione in poi e non prima.

Non è possibile dichiarare più volte lo stesso identificatore all’interno di un bloc-


co, ma è possibile ridichiararlo all’interno di un blocco annidato; in tal caso la nuo-
va dichiarazione “nasconde” quella più esterna, che ritorna visibile non appena si
esce dal blocco ove l’identificatore viene ridichiarato: tale concetto è noto come sha-
dowing della variabile.

Diamo uno sguardo al seguente frammento di codice:

#include <iostream.h>
int K; // questa e' una variabile globale
int main( )
{
int A=10; // questa e' una variabile locale al blocco esterno della funzione main
cout << A << endl;
{
int A=100; // questa e' una variabile locale al blocco interno della funzione main
cout << A << endl; // sara' visualizzato 100
}
cout << A << endl; // sara' visualizzato nuovamente 10
return 0;
}

Per riferirsi a una variabile globale si può utilizzare la notazione:


::<NomeVariabile>

dove l’operatore :: è detto risolutore di scope e permette di riferirsi alla dichiarazio-


ne globale di un identificatore.
Ad esempio:

int A=10; // variabile globale


int main( )
{
int A=80, B=0;
...
B = ::A; // a B viene assegnato 10
B=A; // assegna il valore 80
return 0;
}

Unità didattica A4: Funzioni, moduli software e file header 59


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 60

4 Il passaggio dei parametri


Il passaggio di parametri (dal chiamante al chiamato) può avvenire secondo due
specifiche modalità.
1) Per valore se il chiamante comunica al chiamato il valore che è contenuto in quel
momento in una sua variabile (parametro attuale). Il chiamato accoglierà tale va-
lore all’interno di una variabile locale opportunamente predisposta (parametro
formale). Il chiamato può operare su tale valore, può anche modificarlo ma tali
modifiche riguarderanno solo la copia locale su cui sta lavorando. Terminato il
sottoprogramma, il parametro formale viene deallocato e viene ripristinato il pa-
rametro attuale con il valore che esso conteneva prima della chiamata al sotto-
programma.
2) Per riferimento o per indirizzo se il chiamante comunica al chiamato l’indiriz-
zo di memoria di una determinata variabile (parametro attuale). Il chiamato,
per accogliere il parametro attuale, può utilizzare un parametro formale con un
nome diverso, ma le locazioni di memoria cui ci si riferisce sono sempre le stes-
se. Viene soltanto stabilito un riferimento diverso alle stesse posizioni di memo-
ria: ogni modifica effettuata sul parametro formale si ripercuoterà su quello
attuale, anche se il nuovo nome cessa di esistere alla conclusione del sottopro-
gramma.

In mancanza di specifiche indicazioni (cioè per default) si assume che il passaggio


di parametri avvenga per valore. È doveroso ricordare che la lista dei parametri at-
tuali (quelli inviati dal programma chiamante) e quella dei parametri formali (quelli
presenti nell’intestazione della funzione) devono essere coerenti rispetto a tre ele-
menti fondamentali:
• numero: il numero dei parametri formali deve essere uguale a quello dei parametri
attuali;
• tipo: parametri attuali e parametri formali corrispondenti devono essere dello stes-
so tipo;
• ordine: il passaggio dei parametri è posizionale, nel senso che il primo parametro
attuale invierà il suo contenuto al primo parametro formale, il secondo attuale al
secondo formale e così di seguito.

Al fine di chiarire ulteriori dettagli sul passaggio dei parametri, riprendiamo l’e-
sercizio svolto nel paragrafo 2, che calcolava il prodotto di due numeri interi. Nella
dichiarazione del prototipo della funzione abbiamo scritto:
double moltiplica (int X, int Y); // questo e' il prototipo della funzione

cioè abbiamo indicato i parametri formali completi di tipo e nome. Nel caso in cui
non avessimo inserito i parametri, il compilatore C++ non avrebbe segnalato un er-
rore: l’elenco dei parametri, infatti, può essere completo, o può riportare solo l’indi-
cazione del tipo. Ciò perché il compilatore non esegue alcun controllo sui nomi dei
parametri formali del prototipo. A tal proposito, relativamente alla nostra funzione
sono validi tutti e due i seguenti prototipi:

int moltiplica (int X, int Y); // prototipo valido


int moltiplica (int, int); // prototipo valido

Per meglio comprendere le due modalità di passaggio dei parametri, risolviamo al-
cuni semplici problemi.
Per la risoluzione del primo ci serviremo di un passaggio di parametri per valore:
dati in input due numeri interi A e X, calcolare il valore della potenza AX.

60 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 61

Esempio 1
Calcolo della potenza a base ed esponente interi (vedi sito: CPP.A4.01.C)

#include <iostream.h>
long double Potenza(long double B, unsigned int E);
int main( )
{
long double A;
unsigned int X;
cout << "Inserire la base"; Chiamata della funzione
cin >> A;
cout << "Inserire l’esponente";
cin>> X;
cout << "La potenza e' " << Potenza(A, X) << endl;
return 0;
}
long double Potenza(long double B, unsigned int E) Parametri attuali
{
long double P=1;
while (E>=1)
{
P *= B; Parametri formali
E ––;
}
return P;
}

Nel caso in cui occorra effettuare un passaggio di parametri per indirizzo è neces-
sario anteporre al nome del parametro formale l’operatore unario di indirizzo &
(chiamato anche operatore di dichiarazione), il cui compito è di trasformare un ar-
gomento in uno definito parametro reference. L’operatore & è un modificatore di ti-
po di dato, nel senso che crea un nome alternativo per un oggetto.
Risolviamo un semplice problema utilizzando un passaggio di parametri per indi-
rizzo: scambiare il contenuto di due variabili intere.

Esempio 2
Scambio del contenuto di due variabili intere (vedi sito: CPP.A4.02.C)

#include <iostream.h>
void Scambia (int &X, int &Y);
int main( )
{
int A, B;
cout << "Inserire il primo numero";
cin >> A;
cout << "Inserire il secondo numero";
cin>> B;
Scambia (A, B); // chiamata della funzione
cout << "\nI valori scambiati sono:\n";
cout << "a = " << A << "\n";
cout << "b = " << B << "\n\n";
return 0;
}
void Scambia (int &X, int &Y)
{
int Appoggio;
Appoggio = X;
X = Y;
Y = Appoggio;
}

Unità didattica A4: Funzioni, moduli software e file header 61


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 62

La tabella A4.1 riepiloga le differenze principali tra i due passaggi di parametri ana-
lizzati.
✦ Tabella A4.1
PASSAGGIO PER VALORE RISULTATO

int X int & X


Il parametro formale X è una variabile locale Il parametro formale X è un riferimento locale
È un duplicato del parametro attuale È un sinonimo del parametro attuale
Non può cambiare il parametro attuale Può cambiare il parametro attuale
Il parametro attuale può essere una costante, Il parametro attuale deve essere una variabile
una variabile o un’espressione
Il parametro attuale è in sola lettura Il parametro attuale è in lettura e in scrittura

5 Moduli software, linkage e variabili extern


Negli esempi sviluppati finora abbiamo sempre utilizzato un unico file sorgente
che veniva compilato per ottenere il codice eseguibile. Nei progetti software com-
plessi realizzati da team di programmatori, però, non è possibile utilizzare un solo fi-
le sorgente. Il progetto software prevede generalmente la suddivisione dell’intero la-
voro in più moduli software: un modulo software è composto da un insieme di fun-
zioni concettualmente collegate e create per risolvere un ben preciso compito. Allo
stesso modulo possono così lavorare più programmatori, ognuno dei quali si occupa
di un particolare aspetto relativo a quel modulo.
Un modulo software può essere allora composto da più file sorgenti. La suddivi-
sione di un programma in più file sorgenti è spesso indispensabile se si vuole sem-
plificare notevolmente lo sviluppo di programmi complessi sui quali lavorano più
programmatori.
All’interno di ogni modulo saranno presenti le funzioni che servono per imple-
mentare le funzionalità tipiche del modulo. Ad esempio, nel modulo Magazzino pos-
sono essere presenti le funzioni: Scorta( ), Ricarico( ), Aggiorna( ), Ricerca( ), le pri-
me due presenti sul file Mag1.cpp e le altre due nel file Mag2.cpp (figura A4.1).

✦ Figura A4.1
La composizione di un Progetto software: programma di Gestione aziendale
progetto software in
più moduli.

Modulo Modulo Modulo Modulo


Magazzino Fatturazione Scadenziario Clienti/fornitori

Funzioni: File Mag1.cpp


Scorta()
Ricarico()

Aggiorna()
Ricerca() File Mag2.cpp

Per distribuire le funzionalità dell’intero progetto software su più file occorre ca-
pire come variano le regole di visibilità delle variabili presenti in file distinti: si parla
di visibilità intermodulo (tra moduli distinti) o linkage di una variabile.
Analizziamo il seguente esempio, in cui consideriamo un programma composto da
un solo modulo software, a sua volta composto dalle funzioni: Scambia1( ), Scam-

62 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 63

bia2( ), Leggi( ) e main( ), presenti all’interno di tre file distinti che chiameremo Prin-
cipale.cpp, Scambia1.cpp e Scambia2.cpp. La funzione Leggi( ) viene utilizzata per leg-
gere un valore da input, la funzione Scambia1( ) scambia i valori delle variabili globali
X e Y, mentre la funzione Scambia2( ) scambia i valori dei due parametri passati per
indirizzo. La situazione delle funzioni e dei rispettivi file è mostrata in figura A4.2.

✦ Figura A4.2
File Principale.cpp File Scambia1.cpp Un esempio dei
problemi di visibilità
delle variabili presenti
in file distinti.
# include <iostream.h>
void Scambia1( )
int Leggi( );
{
int X, Y; int Temp1;
int Num1;
Temp1 = X;
int main( ); X = Y;
{ Y = Temp1;
X = Leggi( ); }
Y = Leggi( );
Scambia1( );
cout << "Dopo Scambia1( ): X= "<< X << "Y=" << Y << endl;
Scambia2(X, Y); File Scambia2.cpp
cout << "Dopo Scambia2( ): X= "<< X << "Y=" << Y << endl;
return 0;
}
void Scambia2(int &X, int &Y)
int Leggi( )
{
{
int Temp2;
cout << "Inserisci un valore:"; cin >> Num1;
return(Num1); Temp2 = X;
} X = Y;
Y = Temp2;
}

Affinché i tre file possano essere compilati separatamente senza generare errori di
compilazione occorrerà risolvere i seguenti problemi:
1) quando si compila Scambia1.cpp occorre comunicare al compilatore che le varia-
bili X e Y, utilizzate ma non dichiarate, sono in realtà variabili globali dichiarate
in un altro file sorgente;
2) quando si compila Principale.cpp occorre comunicare al compilatore che le fun-
zioni Scambia1( ) e Scambia2( ) cui si fa riferimento non sono definite nel file ma
in altri file sorgenti.
Per affrontare i due problemi occorrerà inserire apposite istruzioni che spieghino
al compilatore e al linker come procedere.
Per risolvere il primo problema occorre inserire nel file Scambia1.cpp una dichia-
razione (senza inizializzazione) che indica che le variabili X e Y sono variabili extern
(cioè esterne) al file:

extern int X, Y;

Per risolvere il secondo problema si inserisce nel file Principale.cpp una dichiara-
zione che indica che le funzioni Scambia1( ) e Scambia2( ) sono funzioni definite in
altri file. Si specifica quindi il prototipo delle funzioni, preceduto dalla parola chiave
extern (ricordiamo che relativamente alle sole funzioni la parola chiave extern è op-
zionale):

extern void Scambia1( );


extern void Scambia2(int &A, int &B);

Unità didattica A4: Funzioni, moduli software e file header 63


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 64

I tre file così modificati sono mostrati in figura A4.3.


✦ Figura A4.3
Le variabili extern.
File Principale.cpp
File Scambia1.cpp
#include <iostream.h>
int Leggi( ); extern int X, Y;
extern void Scambia1( ); void Scambia1( )
extern void Scambia2(int &A, int &B); {
int Temp1;
int X, Y; Temp1 = X;
int Num1; X = Y;
Y = Temp1;
int main( ) }
{
X = Leggi( );
Y = Leggi( );
Scambia1( );
cout << "Dopo Scambia1( ): X= "<< X << "Y=" << Y << endl; File Scambia2.cpp
Scambia2(X, Y);
cout << "Dopo Scambia2( ): X= "<< X << "Y=" << Y << endl;
return 0; void Scambia2(int &X, int &Y)
} {
int Leggi( ) int Temp2;
{ Temp2 = X;
cout << "Inserisci un valore:"; cin >> Num1; X = Y;
return(Num1); Y = Temp2;
} }

Extern è detto specificatore di “classe di memorizzazione” in quanto il compila-


tore non riserva alcuna locazione di memoria per le variabili dichiarate extern. Tali
variabili, infatti, sono già state definite altrove, in qualche altro file.

In C++ esistono altri specificatori di classe di memorizzazione delle variabili (che


vedremo a breve), ognuno dei quali influenza sia la visibilità che il periodo di vita del-
le variabili sulle quali agisce.
Si tratta di:
• static;
• auto(matic);
• register.
Extern e static sono specificatori che valgono pure per le funzioni, anche se in-
fluenzano solo la visibilità e non il periodo di vita; auto e register valgono invece so-
lo per le variabili. Per le variabili globali è possibile utilizzare solo gli specificatori
extern o static.
Ritornando ai tre file dell’esempio precedente, per effettuare una corretta compi-
lazione e un corretto linkaggio, avendo a disposizione un compilatore e un linker a ri-
ga di comando, scriveremo:
C:\> cl –c principale.cpp
C:\> cl –c Scambia1.cpp produce solo i file oggetto
C:\> cl –c Scambia2.cpp

dove l’opzione –c indica la compilazione al fine di ottenere il solo modulo oggetto.


Con:

C:\> link principale.obj Scambia1.obj Scambia2.obj produce il file eseguibile di nome


principale.exe

il nome dell’eseguibile sarà uguale a quello del primo modulo oggetto, nel nostro ca-
so Principale.exe.

64 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 65

6 File header e information hiding


Quando si realizza un modulo software è spesso conveniente suddividere ulterior-
mente i file sorgenti che lo compongono in due tipologie:
• file di intestazione o file header, caratterizzati dall’estensione .h.
• file di implementazione, caratterizzati dall’estensione .cpp.
I primi sono essenzialmente file di dichiarazioni e contengono le dichiarazioni di:
tipi di dato, variabili e funzioni definite nel file *.cpp. I secondi contengono le defini-
zioni delle funzioni necessarie all’implementazione del modulo.
In questo modo un programmatore che non ha partecipato alla stesura del codice
di quel modulo, osservando il file header, può facilmente individuare ciò che prende
il nome di interfaccia del modulo con l’esterno, ovvero quell’insieme di dichiarazio-
ni di variabili e funzioni che permettono l’utilizzo del modulo pur non conoscendone
l’implementazione, cioè senza entrare nel dettaglio delle definizioni delle funzioni.
Questo processo prende il nome di information hiding (“informazione nascosta”) e
consiste nel nascondere l’informazione relativa all’implementazione delle funzioni
fornendo solo il codice oggetto di quest’ultimo, permettendo l’interazione con il mo-
dulo tramite le dichiarazioni di variabili e funzioni presenti nel file header (figura
A4.4).

✦ Figura A4.4
Organizzazione di un
modulo software.
Modulo software

Interfaccia del Implementazione


modulo: file *.h del modulo:
file *.cpp

Una volta definiti, i file header devono essere inclusi nel file *.cpp mediante la di-
rettiva al preprocessore #include.
Oltre alle dichiarazioni di variabili e funzioni del relativo file *.cpp un file header
può contenere:
• le dichiarazioni delle variabili e funzioni esterne al modulo (dichiarazioni extern);
• le direttive di inclusione di altri file header.
Il modulo software dell’esempio del paragrafo precedente può essere riscritto uti-
lizzando i file header nel modo illustrato in figura A4.5.

Ai file header e ai file *.cpp si aggiungono i file contenenti le librerie di sistema che
occorre linkare agli altri file oggetto per ottenere il file eseguibile. Otteniamo allora il
processo di compilazione e linkaggio completo descritto dalla figura A4.6.

Per progetti complessi, nei quali lavorano insieme diversi team di programmatori,
vengono utilizzati i cosiddetti make file, ovvero file di comandi contenenti direttive
per il compilatore, in modo da effettuare compilazioni e linkaggi “intelligenti”, cioè
eseguiti solo se vengono variati file sorgenti interessati, e non sempre e indiscrimi-
natamente.
Negli ambienti integrati, quali Visual C++, il processo di compilazione e linkaggio
viene ulteriormente agevolato, poiché il programmatore dispone di ambienti grafici
con finestre dedicate a tale scopo. È possibile infatti effettuare il cosiddetto build dei
file .cpp definiti in un file speciale, detto file di progetto, aggiungendo o eliminando
facilmente file al processo di compilazione e linkaggio.

Unità didattica A4: Funzioni, moduli software e file header 65


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 66

✦ Figura A4.5
File Principale.h

#include <iostream.h> Direttive al preprocessore


extern void Scambia1( );
extern void Scambia2(int &A, int &B); Dichiarazioni dei prototipi di funzioni

int Leggi( ); Dichiarazioni di variabili globali


int X, Y;
int Num1;

File Principale.cpp File Scambia1.cpp

#include <Principale.h>
extern int X, Y;
int main( ) void Scambia1( )
{ {
int X,Y; int Temp1;
X = Leggi( );
Y = Leggi( ); Temp1 = X;
Scambia1( ); X = Y;
cout << "Dopo Scambia1( ): X= "<< X << "Y=" << Y << endl; Y = Temp1;
Scambia2(X, Y); }
cout << "Dopo Scambia2( ): X= "<< X << "Y=" << Y << endl;
return 0;
} File Scambia2.cpp
int Leggi( )
{
cout << "Inserisci un valore:"; void Scambia2(int &X, int &Y)
cin >> Num1; {
return(Num1); int Temp2;
} Temp2 = X;
X = Y;
Y = Temp2;
}

✦ Figura A4.6

File header File sorgente File header File sorgente File header File sorgente
*.h *.cpp *.h *.cpp *.h *.cpp

PREPROCESSORE PREPROCESSORE PREPROCESSORE

COMPILATORE COMPILATORE COMPILATORE

File oggetto *.obj File oggetto *.obj File oggetto *.obj

LINKER

File di libreria

File eseguibile *.exe

66 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 67

7 Alcuni importanti file header


Le funzionalità messe a disposizione dalle librerie standard sono numerose e con-
sentono di semplificare considerevolmente il lavoro del programmatore. Per una lo-
ro completa trattazione si rimanda al manuale del linguaggio; in questa sede ripor-
tiamo, e solo per alcune di esse, un elenco ridotto delle principali funzioni a disposi-
zione (tabella A4.2).
✦ Tabella A4.2
LIBRERIA FUNZIONE DESCRIZIONE
conio.h clrscr( ); Pulisce il video e sposta il cursore in alto a sinistra
gotoxy(<X>,<Y>); Sposta il cursore nella posizione indicata dalle coordinate
kbhit ( ); Verifica se è stato battuto un tasto
dos.h delay(<MSec>); Sospende l’esecuzione per <MSec> millisecondi
sleep(<Sec>); Sospende l’esecuzione per <Sec> secondi
sound(<Freq>); Emette un suono alla frequenza specificata
nosound( ); Disattiva l’altoparlante
math.h ceil(<X>); Approssima <X> all’intero per eccesso
floor(<X>); Approssima <X> all’intero per difetto
sqrt(<X>); Restituisce la radice quadrata di <X>
stdlib.h atoi(<Str>); Converte la stringa <Str> in un intero
atof(<Str>); Converte la stringa <Str> in un float
rand( ); Restituisce un numero pseudocasuale compreso tra 0 e RAND_MAX
srand( ); Imposta il punto di partenza per la sequenza di numeri casuali
generati da rand( )
stdio.h Contiene le routine di I/O previste per il C standard

È importante sottolineare che conio e dos non fanno parte della libreria standard,
ma sono implementati solo nei sistemi Windows, e neppure da tutti gli ambienti di
sviluppo.

8 La direttiva #define
La direttiva #define ha la seguente sintassi:
#define <NomeSimbolico> <Valore>

Essa indica al compilatore di sostituire al <NomeSimbolico> il valore specificato da


<Valore>: è questo un altro modo di assegnare valori alle costanti.
Ad esempio, consideriamo le seguenti direttive:
#define AliquotaMassima 40
#define YES 1
#define SALUTO "Ciao"
#define begin {
#define end }
#define then

Sarà possibile scrivere il seguente frammento di programma in stile Pascal:


...
if (X <= AliquotaMassima) then
begin
Y = 30;
Z = 50;
end
...

Il preprocessore leggerà le direttive e sostituirà i valori specificati, dando luogo al


seguente codice:

Unità didattica A4: Funzioni, moduli software e file header 67


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 68

...
if (X <= 40)
{
Y = 30;
Z = 50;
}
...

9 Librerie di funzioni e compilazione


condizionata
I file header sono molto utili quando si vuole suddividere un programma in più mo-
duli, tuttavia la loro potenza si esprime al meglio quando lo scopo è di realizzare una
libreria di funzioni.
L’idea è quella di separare l’interfaccia della libreria dalla sua implementazione: nel
file header vengono dichiarati (ed eventualmente definiti) gli identificatori che devo-
no essere visibili a chi usa la libreria (costanti, funzioni, tipi...); tutto ciò che è priva-
to viene invece messo in un altro file che include l’interfaccia.
Vediamo un esempio di semplicissima libreria per gestire date; ecco il file header
Data.h:

// file Data.h
typedefunsigned short dd; // giorno
typedefunsigned short mm; // mese
typedefunsigned yy; // anno
void StampaData(Data);

Il file che la implementa, Data.cpp, ha la seguente struttura:

// file Data.cpp
#include "Data.h"
#include <iostream.h>

void StampaData(dd giorno, mm mese, yy anno)


{
cout << giorno << '/' << mese << '/' << anno;
}

A questo punto la libreria è pronta: per distribuirla basta compilare il file Data.cpp
e fornire il file oggetto ottenuto e il file header Data.h. Chi deve utilizzare la libreria
non dovrà far altro che includere nel proprio programma il file header e linkarlo al fi-
le oggetto contenente le funzioni di libreria.
Esiste tuttavia un problema. Per illustrarlo consideriamo un programma costituito
da più moduli, quello principale che contiene la funzione main e altri che implemen-
tano le diverse funzioni. Può accadere che più moduli abbiano bisogno di una stessa
libreria. Devono allora includere tale libreria (ossia il file) nei rispettivi file header. Lo
schema di figura A4.7 illustra questa situazione.

68 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 69

// Modulo1.h
// principale.cpp
#include <iostream.h>
#include <iostream.h>
#include <Modulo1.h>
// altre dichiarazioni
#include <Modulo2.h>

int main( )
// Modulo2.h {
// codice del main( )
#include <iostream.h>
return 0;
}
// altre dichiarazioni
✦ Figura A4.7

In questo caso, poiché il file principale include più volte (direttamente e/o indiret-
tamente) lo stesso file header: <iostream.h>, il file che verrà effettivamente compila-
to conterrà più volte le stesse dichiarazioni (e definizioni), che daranno luogo a er-
rori di definizione ripetuta dello stesso oggetto (funzione, costante, tipo...). La solu-
zione al problema è fornita dal precompilatore stesso ed è nota come compilazione
condizionale.
La compilazione condizionale consiste nello specificare quando includere o meno
determinate porzioni di codice.
Per far ciò ci si avvale delle direttive:

#define <Simbolo>
#ifndef <Simbolo>
#endif

La prima, già vista, comunica al compilatore che si sta definendo <Simbolo>, dove
<Simbolo> rappresenta il nome di un identificatore permesso in C++; la seconda è co-
me l’istruzione condizionale e serve a testare se un identificatore è stato già definito:
il risultato della direttiva è vero (true), e si dice che la direttiva è verificata, se l’i-
dentificatore rappresentato da <Simbolo> non è definito, falso (false) altrimenti (la di-
rettiva non è verificata). L’ultima direttiva serve a capire dove finisce l’effetto della
direttiva condizionale.
Le ultime due direttive sono utilizzate per delimitare porzioni di codice. Se #ifndef
è vera viene incluso nel file il codice compreso tra l’#ifndef ed #endif, altrimenti quel-
la porzione di codice viene nascosta al compilatore.
Vediamo allora come utilizzare tali direttive per eliminare l’errore che si verificava
nell’esempio precedente (errore dovuto all’inclusione multipla di <iostream.h>). Nel
file iostream.h si aggiungono le seguenti direttive nidificate:

// Contenuto del file iostream.h


#ifndef __IOSTREAM_H
#define __IOSTREAM_H

// contenuto file header iostream.h


#endif

Il compilatore verifica se il simbolo __IOSTREAM_H è stato definito. Se non lo è


(cioè se #ifndef è verificata) si definisce il simbolo attraverso la direttiva #define, poi
si inserisce il resto del codice C++ delimitato da #endif che contiene il file header.
Vediamo cosa succede nella compilazione degli altri file.
Quando si compila il file main.cpp il preprocessore inizia a elaborare il file per pro-

Unità didattica A4: Funzioni, moduli software e file header 69


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 70

durre un unico file compilabile. Viene incontrata la direttiva #include <iostream.h> e il


file header specificato viene elaborato per produrre codice. A seguito delle direttive
contenute inizialmente nel file iostream.h, viene definito il simbolo __IOSTREAM_H e
prodotto il codice contenuto tra #ifndef __IOSTREAM_H ed #endif (cioè il normale con-
tenuto di iostream.h). Si ritorna al file main.cpp e il precompilatore incontra #include
Modulo1.h, quindi va a elaborare Modulo1.h. La direttiva #include <iostream.h> conte-
nuta in Modulo1.h porta il precompilatore a elaborare di nuovo iostream.h, ma questa
volta il simbolo __IOSTREAM_H è stato già definito, quindi #ifndef __IOSTREAM_H fa sì
che venga “saltato” l’intero contenuto di iostream.h non producendo alcun codice.
Si prosegue l’elaborazione di Modulo1.h e viene generato l’eventuale codice. Finita
l’elaborazione di Modulo1.h, la direttiva #include <Modulo2.h> porta all’elaborazione
di Modulo2.h, che è analoga a quella di Modulo1.h. Elaborato anche Modulo2.h, rima-
ne la funzione main di main.cpp, che produce il corrispondente codice.
Alla fine il precompilatore ha prodotto un unico file contenente tutto il codice di
Modulo1.h, Modulo2.h e main.cpp senza alcuna duplicazione, e riportante tutte le di-
chiarazioni e le definizioni necessarie.
Il file prodotto dal precompilatore è passato al compilatore per la produzione di
codice oggetto. Utilizzando il metodo appena visto in tutti i file header (in particola-
re quelli di libreria) si può stare sicuri che non vi saranno problemi di inclusione mul-
tipla. Tutto il meccanismo richiede però che i simboli definiti con la direttiva #define
siano unici.

10 Le variabili static
Le variabili static sono variabili permanenti all’interno della propria funzione o del
proprio file; sono permanenti nel senso che mantengono il proprio valore tra una
chiamata e la successiva ma, a differenza delle variabili globali, non sono note all’e-
sterno della propria funzione o del proprio file.
Si presti attenzione al significato della parola chiave static, poiché ha effetti diver-
si sulle variabili locali e sulle variabili globali:
• nel caso di variabili locali, static serve a modificarne il tempo di vita (lifetime), cioè
mantengono il loro valore tra una chiamata e la successiva;
• nel caso di variabili globali static modifica la visibilità (linkage), cioè tali variabili
non risultano visibili al di fuori del file in cui sono state definite.
L’importanza di poter restringere il linkage è ovvia; supponete di voler realizzare
una libreria di funzioni. Alcune serviranno solo a scopi interni alla libreria e non ser-
virà (anzi è pericoloso) esportarle: per fare ciò basta dichiarare static le variabili glo-
bali che volete incapsulare. Per questi motivi una variabile globale dichiarata static
può essere definita, con lo stesso nome, anche in altri file sorgente.
Per dichiarare static una variabile basterà far precedere il suo nome dalla parola
chiave static secondo la sintassi:

static <TipoVariabile> <NomeVariabile>;

10.1 Variabili locali dichiarate static


Quando si dichiara static una variabile locale, il compilatore crea in memoria una
cella per contenere il suo valore e la dealloca solo alla fine del programma. In questo
modo esso crea una variabile permanente, che come abbiamo visto conserva il pro-
prio valore tra chiamate successive della funzione nella quale è definita.
Vediamo un esempio. Consideriamo il seguente programma.

70 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 71

Esempio 3
Dichiarazione e inizializzazione di variabile static (vedi sito: CPP.A4.03.C)

#include <iostream.h>
int Incrementa( );
int main( )
{
cout << "Valore da incrementare: " << Incrementa( ) << endl;
cout << "Valore da incrementare: " << Incrementa( ) << endl;
cout << "Valore da incrementare: " << Incrementa( ) << endl;
return 0;
}
int Incrementa( )
{
static int Num = 0;
Num = Num + 1;
return(Num);
}

Il valore della variabile locale Num è conservato anche


quando termina l’esecuzione della funzione Incrementa( ).
Il risultato a video è quello a lato.
Come possiamo osservare dall’esempio, una variabile
locale statica può anche essere inizializzata. Tale valore viene assegnato una sola vol-
ta al primo avvio della funzione e non nelle chiamate successive.

10.2 Variabili globali dichiarate static


Per vedere un esempio di utilizzo delle variabili globali dichiarate static, riprendia-
mo l’esempio del paragrafo 6, introducendo due nuove variabili di cui una static; la
situazione è mostrata in figura A4.8.
✦ Figura A4.8
File Principale.cpp File Scambia1.cpp Variabili globali static.

#include <iostream.h>
extern int X, Y;
extern void Scambia1( ); void Scambia1( )
extern void Scambia2(int &A, int &B); {
int Temp1;
int Leggi( );
int X, Y; Temp1 = X;
int Num1; X = Y;
int Temp2 ; //(1) Y = Temp1;
}
int main( )
{
int X,Y; File Scambia2.cpp
X = Leggi( );
Y = Leggi( );
Scambia1( );
cout << "Dopo Scambia1( ): X= "<< X << "Y=" << Y << endl; static int Temp2; // (2)
Scambia2(X, Y); void Scambia2(int &X, int &Y)
cout << "Dopo Scambia2( ): X= "<< X << "Y=" << Y << endl; {
return 0; int Temp2;
} Temp2=X; /*si riferisce alla
dich. (2)*/
int Leggi( ) X = Y;
{ Y = Temp2;
cout << "Inserisci un valore:"; cin >> Num1; }
Temp2 = Num1; //si riferisce alla dich. (1)
return(Temp2);
}

Unità didattica A4: Funzioni, moduli software e file header 71


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 72

Le due dichiarazioni (1) e (2) dichiarano entrambe una variabile globale Temp2 che
viene vista solo nei rispettivi file sorgente.
Come possiamo notare, ricorriamo a static quando vogliamo evitare conflitti di no-
mi per variabili globali oppure quando desideriamo concedere solo ad alcune fun-
zioni la visibilità di determinate variabili globali.
Una dichiarazione globale senza indicazione di alcuno specificatore di classe di
memorizzazione nella sua dichiarazione e senza inizializzazione, quindi del tipo:
<NomeTipo> <NomeVariabile>

si assume che sia per default, a seconda del contesto:


• una normale variabile se non ci sono altre definizioni con lo stesso nome;
• una dichiarazione extern se in qualche altro file esiste una dichiarazione di una va-
riabile con lo stesso nome e con una inizializzazione a livello di dichiarazione.

11 Le variabili automatic e register


Per le variabili locali senza alcuno specificatore di classe di memorizzazione si as-
sume che per default esso sia auto(matic). Lo specificatore auto dichiara una varia-
bile locale il cui periodo di vita coincide con il periodo di vita del blocco nel quale è
stata dichiarata; quando si esegue l’ultima istruzione del blocco viene deallocato lo
spazio di memoria per quella variabile.
Le variabili auto devono essere inizializzate al momento della dichiarazione o al-
l’interno del blocco nel quale sono definite, altrimenti il loro valore è undefined.
Un ultimo specificatore che analizziamo è register. Esso suggerisce al compilatore
di utilizzare non una cella qualsiasi di memoria per registrare la variabile ma, se di-
sponibile, un registro della CPU. Si fa ricorso a questo tipo di specificatore per quel-
le variabili di uso frequente, in modo da aumentare la velocità di esecuzione del pro-
gramma.
Ovviamente non è possibile abusare di questo specificatore, visto il limitato nu-
mero di registri a disposizione della CPU. Le variabili register hanno la stessa visibi-
lità e stesso periodo di vita delle variabili auto.
È doveroso ricordare che, con i moderni compilatori, raramente ha senso utilizza-
re lo specificatore register, perché generalmente essi si accorgono quando è utile me-
morizzare una variabile in un registro, e lo fanno senza bisogno di suggerimenti da
parte del programmatore.
Riassumendo:

SPECIFICATORE DI SI APPLICA A VISIBILITÀ PERIODO DI VITA INIZIALIZZAZIONE


CLASSE DI
MEMORIZZAZIONE

extern Variabili globali e funzioni Altri file oppure stesso file Durata del file sorgente A inizio esecuzione
prima della definizione nel quale è definita
static Variabili globali e funzioni File nel quale sono definite Durata del file sorgente nel A inizio esecuzione
quale è definita
Variabili locali Blocco nel quale sono Durata del programma Una sola volta alla prima
definite chiamata della funzione
nella quale è definita
automatic Variabili locali Blocco nel quale sono Blocco nel quale sono A ogni chiamata della
dichiarate dichiarate funzione nella quale
è definita
register Variabili locali Blocco nel quale sono Blocco nel quale sono Come le variabili automatic
dichiarate dichiarate
Nessuno Variabili globali Come le extern per default Come le extern per default
Nessuno Variabili locali Come le auto per default Come le auto per default

72 Modulo A: Fondamenti del linguaggio C++


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 73

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Qual è la differenza tra procedure e funzioni? 10. Nell’ambito del passaggio di parametri, qual è la
2. Attraverso quale accorgimento è possibile funzione svolta dall’operatore unario di indirizzo &?
codificare una procedura in C++? 11. Da che cosa è composto un modulo software?
3. Una procedura deve avere al suo interno 12. Che cosa si intende con linkage di una variabile?
l’istruzione return? 13. Qual è la caratteristica delle variabili dichiarate
4. Che cos’è il prototipo di una funzione? extern?
5. Che cosa sono gli header? 14. In che cosa consiste il processo di information
6. Qual è la differenza tra variabili locali e variabili hiding?
globali? 15. Che cosa sono i make file?
7. Qual è la funzione svolta dal risolutore di scope? 16. A che cosa serve la direttiva #define?
8. In che cosa consiste lo shadowing di una variabile? 17. A che cosa servono le variabili dichiarate static?
9. In quanti modi può avvenire il passaggio di 18. A che cosa serve lo specificatore register?
parametri in C++?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI 9. Scrivere un programma C++ che realizzi il
conteggio dei voti riportati da quattro candidati in
Risolvere i seguenti problemi con l’ausilio un’elezione. Dovranno essere richiesti dall’esterno i
delle funzioni. nomi dei quattro candidati, quindi a ogni elettore
occorrerà proporre la seguente scheda:
1. Scrivere un programma C++ che, utilizzando una
1. Candidato1 3. Candidato3
funzione per stabilire se un numero è primo,
2. Candidato2 4. Candidato4
visualizzi l’elenco dei primi 50 numeri primi.
nella quale l’elettore dovrà digitare il nome del
2. Scrivere un programma C++ che visualizzi la candidato che desidera votare (la digitazione dello
successione di Fibonacci (utilizzare il metodo della zero servirà per uscire: con tale digitazione non si
ricorsione). darà alcun voto). Al termine della votazione, si
3. Scrivere un programma C++ per il controllo di dovrà stampare un prospetto riportante i voti
validità di una data di calendario espressa nella ottenuti da ciascun candidato.
forma GG/MM/AA. 10. Scrivere un programma C++ per la gestione di un
4. Scrivere un programma C++ che, passate in input conto corrente. Su tale conto possono essere svolti
due date nel formato GG/MM/AA, determini quale tre tipi di operazioni: versamento, prelievo,
delle due viene prima e la differenza di giorni che emissione assegni. Dopo aver introdotto il saldo
intercorre tra di esse. iniziale, si potranno inserire le varie operazioni
5. Scrivere un programma C++ che permetta di digitandone il tipo e la somma. A richiesta
utilizzare il computer come una calcolatrice dell’utente, il programma dovrà fornire:
tascabile. • il numero dei versamenti effettuati e la somma
totale versata;
6. Scrivere un programma C++ che, a scelta
• il numero dei prelievi effettuati e la somma totale
dell’utente, realizzi la moltiplicazione o la divisione
prelevata;
tra due numeri interi positivi servendosi solo
• il numero degli assegni emessi e la somma totale
dell’operazione di addizione.
prelevata per mezzo degli stessi;
7. Scrivere un programma C++ che, data in input una • il saldo finale.
sequenza di numeri chiusa dallo zero, determini
11. Scrivere un programma C++ che calcoli la potenza
tutti i numeri che risultano maggiori della somma di
di un numero:
tutti i precedenti.
Potenza(X,N) = 1 se N = 0
8. Scrivere un programma C++ che, date in input N
coppie di numeri rappresentanti delle frazioni del Potenza (X,N) = X*Potenza(X,(N–1))
tipo Num/Den con Den > 0, determini la loro
somma algebrica.

Unità didattica A4: Funzioni, moduli software e file header 73


p056-074_CeC_Unità A4 26-09-2007 16:32 Pagina 74

Verifica
di fine unità

Esercizio svolto
Ricevuta in input una data fornita come giorno, mese e anno in forma numerica, stamparla in forma letterale.

Analisi del procedimento risolutivo

Per risolvere il problema è necessario richiedere in input tre valori numerici che rappresentano rispettivamente il
giorno, il mese e l’anno. La richiesta in input dei dati avverrà all’interno della funzione main. I valori inseriti
saranno trasmessi alla funzione StampaData( ) che, dopo aver accertato che i valori inseriti possono rappresentare
quelli di una data stamperà, attraverso un costrutto di selezione multipla, il nome del mese corrispondente al valore
inserito. (Vedi sito: CPP.A4.04.C)
Attenzione! Questo programma effettua un controllo della validità piuttosto “soft”, ritenendo valide date come il 31
giugno 2000 ecc. Apportare le modifiche necessarie in modo che il controllo risulti valido per tutte le situazioni.

#include <iostream.h>
void StampaData (int, int, int);

int main( )
{
int Mese, Giorno, Anno;
cout << "\nInserisci il Giorno ";
cin >> Giorno;
cout << "\nInserisci il Mese ";
cin >> Mese;
cout << "\nInserisci l\’Anno ";
cin >> Anno;
StampaData (Giorno, Mese, Anno);
return 0;
}

void StampaData (int Gio, int M, int A)


{
if (M<1 || M>12 || Gio<1 || Gio>31 || A<0)
{
cout << "Errore: parametro fuori dai limiti.\n";
return;
}
cout << "\n";
cout << Gio << " ";
switch (M)
{
case 1: cout << "Gennaio "; break;
case 2: cout << "Febbraio "; break;
case 3: cout << "Marzo "; break;
case 4: cout << "Aprile "; break;
case 5: cout << "Maggio "; break;
case 6: cout << "Giugno "; break;
case 7: cout << "Luglio "; break;
case 8: cout << "Agosto "; break;
case 9: cout << "Settembre "; break;
case 10: cout << "Ottobre "; break;
case 11: cout << "Novembre "; break;
case 12: cout << "Dicembre "; break;
}
cout << A << endl;
}

74 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 75

Unità didattica

Tipi di dato
strutturati A5
● Conoscere la sintassi delle principali istruzioni del linguaggio C++ Prerequisiti
● Utilizzare appropriatamente le funzioni
● Saper effettuare il passaggio dei parametri

● Comprendere come l’utilizzo delle strutture di dati riesca a risolvere alcune Obiettivi
classiche situazioni problematiche inerenti la programmazione
● Organizzare i dati in strutture
● Acquisire le tecniche di gestione degli array mono e pluridimensionali

● Conoscere il significato di array nelle sue varie implementazioni Conoscenze


● Conoscere le differenti modalità di dichiarazione degli array e le relative clausole da apprendere
● Conoscere i tipi strutturati e il tipo enumerazione

● Saper gestire gli array Competenze


● Riuscire a organizzare i dati all’interno di array a una e più dimensioni da acquisire
● Saper utilizzare array, matrici e tabelle
● Saper implementare le classiche operazioni che vengono comunemente eseguite
sugli array

1 Gli array
Gli array sono variabili in grado di ospitare diversi valori omogenei (cioè tutti del-
lo stesso tipo) in opportuni spazi numerati chiamati elementi. A un elemento di un
array si può accedere indicando il nome della struttura e il numero corrispondente
alla posizione, detto indice.
Il tipo di un elemento può essere un tipo primitivo o un oggetto più complesso:
avremo quindi array di numeri interi, di stringhe o array di array, ma non array che
contengono tipi di dati diversi (ad esempio numeri interi e stringhe, i cosiddetti ar-
ray densi possibili invece in altri linguaggi).

Un array può contenere più indici. Nel caso in cui contenga:


• un solo indice si dice che l’array è a una dimensione e si parla specificamente di
vettore;

Unità didattica A5: Tipi di dato strutturati 75


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 76

• due indici si parla di array bidimensionale o matrice;


• più di due indici si parla di array multidimensionali;

✦ Figura A5.1
Rappresentazione di VALORI DELL’INDICE
un vettore.

0 1 2 3 4 5 6 7

3 8 150 10 85 90 7 34

VALORI DEGLI ELEMENTI

Tutti gli elementi di un array vengono memorizzati uno di seguito all’altro nella me-
moria del computer e, cosa importante da ricordare, l’indice del primo elemento è 0.

2 La dichiarazione di un vettore
Per dichiarare un vettore si deve scrivere il tipo degli elementi, seguito da un no-
me valido e da una coppia di parentesi quadre che racchiudono un’espressione co-
stante. Tale espressione costante definisce le dimensioni dell’array, ossia il numero
di elementi che esso contiene.
La sintassi generale è dunque la seguente:
<TipoElementi> <NomeArray>[<NumeroElementi>];

Ad esempio:
int Numeri[10]; // Array non inizializzato di 10 interi
char Lettere[20]; // Array non inizializzato di 20 caratteri

All’interno delle parentesi quadre non è consentito, in fase di dichiarazione del-


l’array, utilizzare nomi di variabili. Per specificare le dimensioni di un array è invece
possibile usare delle costanti definite, come negli esempi:

#define LIMITE_NUMERI 10
#define LIMITE_LETTERE 20

int Numeri[LIMITE_NUMERI];
char Lettere[LIMITE_LETTERE];

Ai nomi degli array è possibile applicare l’operatore sizeof, che restituisce il nu-
mero di byte che compongono gli array stessi.

3 L’inizializzazione di un vettore
Un vettore può essere inizializzato esplicitamente, al momento della creazione, for-
nendo le costanti di inizializzazione dei dati, oppure durante l’esecuzione del pro-
gramma, assegnando o copiando dati nell’array.
Per inizializzare l’array Numeri degli esempi precedenti in fase di creazione con die-
ci numeri interi scriveremo:

int Numeri[10] = {12, 0, 4, 150, 4500, 2, 34, 5599, 22, 83};

76 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 77

Un metodo alternativo è il seguente:


int Numeri[ ] = {12, 0, 4, 150, 4500, 2, 34, 5599, 22, 83};

In questo secondo modo viene creato un array di dimensione pari al numero di ele-
menti inseriti. Tutti gli elementi devono essere del tipo dichiarato (nel nostro caso
int), altrimenti si avrà un errore in fase di compilazione.
Per inizializzare un array durante l’esecuzione del programma occorre invece ac-
cedere, generalmente con un ciclo, a ogni elemento dell’array stesso e assegnargli un
valore. Come detto, l’accesso a un elemento di un array avviene indicando il nome
dell’array seguito dall’indice dell’elemento desiderato, in base alla sintassi:
<NomeArray>[<IndiceElemento>]

dove <IndiceElemento> inizia da 0 e non da 1. Il primo elemento dell’array è quindi


<NomeArray>[0], il secondo <NomeArray>[1] e così via. Pertanto, Numeri[2] indi-
cherà il terzo elemento dell’array Numeri e non il secondo.
Vediamo un semplice programma che consente di caricare un vettore di N elementi
interi (con N ≤ 50) solo con numeri pari. Per semplicità non utilizzeremo sottopro-
grammi.

Esempio 1
Carica vettore solo con valori pari (vedi sito: CPP.A5.01.C)

#define MASSIMO 50
#include <iostream.h>
int DimensionaVettore( );

int main( )
{
int Vettore[MASSIMO];
int K, NumElementi;
NumElementi = DimensionaVettore( );
for (K = 0; K < NumElementi; K++) // ciclo di caricamento
{
cout << "\nInserire l’elemento di posizione " << K << ": ";
cin >> Vettore[K];
if (Vettore[K] % 2==0)
continue;
K– –;
}
cout <<"\nElementi inseriti\n";
for (K = 0; K < NumElementi; K++) // ciclo di visualizzazione
cout << Vettore[K] << " ";
cout << "\n";
return 0;
}

int DimensionaVettore( )
{
int Elementi;
do
{
cout << "Quanti elementi si vogliono inserire? ";
cin >> Elementi;
}
while ((Elementi < 1) || (Elementi > MASSIMO));
return(Elementi);
}

Unità didattica A5: Tipi di dato strutturati 77


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 78

4 Come passare un vettore a una funzione


Nell’esercizio precedente non abbiamo realizzato tutte le funzioni necessarie per-
ché non conoscevamo un metodo per passare un array come parametro. In C++ è
possibile effettuare solo un passaggio per reference specificando unicamente il nome
del vettore. Ad esempio:

int main( )
{
int A[10]; // dichiarazione di un array di 10 elementi interi
...
Fun(A); // chiamata della funzione. Il parametro attuale è il nome dell’array
return 0;
}

Nel prossimo Modulo, parlando dello stretto legame tra array e puntatori, vedre-
mo una modalità equivalente per passare un array a una funzione. Per il momento,
consideriamo due metodi di passaggio per reference:
1) mediante array dimensionati come parametri formali;
2) mediante array non dimensionati.
void Fun(int X[10]) // primo modo. Parametro formale: array dimensionato
{
...
}

In questo primo modo il parametro è stato dichiarato come array della stessa di-
mensione del parametro attuale. Vediamo il secondo modo:
void Fun(int X[]) // secondo modo. Parametro formale: array non dimensionato
{
...
}

In questo caso il parametro è stato dichiarato come un array senza dimensione.


A titolo illustrativo, riconsideriamo l’esempio 1 del caricamento di un array con va-
lori pari letti da input. Possiamo riscrivere tale esempio nel seguente modo:

Esempio 2
Carica vettore solo con valori pari. Seconda versione (vedi sito: CPP.A5.02.C)

#define MASSIMO 50
#include <iostream.h>
int DimensionaVettore( );
void Carica(int Vett[], int Lung);

int main( )
{
int Vettore[MASSIMO];
int NumElementi;
NumElementi = DimensionaVettore( );
Carica(Vettore, NumElementi); /* chiamata con nome dell’array
come parametro attuale */
return 0;
}

int DimensionaVettore( )
{

78 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 79

int Elementi;
do
{
cout << "Quanti elementi si vogliono inserire? ";
cin >> Elementi;
}
while ((Elementi < 1) || (Elementi > MASSIMO));
return(Elementi);
}

void Carica(int Vett[], int Lung)


{
int K;
for (K = 0; K < Lung; K++) // ciclo di caricamento
{
cout << "\nInserire l’elemento di posizione " << K << ": ";
cin >> Vett[K];
if (Vett[K] % 2==0)
continue;
K– –;
}
cout <<"\nElementi inseriti\n";
for (K = 0; K < Lung; K++) // ciclo di visualizzazione
cout << Vett[K] << " ";
cout << "\n";
}

Come possiamo notare dal codice, durante la chiamata della funzione Carica( ) so-
no stati passati due parametri: il nome dell’array (passaggio per reference) e la sua
lunghezza, precedentemente stabilita dall’utente.

5 Le stringhe
Il linguaggio C++ non dispone di un tipo di dati specifico per rappresentare una
stringa. Per identificare quest’ultima, quindi, sarà necessario definire un array mo-
nodimensionale di caratteri. La dichiarazione di una variabile di tipo stringa avverrà
pertanto in base alla seguente sintassi:
char <NomeStringa>[ ];

Ad esempio, la seguente dichiarazione:


char Stringa[ ] = "Stringa di prova";

ha l’effetto di creare una variabile (array di caratteri) di nome Stringa di lunghezza pari
alla lunghezza della costante stringa “Stringa di prova”, e cioè 16 caratteri più il carat-
tere speciale “\0”, che è come un punto alla fine di una frase, ossia non viene contato
come una lettera, però occupa uno spazio. In totale avremo una stringa di 17 caratte-
ri. Ricapitolando, per dichiarare una stringa di N elementi occorrerà creare un array di
N + 1 caratteri. La precedente dichiarazione corrisponde quindi alla seguente:

char Stringa[17];

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
S t r i n g a d i p r o v a \0

Unità didattica A5: Tipi di dato strutturati 79


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 80

Il carattere speciale “\0” prende il nome di terminatore di una stringa e corri-


sponde alla configurazione di 8 zeri in un byte.
Il linguaggio C++ prevede più metodi per inizializzare una stringa; vediamone tre:

char Stringa1[6];
char Stringa2[10];
char Stringa3[5] = "casa"; // Inizializzazione durante la definizione
// Inizializzazione della stringa Stringa1 a mo' di vettore
Stringa1[0] = 'p';
Stringa1[1] = 'i';
Stringa1[2] = 'a';
Stringa1[3] = 'n';
Stringa1[4] = 'o';
Stringa1[5] = '\0';
// Inizializzazione della stringa Stringa2 da input dell’utente
cout << "Inserire una stringa di dieci caratteri: " << endl;
cin.getline(Stringa2, 10);

Le stringhe vanno sempre racchiuse tra doppi apici " ", i caratteri vanno tra apici
singoli ' ':

char Carattere = "c"; // non e' corretto


char Stringa[8] = 'Ciao'; // non e' corretto

È possibile accedere ai singoli caratteri che compongono una stringa per modifi-
carli.
Se ad esempio si volesse modificare la stringa Stringa, il cui contenuto supponia-
mo sia ora “Ciao” in una stringa costituita da tutti caratteri “x” si potrebbe imposta-
re un ciclo for del tipo:

for (int J = 0; J < 7; J++)


Stringa[J] = 'x';

Il C++ non dispone di operazioni predefinite sulle stringhe. Ad esempio, non con-
sente di utilizzare l’operatore = per assegnare una stringa a un’altra. Tutte le elabo-
razioni dovranno pertanto essere realizzate seguendo le regole analizzate per i vet-
tori.
A tal proposito, se si volesse copiare la stringa Str1 che contiene “Ciao a tutti” al-
l’interno della stringa Str2 occorrerebbe prevedere un apposito ciclo while non cal-
colato, come di seguito riportato:

Esempio 3
Copia di una stringa (vedi sito: CPP.A5.03.C)

#include <iostream.h>
void CopiaStringa(char[ ], char[ ]);
int main( )
{
char Stringa1[ ] = "Ciao a tutti", Stringa2[13] = “ “;
CopiaStringa(Stringa1, Stringa2);
cout << Stringa2 << endl;

80 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 81

return 0;
}
void CopiaStringa(char Str1[], char Str2[])
{
int K = 0;
while (Str1[K])
{
Str2[K] = Str1[K];
K++;
}
[K];
}

Vedremo nel prossimo paragrafo che è possibile evitare questo inconveniente ri-
correndo alle funzioni che operano sulle stringhe.
Riprendiamo l’inizializzazione di una stringa tramite la lettura da tastiera. Osser-
viamo il seguente esempio:

Esempio 4
L’input da tastiera con il flusso cin (vedi sito: CPP.A5.04.C)
#include <iostream.h>

int main( )
{
char S[50]; //dichiarazione di una variabile stringa di 50 caratteri
cout << "Inserire la stringa ";
cin >> S; // viene letta la stringa proveniente dalla tastiera
cout << "\nLa stringa inserita e' la seguente: " << S << endl;
}

Se alla stringa S viene assegnata da tastiera la frase “Ciao mondo” la successiva


istruzione cout visualizzerà soltanto “Ciao”. Questo perché il flusso di input cin rico-
nosce la fine della stringa in input quando incontra un carattere qualsiasi (tra spazio,
tabulazione o ritorno a capo). Per ovviare a questo inconveniente le due istruzioni
evidenziate possono essere sostituite con le funzioni printf( ) e getsf( ). La funzione
printf(<Stringa>) visualizza sul video la <Stringa> fornita come parametro, mentre la
funzione gets(<VariabileSringa>) esegue l’input di una stringa dalla tastiera. Gets( )
è sostanzialmente una funzione che legge una stringa e si ferma al primo a capo, ad
esempio quando un utente preme Invio.
Per utilizzare queste funzioni è necessario includere all’interno del programma l’hea-
der stdio.h. Effettuando la sostituzione, il nostro programma diviene il seguente:

Esempio 5
L’input da tastiera con la funzione gets( ) (vedi sito: CPP.A5.05.C)

#include <iostream.h>
#include <stdio.h>
int main( )
{
char S[50]; //dichiarazione di una variabile stringa di 50 caratteri
printf("Inserisci la stringa: "); // viene visualizzato il messaggio sul video
gets(S); // viene letta la stringa proveniente dalla tastiera
cout <<"\nLa stringa inserita e' la seguente: " << S << endl;
}

Unità didattica A5: Tipi di dato strutturati 81


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 82

È doveroso ricordare che la funzione gets( ) può essere utilizzata anche in asso-
ciazione con cout e non necessariamente con la funzione printf.

6 Le funzioni che operano sulle stringhe


La gestione delle stringhe è agevolata da alcune funzioni contenute nella libreria
string.h. Pertanto, quando occorre utilizzare tali funzioni è necessario includere nel
programma la direttiva #include <string.h>. Le funzioni più utilizzate sono:

FUNZIONE DESCRIZIONE

strcpy(<Str1>, <Str2>); Copia la stringa <Str2> all’interno di <Str1>.


strncpy(<Str1>, <Str2>,<N>); Copia all’interno della stringa <Str1> i primi N caratteri della stringa <Str2>.
strcat(<Str1>, <Str2>); Concatena la stringa <Str2> alla fine di <Str1>.
strlen(<Str>); Conta semplicemente il numero dei caratteri nella stringa specificata. Resti-
tuisce, pertanto, la lunghezza della stringa <Str>.
strcmp(<Str1> <Str2>); Confronta, carattere per carattere, le due stringhe fornite come parametri e
restituisce il valore intero zero se le due stringhe sono uguali, un valore in-
tero minore di zero se <Str1> è minore di <Str2> e maggiore di zero se
<Str1> è maggiore di <Str2>.
strcmpi(<Str1>, <Str2>); Confronta, carattere per carattere, le due stringhe fornite come parametri e
restituisce il valore intero zero se le due stringhe sono uguali, un valore in-
tero minore di zero se <Str1> è minore di <Str2> e maggiore di zero se
<Str1> è maggiore di <Str2>. A differenza della precedente funzione, strcm-
pi non è case sensitive. Ricordiamo che questa funzione, come le due suc-
cessive, non è standard ANSI e, come tale, potrebbe causare errori su alcu-
ni compilatori.
strupr(<Stringa>) Converte <Stringa> in maiuscolo. Restituisce anche una stringa, che sarà
tutta in maiuscole. Se <Stringa> è un array, sarà anche esso trasformato tut-
to in maiuscolo.
strlwr(<Stringa>) Converte <Stringa> in minuscolo. Restituisce anche una stringa, che sarà
tutta in minuscole. Se <Stringa> è un array, sarà anche esso trasformato tut-
to in minuscolo.

Vediamo un esempio utile per comprendere l’utilizzo della funzione strcmpi( ).


Esempio 6
La funzione strcmpi( ) (vedi sito: CPP.A5.06.C)

#include <iostream.h>
#include <string.h>
#include <stdio.h>
int main( )
{
char Nome[50];
printf("Inserisci il tuo nome: ");
gets(Nome);
if(!strcmpi("Piero", Nome)) // significa NOT strcmpi
cout << "Perfetto! Anche io mi chiamo cosi'." << endl;
else
cout << "Mi dispiace. Io non mi chiamo cosi'." << endl;
return 0;
}

In questo esercizio, alla variabile Nome può essere assegnata una stringa scritta in
maiuscolo, in minuscolo o in vari modi: non essendo case sensitive, la funzione strcm-
pi( ) non darà problemi.
E ora un ultimo esempio, stavolta sulla funzione strncpy( ).

82 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 83

Esempio 7
La funzione strncpy( ) (vedi sito: CPP.A5.07.C)

#include <string.h>
#include <iostream.h>
int main( )
{
char Stringa[100] = "Piero e\' mio amico";
cout << "La stringa e\' : " << Stringa << endl;
strncpy(Stringa, "Peppe", 5);
strncpy(Stringa + 9, "tuo", 3);
cout << "Ora la stringa e\' : " << Stringa << endl;
return 0;
}

7 Gli array bidimensionali e multidimensionali


Un array a due dimensioni, comunemente chiamato matrice, è un array i cui ele-
menti sono a loro volta array. Gli array finora esaminati erano monodimensionali e ri-
chiedevano l’uso di un solo indice. È facile determinare il numero di dimensioni di un
array osservando la sua dichiarazione: se è presente una sola coppia di parentesi
quadre ([ ]), l’array è monodimensionale; due coppie di parentesi quadre ([ ] [ ]) in-
dicano invece un array bidimensionale e così via. Il numero massimo di dimensioni
solitamente non supera 3 (array multidimensionali).
Per immaginare la rappresentazione di un array bidimensionale si può far Colonne -1
riferimento a una tabella (o a una matrice) in cui il primo indice dell’array 0 1 ....................

rappresenta la riga e il secondo rappresenta la colonna. 0

Per dichiarare una matrice utilizziamo la seguente sintassi, che evidenzia 1

come sia necessario far seguire al nome della variabile due volte l’operatore .....................
di dichiarazione [ ]:
<TipoElementi> <NomeArray>[<Righe>][<Colonne>];
Righe -1
dove <Righe> e <Colonne> indicano rispettivamente il numero di righe e il
numero di colonne della matrice.

Ad esempio: creiamo una matrice utilizzata per registrare la temperatura misura-


ta, in una settimana, ogni 4 ore. Per dichiarare e creare tale matrice scriveremo:

float Temperature [7] [4];

numero righe numero colonne


(una per ogni giorno (una per ogni misura)
della settimana)

Per assegnare a una variabile X la temperatura delle ore 7,00 di sabato scriveremo:

float X = Temperature [5][1]

Osserviamo che, proprio come negli array monodimensionali, le righe e le colon-


ne di una matrice hanno come indice iniziale 0.
Per riempire l’array e calcolare la temperatura media della settimana procederemo
come nell’esempio seguente.

Unità didattica A5: Tipi di dato strutturati 83


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 84

Esempio 8
Riempi, calcola e stampa la temperatura media della settimana
(vedi sito: CPP.A5.08.C)

#include <iostream.h>
void Inserisci (float Temp[7][4], int R, int C);
float Somma(float Temp[7][4], int R, int C);

int main( )
{
float Temperature[7][4];
int Righe = 7, Colonne = 4;
float S = 0.0, Media = 0.0;
Inserisci(Temperature, Righe, Colonne);
S = Somma(Temperature, Righe, Colonne);
Media = S / (Righe * Colonne);
cout << "\n Media: " << Media << endl;
return 0;
} // fine main

void Inserisci (float Temp[7][4], int R, int C)


{
int I = 0;
while (I < R)
{
int J = 0;
while (J < C)
{
cout << "\nInserire la temperatura di posizione " << I << " " << J << " ";
cin >> Temp[I][J];
J ++;
} // fine while interno
I ++;
} // fine while esterno
} // fine Inserisci( )

float Somma(float Temp[7][4], int R, int C)


{
int I = 0;
float Somma = 0.0;
while (I < R)
{
int J = 0;
while (J < C)
{
Somma = Somma + Temp[I][J];
J++;
} // fine while interno
I ++;
} // fine while esterno
return (Somma);
} // fine Somma( )

84 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 85

8 Le strutture
Quando si devono aggregare informazioni di tipo eterogeneo, aventi cioè tipo di-
verso tra loro, ma unite da caratteristiche comuni, si ricorre alle strutture. Le strut-
ture del linguaggio C++ coincidono con quelli che in informatica sono comunemente
definiti record. Nel seguito, dunque, utilizzeremo indifferentemente i termini “strut-
tura” e “record”.
Il raggruppamento delle suddette informazioni sotto un nome comune permette di
rappresentare tramite le strutture entità logiche, i cui attributi sono rappresentati
dalle variabili comprese nella struttura. Un esempio può essere l’aggregazione di
informazioni quali Marca, Modello, Targa, Cilindrata, Potenza, che costituiscono un in-
sieme di informazioni di tipo eterogeneo ma che sono accomunate dall’essere le ca-
ratteristiche peculiari di una struttura che possiamo chiamare Automobile.
Per dichiarare una struttura si utilizza la seguente sintassi:
struct <NomeStruttura>
{
<TipoCampo1> <NomeCampo1>;
...
<TipoCampoN> <NomeCampoN>;
};

dove <TipoCampo> e <NomeCampo> indicano rispettivamente il tipo e il nome di


ogni elemento che concorre a caratterizzare la struttura che stiamo definendo. Tali
elementi si chiamano campi.
Possiamo allora dire che una struttura, o meglio un particolare tipo di struttura, è
dichiarata definendone il nome ed elencando i nomi e i tipi dei suoi campi.
La precedente dichiarazione è considerata una dichiarazione di tipo. Ad esempio,
per dichiarare la struttura Automobile o equivalentemente “una struttura di tipo Au-
tomobile” o ancora “un tipo di struttura Automobile” scriveremo:
struct Automobile
{
char Marca[15]; //definiamo un campo stringa di 15 caratteri
char Modello[20]; //definiamo un campo stringa di 20 caratteri
char Targa[7]; //definiamo un campo stringa di 7 caratteri
unsigned Cilindrata; //definiamo un campo intero senza segno
float Potenza; //definiamo un campo a virgola mobile
};

All’interno della dichiarazione:


• non è consentita alcuna inizializzazione dei campi;
• non è consentito utilizzare un nome per un campo uguale al nome della struttura;
• è consentito usare lo stesso nome per campi appartenenti a strutture diverse;
• si possono utilizzare tipi semplici, tipi aggregati o tipi definiti dall’utente (come ve-
dremo successivamente).
Per dichiarare tre variabili di tipo Automobile scriveremo:
struct Automobile A1, A2, A3;

Tutti i compilatori aderenti al C++ standard consentono di omettere la parola chia-


ve struct e di scrivere semplicemente:
Automobile A1, A2, A3;

Ovviamente, la struct di tipo Automobile deve necessariamente essere dichiarata


prima del suo utilizzo.
È possibile elencare le variabili anche subito dopo la dichiarazione di struct, se-
condo la seguente sintassi:

Unità didattica A5: Tipi di dato strutturati 85


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 86

struct <NomeStruttura>
{
<TipoCampo1> <NomeCampo1>;
...
<TipoCampoN> <NomeCampoN>;
} <NomeVariabile1>, ..., <NomeVariabileN>;

In questo caso la dichiarazione non assume più la forma di una dichiarazione di ti-
po. Ad esempio:
struct Automobile
{
char Marca[15];
char Modello[20];
char Targa[10];
unsigned Cilindrata;
} A1, A2, A3;

La dichiarazione:
Automobile A3 = {"Marca1", "Modello1", "XF 345 RT", 1900}

inizializza i valori dei campi della variabile A3 di tipo Automobile allocando memoria
per essi.
Per fare riferimento a un particolare campo di una struttura ricorriamo alla dot no-
tation secondo la seguente sintassi:
<VariabileDiTipoStruct>.<NomeCampo>

Ad esempio, per assegnare a una variabile X di tipo unsigned il valore della cilin-
drata di una variabile A1 di tipo Automobile scriveremo:
unsigned X;
X = A1.Cilindrata;

In modo analogo, per assegnare al campo Cilindrata della variabile A1 di tipo Au-
tomobile un nuovo valore scriveremo:
A1.Cilindrata = 1200;

Cerchiamo ora di comprendere il contenuto del seguente frammento di codice:


struct Persona
{
char Nome[20];
unsigned short Eta;
char CodiceFiscale[7];
};

Persona Pluto = {"Pluto", 40, "PPP718"}; // (1)


Persona AmiciDiPluto [2] = {{"Pippo", 40, "PLT712"},{"Minnie", 35, "MNN431"}}; // (2)
// esempi di uso di strutture:
Pluto.Eta = 41;
unsigned short Var = Pluto.Eta;
strcpy(AmiciDiPluto[0].Nome, "Paperino"); // (3)

Abbiamo dichiarato il tipo Persona, quindi la variabile Pluto di tale tipo; in partico-
lare, abbiamo inizializzato la variabile con una inizializzazione aggregata del tutto
simile a quanto abbiamo visto per gli array, eccetto che i valori forniti devono essere
compatibili con il tipo dei campi e dati nell’ordine definito nella dichiarazione (1). Vie-
ne mostrata anche la dichiarazione di un array i cui elementi sono di tipo struttura,
e il modo in cui eseguire una inizializzazione fornendo i valori necessari all’inizializ-

86 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 87

zazione dei singoli campi di ciascun elemento dell’array (2). Le righe successive mo-
strano come accedere ai campi di una variabile di tipo struttura; in particolare, l’ul-
tima riga assegna un nuovo valore al campo Nome del primo elemento dell’array tra-
mite una funzione di libreria già nota (3). Si noti che prima viene selezionato l’ele-
mento dell’array, poi il campo Nome di tale elemento.
Se la struttura contiene un campo di tipo non primitivo, prima si seleziona il cam-
po e poi si seleziona l’elemento del campo che interessa. Ad esempio:

struct Data
{
unsigned short Giorno, Mese;
unsigned Anno;
};
struct Persona
{
char Nome[20];
Data DataNascita;
};

Persona Pluto = {"Pluto", {10, 9, 1950}};


Pluto.Nome[0] = 'P';
Pluto.DataNascita.Giorno = 20;
unsigned short NuovoGiorno = Pluto.DataNascita.Giorno;

A differenza degli array, per le strutture è definito l’operatore di assegnazione; ciò


accade solo perché, come vedremo nei prossimi Moduli, il compilatore tratta le strut-
ture di dati come classi i cui membri sono tutti pubblici.

struct Data
{
unsigned short Giorno, Mese;
unsigned Anno;
};

Data Oggi = {10, 05, 2004};


Data Ieri = {9, 05, 2004};
Ieri = Oggi;

9 Gli array di strutture: le tabelle


È possibile costruire strutture di dati più complesse mettendo assieme array e
strutture. Nell’esempio dell’Automobile in realtà abbiamo già utilizzato array per de-
finire il tipo di alcuni campi. Vediamo ora come sia possibile definire un array i cui
elementi sono di tipo struct.

struct Automobile
{
char Marca[15];
char Modello[20];
char Targa [7];
unsigned Cilindrata;
} Concessionaria[20];

L’esempio precedente definisce un array di 20 elementi chiamato Concessionaria i


cui elementi sono oggetti di tipo Automobile. La dichiarazione è equivalente a:

Unità didattica A5: Tipi di dato strutturati 87


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 88

struct Automobile
{
char Marca[15];
char Modello[20];
char Targa [7];
unsigned Cilindrata;
};
struct Automobile Concessionaria[20];

Per assegnare il valore della cilindrata alla terza automobile dell’array Concessio-
naria scriveremo:
Concessionaria[2].Cilindrata = 1200;

10 Le unioni
Nel linguaggio C++ il tipo di una variabile deve essere noto a tempo di compilazio-
ne. In alcuni programmi può essere comodo però decidere il tipo di una variabile a
tempo di esecuzione. In queste situazioni si ricorre a una particolare struttura, nella
quale si dichiarano tutti i tipi che quella variabile può assumere. Tale struttura pren-
de il nome di unione o union. Il tipo union consente di memorizzare in una stessa va-
riabile tipi differenti in istanti differenti.
Per dichiarare un’unione si utilizza la stessa dichiarazione vista per le strutture,
sostituendo alla parola chiave struct la parola chiave union:
union <NomeUnione>
{
<TipoCampo1> <NomeCampo1>;
...
<TipoCampoN> <NomeCampoN>;
} <NomeVariabile1>, ... ,<NomeVariabileN>;

Ad esempio, siano date due variabili X1 e X2 che rappresentano le ascisse in un


piano cartesiano; se abbiamo strutturato il programma in modo tale che il tipo del
valore da assegnare loro dipende dal flusso di esecuzione del programma, possiamo
scrivere:
union Ascissa
{
unsigned U;
float F;
double D;
} X1, X2;

oppure:
union Ascissa
{
unsigned U;
float F;
double D;
};
union Ascissa X1, X2;

Come per i tipi struttura, la selezione di un dato campo di una variabile di tipo unio-
ne viene eseguita tramite l’operatore di selezione “punto” (dot notation). Pertanto:
X1.U = 100; // si assegna a X1 un valore intero
X1.F = 100.30; // si assegna a X1 un valore float

88 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 89

La dichiarazione:
union Ascissa X2 = {130.00}

inizializza i valori dei campi della variabile X2 di tipo Ascissa allocando quindi me-
moria per essa, convertendo implicitamente il valore indicato tra parentesi graffe al ti-
po del primo campo specificato nella dichiarazione; nel nostro caso convertirebbe
130.00 a un unsigned.

11 Le enumerazioni
Spesso è utile poter definire un nuovo tipo “estensionalmente”, cioè elencando
esplicitamente i valori che una variabile (o una costante) di quel tipo può assumere.
Tali tipi vengono detti enumerati e sono definiti tramite la parola chiave enum con la
seguente sintassi:
enum <NomeTipo>
{<Identificatore1>, ..., <IdentificatoreN>};

Ad esempio:
enum Colori
{Rosso, Verde, Giallo, Azzurro};
Colori ColoreScelto = Rosso;

Gli identificatori Rosso, Verde, Giallo, Azzurro costituiscono l’intervallo dei valori
del tipo Colori. Si osservi che, come da sintassi, i valori di una enumerazione devono
essere espressi tramite identificatori: non sono ammessi valori espressi in altri modi
(interi, numeri in virgola mobile, costanti carattere ecc.). Inoltre gli identificatori uti-
lizzati per esprimere tali valori devono essere distinti da qualsiasi altro identificato-
re visibile nello scope dell’enumerazione, onde evitare ambiguità.
Il compilatore rappresenta internamente i tipi enumerati associando a ciascun
identificatore di valore una costante intera, così che un valore enumerazione possa
essere utilizzato in luogo di un valore intero, ma non viceversa:
enum Colori
{Rosso, Verde, Giallo, Azzurro};
Colori ColoreScelto = Rosso;
int Numero;
Numero = Verde; // Ok!
ColoreScelto = 3; // Errore!

Nell’ultima riga dell’esempio precedente si verifica un errore perché non esiste un


operatore di conversione da int a ColoreScelto, mentre essendo i valori enumerazio-
ne in pratica delle costanti intere, il compilatore è in grado di eseguire la conversio-
ne a int.
È possibile infine forzare il valore intero da associare ai valori di una enumerazione:
enum Colori
{Rosso = 1, Verde = 4, Giallo = Rosso + 5, Azzurro = 3};

Unità didattica A5: Tipi di dato strutturati 89


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 90

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI 7. Quale file di intestazione occorre specificare per
utilizzare le funzioni che operano sulle stringhe?
1. Per specificare le dimensioni di un array è possibile 8. Qual è la differenza tra le funzioni strcmp( ) e
usare delle variabili? strcmpi( )?
2. In quanti modi può essere inizializzato un array? 9. Come si dichiara una matrice in C++?
3. Come si può identificare una stringa in C++? 10. Da che cosa è caratterizzato un array
multidimensionale?
4. A livello di stringhe, qual è il nome assunto dal
carattere speciale \0? Qual è la sua funzione? 11. Che cosa sono le strutture?
5. Qual è la differenza tra cin e gets( )? 12. Che cosa sono i campi?
6. Quale file di intestazione occorre specificare per 13. Qual è la differenza tra struct e union?
utilizzare le funzioni printf( ) e gets( )? 14. A che cosa servono le enumerazioni?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI costruire un nuovo vettore ottenuto dal primo
sopprimendo le eventuali componenti uguali.
1. Trovare gli errori presenti nelle seguenti istruzioni: 9. Servendosi di due vettori, determinare la somma di
due numeri binari.
char Topolino[ ] = "investigatore"; 10. Dato un vettore di N componenti, con N ≤ 150,
costruire due nuovi vettori: uno composto dalle
Topolino[4] = 't';
componenti di posto pari del vettore dato, l’altro
Topolino[ ] = "basso"; composto da quelle di posto dispari.
Topolino = "basso";
11. Dati due vettori di N componenti, con N ≤ 100,
costruire un terzo vettore le cui componenti sono
prese alternativamente una dal primo vettore e
l’altra dal secondo. Nel caso in cui i due vettori non
Vettori dovessero avere la stessa lunghezza, copiare la
parte rimanente del vettore più lungo.
2. Caricare un vettore di N elementi, con N ≤ 100.
12. Dato un vettore di N componenti, con N ≤ 100, e un
Successivamente, calcolare il prodotto delle
numero X, costruire un vettore le cui componenti di
componenti di posto pari e la somma di quelle di
indice pari siano uguali a quelle di indice pari del
posto dispari.
vettore moltiplicate per X e quelle di indice dispari
3. Calcolare la radice quadrata della somma dei siano uguali a quelle di indice dispari del vettore
quadrati delle componenti di indice dispari di un dato, incrementate di X.
vettore di N componenti, con N ≤ 10.
4. Dato un vettore di N componenti, con N ≤ 100, Matrici
calcolare la somma delle componenti positive e
quella delle componenti negative. 13. Data una matrice Mat quadrata, trovare gli indici e
5. Memorizzare in un vettore N numeri reali, con il valore della componente più grande e di quella
N ≤ 100. Calcolare i quadrati degli N numeri e più piccola.
memorizzarli, ordinatamente, in un altro vettore. 14. Generare un “quadrato magico” di ordine N, con N
Alla fine, stampare la differenza fra ogni numero e intero dispari e minore di 20. Per “quadrato
il suo quadrato. magico” si intende una matrice quadrata di N righe
6. Calcolare i vettori somma e differenza di due vettori e N colonne in cui ciascun elemento è un numero
di N componenti, con N ≤ 50. intero da 1 a N2, e la somma degli elementi in
7. Caricare un vettore di N componenti, con N ≤ 100, e ciascuna riga, in ciascuna colonna e in ciascuna
memorizzarli in un nuovo vettore nell’ordine delle due diagonali principali è uguale alla costante
inverso. Stampare il vettore così ottenuto. N⋅(N 2 +1)
8. Dato un vettore di N componenti, con N ≤ 50, 2

90 Modulo A: Fondamenti del linguaggio C++


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 91

Verifica
di fine unità

15. Scrivere un programma C++ che consideri una • codice fiscale;


matrice rettangolare intera con N righe e M • comune di residenza.
colonne, con N e M minori di 50, e che svolga Realizzare i programmi che implementano le
le seguenti operazioni: seguenti funzioni:
• determini se gli elementi nulli della matrice sono • stampare i nominativi di tutte le persone che
in percentuale maggiore dell’80% del totale degli vivono in un determinato comune;
elementi; • stampare il codice fiscale della persona di cui
• se nel passo precedente la risposta è stata viene fornito nome e cognome;
affermativa, visualizzi i soli elementi non nulli • ordinare la tabella in ordine alfabetico.
della matrice stessa. 21. Si ha una tabella Nomi costituita da record così
16. Scrivere un programma C++ che esamini una strutturati:
matrice quadrata e determini quante sono le righe e
Codice : Stringa[5]
le colonne composte da elementi tutti nulli.
Nominativo : Stringa[40]
17. Scrivere un programma C++ che crei una matrice Citta : Stringa[25]
contenente una tavola pitagorica di ordine N.
18. Data una matrice di ordine N × M, con N e M minori La tabella è ordinata per codice. Effettuare
di 20, scrivere un programma C++ che, a richiesta successivi inserimenti rispettando l’ordinamento.
dell’utente, esegua le seguenti operazioni: 22. È data una tabella Giacenze costituita da record col
• trasli verso il basso ogni riga della matrice; seguente tracciato:
• trasli verso l’alto ogni riga della matrice;
• trasli verso sinistra ogni colonna della matrice; Codice : Stringa[5]
• trasli verso destra ogni colonna della matrice. Descrizione : Stringa[40]
ScortaMinima : Reale
Costrutto struct e array di strutture RimanenzeIniziali : Reale
TotaleEntrate : Reale
19. Si vuole gestire la rubrica telefonica dell’agenda TotaleUscite : Reale
all’interno della quale sono presenti i nomi dei
conoscenti e il loro numero telefonico. Scrivere gli Costruire la tabella Scorte contenente i soli record
algoritmi che, servendosi di una tabella gestiscano: relativi ad articoli la cui giacenza aggiornata
• l’inserimento dei dati; (rimanenze iniziali + totale entrate – totale uscite)
• l’ordinamento dell’agenda; sia inferiore alla scorta minima. I record della
• la visualizzazione dei dati. tabella Scorte devono avere il seguente tracciato:
20. In una tabella vengono memorizzate le generalità di Codice : Stringa[5]
N persone. I dati da memorizzare sono i seguenti: Descrizione : Stringa[40]
• nome; GiacenzaAttuale : Reale
• cognome;

23. Fusione di due vettori ordinati. Per lo svolgimento di questo esercizio non è stata adottata la metodologia top-
down. Prova a utilizzare le funzioni per rendere più snello il seguente codice. (Vedi sito: CPP.A5.09.C)

#include<iostream.h>
#include<stdio.h>

const int Dimens1=10, Dimens2=20, Dimens3=30;


double V1[Dimens1], V2[Dimens2], V3[Dimens3];
int I,J,K;

int main( )
{
cout << "Inserisci elemento di indice :" << I << " del vettore V1: ";
cin >> V1[I];
for (I = 1; I < Dimens1; I ++)
do
{
cout << "Inserisci elemento di indice " << I << " del vettore V1: ";
cin >> V1[I];
}

Unità didattica A5: Tipi di dato strutturati 91


p075-092_CeC_Unità A5 26-09-2007 16:36 Pagina 92

Verifica
di fine unità

while (V1[I]< V1[I-1]);


I = 0;
cout << "Inserisci elemento di indice " << I << " del vettore V2: ";
cin >> V2[I];
for (I = 1; I < Dimens2; I ++)
do
{
cout<<"Inserisci elemento di indice " << I << " del vettore V2: ";
cin >> V2[I];
}
while (V2[I] < V2[I-1]);
I = 0;
J = 0;
W = 0;
while (I<Dimens1 && J<Dimens2)
{
if (V1[I]<V2[J])
V3[W++]=V1[I++];
else if (V1[I]>V2[J])
V3[W++]=V2[J++];
else
{
V3[W++]=V1[I++];
V3[W++]=V2[J++];
}
}
if (I < Dimens1)
while (I<Dimens1)
V3[W++]=V1[I++];
else if (J<Dimens2)
while (J<Dimens2)
V3[W++]=V2[J++];
for (W=0; W<Dimens3; W++)
{
cout << "Elemento di indice " << W;
cout << " del vettore V3 e' " << V3[W]<<endl;
}
return 0;
}

92 Modulo A: Fondamenti del linguaggio C++


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 93

Allocazione Modulo B
dinamica
della memoria
• U.D. B1. Puntatori e reference
• U.D. B2. Allocazione e deallocazione
dinamica della memoria

Prerequisiti
● Variabili e costanti
● Algoritmi
● Costrutti fondamentali della programmazione strutturata
● Metodologia top-down
● Caratteristiche dei linguaggi compilati

Obiettivi
● Costruire i primi programmi in C++ privilegiando l’utilizzo dei puntatori e l’allocazione dinamica
della memoria
● Comprendere le peculiarità dei puntatori
● Riuscire a sfruttare i vantaggi offerti dall’allocazione dinamica della memoria

Conoscenze da apprendere
● Conoscere il tipo di dato puntatore e le sue peculiarità
● Conoscere le tecniche per dichiarare e inizializzare i puntatori
● Conoscere i vari metodi per allocare dinamicamente le variabili

Competenze da acquisire
● Saper dichiarare e inizializzare i puntatori
● Saper gestire i puntatori
● Saper realizzare semplici programmi che sfruttino la memoria heap

93
p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 94

Unità didattica

B1 Puntatori e reference

Prerequisiti ● Compilare ed eseguire un’applicazione C++


● Utilizzare appropriatamente le funzioni
● Saper effettuare il passaggio dei parametri

Obiettivi ● Far percepire la versatilità del tipo di dato puntatore


● Far emergere i pericoli e le potenzialità legate all’utilizzo di questo particolare tipo
di dato
● Affrontare le problematiche inerenti l’uso dei puntatori e le strutture di dati

Conoscenze ● Conoscere il tipo di dato puntatore e le sue peculiarità


da apprendere ● Conoscere le tecniche per dichiarare e inizializzare i puntatori

Competenze ● Saper gestire i puntatori


da acquisire ● Saper dichiarare e inizializzare un puntatore
● Saper utilizzare i puntatori nell’ambito del passaggio dei parametri
● Saper gestire puntatori e strutture di dati

1 I puntatori
Una variabile puntatore (o di tipo puntatore) è una variabile che contiene un in-
dirizzo di memoria centrale. Tale indirizzo corrisponde alla posizione in memoria in
cui è allocata un’altra variabile (detta variabile puntata) o una struttura di dati.
Per dichiarare una variabile puntatore si fa precedere il nome della variabile dal-
l’operatore di indirizzamento indiretto *, secondo la sintassi:

<TipoOggettoPuntato> *<VariabilePuntatore>

dove <TipoOggettoPuntato>, chiamato anche tipo base, indica un qualsiasi tipo con-
sentito dal linguaggio. La situazione è rappresentata nella seguente figura:

variabile puntatore: contiene l’indirizzo


di un’altra posizione in memoria centrale 500 Indirizzo 200

variabile puntata valore Indirizzo 500


Indirizzo 502
Indirizzo 504

Memoria centrale

94 Modulo B: Allocazione dinamica della memoria


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 95

Puntatore e variabile puntata si rappresentano schematicamente come segue:

valore

variabile puntatore variabile puntata

Ad esempio:
int *Punt1;

indica che la variabile puntatore di nome Punt1 punta a un valore intero, cioè Punt1
contiene l’indirizzo della variabile puntata, che è di tipo intero.
Possiamo anche dire che *Punt1 è di tipo intero, oppure che il contenuto della cel-
la cui punta Punt1 sarà di tipo intero, o ancora che nella variabile puntatore Punt1
sarà memorizzato l’indirizzo di una variabile di tipo intero. Utilizziamo il tempo futu-
ro perché, appena dichiarata, una variabile puntatore contiene il valore NULL (cioè
non punta a nulla) se è una variabile globale, altrimenti contiene un valore non defi-
nito.
Per riferirci al contenuto della variabile puntata utilizziamo l’operatore *, chiama-
to appunto “contenuto di”, già menzionato nella precedente Unità. Per riferirci inve-
ce all’indirizzo della variabile puntata utilizziamo l’operatore &, detto “indirizzo di”,
anch’esso già menzionato.
Consideriamo
int X = 100;
il seguente frammento di codice:
int Y;
int *Punt1;

Per assegnare alla variabile Y il contenuto della variabile X utilizzando il puntato-


re Punt1 possiamo scrivere:
Punt1 = &X; // (1)
Y = *Punt1; // (2)

L’istruzione (1) assegna alla variabile puntatore Punt1 l’indirizzo della variabile X,
mentre l’istruzione (2) assegna alla variabile Y il contenuto della variabile puntata dal
puntatore Punt1. Possiamo rappresentare il tutto graficamente, come segue; dopo l’e-
secu-
zione
Punt1 100 X
dell’i-
struzio-
variabile puntatore variabile puntata
ne (1)
avre-
mo:

(1)
Punt1 100 X variabile puntata

variabile puntatore 100 Y una variabile qualsiasi

Dopo l’esecuzione dell’istruzione (2) avremo:

Unità didattica B1: Puntatori e reference 95


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 96

2 L’algebra dei puntatori


Si parla di algebra dei puntatori per indicare le operazioni possibili tra più variabi-
li puntatore, ossia:
• assegnazione
– tra i valori puntati;
– tra due puntatori dello stesso tipo;
• confronto tra puntatori dello stesso tipo. Gli operatori che è consentito utilizzare
sono "==" (uguale) e "!=" (diverso);
• incremento e decremento di un puntatore;
• addizione e differenza tra puntatori.

Consideriamo a titolo di esempio il seguente frammento di codice C++:

float X = 100.50;
float Y = 200.75;
Punt1 100.50 X
int Z = 50;
float *Punt1;
float *Punt2; Punt2 200.75 Y
int *Punt3;
Punt1 = &X;
Punt2 = &Y; Punt3 50 Z
Punt3 = &Z;

Sono ammissibili le seguenti operazioni.

• Assegnazione tra i valori puntati: Copia nella locazione di memoria puntata da Punt1 il dato intero pun-
*Punt1 = *Punt2 tato da Punt2, cioè copia il valore 200.75 in X. Otterremo quindi:

• Confronto: Punt1 200.75 X


Punt1 == Punt2
Restituisce false
Punt2 200.75 Y

Punt3 50 Z

• Assegnazione tra due puntatori: Assegna al puntatore Punt1 l’indirizzo di memoria contenuto in Punt2.
Punt1 = Punt2 Dopo l’assegnazione, i due puntatori punteranno alla stessa locazione
di memoria. Otterremo quindi:
• Confronto:
Punt1 == Punt2 Punt1 200.75 X
Restituisce true

Punt2 100.50 Y

Punt3 50 Z

• Assegnazione: Assegna al dato reale puntato da Punt1 il dato intero puntato da


*Punt1 = *Punt3 Punt3. I valori dei due puntatori non vengono modificati. È da nota-
re che dopo questa istruzione *Punt1 sarà 50.00.

Punt1 200.75 X

Punt2 50.00 Y

Punt3 50 Z

96 Modulo B: Allocazione dinamica della memoria


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 97

• Incremento: Poiché Punt3 è un puntatore a interi, il risultato dell’operatore ++ è di


Punt3 ++ far puntare Punt3 all’elemento successivo, o meglio Punt3 viene in-
crementato di un valore pari alla dimensione del dato puntato, cioè al-
la dimensione di un intero.
• Addizione: Punt3 punterà all’indirizzo pari al suo contenuto precedente incre-
Punt3 = Punt3 + 10 mentato di (10*<Dim>), dove <Dim> è la dimensione in byte che oc-
cupa in memoria un oggetto dello stesso tipo puntato da Punt3.

Riassumiamo quanto appena visto nel seguente esempio.

Esempio 1
Algebra dei puntatori (vedi sito: CPP.B1.01.C)
#include <iostream.h>
int main( )
{
float X = 100.50;
float Y = 200.75;
int Z = 50;
float *Punt1;
float *Punt2;
int *Punt3;
Punt1 = &X;
Punt2 = &Y;
Punt3 = &Z;
cout << "Punt1 = " << Punt1 << " " << "Punt2 = " << Punt2 << " " << "Punt3 = " << Punt3 << endl;
cout << "*Punt1 = "<<*Punt1 << " " << "*Punt2 = "<<*Punt2 << " " <<"*Punt3 = "<< *Punt3 << endl;
cout << " \n\nAssegnazione tra i valori puntati \n";
*Punt1 = *Punt2;
cout << "Punt1 = " << Punt1 << " " << "Punt2 = " << Punt2 << " " << "Punt3 = " << Punt3 << endl;
cout << "*Punt1 = " << *Punt1 << " " << "*Punt2 = "<<*Punt2 << " "<<"*Punt3 = "<<*Punt3 << endl;
cout << "\n\nAssegnazione tra due puntatori \n";
Punt1 = Punt2;
cout << "Punt1 = " << Punt1 << " " << "Punt2 = " << Punt2 << " " << "Punt3 = " << Punt3 << endl;
cout << "*Punt1 = "<<*Punt1 <<" "<< "*Punt2 = "<<*Punt2 << " " << "*Punt3 = " << *Punt3 << endl;
cout << "\n\nIncremento \n";
Punt3 ++;
cout << "Punt1 = " << Punt1 << " " << "Punt2 = " << Punt2 << " " << "Punt3 = " << Punt3 << endl;
cout << "\n\nAddizione \n";
Punt3 = Punt3 + 10;
cout << "Punt1 = " << Punt1 << " " << "Punt2 = " << Punt2 << " " << "Punt3 = " << Punt3 << endl;
cout << "*Punt1 = "<<*Punt1<<" "<< "*Punt2 = "<<*Punt2<< " " <<"*Punt3 ="<<*Punt3<<endl<< endl;
return 0;
}

Dalla videata riprodotta a lato si può notare


che:
• dopo l’incremento Punt3 passa dal valore
(espresso in esadecimale) 0x0012FF74 al valore
0x0012FF78, viene cioè incrementato di 4 byte
(rappresentazione in memoria di un intero);
• dopo l’addizione Punt3 passa dal valore
0x0012FF78 al valore 0x0012FFA0, viene cioè
incrementato di 4 · 10 byte. Come possiamo
notare, l’output di *Punt3 è assolutamente
casuale, dipende infatti dal valore contenuto,
in quel momento, dalla locazione di memoria
puntata da Punt3.

Unità didattica B1: Puntatori e reference 97


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 98

Non sono ammissibili le seguenti operazioni.


Punt3 = Punt1 I due puntatori non sono dello stesso tipo. Il compilatore dà un errore.
*Punt3 = *Punt1 Non si può assegnare un dato reale a una variabile intera. Il compilatore dà un warning.

Il linguaggio C++ consente di effettuare un particolare tipo di assegnazione a un


puntatore. È possibile, infatti, assegnare il valore NULL; in questo modo non lo si fa
puntare a nessun indirizzo di memoria. Ad esempio:
int X=100;
int *Punt;
Punt = &X; // Punt contiene l’indirizzo di X
Punt = NULL; // ora Punt non punta a nessun indirizzo di memoria

Quando però si inizializza un puntatore a NULL si deve tener presente che non è
possibile utilizzare la variabile puntatore per nessuna operazione finché questa non
venga inizializzata con un indirizzo valido. Infatti, se provassimo a scrivere:
int *Punt = NULL;
cout << *Punt << endl; // errore!!

otterremmo un errore in esecuzione, poiché la variabile puntatore Punt non punta ad


alcuna variabile intera che contenga un valore valido. È necessario, quindi, far pun-
tare Punt a una variabile intera:

int A = 20;
int *Punt = NULL;
...
...
Punt = &A;
cout << *Punt << endl; // ora e' corretto

3 Puntatori e passaggio di parametri


È possibile utilizzare i puntatori nel passaggio di parametri per indirizzo. Realizzia-
mo a titolo illustrativo un semplice programma che scambia il contenuto di due varia-
bili, servendoci dapprima del passaggio di parametri per indirizzo fin qui conosciuto
(con parametri reference), poi del passaggio di parametri per indirizzo mediante i pun-
tatori. Il risultato finale dei due esempi, che presentiamo affiancati, sarà lo stesso.

/* Passaggio di parametri per indirizzo /* Passaggio di parametri per indirizzo


// con parametri reference */ con i puntatori */
#include <iostream.h> #include <iostream.h>
void scambio(int &X, int &Y) void scambio(int *X, int *Y)
{ {
int Temp; int Temp;
Temp = X; X = Y; Y = Temp; Temp = *X; *X = *Y; *Y = Temp;
} }
int main( ) int main( )
{ {
int A, B; int A, B;
A = 10; B = 20; int *pA, *pB;
cout << A << B << endl; A = 10; B = 20;
scambio(A,B); pA = &A; pB = &B
cout << A << B << endl; cout << A << B << endl;
return 0; scambio(pA,pB);
} cout << A << B << endl;
return 0;
}

98 Modulo B: Allocazione dinamica della memoria


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 99

4 Puntatori e array
Vi è uno stretto legame tra puntatori e array, dovuto al modo con il quale il com-
pilatore C++ valuta un’espressione del tipo:
<NomeArray>[<IndiceArray>]

Per ottenere il risultato, il compilatore valuta questa espressione nel seguente modo:

*(<NomeArray> + <IndiceArray>)

cioè applica l’operatore “contenuto di” alla locazione di memoria il cui indirizzo si
trova <IndiceArray> celle dopo la cella di indirizzo <NomeArray>. Ad esempio:

Vettore[I]

viene valutata:

*(Vettore + I)

Pertanto Vettore viene considerato un indirizzo, mentre I viene considerato un in-


tero, che rappresenta il numero di celle (della dimensione del tipo degli elementi del-
l’array) cui bisogna saltare per recuperare il valore dell’intera espressione.

Alla luce di quanto affermato consideriamo le seguenti dichiarazioni:


int V[5];
int *P;

Relativamente a esse, sono tutte ammissibili le istruzioni che seguono.

• P = V, che significa: il puntatore P punta all’indirizzo


P 10 V
del primo elemento del vettore V; equivale pertanto
a P = &(V[0]); 20
30
40
50

• P = P+2, che significa: il puntatore P prende l’indi-


P 10
rizzo del terzo elemento dell’array V;
20
V
30
40
50

• X = *(P+1), che significa: la variabile X (di tipo inte-


P 10
ro) prende il contenuto del quarto elemento del vet- V
20
tore V; equivale pertanto a V[3];
30
40
X 40 50

• V[4] = *(P+2) + 100, che significa: aggiungere il va- P 10 V


lore 100 al quinto elemento dell’array V; equivale 20
pertanto a V[4] += 100. 30
40
150

Unità didattica B1: Puntatori e reference 99


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 100

Invece le seguenti istruzioni non sono ammissibili:


• V = P;
• V ++.
Vediamo ora un esercizio completo relativo alle operazioni di riempimento, ricer-
ca, visualizzazione e addizione degli elementi di un array, utilizzando i puntatori.

Esempio 2
Operazioni sui vettori con i puntatori (vedi sito: CPP.B1.02.C)

#include <iostream.h>
void Riempi(int *V, int MAXEL);
void Visualizza(int *V, int MAXEL);
int Somma(int *V, int MAXEL);
bool Ricerca(int *V, int MAXEL, int Num);
int Occorrenza(int *V, int MAXEL, int Num);
int main()
{
int S = 0;
int Vett[10]; /* crea un vettore di 10 elementi interi */
cout << "Riempimento del vettore " << '\n';
Riempi(Vett,10);
cout << "----------"<< '\n';
cout << "Visualizza gli elementi inseriti" << '\n';
Visualizza(Vett,10 );
cout << "----------" << '\n';
if (Ricerca(Vett,10,12)) // ricerca nel vettore l’elemento con valore 12
cout << "elemento trovato" << '\n';
else
cout << "elemento non trovato";
cout << "----------" << '\n';
S = Somma(Vett,10);
cout << "Somma degli elementi:" << S << '\n';
return(0);
} /* fine main */
void Riempi(int *V, int MAXEL)
{
int I;
I = 0;
while (I < MAXEL)
{
cout << "Inserisci il nuovo elemento:"; cin >> V[I];
I++;
}
} /* fine Riempi*/

void Visualizza(int *V, int MAXEL)


{
int I = 0;
while (I < MAXEL)
{
cout << I << " elemento: " << V[I] << '\n';
I++;
}
}

int Occorrenza(int *V, int MAXEL, int Num)


{
int T = 0;
int I = 0;
while (I < MAXEL)
{

100 Modulo B: Allocazione dinamica della memoria


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 101

if (V[I] == Num)
T++;
I++;
}
return(T);
}
bool Ricerca(int *V, int MAXEL, int Num)
{
if(Occorrenza(V, MAXEL, Num) == 0)
return(false);
else
return(true);
}
int Somma(int *V, int MAXEL)
{
int S = 0;
int I = 0;
while (I < MAXEL)
{
S = S + V[I];
I++;
}
return(S);
}

5 Puntatori e stringhe
Abbiamo appena visto che vi è uno stretto legame tra puntatori e array, e sappia-
mo anche che una stringa è un array di caratteri terminato dal carattere speciale \0.
Possiamo allora dedurre che vi è uno stretto legame tra puntatori e stringhe.
Consideriamo la seguente dichiarazione:
char V[]= "Stringa di prova"; // (1)

Se consideriamo ora la dichiarazione di un puntatore a caratteri P:


char *P;
possiamo scrivere:
P = V;
che equivale a scrivere:
P = &V[0];

La dichiarazione (1) può allora essere riscritta nel seguente modo:


char *P = "Stringa di prova"; // (2)

Attenzione, quest’ultimo assegnamento, anche se corretto, è un po’ rischioso per-


ché il compilatore non può controllare che l’indirizzo di P non vari; si potrebbe per-
dere la zona di memoria che contiene la stringa, evento che non può accadere per un
vettore. In altre parole, le variabili di tipo stringa possono essere dichiarate sia come
array di caratteri che come puntatori a caratteri. Nel primo caso i caratteri della
stringa costante vengono copiati nell’array V, nel secondo caso alla variabile P è as-
segnato l’indirizzo del primo elemento della stringa costante. Grazie a questo risul-
tato, per accedere al carattere terminatore della stringa sarà possibile scrivere:
char C = *(P+16);

Come esempio di legame tra array di caratteri, puntatori e stringhe, scriviamo due
versioni di un programma che effettua la copia tra due stringhe.

Unità didattica B1: Puntatori e reference 101


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 102

Esempio 3
Copia di una stringa mediante puntatori a caratteri

#include <iostream.h>

int main( )
{
char Stringa1[80] = "Stringa da copiare ", Stringa2[80];
char *Str1 = Stringa1;
char *Str2 = Stringa2;

while ((*Str2 = *Str1) != '\0')


{
Str1++;
Str2++;
}
cout << "Str1 = " << Stringa1 << "Str2 = " << Stringa2 << '\n';
return 0;
}

Come possiamo notare, nel corpo del while vengono fatti avanzare i puntatori un
carattere alla volta finché non si raggiunge la fine della stringa da copiare, che ter-
mina con \0.

La prossima versione utilizza una funzione con parametri formali puntatori; nota-
re come all’interno della funzione CopiaStringa( ) vengono utilizzati indifferentemen-
te puntatori a carattere e array di stringhe.

Esempio 4
Terzo modo, parametro formale puntatore

#include <iostream.h>

void CopiaStringa(char *Str1, char *Str2);


int main( )
{
char Str1[80], Str2[80];
cout << "Inserire una stringa senza spazi"; cin >> Str1; cout << '\n';
CopiaStringa(Str1, Str2);
cout << "Str1 = " << Str1 << " Str2= " << Str2 << '\n';
return 0;
}

void CopiaStringa(char *Str1, char *Str2)


{
int I = 0;
while ((Str2[I] = Str1[I]) != '\0')
I++;
}

6 Puntatori e strutture
Sappiamo che una variabile puntatore può puntare a una variabile strutturata: ve-
diamo ora un esempio di utilizzo dei puntatori con una struct. Definiamo una struttu-
ra Libro, una variabile Lib1 di tipo Libro e una variabile pLib di tipo puntatore a una
struttura Libro. Nel codice che segue illustreremo come far riferimento attraverso i
puntatori ai campi della struttura Libro.

102 Modulo B: Allocazione dinamica della memoria


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 103

Esempio 5
Accesso ai campi delle strutture tramite puntatori

#include <iostream.h>

struct Libro
{
char Titolo[50];
char Autore[20];
char Editore[20];
long int Prezzo;
};

int main( )
{
Libro Lib1={"I promessi sposi", "Manzoni", "MinervaItalica", 80};
cout << "\nPrezzo del libro " << Lib1.Titolo << ": " << Lib1.Prezzo; // (1)
Libro *pLib; // dichiarazione di un puntatore alla struct Libro
pLib=&Lib1; // inizializzazione del puntatore
pLib->Prezzo = pLib->Prezzo *1.10; // (2)
cout << "\nNuovo prezzo del libro " << pLib->Titolo << ": " << pLib->Prezzo; // (3)
return 0;
}

Come possiamo notare, nell’istruzione (1) per accedere al campo Prezzo si utilizza
la solita dot notation, mentre nelle istruzioni (2) e (3) si usa l’operatore freccia (->),
che ha la funzione di separare il nome del puntatore dal nome del campo poiché stia-
mo accedendo al campo Prezzo tramite il puntatore pLib.

Per selezionare un campo di una variabile struct attraverso un puntatore utilizzia-


mo quindi la seguente sintassi:
<NomePuntatoreAStruttura>-><NomeCampo>

dove <NomePuntatoreAStruttura> è un puntatore alla struttura cui vogliamo accede-


re, precedentemente definito.

7 Gli argomenti della funzione main( )


Quando si compila un programma per mandarlo in esecuzione, lo si richiama at-
traverso il nome dell’eseguibile. Ad esempio, per compilare e linkare un programma
che visualizza il nome e cognome dell’utente, scriveremo:
cl visualizza.cpp

mentre per eseguirlo scriveremo:


visualizza

Mandare in esecuzione un programma equivale a richiamare la funzione main( ): è


infatti quest’ultima a essere eseguita. Tuttavia la funzione main( ) è una funzione co-
me le altre, ed è quindi possibile passarle dei parametri (può infatti risultare utile
passare informazioni al main( ) al momento della sua esecuzione).
I parametri attuali da passare al main( ) devono seguire il nome dell’eseguibile: si
dice che vengono passati sulla linea di comando.

I parametri formali devono avere la seguente sintassi:

Unità didattica B1: Puntatori e reference 103


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 104

int main(int argc, char *argv[])


{
<Istruzioni>
return [(]<Risultato>[)]
}

dove:
• il parametro argc è un intero che contiene il numero di argomenti che si trovano
sulla linea di comando. Il suo valore è almeno pari a 1, poiché si considera il nome
del programma come il primo parametro;
• il parametro argv è un puntatore a un array di puntatori a caratteri, cioè ogni ele-
mento di questo array punta a ogni parametro presente sulla riga di comando.
Chiariamo questi concetti con un esempio. Consideriamo il seguente programma
che visualizza il nome e cognome dell’utente:

#include <iostream.h>

int main( )
{
char Nome[20], Cognome[20];
cout << "Inserire il nome: ";
cin >> Nome;
cout << endl;
cout << "Inserire il cognome: ";
cin >> Cognome;
cout << endl;
cout << "Benvenuto : " << Nome << " " << Cognome;
return 0;
}

Il nome e cognome dell’utente sono letti con due istruzioni di input. Se volessimo
passare questi parametri al momento della chiamata del programma dovremmo scri-
vere:

#include <iostream.h>

int main(int argc, char *argv[])


{
cout << "Benvenuto : " << argv[1] << " " << argv[2];
return 0;
}

Sulla riga di comando scriveremo:


C:\C++ esempi>visualizza Paolo Rossi

argv[0] argv[1] argv[2]

ottenendo come risultato a video:


Benvenuto : Paolo Rossi

104 Modulo B: Allocazione dinamica della memoria


p093-105_CeC_Unità B1 26-09-2007 16:37 Pagina 105

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Che cos’è una variabile puntatore? 4. Perché si parla di algebra dei puntatori?
2. Qual è la differenza tra gli operatori & e *? 5. Che cosa contengono i parametri argc e argv?
3. A che cosa serve il valore NULL?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI A5, che prende come parametro una stringa e
restituisce la sua lunghezza.
1. Date le dichiarazioni: 4. Scrivere una versione con i puntatori della funzione
int Vet[8] = {10, 20, 15, 12, 5, 8, 41, 28}; strcmpi ( ) (confronto tra due stringhe) vista
nell’Unità A5, che prende come parametro una
int *Punt
stringa e restituisce la sua lunghezza.
completare la seguente tabella indicando il valore
5. Scrivere una versione con i puntatori della funzione
puntato dalla variabile Punt, supponendo che le
Occorrenza( ) che prende come parametri una
istruzioni vengano eseguite in sequenza.
stringa e un carattere e restituisce il numero di
ISTRUZIONE VALORE PUNTATO occorrenze del carattere nella stringa (cioè quante
DALLA VARIABILE PUNT volte il carattere è presente nella stringa).
Punt = Vet; 6. Scrivere una versione con i puntatori della funzione
Punt ++; NumeroVocali( ) che, presa una stringa come
Vet[2] = 54;
parametro, restituisce il numero di occorrenze di
ogni vocale nella stringa. [Suggerimento: restituire
Punt = Punt +4; un array come valore di ritorno.]
7. Se Str1 e Str2 rappresentano due stringhe, che cosa
2. Scrivere una versione con i puntatori della funzione succede dopo l’esecuzione del seguente frammento
strcat( ) (concatenazione di stringhe) vista di codice?
nell’Unità A5, che prende due stringhe come
parametri e concatena la seconda alla prima. while (*Str1++ = *Str2++)
3. Scrivere una versione con i puntatori della funzione
strlen( ) (lunghezza della stringa) vista nell’Unità

Esercizio svolto
1. Scrivere una funzione C++ che inverta una stringa int N;
ricevuta come parametro. char Str[20];
Analisi del processo risolutivo cout << "Stringa da invertire: "; cin >> Str;
cout<< endl;
La funzione Inverti( ) ha come primo parametro (S)
un puntatore a carattere, pertanto la stringa da N = strlen(Str); /* utilizziamo la funzione
invertire sarà passata per riferimento. La lunghezza strlen( ) di string.h */
di tale stringa sarà il secondo parametro. Il corpo Inverti(Str,N);
del while utilizza due puntatori a carattere: uno che cout << "Stringa invertita : " << Str << endl;
parte dal primo carattere della stringa e l’altro che return 0;
parte dall’ultimo. Finché i due puntatori non si }
sovrappongono (Primo < Ultimo), essi scorrono uno
void Inverti(char *S, int N)
in avanti e l’altro all’indietro, scambiandosi (con
{
l’aiuto di una variabile temporanea) i valori cui
puntano. char *Primo = &S[0];
char *Ultimo =&S[N-1];
while(Primo < Ultimo)
#include <iostream.h> {
#include <string.h> char Temp = *Primo;
*Primo++ = *Ultimo;
void Inverti(char *S, int N);
*Ultimo–– = Temp;
int main( ) }
{ }

Unità didattica B1: Puntatori e reference 105


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 106

Unità didattica

B2 Allocazione e deallocazione
dinamica della memoria

Prerequisiti ● Puntatori
● Nozioni di base sulla struttura della memoria centrale

Obiettivi ● Caratteristiche e vantaggi dell’allocazione dinamica delle variabili

Conoscenze ● Conoscere le caratteristiche del segmento stack e del segmento heap della
da apprendere memoria centrale
● Conoscere le tecniche per l’allocazione/deallocazione dinamica delle variabili

Competenze ● Saper allocare dinamicamente una variabile


da acquisire ● Saper deallocare variabili

1 Stack e heap
Nell’Unità precedente abbiamo visto come si utilizzano i puntatori; cerchiamo ora
di capire dove opera un puntatore. A tale scopo, è necessario sapere che la memoria
del computer in cui possono risiedere le variabili è suddivisa in due parti: lo stack e
l’heap.
Nello stack vengono immagazzinate tutte le variabili statiche automatiche che il
programma utilizza. Una variabile automatica è una variabile con periodo di vita lo-
cale visibile solo entro il blocco nel quale è dichiarata. Ad esempio, quando si defi-
nisce:
int Lato = 15;

il computer riserva due byte di memoria dello stack per la variabile Lato.
Con l’utilizzo delle variabili memorizzate nello stack non è possibile in alcun mo-
do ottenere più variabili di quelle dichiarate; in particolare, l’applicazione non è in
grado di deallocare (cioè recuperare) la memoria di una variabile. Ciò rappresenta
un potenziale problema allorché si manipolano grosse quantità di informazioni, per-
ché lo stack potrebbe riempirsi: di conseguenza, se si tentasse di allocare altra me-
moria potrebbe verificarsi il cosiddetto stack overflow. Un classico esempio lo si ha
nel caso di funzioni ricorsive molto “profonde”.
Per consentire al programmatore un impiego “intelligente” della memoria, è utiliz-
zabile la parte heap della stessa, detta anche memoria dinamica (diversamente dal-
lo stack, che è considerato memoria statica). Il termine “dinamica” sta a indicare pro-
prio la possibilità per il programmatore di allocare e deallocare la memoria a suo pia-
cimento. La manipolazione dell’heap avviene tramite i puntatori.

106 Modulo B: Allocazione dinamica della memoria


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 107

2 L’allocazione dinamica della memoria


Nella precedente Unità didattica abbiamo visto come si dichiara e si inizializza una
variabile puntatore. Esiste ancora un’altra possibilità per eseguire tale operazione:
quella che prevede l’utilizzo dell’operatore new.
L’operatore new si occupa di allocare dinamicamente la memoria con un qualsiasi
tipo di dato valido. La sintassi generale per il suo utilizzo è:
<VariabilePuntatore> = new <TipoVariabile>

dove <VariabilePuntatore> è un puntatore di tipo <TipoVariabile>. L’operatore new al-


loca memoria sufficiente a contenere un valore del tipo <TipoVariabile> e restituisce
il puntatore ad esso.
Ad esempio, quando scriviamo:

int A = 20;
int *PuntA = NULL;
PuntA = new int; // si poteva scrivere direttamente: int *PuntA = new int;
*PuntA = A;

otteniamo un risultato apparentemente simile a quello visto nell’Unità precedente,


ma in realtà abbastanza diverso. Infatti, l’istruzione:
PuntA = new int;

non fa altro che allocare una quantità di memoria necessaria per contenere un inte-
ro (ovvero 2 byte, ma dipende dall’implementazione e dalla macchina) e assegnare
l’indirizzo del primo byte di memoria alla variabile PuntA. Questo indirizzo è indub-
biamente diverso da quello in cui è contenuta la variabile A stessa, per cui avremo
due variabili intere differenti che contengono lo stesso valore.
È anche possibile usare l’istruzione:
PuntA = new int(20);

per inizializzare direttamente il valore *PuntA a 20.


Vediamo un altro esempio:
#include <iostream.h>

int main( )
{
int *Punt; // dichiara un puntatore a intero
Punt = new int; // allocazione dinamica per un intero
if(Punt == NULL) // verifica se l’allocazione è andata a buon fine
cout << "Non e' possibile allocare\n";
*Punt=20; // assegna a quella locazione il valore 20
cout<<*Punt<<"\n"; // verifica in output il valore attribuito a Punt
if (Punt) // prima di deallocare verifica se è stato allocato qualcosa
delete Punt; // rilascia la memoria assegnata a Punt
return 0;
}

Trascuriamo per un momento l’istruzione delete, il cui significato verrà chiarito


ampiamente nel paragrafo successivo, e cerchiamo di chiarire le varie istruzioni. Do-
po aver dichiarato il puntatore a variabile intera Punt, grazie all’utilizzo di new il pro-
gramma assegna a Punt l’indirizzo di un’area di memoria abbastanza grande da con-
tenere un intero. Se per qualsiasi motivo, ad esempio uno spazio di memoria insuffi-
ciente, l’allocazione non dovesse avvenire correttamente, il puntatore Punt assumerà
valore NULL.

Unità didattica B2: Allocazione e deallocazione dinamica della memoria 107


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 108

È una buona norma di programmazione verificare sempre l’esito delle allocazioni


dinamiche: è consigliabile pertanto, dopo ogni utilizzo dell’operatore new, effettuare
un test di verifica sul puntatore ed eventualmente gestire la mancata allocazione. Nel
nostro esempio, in caso di fallimento viene stampato il messaggio di errore “Non è
possibile allocare”, ma è conveniente scegliere altre soluzioni, come l’uscita dal pro-
gramma o altro. Tutto ciò al fine di non avere spiacevoli sorprese, difficilmente indi-
viduabili in fase di esecuzione del programma.
Una volta effettuata correttamente l’allocazione dinamica è possibile attribuire un
valore a Punt: nell’esempio la locazione puntata da Punt conterrà l’intero 20. Visua-
lizzando sullo schermo il valore *Punt possiamo verificare se effettivamente la loca-
zione contiene ora il valore desiderato.

Passiamo ora all’uso di new con gli array. Per creare dinamicamente un array si uti-
lizza la seguente sintassi:
<NomeArray> = new <TipoElementi>[<NumeroElementi>]

Ad esempio, per creare un nuovo oggetto array Numeri, precedentemente già di-
chiarato e contenente cinque stringhe, scriveremo:
Numeri = new int; // creazione di un oggetto array

Se l’array non è stato già dichiarato, per dichiarare e creare un nuovo oggetto ar-
ray si utilizza la sintassi:
<TipoElementi>* <NomeArray> = new <TipoElementi>[<NumeroElementi>]

Ad esempio:

int* Numeri = new int[5]; /* dichiarazione e creazione di un


oggetto array */

Quando si utilizza l’operatore new per creare un array, bisogna necessariamente


indicare il numero di elementi tra parentesi quadre.
Tale operatore, infatti, alloca lo spazio in memoria per contenere gli elementi di
quel tipo.
Quando si crea un array con new, il valore dei suoi elementi (non ancora inizializ-
zati) è indefinito.

3 La deallocazione dinamica
Abbiamo detto che le variabili puntatore fanno riferimento alla memoria dinamica
(heap). Diversamente dalla memoria statica, tutte le variabili puntatore allocate dal
programmatore devono essere poi distrutte quando non servono più, per evitare
garbage (rifiuti): un programma che rimane molto tempo in esecuzione potrebbe al-
lora “mangiarsi” tutta la memoria disponibile. Infatti il C++, diversamente da altri lin-
guaggi di programmazione, non possiede proprie routine di garbage collection (rac-
colta di rifiuti).
L’operatore C++ che serve per rilasciare la memoria precedentemente allocata in
maniera dinamica con new è delete.
Applicandolo nel caso del primo esempio del paragrafo 2 avremmo:
delete PuntA; // la variabile PuntA non serve piu', possiamo deallocarla

Uno dei problemi più frequenti in relazione all’allocazione/deallocazione dinamica


di memoria è il cosiddetto memory leak (perdita di memoria).

108 Modulo B: Allocazione dinamica della memoria


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 109

Vediamone il significato con un esempio:

int K = 20;
int *PuntK = NULL;

PuntK = new int; // (1)


PuntK = &K; // (2) Attenzione!! Memory leak!!
* PuntK = 20;
...

Perché quando si esegue l’istruzione (1) si sta creando un memory leak? La rispo-
sta è elementare.
La variabile PuntK è stata inizializzata utilizzando l’istruzione (1); questa ha fatto
allocare due byte e ha assegnato l’indirizzo del primo di questi byte alla variabile
PuntK.
Quando poi si esegue l’istruzione (2), la variabile PuntK viene fatta puntare all’in-
dirizzo in cui è contenuta la variabile K, ma in nessun modo viene deallocata la me-
moria che era stata allocata in precedenza.
Ciò produce il suddetto memory leak; nel caso in esame, per evitarlo avremmo do-
vuto scrivere:

int K = 20;
int *PuntK = NULL;

PuntK = new int;


delete PuntK; // viene deallocata la memoria allocata da PuntK
PuntK = &K; // corretto!
* PuntK = 20;
...

In conclusione di paragrafo vediamo un programma che calcola la somma di due


numeri interi contenuti in variabili allocate dinamicamente e accessibili tramite pun-
tatori:

#include <iostream.h>

int main( )
{
int *Primo,*Secondo,*Terzo; // vengono dichiarati tre puntatori a interi
Primo = new int; /* si alloca lo spazio per conservare i valori
dei tipi specificati */
Secondo = new int;
Terzo = new int;
cout << "\nPrimo numero ";
cin >> *Primo; /* si utilizza lo spazio allocato per conservare
i dati di input */
cout << "\nSecondo numero ";
cin >> *Secondo;
*Terzo = *Primo + *Secondo;
cout << "\nSomma = " << *Terzo;
delete Primo; // si libera lo spazio di memoria allocato
delete Secondo;
delete Terzo;
return 0;
}

Unità didattica B2: Allocazione e deallocazione dinamica della memoria 109


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 110

4 Un esempio di allocazione dinamica:


il tipo di dato astratto Pila
Per vedere un concreto esempio di allocazione dinamica di oggetti in memoria me-
diante gli operatori new e delete, proviamo a implementare la struttura dati astratta
“pila” di interi. L’esempio utilizza tra l’altro le strutture e i puntatori per realizzare
una struttura di dati concatenata.
Sappiamo che gli elementi di base che costituiscono una pila sono i nodi. Conside-
reremo nodi caratterizzati da una parte relativa all’informazione associata al nodo e
da una parte relativa al collegamento con il nodo successivo (in quanto struttura con-
catenata). Possiamo allora pensare di creare una struct Nodo definita tramite due
campi: Info, che conterrà il valore intero del nodo (nel nostro caso infatti stiamo trat-
tando pile di interi), e Next, che conterrà un puntatore al nodo successivo.
Le operazioni che implementiamo sono:
• Push( ) ovvero inserimento in testa;
• Pop( ) ovvero estrazione in testa;
• creazione di una nuova struttura Pila;
• test per verificare se Pila è vuota;
• stampa degli elementi;
• somma degli elementi.

#include <iostream.h>
struct Nodo {
int Info;
Nodo *Next;
};
struct Pila {
Nodo *Testa;
};
void Crea(Pila &P); /* crea una Pila vuota*/
bool TestVuota(Pila P);
Nodo *Pop(Pila &P);
void Push(Pila &P, Nodo *N);
void Stampa(Pila &P);
int Somma(Pila P);
int main()
{
Pila P1;
Crea(P1);
for(int i=0; i<4; i++)
{
Nodo *Temp;
Temp = new Nodo;
Temp–>Info = i*10;
Temp–>Next =NULL;
Push(P1,Temp);
}
Stampa(P1);
cout << "somma elementi = " << Somma(P1) << endl;
Nodo *NTemp = Pop(P1);
delete NTemp;
Stampa(P1);
cout << "somma elementi = " << Somma(P1) << endl;
return(0);
}

110 Modulo B: Allocazione dinamica della memoria


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 111

void Crea(Pila &P) /* crea una Pila vuota*/


{
P.Testa=NULL;
}
bool TestVuota(Pila P)
{
return (P.Testa==NULL);
}
Nodo *Pop(Pila &P)
{
Nodo *N;
if (!TestVuota(P))
{
N=P.Testa;
P.Testa=(P.Testa)–>Next;
N–>Next=NULL;
}
else
cout << "Pila Underflow";
return(N);
}
void Push(Pila &P, Nodo *N)
{
N–>Next=P.Testa;
P.Testa=N;
}
void Stampa(Pila &P)
{
Nodo *Temp=P.Testa;
while(Temp!=NULL)
{
cout << Temp–>Info << endl;
Temp=Temp–>Next;
}
}
int Somma(Pila P)
{
int Somma=0;
Nodo *Temp=P.Testa;
while(Temp!=NULL)
{
Somma= Somma+ Temp–>Info;
Temp=Temp–>Next;
}
return (Somma);
}

Come possiamo notare, abbiamo utilizzato gli operatori new e delete per creare
nuovi nodi e per rimuovere il nodo di cui abbiamo effettuato il Pop( ).
Il programma principale effettua le seguenti operazioni:
• crea una nuova pila;
• la riempie con multipli di 10;
• calcola e visualizza la somma dei suoi elementi;
• estrae un elemento;
• ricalcola e visualizza la somma dei suoi elementi.
producendo a video il risultato a lato.

Unità didattica B2: Allocazione e deallocazione dinamica della memoria 111


p106-112_CeC_Unità B2 26-09-2007 16:38 Pagina 112

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. In quante parti è suddivisa la memoria di un 4. Che cosa si intende con il termine garbage?
computer? 5. Perché si parla di allocazione dinamica della
2. Che cos’è l’heap? memoria?
3. Qual è la funzione dell’operatore new? 6. Qual è la funzione dell’operatore delete?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI 5. Implementare un ADT Sequenza di interi (struttura
di dati astratta in cui è possibile prelevare e
1. Qual è il risultato del seguente frammento di codice? inserire in una qualsiasi posizione); in particolare,
implementare le operazioni di:
int *A,*B;
A = new int; • creazione una nuova sequenza;
B = new int; • inserimento di un nodo nella sequenza:
A = B; a) in testa;
cin >> *B; b) in fondo;
c) in una posizione qualsiasi;
2. Qual è l’output fornito dal seguente programma? • estrazione di un nodo dalla sequenza:
#include <iostream.h> a) in testa;
int Numero = 14; b) in fondo;
int *Punt; c) in una posizione qualsiasi;
int main( ) • verifica che la sequenza non sia vuota;
{ • ricerca un nodo nella sequenza.
cout << "Indirizzo contenuto nella variabile Punt " 6. Riempire una Pila con i primi 20 multipli di 3.
<< Punt << endl; 7. Incrementare del valore 10 ogni nodo intero di una
Punt=&Numero; Pila.
cout << "Indirizzo contenuto nella variabile Punt " 8. Calcolare il Max e il Min di una Pila di interi.
<< Punt << endl;
cout << "Il contenuto della locazione di indirizzo 9. Calcolare la media di una Pila riempita di interi.
Punt e' " << *Punt << endl; 10. Calcolare il numero di elementi maggiori della
Punt = new int; media e quelli minori della media di una pila di
*Punt=30; interi.
cout << "Indirizzo contenuto nella variabile Punt " 11. Invertire gli elementi di una Pila, in modo che il
<< Punt << endl; primo diventi l’ultimo e l’ultimo diventi il primo.
cout << "Il contenuto della locazione di indirizzo (Suggerimento: utilizzare un array o una coda di
Punt e' " << *Punt << endl; appoggio).
} 12. Ripetere l’esercizio precedente, ma senza la pila
3. Creare una Pila di studenti; per ogni studente d’appoggio.
memorizzare Nome, Cognome e Numero di 13. Ordinare una Pila servendosi di un’altra struttura di
matricola. Creare un programma principale per: appoggio a scelta.
a) inserire 10 studenti nella Pila;
14. Fondere due pile di interi ordinate, cioè creare una
b) togliere dalla pila l’ultimo studente inserito;
terza pila che contenga i nodi delle due pile.
c) togliere dalla pila il primo studente inserito.
15. Scrivere in versione iterativa e in versione ricorsiva
4. Implementare un tipo di dato astratto Coda di interi
un algoritmo che svuoti una Pila.
(struttura di dati astratta in cui è possibile
prelevare solo dalla testa della coda e inserire solo 16. Date due pile di interi P1 e P2, trovare quanti nodi
in fondo alla coda); in particolare, implementare le di P1 sono maggiori del massimo di P2.
operazioni di: 17. Implementare l’operazione di concatenazione tra
• creazione di una nuova coda; due pile o tra due code o tra due sequenze.
• inserimento di un nodo nella coda; 18. Creare una funzione membro PulisciPila( ) che
• estrazione di un nodo dalla coda; pulisce la Pila dai valori contenuti.
• verifica che la coda non sia vuota.

112 Modulo B: Allocazione dinamica della memoria


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 113

C++ Modulo C
e gli oggetti

• U.D. C1. Iniziamo a lavorare con gli oggetti


• U.D. C2. Allocazione dinamica, oggetti
e puntatori
• U.D. C3. Ereditarietà e polimorfismo
• U.D. C4. Overloading di operatori e classi
generiche
• U.D. C5. Flussi e database

Prerequisiti
● Operatori, operandi, strutture di controllo
● Concetti fondamentali della programmazione a oggetti

Obiettivi
● Saper programmare a oggetti in C++

Conoscenze da apprendere
● Conoscere la sintassi di classi, variabili e funzioni membro e il loro livello di visibilità
● Conoscere la sintassi delle funzioni costruttore e distruttore
● Conoscere il passaggio di oggetti come parametri e come valori di ritorno
● Conoscere le relazioni esistenti tra oggetti e puntatori
● Conoscere la sintassi per creare funzioni amiche
● Conoscere la sintassi per creare gerarchie di ereditarietà
● Conoscere i concetti di variabili e funzioni membro di classe
● Conoscere il concetto di overloading di operatori
● Conoscere le funzioni per interagire con flussi e database

Competenze da acquisire
● Saper implementare classi con variabili e funzioni membro
● Saper creare e far interagire oggetti
● Saper utilizzare funzioni membro polimorfe
● Saper creare gerarchie di classi con ereditarietà
● Saper estendere e ridefinire funzioni membro
● Saper operare con i flussi
● Saper interagire con i database tramite classi e oggetti

113
p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 114

Unità didattica

C1 Iniziamo a lavorare
con gli oggetti
Prerequisiti ● Caratteristiche di base del linguaggio C++
● Conoscenze e competenze di base necessarie a scrivere semplici programmi C++
● Concetti fondamentali della programmazione a oggetti

Obiettivi ● Saper dichiarare e implementare in C++ semplici classi con variabili e funzioni
membro
● Confrontare il C++ procedurale e il C++ a oggetti

Conoscenze ● Conoscere la sintassi di classi, variabili e funzioni membro


da apprendere ● Conoscere la sintassi per i livelli di visibilità di variabili e funzioni membro
● Conoscere il ciclo di vita di un oggetto e la sua interazione con gli altri oggetti
● Conoscere la sintassi delle funzioni costruttore
● Conoscere il passaggio di oggetti come parametri e valori di ritorno

Competenze ● Saper implementare semplici classi con variabili e funzioni membro


da acquisire ● Saper distinguere variabili locali da variabili istanza
● Saper creare oggetti
● Saper far interagire oggetti
● Saper creare classi che utilizzano altre classi
● Saper impostare diversamente un programma scritto in C++ procedurale e uno
scritto in C++ a oggetti

1 Classi di oggetti
Il paradigma della programmazione orientata agli oggetti (OOP, Object Oriented
Programming) ha come concetto chiave quello di oggetto, che è una metafora del
concetto di oggetto del mondo reale. In un linguaggio di programmazione orientato
agli oggetti non si definiscono i singoli oggetti, ma si determinano le caratteristiche
generiche che ogni singolo oggetto deve possedere per poter far parte di una fami-
glia di oggetti, detta classe. Se consideriamo, ad esempio, la classe Albero, possiamo
dire che, come classe, Albero descrive le caratteristiche comuni che i singoli oggetti
“albero” devono possedere per far parte di questa famiglia, e cioè: “ha foglie e radi-
ci”, “cresce”, “possiede la clorofilla” ecc.
La classe Albero fa quindi da modello astratto del concetto di albero; “modello
astratto” vuol dire che descrive soltanto le caratteristiche di un albero ma ancora
non è un albero concreto: per poterlo essere (e, quindi, per potercisi aggrappare, per
poterlo tagliare, per interagire con lui) bisogna creare esplicitamente ciò che si chia-
ma istanza concreta.

114 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 115

Riepilogando:
• una classe è un modello astratto generico per una famiglia di oggetti con caratteri-
stiche simili;
• un’istanza (od oggetto) è la rappresentazione concreta e specifica di una classe.
“Istanza” e “oggetto” saranno usati come sinonimi; anche se “oggetto” è un termi-
ne più generale, entrambi sono rappresentazioni concrete di una classe, che è inve-
ce una rappresentazione astratta.
Ad esempio:
CLASSE = Automobile
ISTANZA = OGGETTO = “La mia auto” (un particolare esemplare della classe Auto-
mobile)
Un’applicazione C++ qualsiasi può essere composta da una o più classi. Una clas-
se è costituita generalmente da due parti: attributi (o proprietà) e metodi.
Le proprietà o attributi specificano le caratteristiche della classe determinandone
l’aspetto, lo stato, le altre qualità. Esempi di proprietà possono essere l’altezza, il co-
lore, la larghezza, il funzionamento, il nome. È utile pensare alle proprietà come agli
aggettivi che rappresentano gli oggetti. In C++ gli attributi sono definiti mediante va-
riabili membro (di una classe) o, anche, variabili istanza perché ogni oggetto (istan-
za di una classe) possiede le proprie variabili, nelle quali memorizza i valori dei pro-
pri attributi.
I metodi specificano le funzionalità che la classe offre, cioè le operazioni che un og-
getto è in grado di compiere e che corrispondono ai comportamenti dell’oggetto. È
utile pensare ai metodi come ai verbi che esprimono l’azione da compiere. Esempi di
metodi possono essere stampa, pulisci, muovi ecc. In C++ i metodi sono definiti me-
diante funzioni membro (di una classe).
Un programma object oriented scritto in C++ è solitamente composto da più classi.
Ogni classe deve essere dichiarata specificando quali sono le sue variabili e le sue
funzioni membro. La struttura base della dichiarazione di una classe è la seguente:

class <NomeClasse>
{
<ListaVariabiliMembro>
<ListaFunzioniMembro>
};

La parola chiave class serve per iniziare la definizione di una classe ed è seguita
dal nome della classe stessa. Le parentesi graffe delimitano il corpo o il blocco rela-
tivo alla classe: tra di esse si inserisce tutto il contenuto della classe, costituito dal-
le definizioni delle variabili membro e dalle definizioni delle funzioni membro. Natu-
ralmente possono esistere classi formate da sole variabili membro, così come classi
formate da sole funzioni membro o, anche, classi vuote.
Come possiamo osservare, ogni dichiarazione di classe è considerata un’istruzio-
ne, pertanto termina con il punto e virgola “;”.

Nella sintassi precedente <ListaVariabiliMembro> è una lista di definizione di va-


riabili membro, ognuna delle quali ha la forma:
<Tipo> <NomeVariabileMembro>;

Le variabili membro devono essere dichiarate nel blocco di una classe, ma ester-
namente alle funzioni membro.
Sempre nella sintassi precedente, <ListaFunzioniMembro> è invece una lista di de-
finizioni delle funzioni membro che descrivono il comportamento della classe, ognu-
na delle quali ha la forma:

Unità didattica C1: Iniziamo a lavorare con gli oggetti 115


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 116

<TipoValoreRestituito> <NomeFunzioneMembro> ([<Parametri>])


{
<Istruzioni>
}

La dichiarazione di una funzione membro è fatta all’interno del blocco di una clas-
se. Ogni funzione membro è caratterizzata da:
• un nome, un elenco di parametri;
• un tipo del valore di ritorno;
• le istruzioni che formano il corpo della funzione membro.
I parametri sono elencati dopo il nome della funzione membro, racchiusi all’inter-
no delle parentesi tonde e separati uno dall’altro mediante la virgola. Se una funzio-
ne membro non possiede parametri viene dichiarata usando solo le due parentesi
tonde.
Iniziamo creando dunque una nuova classe, la classe Automobile, aggiungendo al
suo interno le definizioni di alcune variabili membro e di una funzione membro.

Esempio 1
Una prima definizione della classe Automobile

class Automobile
{
int Serbatoio; // definizione di variabili membro
char* Marca, Colore;
int Cilindrata, Marcia;

void Rifornimento (int B) // definizione di una funzione membro


{
Serbatoio = Serbatoio + B;
}
}; // fine della dichiarazione di classe

In C++ una classe è vista come la realizzazione di un tipo di dato astratto (ADT, Ab-
stract Data Type) e per questo motivo viene anche chiamata tipo di dato concreto
(CDT, Concrete Data Type).
La dichiarazione precedente definisce quindi un nuovo tipo di dato concreto, il ti-
po Automobile, all’interno del quale sono presenti le dichiarazioni delle variabili
membro: Serbatoio, Marca, Colore, Cilindrata, Marcia e la definizione della funzione
membro Rifornimento( ).

2 Dichiarazione e implementazione
delle funzioni membro di una classe
Quando si è in presenza di funzioni membro molto lunghe e complesse, per una
scrittura più chiara del codice è possibile spezzare la definizione delle funzioni mem-
bro di una classe in due parti:
• una parte dichiarativa;
• una parte implementativa.
La parte dichiarativa di una funzione membro consiste nella definizione della so-
la segnatura (signature in inglese) anche detta (in C++) prototipo della funzione mem-
bro, ovvero nella dichiarazione di:

116 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 117

• nome, elenco e tipo dei parametri;


• tipo del valore di ritorno:
<TipoValoreRestituito> <NomeFunzioneMembro> ([<Parametri>]);

La parte implementativa consiste nella definizione del corpo di ogni sua funzione
membro, ciascuna delle quali dovrà essere implementata con la seguente sintassi:

<TipoValoreRestituito> <NomeClasse>:: <NomeFunzioneMembro> ([<Parametri>])


{
<Istruzioni>
}

Come possiamo notare, abbiamo introdotto un nuovo operatore :: (un doppio “due
punti” scritti in sequenza e senza spazi), detto operatore di scope o di risoluzione
del campo di azione. In sostanza tale operatore indica al compilatore che la funzio-
ne membro che si sta definendo appartiene alla classe specificata in <NomeClasse>,
ovvero che <NomeFunzioneMembro> è nel campo di azione di <NomeClasse>.
Se, ad esempio, volessimo dichiarare e poi implementare separatamente la funzione
membro Rifornimento( ) della classe Automobile procederemmo come nell’esempio 2.

Esempio 2
Dichiarazione e implementazione della classe Automobile (in uno stesso file)

class Automobile
{
int Serbatoio; // dichiarazione di variabili membro
char* Marca, Colore;
int Cilindrata, Marcia;

void Rifornimento(int B); // dichiarazione della funzione membro Rifornimento( )


}; // fine della dichiarazione di classe Automobile

// inizio parte implementativa della classe Automobile


void Automobile::Rifornimento(int B) // implementazione della funzione membro Rifornimento( )
{
Serbatoio = Serbatoio + B;
}

È anche possibile inserire la parte dichiarativa in un file header (cioè un file con
estensione .h) e la parte implementativa in un file sorgente con estensione .cpp. In
questo modo, mediante la direttiva #include si include il file header all’inizio del file
.cpp (la parte dichiarativa deve infatti sempre precedere la parte implementativa).
Per l’esempio precedente avremmo allora:

Esempio 3
Dichiarazione e implementazione della classe Automobile (in file separati)

File Automobile.h contenente la dichiarazione della classe Automobile

class Automobile
{
int Serbatoio; // dichiarazione di variabili membro
char* Marca, Colore;
int Cilindrata, Marcia;

Unità didattica C1: Iniziamo a lavorare con gli oggetti 117


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 118

void Rifornimento(int B); // definizione della funzione membro Rifornimento( )


}; // fine della dichiarazione di classe Automobile

File Automobile.cpp contenente la parte implementativa della funzione membro Rifornimento( )

#include "Automobile.h"

void Automobile::Rifornimento(int B) // implementazione della funzione membro Rifornimento( )


{
Serbatoio = Serbatoio + B;
}

3 Visibilità di variabili e funzioni membro:


incapsulamento e information hiding
Nella definizione di una classe è possibile utilizzare appositi specificatori per sta-
bilire quali funzioni e variabili membro sono visibili dall’esterno e quali sono invece
incapsulati all’interno della classe e, quindi, non visibili all’esterno. “Essere visibili”
per una variabile membro vuol dire permettere che oggetti esterni alla classe possa-
no leggere o modificarne il valore; “essere visibile” per una funzione membro signifi-
ca che oggetti esterni possono richiamare e mandare in esecuzione tale funzione.
Gli specificatori possibili che stabiliscono i livelli di visibilità di funzioni e varia-
bili membro sono:
• public: la variabile o la funzione membro è accessibile a qualsiasi altra classe;
• private: la variabile o la funzione membro non è accessibile alle altre classi.
Quest’ultimo livello incapsula la variabile membro all’interno dell’oggetto: solo la
classe che lo contiene può utilizzarlo o modificarne il valore; si realizza così il cosid-
detto information hiding.
Vedremo in seguito (parlando dell’ereditarietà) che esiste anche un altro specifi-
catore molto utilizzato per stabilire le modalità di accesso delle sottoclassi di una
classe; si tratta dello specificatore protected: la variabile o la funzione membro è ac-
cessibile solo alle sue sottoclassi.
Se non si inserisce alcuno degli specificatori precedenti vuol dire che la variabile
o la funzione membro non è visibile all’esterno, il livello di visibilità di default è per-
tanto private.
Vediamo subito un esempio di utilizzo degli specificatori, riferendoci all’esempio
della classe Automobile.

Esempio 4
Utilizzo degli specificatori di visibilità

class Automobile
{
public: // dichiarazione di variabili membro visibili all’esterno
char* Marca, Colore;
int Cilindrata, Marcia;

void Rifornimento(int B); // dichiarazione di una funzione membro visibile all’esterno


private:

118 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 119

int Serbatoio; // dichiarazione di variabili membro incapsulate


}; // fine della dichiarazione di classe Automobile

void Automobile::Rifornimento(int B) // implementazione della funzione membro Rifornimento( )


{
Serbatoio = Serbatoio + B;
}

Notiamo che Serbatoio è una variabile membro incapsulata, Marcia e Cilindrata sono
variabili membro visibili all’esterno, così come la funzione membro Rifornimento( ).
Facciamo un altro esempio di dichiarazione, quella della classe Cerchio del piano
cartesiano.

Esempio 5
Una prima dichiarazione della classe Cerchio

class Cerchio
{
public:
double X // variabili membro che rappresentano le coordinate del centro
double Y;
double Raggio;

double Circonferenza( ); // dichiarazione della funzione membro per il calcolo della circonferenza
double Area( ); // dichiarazione della funzione membro per il calcolo dell’area
}; // fine della dichiarazione di classe

Come abbiamo potuto notare dagli esempi precedenti, così come già visto per le
funzioni, una funzione membro che non restituisce alcun valore utilizza la parola
chiave void.
Per restituire invece un valore si utilizza l’istruzione return, che provoca anche la
conclusione dell’esecuzione della funzione membro. Ad esempio, la funzione membro
Circonferenza( ) della classe Cerchio restituisce un valore reale che rappresenta la
lunghezza della circonferenza del cerchio e la cui implementazione (pur con valori
approssimati) può essere la seguente:

double Cerchio::Circonferenza( )
{
const double Pigreco = 3.141593; // valore approssimato
return(2*Raggio*Pigreco);
}

L’istruzione return può essere presente senza alcun valore di ritorno; ad esempio,
la funzione membro Rifornimento( ) della classe Automobile può essere scritta nel se-
guente modo:

public void Automobile::Rifornimento(int B)


{
Serbatoio = Serbatoio + B;
return;
}

Unità didattica C1: Iniziamo a lavorare con gli oggetti 119


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 120

Si noti che ora la funzione membro è stata dichiarata void, quindi l’istruzione re-
turn non contiene alcun valore da restituire. In questo caso l’istruzione return è su-
perflua in quanto è l’ultima istruzione, e la funzione avrebbe comunque terminato la
sua esecuzione. Si ricorre all’istruzione return quando si vuole uscire dalla funzione
prima della sua fine.
Ogni classe, corredata delle sue variabili membro e delle sue funzioni membro, può
essere rappresentata graficamente con notazione UML (Unified Modelling Language),
una metodologia di analisi e progettazione a oggetti. I diagrammi UML che utilizzere-
mo in questa Unità e nelle successive servono per rappresentare le classi e le rela-
zioni tra classi in modo grafico e prendono il nome di diagrammi delle classi. In
realtà, per scopi didattici, utilizzeremo una forma “italianizzata” e semplificata dei
diagrammi delle classi.
Ad esempio, la classe Automobile può essere rappresentata graficamente nel se-
guente modo:

Automobile nome classe

+ char* Marca variabili membro


+ char* Colore
+ int Cilindrata
+ int Marcia
– int Serbatoio

+ void Rifornimento(IN int B) Modificatore funzioni membro

mentre la classe Cerchio sarebbe rappresentata come:

Cerchio nome classe

+ double X variabili membro


+ double Y
+ double Raggio

+ double Circonferenza( ) Query funzioni membro


+ double Area( ) Query

Come si può notare, per indicare il livello di visibilità sia delle funzioni membro, sia
delle variabili membro abbiamo utilizzato i seguenti simboli:
+ livello di visibilità pubblico
– livello di visibilità privato
È possibile anche utilizzare il simbolo:
# livello di visibilità protected
La parola riservata IN indica che il parametro della funzione membro Rifornimen-
to( ) è un parametro di input. Utilizzeremo poi OUT per i parametri di output e INOUT
per quelli di input e output.
Aggiungiamo tre nostre parole riservate accanto a ogni funzione membro, per spe-
cificarne il genere:
• Modificatore per indicare che tale funzione modifica lo stato dell’oggetto;
• Query per le funzioni che non modificano lo stato dell’oggetto;
• Costruttore per le funzioni membro costruttore di classe (che introdurremo a
breve).

120 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 121

4 Variabili locali e variabili membro


Occorre prestare attenzione a non confondere le variabili utilizzate all’interno del-
le funzioni membro per elaborazioni locali con le variabili membro.
Vediamo un esempio che mostra la differenza tra le due, riscrivendo la funzione
membro Rifornimento( ) nel seguente modo:

int Rifornimento(int B)
{
int Temp;
Temp = Serbatoio + B;
Serbatoio = Temp;
return(Temp);
}

La variabile semplice Temp, e in generale tutte le variabili dichiarate e utilizzate al-


l’interno delle funzioni membro, prendono il nome di variabili locali. La visibilità del-
le variabili locali è solo all’interno della funzione in cui sono definite. Al termine del-
l’esecuzione della funzione, queste variabili vengono deallocate.
Le variabili membro, invece, contengono i valori delle proprietà e possono assu-
mere diversi valori all’interno di ogni istanza; inoltre, permangono anche dopo l’ese-
cuzione di una funzione membro di quell’istanza.

5 Relazione tra classi, struct e union


In C++ esiste una relazione tra classi e strutture di dati di tipo struct e union. Val-
gono infatti le seguenti affermazioni:
• una struct è una classe (senza funzioni membro);
• una union è una classe in cui tutte le variabili membro condividono la stessa zona
di memoria. Tale zona viene dimensionata in base al tipo della variabile che occu-
pa lo spazio maggiore.
Per quanto appena affermato (essendo cioè considerate classi), anche per le va-
riabili struct e union è quindi possibile utilizzare i concetti relativi alle classi quali, ad
esempio, la visibilità delle variabili membro e la possibilità di incapsulare funzioni
membro al loro interno. Esaminiamo un caso specifico. È lecito definire la seguente
struct:
struct Cerchio
{
public:
double X; // prima coordinata (ascissa) del centro
double Y; // seconda coordinata (ordinata) del centro
double Raggio;

double Circonferenza( ); // funzione membro per il calcolo della circonferenza


double Area( ); // funzione membro per il calcolo dell’area
}; // fine della dichiarazione struct

A differenza delle classi, le variabili e le funzioni membro non precedute da uno


specificatore di visibilità sono per default public. Tale relazione è stata creata per
mantenere la compatibilità di alcune applicazioni scritte in C++ procedurale, tutta-
via nel C++ a oggetti conviene dichiarare esplicitamente il livello di visibilità di ogni
membro.

Unità didattica C1: Iniziamo a lavorare con gli oggetti 121


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 122

6 Creazione di oggetti,
funzioni costruttore e distruttore
Dai concetti di base della programmazione orientata agli oggetti sappiamo che una
classe non può essere utilizzata direttamente: è necessario creare un oggetto o istan-
za di quella classe. Facendo un parallelo tra le variabili e gli oggetti, si può dire che
alle variabili è associato un tipo, così come agli oggetti è associata una classe. Pos-
siamo riassumere il concetto nella figura C1.1.
✦ Figura C1.1
Analogie tra tipi Automobile
e classi.
Tipo int

Marca1
Bianco


➙ 100 1200


X A1



Valore della variabile Valori degli attributi
dell’oggetto
Nome della variabile Nome dell’oggetto

La creazione di un nuovo oggetto passa attraverso la sua dichiarazione e succes-


sivamente la sua allocazione.

6.1 La dichiarazione di un oggetto


La dichiarazione di un oggetto è il passo necessario per arrivare all’istanza di una
classe. Per dichiarare un oggetto utilizziamo la seguente sintassi, simile alla dichia-
razione di variabile di un qualsiasi tipo:
<NomeClasse> <NomeVariabileOggetto>;

dove <NomeVariabileOggetto> è il nome della variabile che si vuole associare all’og-


getto.
Ad esempio, in:
Automobile A1;

la variabile A1 sarà associata a un oggetto di classe Automobile dopo che tale ogget-
to sarà stato allocato.
L’allocazione dell’oggetto dichiarato avviene in seguito richiamando esplicita-
mente il costruttore su tale oggetto. Un costruttore è una particolare funzione mem-
bro occorrente per allocare lo spazio necessario alle strutture dati dell’oggetto. Il co-
struttore può essere definito dal programmatore, e in tal caso può avere dei para-
metri che servono per inizializzare le variabili membro dell’oggetto, ma non ha valo-
re di ritorno (neppure void).
Oltre ai costruttori definiti dal programmatore, per ciascuna classe è definito dal
sistema un costruttore di default privo di parametri. Tale costruttore è richiamato
dal sistema solo se non esistono costruttori definiti dal programmatore.

6.2 Allocazione con chiamata esplicita del costruttore


Per allocare un oggetto con chiamata esplicita del costruttore utilizziamo la se-
guente sintassi:
<NomeVariabileOggetto> = <NomeCostruttore> ([<Parametri>]);

122 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 123

dove <NomeCostruttore> è il nome della funzione membro costruttore della classe,


che in C++ deve essere necessariamente uguale al nome della classe.
Ad esempio:
A1 = Automobile( );

6.3 Dichiarazione e allocazione con chiamata esplicita


al costruttore
Le due fasi precedenti – dichiarazione e allocazione – possono essere sintetizzate
in un’unica istruzione in base alla seguente sintassi:
<NomeClasse> <NomeVariabileOggetto> = <NomeCostruttore> ([<Parametri>]);

Ad esempio:
Automobile A1 = Automobile( );

Creiamo il seguente costruttore per la classe Automobile:


public Automobile (int Carburante) costruttore
{
Serbatoio = Serbatoio + Carburante;
Marcia = 0;
}

dove Carburante rappresenta i litri di carburante da inserire nel serbatoio appena


l’oggetto sarà creato.
Quindi, per creare un nuovo oggetto Automobile scriveremo:
Automobile A1 = Automobile(20); /* crea un’automobile con 20 litri
di carburante nel serbatoio */

Il livello di visibilità per le funzioni membro costruttore è generalmente public. Se


una classe è costruita senza specificare il costruttore, a essa viene associato un co-
struttore vuoto e al momento della creazione dell’oggetto non vengono eseguite ope-
razioni.
Dopo la dichiarazione viene allocato l’oggetto e chiamato il costruttore, quindi in
memoria esiste un oggetto avente dimensioni date dalla somma delle dimensioni del-
le variabili membro e i cui valori contenuti, se non esiste un costruttore appropria-
to, sono indefiniti.

6.4 Dichiarazione di più funzioni costruttore


Possiamo aggiungere più funzioni membro costruttore nella stessa classe (quindi più
funzioni membro con lo stesso nome della classe) purché abbiano diverso numero o
tipo di parametri.
Ad esempio, avremmo potuto aggiungere alla classe Automobile anche la seguente
funzione membro costruttore:
Automobile(char* Mmarca, char* Mcolore, int Mcilindrata, int Carburante)
{
Marca = Mmarca;
Colore = Mcolore;
Cilindrata = Mcilindrata;
if (Carburante <= CapacitàMax)
Serbatoio = Carburante;
else
cout << "Troppo carburante" << endl;
}

Unità didattica C1: Iniziamo a lavorare con gli oggetti 123


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 124

Ovviamente bisogna aggiungere alla classe Automobile la variabile membro che in-
dica la massima capacità del serbatoio e inizializzarla a 45 (litri) all’interno del co-
struttore:
int CapacitàMax;

Avremmo potuto creare un’auto Auto1 di marca Marca1, di colore giallo, 1200 di ci-
lindrata e con 10 litri nel serbatoio in questo modo:
Automobile Auto1 = Automobile("Marca1", "giallo", 1200, 10);

e ancora un’auto Auto2 di marca Marca2, di colore verde, 1800 di cilindrata e con 30
litri nel serbatoio:
Automobile Auto2 = Automobile("Marca2", "verde", 1800, 30);

Possiamo rappresentare i due oggetti appena creati con i seguenti diagrammi UML:

Auto1 Auto2
Istanza Di Istanza Di
Automobile Automobile

Marca = "Marca1" Marca = "Marca2"


Colore = "giallo" Colore = "verde"
Cilindrata = 1200 Cilindrata = 1800
Serbatoio = 10 Serbatoio = 30

Notiamo come, rispetto ai diagrammi di classe, gli angoli sono arrotondati e so-
no presenti gli attuali valori delle variabili membro all’interno di ogni singolo og-
getto.
Riconsideriamo ora la classe Cerchio. Per creare un nuovo cerchio potremmo uti-
lizzare i costruttori dell’esempio 6.

Esempio 6
I costruttori della classe Cerchio

Cerchio( ) /* primo costruttore: crea un cerchio con centro nell’origine


con raggio unitario */
{
X = 0;
Y = 0;
Raggio = 1;
}

Cerchio(double VX, double VY, double VRaggio) /* secondo costruttore: crea un cerchio
con centro e raggio qualsiasi */
{
X = VX;
Y = VY;
Raggio = VRaggio;
}

Il primo costruttore crea un cerchio con centro nell’origine degli assi e raggio uni-
tario, il secondo crea un cerchio con centro e raggio qualsiasi. Allora, per creare due
cerchi utilizzando i precedenti costruttori scriveremo ad esempio:
Cerchio C1 = Cerchio( );
Cerchio C2 = Cerchio(2,3,56);

124 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 125

Riassumendo, avremo la classe Cerchio dichiarata nel file Cerchio.h, l’implementa-


tore nel file Cerchio.cpp e il programma principale nel file Main.cpp. Per compilare tut-
ti e tre i file, per come sono state impostate le direttive di include, basterà richiama-
re il compilatore sul file Main.cpp.

Esempio 7
Un nuova definizione della classe Cerchio (vedi sito: CPP.C1.01.C)
File Cerchio.h
#include <iostream.h>
class Cerchio
{
public:
double X; // prima coordinata del centro (ascissa)
double Y; // seconda coordinata del centro (ordinata)
double Raggio;
Cerchio( ); // dichiarazione del primo costruttore
Cerchio(double VX, double VY, double VRaggio); // dichiarazione del secondo costruttore
double Circonferenza( ); // dichiarazione della funzione membro per il calcolo della circonferenza
double Area( ); // dichiarazione della funzione membro per il calcolo dell’area
}; // fine della dichiarazione di classe

File Cerchio.cpp
#include "Cerchio.h"
Cerchio::Cerchio ( ) // implementazione del primo costruttore
{
X = 0;
Y = 0;
Raggio = 1;
}
Cerchio::Cerchio(double VX, double VY, double VRaggio) // implementazione del secondo costruttore
{
X = VX;
Y = VY;
Raggio = VRaggio;
}
double Cerchio::Circonferenza( )
{
const double Pigreco = 3.141593; // valore approssimato
return(2*Raggio*Pigreco);
}
double Cerchio::Area( )
{
const double Pigreco = 3.141593; // valore approssimato
return(Raggio*Raggio*Pigreco);
}

File Main.cpp
#include "Cerchio.cpp"
int main( )
{
cout << " Valore coordinata X: "; cin >> X;
cout << " Valore coordinata Y: "; cin >> Y;
cout << " Valore raggio: "; cin >> Raggio1;
Cerchio C1 = Cerchio(X1, Y1, Raggio1);
cout << " Valore coordinata X: "; cin >> X2;
cout << " Valore coordinata Y: "; cin >> Y2;
cout << " Valore raggio: "; cin >> Raggio2;
Cerchio C2 = Cerchio(X2, Y2, Raggio2);
return 0;
}

Unità didattica C1: Iniziamo a lavorare con gli oggetti 125


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 126

6.5 Forma abbreviata di istanziazione


Negli esempi svolti finora abbiamo sempre considerato la chiamata alla funzione
membro costruttore come una chiamata a una qualsiasi funzione membro della clas-
se; infatti abbiamo utilizzato la sintassi:
<NomeClasse> <NomeOggetto> = <NomeClasse([<Parametri>]);

Esiste un altro modo, che utilizza una forma abbreviata semplificata che non fa uso
del nome del costruttore, la cui sintassi è:
<NomeClasse> <NomeOggetto>([<Parametri>]);

Ad esempio, per creare gli oggetti C1 e C2 di classe Cerchio dell’esempio 6 è possi-


bile scrivere:
Cerchio C1( );
Cerchio C2(2,3,56);

7 Funzioni membro distruttore


Una funzione membro distruttore, analogamente a quanto avviene per le funzioni
membro costruttore, serve per eseguire alcune operazioni prima che un oggetto,
creato precedentemente, venga distrutto.
Un distruttore deve avere lo stesso nome della classe ma preceduto da ~ (tilde), e
non può avere argomenti né valori di ritorno:
~<NomeClasse>( );

Ad esempio:
~Cerchio( ); // dichiarazione di una funzione membro distruttore per la classe Cerchio

Cerchio::~Cerchio( ) // definizione del distruttore


{
// Qui si inseriscono le istruzioni da far eseguire prima che l’oggetto venga distrutto
}

In una classe è possibile dichiarare diversi metodi costruttori ma al più un solo me-
todo distruttore. Vedremo in seguito alcuni esempi dell’utilità delle funzioni distrut-
tore, quando parleremo dell’allocazione dinamica della memoria.

8 Ciclo di vita di un oggetto


Ogni oggetto è caratterizzato da diversi tempi, come illustrato di seguito.
• Un istante in cui viene creato. Al tempo della creazione viene allocata memoria per
le sue variabili membro. In questo momento il costruttore viene invocato automa-
ticamente dal compilatore.
• Un tempo di esistenza. Durante il tempo di esistenza l’oggetto risponde ai messaggi
provenienti da altri oggetti che richiedono l’accesso a variabili e funzioni presenti
nella sua interfaccia pubblica.
• Un istante in cui termina la sua vita o cessa di esistere. Questo istante corrisponde
alla fine del blocco di elaborazione in cui tale oggetto è stato allocato o, in ogni
caso, con la fine del programma principale. Questo istante può anche coincidere
con la chiamata al distruttore, che può avvenire:
– in modo esplicito tramite richiesta del programmatore;
– in modo implicito tramite chiamata automatica da parte del sistema.

126 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 127

9 Interazione tra oggetti


Una volta create le classi (con variabili membro e funzioni membro) e le istanze
che ci occorrono, vediamo come fanno tali istanze a comunicare tra loro.

9.1 Invocazione di una funzione membro


Gli oggetti comunicano mediante scambio di messaggi. Un messaggio parte da un
oggetto mittente e arriva a un oggetto destinazione il quale, all’atto della ricezione,
attiva il comportamento richiesto dal mittente.
Un messaggio è quindi costituito da:
• il nome dell’oggetto destinatario;
• il nome della funzione membro del destinatario che si vuole attivare;
• un elenco di parametri attuali che vengono passati alla funzione membro destina-
tario.
La sintassi da utilizzare è la seguente:
<NomeOggetto>.<NomeFunzioneMembro>([<Parametri>]);

dove il nome del destinatario e il nome della funzione membro richiesta vengono se-
parati da un punto (dot notation).
Ad esempio, se volessimo fare rifornimento di 15 litri per l’oggetto Auto1 dovrem-
mo scrivere l’istruzione:
Auto1.Rifornimento(15);

all’interno di un qualsiasi altro oggetto che riesca a “vedere” l’oggetto Auto1.


Quando arriva un messaggio da parte di un oggetto mittente, l’oggetto destinata-
rio controlla se tra le sue variabili membro ne esiste una che abbia la stessa segna-
tura della funzione membro richiesta, ovvero che coincida per nome e per numero e
tipo dei parametri.
Le funzioni membro, per essere richiamate, devono possedere un giusto livello di
visibilità: una funzione membro dichiarata private non può essere invocata con la
modalità vista in precedenza.

9.2 Accesso alle variabili membro


Per accedere direttamente a una variabile membro basterà usare la stessa nota-
zione utilizzata per le funzioni membro:
<NomeOggetto>.<NomeVariabileMembro>

Ad esempio:
Auto1.Cilindrata = 1200;

Un approccio più “pulito” per accedere a una variabile membro di un oggetto con-
siste nel far ricorso a una funzione membro apposita che possa effettuare modifiche
sulla variabile membro, come nel caso della funzione membro Rifornimento( ). Per
modificare la variabile membro Serbatoio, cioè per modificare lo stato dell’oggetto,
scriveremo:
Auto1.Rifornimento(10);

Un approccio molto utilizzato nella programmazione per componenti è rappresen-


tato dal ricorso per ogni variabile membro alle funzioni membro Get( ) e Set( ): si ag-
giunge una nuova funzione membro Set( ) per ogni variabile membro di cui si voglia
modificare il valore e una nuova funzione membro Get( ) per ogni variabile membro
che si voglia leggere, con le seguenti sintassi:

Unità didattica C1: Iniziamo a lavorare con gli oggetti 127


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 128

Get<NomeVariabileMembro>([<Parametri>])
Set<NomeVariabileMembro>([<Parametri>])

Ad esempio, potremo scrivere due funzioni membro: GetSerbatoio( ) e SetSerba-


toio( ) rispettivamente per leggere e scrivere un valore della variabile membro Ser-
batoio. Attenzione: pur se possibile, non è opportuno far ricorso all’accesso diretto
alla variabile membro, ma conviene sempre utilizzare le funzioni membro Get( ) e
Set( ); vediamo perché.
Con l’accesso diretto potremmo scrivere:
m.Serbatoio = 10.000;

Istruzione legittima, ma che non ci consente di effettuare alcun controllo sulla ca-
pacità massima del serbatoio, controllo che possiamo effettuare aggiungendo la fun-
zione membro SetSerbatoio( ).
Nell’esempio della classe Cerchio possiamo invece pensare alle funzioni GetRag-
gio( ) e SetRaggio( ) così definite:
double GetRaggio( ) // un esempio di funzione Get( )
{
return Raggio;
}
void SetRaggio(double R) // un esempio di funzione Set( )
{
Raggio = R;
}

10 Creiamo un’applicazione completa:


la classe Automobile
Abbiamo imparato a definire una classe Automobile, sappiamo come creare gli og-
getti di questa classe, sappiamo come far interagire gli oggetti: non ci rimane che
mettere il tutto insieme per creare un’applicazione C++ che utilizzi gli oggetti.
Creiamo tre file:
• un file che contiene la dichiarazione della classe Automobile (Automobile.h);
• un file che contiene l’implementazione della classe Automobile (Automobile.cpp);
• un file che contiene il programma principale (EsempioAutomobile.cpp).
In questo modo possiamo compilare la classe Automobile una volta sola e render-
la così disponibile per ogni altra applicazione od oggetto che la voglia utilizzare. Que-
sto modo di procedere è dettato quindi da princìpi di ingegneria del software: “si scri-
vono moduli (nel nostro caso un modulo coincide con un file) indipendenti e consi-
stenti”. Qualsiasi modifica alla nostra classe di prova non riguarderà dunque il mo-
dulo già compilato e funzionante della classe Automobile. Nel seguito, per motivi di
sinteticità, non sempre suddivideremo i nostri programmi in più file.

Esempio 8
La classe Automobile (vedi sito: CPP.C1.02.C)
File Automobile.h
#include <iostream.h>
class Automobile
{
public: // dichiarazione delle variabili membro pubbliche
char* Marca;
char* Colore;
int Cilindrata, Marcia;

128 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 129

Automobile(char* Mmarca, char* Mcolore, int Mcilindrata); // dichiarazione delle funzioni


int SetSerbatoio(int B); // membro pubbliche
double GetSerbatoio( );
void SaliMarcia( );
void ScendiMarcia( );
void AvviaMotore( );
void MostraStato( );
private: // dichiarazione delle variabili membro private
int Serbatoio;
int CapacitaMax;
bool StatoMotore;
}; // fine dichiarazione della classe Automobile

File Automobile.cpp
#include "Automobile.h" // inizio parte implementativa della classe Automobile
Automobile::Automobile(char* Mmarca, char* Mcolore, int Mcilindrata)
{
Marca = Mmarca;
Colore = Mcolore;
Cilindrata = Mcilindrata;
StatoMotore = false;
Marcia = 0; // l’auto e’ in folle
Serbatoio = 0;
CapacitaMax = 45;
}
int Automobile::SetSerbatoio(int B)
{
if (B+Serbatoio < CapacitaMax)
{
Serbatoio = Serbatoio + B;
return 0; // restituisce 0 in caso di inserimento corretto del carburante
}
else
return -1; // restituisce –1 in caso di inserimento di troppo carburante
}
double Automobile::GetSerbatoio( )
{
return Serbatoio;
}
void Automobile::SaliMarcia( )
{
if (Marcia<5)
Marcia ++;
}
void Automobile::ScendiMarcia( )
{
if (Marcia>2)
Marcia – –;
}
void Automobile::AvviaMotore( )
{
StatoMotore = true;
}
void Automobile::MostraStato( )
{
cout << "Automobile: " << Marca << " " << Cilindrata << " " << Colore <<'\n';
cout << "serbatoio = " << Serbatoio << " litri di carburante" << '\n' ;
cout << "marcia = " << Marcia << '\n';
if (StatoMotore == 1)
cout << "Motore acceso" << '\n';
else
cout << "Motore spento" << '\n';
}

Unità didattica C1: Iniziamo a lavorare con gli oggetti 129


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 130

File EsempioAutomobile.cpp
int main( )
{
Automobile Auto1 = Automobile("Marca1", "giallo", 1200);
cout << "Chiamo la funzione membro MostraStato( ) " << '\n' ;
Auto1.MostraStato( );
cout << "=====================" << '\n';
cout << "Avvio il motore " << '\n';
Auto1.AvviaMotore( );
cout << "=====================" << '\n';
cout << "Chiamo la funzione membro SetSerbatoio( ) " << '\n';
Auto1.SetSerbatoio(5);
cout << "=====================" << '\n';
cout << "Chiamo la funzione membro SaliMarcia( ) " << '\n';
Auto1.SaliMarcia( );
cout << "=====================" << '\n';
cout << "Chiamo la funzione membro MostraStato( ) " << '\n';
Auto1.MostraStato( );
return 0;
} // fine main( )

Come possiamo vedere, abbiamo aggiunto alla classe Automobile le funzioni mem-
bro e le relative variabili membro necessarie a simulare un’auto che possa:
mettersi in moto ➔ funzione AvviaMotore( )
fare rifornimento ➔ funzione SetSerbatoio( )
cambiare marcia ➔ funzioni SaliMarcia( ) e ScendiMarcia( )
La presenza della funzione query MostraStato( ) ci permette di vedere come varia-
no le variabili membro a seconda delle funzioni membro che vengono richiamate.
Una volta compilato il programma e mandato in esecuzione il file eseguibile, ot-
terremo a video il seguente risultato:

11 Passaggio di oggetti come parametri


e valori di ritorno
Un oggetto può essere passato come parametro a una funzione, così come può es-
sere restituito quale valore di ritorno.
Gli oggetti vengono passati come parametri utilizzando le stesse regole che si se-
guono per le variabili strutturate. Analizzeremo le diverse modalità di passaggio di
parametri nella prossima Unità; per il momento impiegheremo il normale passaggio
per valore.
Quando una funzione restituisce un oggetto come valore di ritorno, viene creato
un oggetto temporaneo che contiene i valori delle variabili membro dell’oggetto da
restituire. Dopo la restituzione, tale oggetto temporaneo viene distrutto.

130 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 131

Facciamo ora un esempio in cui definiamo una classe che al suo interno utilizza
un’altra classe; l’esempio ci servirà per mostrare sia il passaggio di un oggetto come
parametro sia la sua restituzione come valore di ritorno. Cominciamo definendo la
classe Punto che rappresenta un punto del piano cartesiano (esempio 9).

Esempio 9
La classe Punto (vedi sito: CPP.C1.03.C)
#include <iostream.h>
class Punto
{
public:
float X;
float Y;
Punto( ) // primo costruttore: crea un punto nell’origine
{
X = 0;
Y = 0;
}
Punto(float X1) // secondo costruttore: crea un punto sull’asse ascisse
{
X = X1;
Y = 0;
}
Punto(float A, float B) // terzo costruttore
{
X = A;
Y = B;
}
int InQualeQuadrante( ) // restituisce il quadrante dove si trova il punto
{
if (X>0 && Y>0) // primo quadrante
return(1);
if (X>0 && Y<0) // quarto quadrante
return(4);
if (X<0 && Y>0) // secondo quadrante
return(2);
if (X<0 && Y<0) // terzo quadrante
return(3);
if (X=0 && Y=0) // origine
return(0);
return(-1); // su di un asse
}
}; // fine classe Punto

Le uniche variabili membro sono le coordinate X e Y di un punto. Abbiamo defini-


to tre funzioni costruttore, rispettivamente per creare: un punto nell’origine, un pun-
to sull’asse delle ascisse, un punto in una posizione generica sul piano, e una funzio-
ne che restituisce il numero del quadrante nel quale si trova il punto.
Un possibile programma main( ) di utilizzo della classe Punto è il seguente:

int main( )
{
float X1, Y1;
double D = 0;
Punto P0 = Punto( ); // crea un punto origine degli assi
cout << "X: " ; cin >> X1;
cout << "Y: " ; cin >> Y1;
Punto P1 = Punto(X1, Y1); // crea un nuovo punto con coordinate X1 e Y1 appena lette
return 0;
}

Unità didattica C1: Iniziamo a lavorare con gli oggetti 131


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 132

Utilizziamo ora questa classe per ridefinire la classe Cerchio.

Esempio 10
Una definizione alternativa della classe Cerchio (vedi sito: CPP.C1.04.C)

class Cerchio
{
public:
Punto Centro; // variabile membro che rappresenta il centro del cerchio
double Raggio;

Cerchio( ); // dichiarazione della funzione costruttore


Cerchio(Punto C1, double R1); // oggetto di classe Punto come parametro

double Circonferenza( ); // dichiarazione funzione per il calcolo della circonferenza


double Area( ); // dichiarazione funzione per il calcolo dell’area
Punto GetCentro( ); // oggetto di classe Punto come valore di ritorno
}; // fine della dichiarazione di classe

Cerchio::Cerchio( )
{
Centro.X = 0;
Centro.Y = 0;
Raggio = 1;
}

Cerchio::Cerchio(Punto C1, double R1)


{
Centro.X = C1.X;
Centro.Y = C1.Y;
Raggio = R1;
}

double Cerchio::Circonferenza( )
{
const double Pigreco = 3.141593; // valore approssimato
return(2*Raggio*Pigreco);
}
double Cerchio::Area( )
{
const double Pigreco = 3.141593; // valore approssimato
return(Raggio*Raggio*Pigreco);
}
Punto Cerchio::GetCentro( )
{
return(Centro);
}

Come possiamo notare:


• la variabile membro Centro, all’interno della classe Cerchio, è di classe Punto;
• un costruttore utilizza un oggetto di classe Punto come parametro;
• la funzione membro GetCentro( ) restituisce un oggetto di classe Punto (il centro
del cerchio) come valore di ritorno.
Un possibile programma main( ) di utilizzo della nuova classe Cerchio è il seguente:

int main( )
{
Punto P1 = Punto(2,3);

132 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 133

Cerchio C1 = Cerchio(P1,10);
cout << " Valore coordinata X di C1: " << C1.Centro.X << endl;
cout << " Valore coordinata Y di C1: " << C1.Centro.Y<< endl;
cout << " Valore raggio di C1: " << C1.Raggio<< endl;
Punto C = Punto( );
C = GetCentro( );
return 0;
}

Come possiamo osservare, definiamo prima un oggetto di classe Punto e poi lo pas-
siamo come parametro al costruttore della classe Cerchio.
Il metodo getCentro( ) restituisce nella variabile locale C (di classe Punto) il centro
del cerchio.

12 Un confronto tra la programmazione


imperativa e quella a oggetti
Consideriamo le operazioni necessarie per:
• caricare un vettore;
• visualizzare il contenuto di un vettore;
• ricercare un elemento;
• determinare la somma dei suoi elementi.
Tali operazioni sono state implementate tramite funzioni quando abbiamo parlato
del C++ imperativo. Impostiamo, questa volta a oggetti, un programma che esegua le
stesse operazioni.
Approfittiamo dell’esempio per effettuare in modo più sistematico analisi e pro-
gettazione a oggetti.

12.1 Analisi e progettazione a oggetti per il nostro esempio


L’obiettivo di un buon programmatore deve essere di iniziare a “pensare a ogget-
ti”: oggetti che hanno vita autonoma e che interagiscono in un mondo di altri ogget-
ti. Ricordiamo che i nostri obiettivi, che si identificano con quelli più generali della
programmazione a oggetti, sono:
• facilità di comprensione e conseguentemente facilità di modifica del programma;
• facilità di riutilizzo del programma per altri scopi.
Dunque, come si procede? Elenchiamo di seguito alcune semplici regole da tenere
presenti nella fase di analisi e di progettazione a oggetti, facendo direttamente riferi-
mento all’esempio che stiamo considerando.
1. Individuare le classi che occorrono. Questa attività è la più difficile: per essa serve
un’esperienza di programmazione che si acquisisce nel tempo. Nel nostro esem-
pio l’unica classe è Vettore.
2. Individuare le variabili membro ovvero le proprietà delle classi individuate al punto
precedente.
3. Individuare le funzioni membro ovvero i comportamenti che si vuole che tali classi
abbiano.
4. Stabilire la visibilità di variabili e funzioni membro (information hiding).
5. Implementare le funzioni membro.

Il codice C++ a oggetti relativo alla classe Vettore è riportato nell’esempio 11.

Unità didattica C1: Iniziamo a lavorare con gli oggetti 133


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 134

Esempio 11
La classe Vettore (vedi sito: CPP.C1.05.C)

File Vettore.cpp
#include <iostream.h>
class Vettore
{
private:
int* V; // variabili istanza
int MAXEL;
int Occorrenza(int Num);
public:
Vettore(int N);
void Riempi( );
void Visualizza( );
bool Ricerca(int Num);
int Somma( );
}; // fine dichiarazione classe Vettore
Vettore::Vettore(int N) // inizio parte implementativa classe Vettore
{
V = new int[N]; // crea un vettore di dimensione N
MAXEL = N;
}
void Vettore::Riempi( )
{
int I;
I = 0;
while (I<MAXEL)
{
cout << "Inserisci il nuovo elemento:"; cin >> V[I];
I++;
}
} // fine Riempi( )
void Vettore::Visualizza( )
{
int I = 0;
while (I<MAXEL)
{
cout << I << " elemento: " << V[I] << '\n';
I = I + 1;
}
}
bool Vettore::Ricerca(int Num)
{
if(Occorrenza(Num) == 0)
return(false);
else
return(true);
}
int Vettore::Occorrenza(int Num)
{
int T = 0;
int I = 0;
while (I<MAXEL)
{
if (V[I] == Num)
T++;
I = I+1;
}
return(T);
}
int Vettore::Somma( )

134 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:40 Pagina 135

{
int S = 0;
int I = 0;
while (I<MAXEL)
{
S = S+V[I];
I = I+1;
}
return(S);
}
int main( )
{
int S = 0;
Vettore V1 = Vettore(10); // crea un vettore di 10 elementi interi
cout << "Riempimento del vettore ..." << '\n';
V1.Riempi( );
cout << "—————"<< '\n';
cout << "Visualizza gli elementi inseriti" << '\n';
V1.Visualizza( );
cout << "—————" << '\n';
if (V1.Ricerca(12)) // ricerca nel vettore l’elemento con valore 12
cout << "elemento trovato" << '\n';
else
cout << "elemento non trovato";
cout << "—————" << '\n';
S = V1.Somma( );
cout << "Somma degli elementi:" << S << '\n';
return 0;
} // fine main

Compilandola ed eseguendola avremo a video un risultato come il seguente:

È ora facile pensare ad altre funzioni membro che possono essere implementate,
quali quelle che consentono di:
• ordinare un vettore secondo l’ordine crescente;
• ricercare un valore con ricerca binaria.

12.2 Confronto del codice a oggetti con quello imperativo


Avendo svolto lo stesso esempio, quello del Vettore, con due differenti approcci
(procedurale e a oggetti), possiamo ora confrontare i due codici sorgenti per sco-
prirne le diversità. Attenzione: parliamo di diversità di due approcci che comunque
sono equivalenti per quel che riguarda la capacità risolutiva. In altre parole, la OOP
non fa nulla di più e niente di meno della programmazione procedurale, semplice-

Unità didattica C1: Iniziamo a lavorare con gli oggetti 135


p113-138_CeC_Unità C1 26-09-2007 16:41 Pagina 136

mente consente di assolvere “meglio” allo stesso compito. Il codice a oggetti pre-
senta le seguenti caratteristiche.
• Si ha subito una visione precisa e chiara dell’interfaccia del “modulo” ovvero ciò che
il modulo offre al resto del mondo. Questo significa che se si vuole modificare l’im-
plementazione di una funzione membro si può farlo senza problemi in quanto la sua
implementazione non è visibile all’esterno. Nel nostro esempio, se vogliamo cam-
biare l’implementazione della funzione membro Ricerca( ), possiamo farlo. Nell’e-
sempio imperativo invece si ricorreva alle variabili globali, la cui gestione è sempre
problematica.
• Le funzioni membro della classe Vettore del codice a oggetti trovano i loro corri-
spettivi nelle funzioni con lo stesso nome del codice procedurale. Come possiamo
notare, le funzioni membro hanno un parametro in meno perché non occorre spe-
cificare (come invece avviene per le funzioni) l’oggetto sul quale si agisce (nelle
funzioni, il parametro V). Tale parametro potrebbe non essere utilizzato anche
nelle funzioni se si ricorresse alla variabile globale Vett, visibile quindi da tutte le
funzioni, ma come già sappiamo è buona regola di programmazione non generare
troppe variabili globali, ricorrendo piuttosto all’uso del passaggio dei parametri.
• Per avere un programma che funzioni su vettori di qualsiasi dimensione è stata
introdotta nel codice a oggetti la variabile membro MAXEL. Il suo equivalente può
essere o la variabile globale MAXEL (soluzione da noi scartata, in questo caso infat-
ti ritorneremmo a uno stile di programmazione non ideale) o l’aggiunta di un nuovo
parametro a ogni funzione (soluzione da noi adottata anche se ciò presenta altri
inconvenienti, si pensi alle operazioni su più vettori).
• La classe Vettore è un ottimo punto di partenza per sviluppi futuri. Ad esempio, se
volessimo aggiungere altri comportamenti come le funzioni membro per la ricerca
binaria o per l’ordinamento potremmo farlo molto facilmente. Nell’esempio impe-
rativo, invece, i dati (ad esempio la dichiarazione int Vett[10]) sono scollegati dalle
funzioni (Riempi( ), Visualizza( ) ecc.); non vi è quindi un’unica entità che incapsu-
la le operazioni permesse sui dati.
• Non è infine possibile definire quali sono le sole operazioni visibili all’esterno. Non
si può cioè nascondere parte delle funzioni (ad esempio la funzione Occorrenza( ),
che può essere considerata una funzione di servizio).

136 Modulo C: C++ e gli oggetti


p113-138_CeC_Unità C1 26-09-2007 16:41 Pagina 137

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Come si fa, in C++, a dichiarare una variabile locale?


2. Quali sono i livelli di visibilità associati alle variabili membro, e quali quelli associati alle funzioni membro?
3. Che differenza c’è tra il livello public e il livello private?
4. Qual è la differenza tra un costruttore e un distruttore?
5. Che differenza c’è tra dichiarazione e allocazione di un oggetto?
6. Come avviene il passaggio di un oggetto come parametro?
7. Quanti costruttori può avere una classe C++?
8. In C++ è possibile avere più funzioni membro costruttore? Se sì, fare un esempio.

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Creare una semplice classe per descrivere la gestione di un conto corrente bancario, supponendo di poter
svolgere almeno le seguenti operazioni:
• depositare denaro;
• prelevare denaro;
• chiedere il saldo corrente (l’importo complessivo presente sul conto).
2. Utilizzare la classe creata per l’esercizio precedente, eventualmente modificandola, per:
• creare la classe “movimento su conto corrente”;
• creare un programma principale che trasferisca una somma di denaro tra due conti correnti.
3. Creare una classe Motocicletta in cui sono presenti le stesse funzioni membro viste per la classe Automobile
definita in questa Unità e, inoltre, aggiungere le seguenti funzioni membro:
• Accelera( )
• Frena( )
• AccendiFreccia( )
• AccendiLuci( )
4. Creare una classe Televisore con la quale si possa interagire per:
• accendere il televisore;
• spegnere il televisore;
• cambiare canale;
• alzare il volume.
5. Definire una classe BIT con le funzioni membro Set( ), Reset( ), Complementa( ).
6. Definire una classe BYTE con le funzioni membro Set( ), Reset( ), Shift( ), or( ), xor( ).
7. Definire la classe TelefonoCellulare in modo che sia possibile effettuare e ricevere telefonate.

Unità didattica C1: Iniziamo a lavorare con gli oggetti 137


p113-138_CeC_Unità C1 26-09-2007 16:41 Pagina 138

Verifica
di fine unità

Esercizio svolto
1. Classe Calcolatrice
Definire la classe Calcolatrice che possa svolgere le quattro operazioni elementari.

Analisi del processo risolutivo


Le uniche variabili membro (private) sono i due operandi Op1 e Op2. Definiamo quattro funzioni membro, una
per ogni operazione elementare. Dal programma principale (che occorrerà sviluppare separatamente) si
propone dapprima la scelta dell’operazione che si vuole eseguire, poi si richiede l’inserimento dei due operandi
(vedi sito: CPP.C1.06.C).

#include <iostream.h>

class Calcolatrice
{
private:
double Op1;
double Op2;
public:
Calcolatrice( )
{
Op1=0
Op2=0;
} // fine costruttore
double Addiziona( )
{
return (Op1+Op2);
}
double Sottrai( )
{
return (Op1-Op2);
}
double Moltiplica( )
{
return (Op1*Op2);
}
double Dividi( )
{
return (Op1/Op2);
}
}; // fine classe

138 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 139

Unità didattica

Allocazione dinamica,
oggetti e puntatori C2
● Caratteristiche di base del linguaggio C++ Prerequisiti
● Conoscenze e competenze di base necessarie a scrivere semplici programmi
C++
● Concetti di base per utilizzare in C++ classi, variabili membro e funzioni
membro

● Utilizzare i puntatori a oggetti come parametri nelle funzioni Obiettivi


● Far interagire le classi tramite funzioni amiche

● Conoscere la sintassi per creare puntatori a oggetti Conoscenze


● Conoscere il passaggio dei puntatori a oggetti come parametri da apprendere
● Conoscere la sintassi per variabili e funzioni membro di classe
● Conoscere il concetto di funzione amica di una classe

● Saper distinguere variabili e funzioni di classe da variabili e funzioni istanza Competenze


● Saper stabilire quando è opportuno utilizzare le funzione amiche da acquisire

1 Allocazione dinamica di oggetti in memoria


Finora, quando abbiamo definito un oggetto di una determinata classe, abbiamo
sempre supposto che tale oggetto occupasse una certa dimensione in memoria, pa-
ri alla dimensione delle sue variabili membro, e che il momento della sua fine coin-
cidesse con il momento della fine del blocco in cui era stato definito, come qualun-
que altra variabile statica.
Abbiamo inoltre già visto nel Modulo B la possibilità di allocare dinamicamente,
cioè a tempo di esecuzione, la memoria che ci serve per i tipi primitivi o per quelli
definiti dall’utente.
È possibile definire dinamicamente anche un oggetto ricorrendo a new, ma questa
volta new richiama esplicitamente il costruttore dell’oggetto.

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 139


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 140

1.1 L’operatore new


L’operatore new crea oggetti la cui esistenza è limitata al solo periodo di effettiva
utilità; l’allocazione di tali oggetti avviene nell’area di memoria heap.
La creazione di un nuovo oggetto avviene attraverso due fasi. La prima è la di-
chiarazione, e segue la sintassi:

<NomeClasse> *<NomePuntatoreAOggetto>;

dove <NomePuntatoreAOggetto> è il nome della variabile puntatore che si vuole far


puntare all’oggetto.
Ad esempio, consideriamo la classe Punto definita nell’Unità precedente. Con l’i-
struzione:

Punto P1;

la variabile P1 rappresenta direttamente l’oggetto di classe Punto.


Invece, con l’istruzione:

Punto *pP1;

la variabile pP1 è un puntatore a un oggetto di classe Punto. P1 è un oggetto allocato,


pP1 è ancora solo un puntatore. La variabile pP1 potrà riferirsi a un vero oggetto in
due modi. Nel primo modo dichiariamo che pP1 è un “altro nome” per P1; l’oggetto è
uno solo, ma con due “nomi” diversi:

pP1 = &P1;

Nel secondo modo occorre introdurre la sintassi per l’allocazione di un oggetto at-
traverso l’operatore new:

<NomeVariabileOggetto> = new <NomeCostruttore>([<Parametri>]);

dove <NomeCostruttore> è il nome della funzione membro costruttore della classe.


Dunque, per assegnare un nuovo oggetto di classe Punto alla variabile pP1, scrivere-
mo ad esempio:

pP1 = new Punto(12,45);

Le fasi di dichiarazione e di allocazione possono essere raggruppate in un’unica


istruzione con la seguente sintassi:

<NomeClasse> *<NomePuntatoreAOggetto> = new <NomeCostruttore>([<Parametri>]);

Scrivendo in un’unica riga avremo:

Punto * pP1 = new Punto(12,45);

Costruttore: suddivide e riempie lo spazio allocato per l’oggetto


di classe Punto inizializzando i valori delle variabili membro
Viene allocato spazio (fisico) per un oggetto delle dimensioni di Punto
Viene assegnato al puntatore pP1 l’indirizzo iniziale dell’area di memoria
in cui è allocato l’oggetto di classe Punto
Viene allocato lo spazio per un indirizzo, cioè la variabile pP1 che conterrà un puntatore
a un oggetto di classe Punto

140 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 141

Riassumendo, in riferimento al nostro esempio di classe Punto possiamo dare la se-


guente rappresentazione:

pP1

12 Oggetto di classe Punto


Valore iniziale della variabile membro X
45 Valore iniziale della variabile membro Y

La variabile pP1 del nostro esempio sarà quindi un puntatore allo spazio di me-
moria in cui saranno presenti i valori delle variabili membro.
Attenzione: new può essere utilizzato anche per creare dinamicamente tipi primi-
tivi o definiti dall’utente. Ad esempio, sono perfettamente lecite le seguenti istru-
zioni:

float *F = new float(4,5); /* alloca un’area di memoria adatta a contenere un


float inizializzandola con un valore */
float *F = new float; /* alloca un’area di memoria adatta a contenere un
float ma non la inizializza */
char *Str = new char[80]; /* alloca un’area di memoria adatta a contenere un
array di 80 caratteri */

1.2 Accesso alle variabili e alle funzioni membro


Per accedere alle variabili e alle funzioni membro di oggetti creati utilizzando pun-
tatori ovvero creati utilizzando new, non si ricorre più alla dot notation che abbiamo
utilizzato finora, bensì all’operatore freccia –>, seguendo quindi la sintassi:
<NomePuntatore> –> <NomeVariabileMembro>;

oppure la sintassi:
<NomePuntatore> –> <NomeFunzioneMembro>([<Parametri>]);

Ad esempio, per accedere alla variabile membro X e alla funzione membro InQua-
leQuadrante( ) dell’oggetto di classe Punto puntato da pP1, scriveremo:

pP1–>X;
pP1–>InQualeQuadrante( );

2 Puntatori e oggetti
Occorre prestare attenzione alla differenza tra i due diversi tipi di creazione di
istanze finora visti. Per chiarire in che senso, serviamoci di un esempio:
Punto P1 = Punto(12,45);
Punto *pP1 = new Punto(5,14);

Entrambe le dichiarazioni precedenti riguardano la creazione di un oggetto di clas-


se Punto. Nella prima dichiarazione il compilatore alloca staticamente spazio in me-
moria per contenere un oggetto di classe Punto. La variabile P1 è il nome dell’ogget-
to appena creato.

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 141


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 142

Nella seconda, invece, il compilatore alloca dinamicamente memoria per contene-


re un oggetto di classe Punto e alloca staticamente la variabile pP1, che è un vero e
proprio puntatore all’oggetto appena creato ed è quindi soggetta alle regole di ge-
stione dei puntatori. Ad esempio può essere inizializzato con il valore NULL. È quin-
di differente l’accesso alle variabili e alle funzioni membro: nel primo caso si adotta
la dot notation, nel secondo (avendo a che fare con un puntatore per riferirsi all’og-
getto) si adotta l’operatore freccia.
Attenzione: a scopo unicamente didattico, per differenziare gli oggetti creati con
new, spesso faremo precedere il nome del loro puntatore dal carattere “p” (dove “p”
sta per “puntatore”).

3 Assegnazione di oggetti di una stessa classe


Una delle conseguenze immediate delle due diverse dichiarazioni del paragrafo
precedente consiste nella differenza semantica dell’assegnazione di un oggetto a un
altro oggetto. Facciamo un esempio. Scrivere:

Punto P1 = Punto(12,45);
Punto P2 = Punto(15,50);

P1 = P2;

significa che i valori delle variabili membro dell’oggetto P2 vengono copiati nei valo-
ri delle variabili membro dell’oggetto P1. Tuttavia P1 e P2 rimangono oggetti diffe-
renti, ognuno con il proprio differente spazio in memoria.
La situazione, dopo l’istruzione di copia, può essere rappresentata con il seguente
schema:

P1 Spazio fisico di memoria nel quale si trovano i valori


15
delle variabili membro dell’oggetto P1 dopo
50 l’istruzione di assegnamento P1 = P2

P2 Spazio fisico di memoria nel quale si trovano i valori


15
delle variabili membro dell’oggetto P2 dopo
50 l’istruzione di assegnamento P1 = P2

Consideriamo ora le seguenti istruzioni:

Punto *pP1 = new Punto(12,45);


Punto *pP2 = new Punto(15,50);

pP1 = pP2;

Quel che accade è che il valore del puntatore pP2 viene copiato all’interno del pun-
tatore pP1. I due puntatori, pur essendo variabili diverse (cioè ognuna con la propria
area di memoria), puntano allo stesso oggetto (in questo caso il secondo punto), e
condividono pertanto la stessa area di memoria nella quale sono presenti i valori del-
le variabili membro del secondo oggetto.
La situazione, dopo l’istruzione di copia dei puntatori, può essere rappresentata
con il seguente schema:

142 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 143

pP1 Spazio fisico di memoria nel quale si trovano


12
i valori degli attributi dell’oggetto puntato da pP1
45 dopo l’istruzione di assegnamento pP1 = pP2

garbage

pP2 Spazio fisico di memoria nel quale si trovano i valori


15
degli attributi dell’oggetto puntato da pP2
50 dopo l’istruzione di assegnamento pP1 = pP2

Attenzione: lo spazio fisico di memoria tratteggiato è stato chiamato garbage (ter-


mine inglese per spazzatura, rifiuto) perché non è più accessibile; nessun oggetto, in-
fatti, conosce più il suo indirizzo.
Non è quindi possibile allocare nuovamente tale spazio, perché risulta già allocato
(e mai restituito).
Ricordiamo che il C non ha un sistema di garbage collection automatico, cioè un
sistema che automaticamente riconosce il garbage e lo elimina restituendo la me-
moria occupata.

Vi è una terza ipotesi, che riguarda la seguente istruzione:


*pP1 = *pP2;

In questo caso viene copiato il contenuto dell’oggetto puntato da pP2 nel conte-
nuto dell’oggetto puntato da pP1. La situazione dopo la copia del contenuto è la se-
guente:

pP1 Spazio fisico di memoria nel quale si trovano i valori


15
degli attributi dell’oggetto puntato da pP1 dopo
50 l’istruzione di assegnamento

pP2 Spazio fisico di memoria nel quale si trovano i valori


15
degli attributi dell’oggetto puntato da pP2 dopo
50 l’istruzione di assegnamento

4 Puntatori a oggetti e passaggio di oggetti


come parametri
Quando si passa un oggetto come parametro a una funzione membro, è possibile
seguire le solite due strade finora viste nel normale passaggio di parametri a una fun-
zione:
• passaggio per valore;
• passaggio per indirizzo.
Nel passaggio di parametri per valore, già introdotto nell’Unità precedente, viene
creata una copia dell’oggetto che sarà passata alla funzione membro. Quest’ultima
potrà eventualmente modificare la copia e non l’oggetto originale.
Nel passaggio per indirizzo viene invece passato alla funzione membro un punta-

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 143


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 144

tore oppure un reference all’oggetto originale, pertanto un’eventuale modifica al pa-


rametro si ripercuoterà sull’oggetto originale.

4.1 Passaggio per valore


Consideriamo la classe Punto introdotta precedentemente e aggiungiamo la se-
guente funzione membro Origine( ) per azzerare le coordinate di un punto trasfor-
mandolo in un punto nell’origine degli assi:

void Origine(Punto P)
{
P.X=0;
P.Y=0;
}

Utilizziamo il seguente programma principale:

int main( )
{
Punto P0 = Punto(2,3); // crea un punto origine di coordinate (2,3)
Punto P1 = Punto(4,5); // crea un nuovo punto con coordinate (4,5)
P1.Origine(P0); // richiama Origine( ) per P0
cout << "X di P0 = " << P0.X;
cout << "Y di P0 = " << P0.Y;
return 0;
}

Come possiamo notare, il parametro P di classe Punto è passato per valore (tecni-
ca di default); ciò significa che quando si passa il parametro attuale P0 alla funzione
membro Origine( ) viene in realtà passata una copia delle variabili membro dell’og-
getto P0, e su tale copia la funzione membro eseguirà i suoi calcoli senza modificare
i valori delle variabili membro dell’oggetto originale P0. I valori finali di P0 sono per-
tanto rimasti invariati. La situazione dopo l’esecuzione della funzione Origine( ) è
riassunta nella figura seguente:

Spazio di indirizzamento Memoria centrale


del programma main( )

Punto P0 creato nel X di P0 2


programma main( )
Y di P0 3

Copia di P0 dopo
l’esecuzione della Copia di X di P0 0
funzione Origine( )
Copia di Y di P0 0

Spazio di indirizzamento
della funzione Origine( )

144 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 145

4.2 Passaggio per indirizzo tramite puntatore


Abbiamo detto che per passare l’oggetto parametro P per indirizzo occorre passa-
re un puntatore all’oggetto P1. Possiamo, pertanto, riscrivere la funzione membro
Origine( ) nel seguente modo:

void Origine(Punto *P)


{
P–>X=0;
P–>Y=0;
}

Il programma principale di prova è:

int main( )
{
Punto *pP0 = new Punto(2,3); // crea un punto di coordinate (2,3)
Punto P1 = Punto(4,5); // crea un nuovo punto con coordinate (4,5)
P1.Origine(pP0); // richiama Origine( ) per l’oggetto puntato da pP0
cout << "X di pP0 = " << pP0.X;
cout << "Y di pP0 = " << pP0.Y;
return 0;
}

La situazione dopo l’esecuzione della funzione Origine( ) è riassunta nella seguen-


te figura.

Spazio di indirizzamento Memoria centrale


del programma main( )

Oggetto puntato da pP0 X di pP0 0


creato nel programma main( )
Y di pP0 0

Viene passato
l’indirizzo iniziale Indirizzo iniziale di pP0
di pP0

Spazio di indirizzamento
della funzione Origine( )

4.3 Passaggio per reference di un oggetto


L’oggetto parametro P può anche essere passato per reference. Vediamo con un
esempio come realizzare questo tipo di passaggio di parametri. Utilizziamo il meto-
do Origine( ), così come definito del paragrafo precedente, e il seguente programma
principale di prova:

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 145


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 146

int main( )
{
Punto P0 = Punto(2,3); // crea un punto origine di coordinate (2,3)
Punto P1 = Punto(4,5); // crea un nuovo punto di coordinate (4,5)
P1.Origine(&P0); // richiama Origine( ) con l’indirizzo di P0
cout << "X di P0 = " << P0.X;
cout << "Y di P0 = " << P0.Y;
return 0;
}

Quel che accade è che questa volta viene passato un indirizzo dell’oggetto P0, quin-
di la funzione membro Origine( ) modifica l’oggetto P0. La situazione dopo l’esecuzio-
ne della funzione Origine( ) è analoga a quella riassunta nella figura precedente.

5 L’operatore delete
L’operatore delete consente di liberare la memoria precedentemente allocata con
l’operatore new. L’operatore delete, prima di liberare definitivamente la memoria, chia-
ma automaticamente il distruttore dell’oggetto, sempre se questo è stato definito nella
classe.
L’operatore delete si applica solo ai puntatori ritornati da new.
Riprendendo l’esempio del paragrafo 4.2, per liberare l’area di memoria relativa a
un nuovo oggetto di classe Punto, allocata da new, scriveremo:
delete pP0;

Analogamente, per liberare la memoria allocata negli esempi del paragrafo 1.1, re-
lativi ai tipi primitivi, scriveremo:
delete F; // dealloca l’area di memoria contenente un float
delete [ ] Str; // dealloca l’area di memoria contenente l’array Str

Ad esempio, il distruttore per la classe Vettore definita nell’Unità C1 può essere:


Vettore::~Vettore( )
{
delete [ ] V;
}

il cui obiettivo è di deallocare l’area di memoria puntata da V.

6 Il puntatore all’oggetto corrente: this


In C++ è possibile fare riferimento all’oggetto corrente, qualunque esso sia, utiliz-
zando la parola chiave this, che assume il significato di puntatore all’oggetto cor-
rente. Per fare un esempio di utilizzo del puntatore this definiamo un costruttore del-
la classe Automobile usando come nome del parametro lo stesso nome della variabi-
le membro; ad esempio:

class Automobile
{
public:
int Serbatoio;
private:
int Marcia;
...

146 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 147

Automobile (int Serbatoio) // funzione costruttore


{
this–>Serbatoio = Serbatoio;
Marcia = 0;
}
}

L’uso dello stesso nome consente di non dover inventare nomi diversi di variabile,
rendendo così più semplice la lettura del codice. Il parametro Serbatoio, all’interno
del costruttore, diventa una variabile locale che maschera la variabile membro di-
chiarata con lo stesso nome: in altri termini, se scriviamo soltanto Serbatoio ci rife-
riamo sempre al parametro e non alla variabile membro. Per accedere alla variabile
membro si deve esplicitamente usare la parola chiave this, che costituisce un riferi-
mento all’oggetto corrente: infatti this–>Serbatoio fa riferimento alla variabile mem-
bro Serbatoio dell’oggetto corrente.

7 La classe Pila
Per vedere un concreto esempio di allocazione dinamica di oggetti in memoria uti-
lizzando gli operatori new e delete e per vedere, in generale, un concreto utilizzo dei
puntatori a oggetti, definiamo la struttura dati astratta “pila di interi”. Le funzioni
membro di tale classe verranno implementate utilizzando l’allocazione dinamica di
oggetti in memoria. È utile confrontare questo esempio con quello analogo svolto nel-
l’Unità B2 relativo alla gestione di una pila di interi utilizzando l’allocazione dinami-
ca, ma senza ricorrere agli oggetti.

7.1 Le specifiche della classe Pila


Le specifiche della classe Pila già esaminate nel Modulo B (ovvero le caratteristi-
che e i comportamenti che la classe Pila deve possedere) sono quelle riassunte dal
seguente diagramma delle classi:

Pila
– Nodo* Testa
+ Pila ( ) costruttore
+ void Push (IN N : Nodo) modificatore
+ Nodo* Pop ( ) modificatore
– void Stampa ( ) query

7.2 Implementazione della classe Pila con allocazione dinamica


Sappiamo che gli elementi di base che costituiscono una pila sono i nodi. Ogni no-
do, a sua volta, è caratterizzato da una parte relativa all’informazione associata al no-
do e una parte relativa al collegamento con il nodo successivo. Possiamo allora crea-
re due classi: la classe Nodo e la classe Pila. La classe Nodo definisce le variabili mem-
bro e le funzioni membro necessarie per manipolare un nodo all’interno della pila. In
particolare ricorriamo a due variabili membro: Info, che conterrà il valore intero del
nodo (nel nostro caso infatti stiamo trattando pile di interi), e Next, che conterrà un
puntatore al nodo successivo.

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 147


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 148

Esempio 1
La classe Pila con allocazione dinamica (vedi sito: CPP.C2.01.C)

#include <iostream.h>

class Nodo
{
public:
int Info;
Nodo *Next;
Nodo(int Num, Nodo *N) // costruttore - crea un nodo contenente un valore intero
{
Info = Num;
Next = N;
}
}; // fine classe Nodo

class Pila
{
private:
Nodo *Testa; // testa della Pila
public:
Pila( ) // costruttore - crea una pila vuota
{
Testa = NULL;
}
~Pila( ) // distruttore - dealloca la memoria occupata dalla pila
{
while(Testa!= NULL)
{
Nodo *Temp=Testa;
Testa=Testa–>Next;
Temp=Testa;
delete Temp;
}
}
Nodo *Pop( )
{
Nodo *N;
if (Testa != NULL)
{
N=Testa;
Testa=Testa–>Next;
N–>Next=NULL;
}
else
cout << "Pila Underflow";
return(N);
}
void Push(Nodo *N)
{
N–>Next=Testa;
Testa=N;
}
void Stampa( )
{
Nodo *Temp=Testa;
while(Temp!=NULL)
{
cout << Temp->Info << endl;

148 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 149

Temp=Temp–>Next;
}
}
}; // fine classe Pila

int main( )
{
Pila *P1 = new Pila( );
for(int I=0; I<4; I++)
{
Nodo *Temp = new Nodo(I*10, NULL);
P1->Push(Temp);
}
P1–>Stampa( );
cout << "---" << endl;
P1–>Pop( );
P1–>Stampa( );
return 0;
}

Nel codice precedente possiamo notare la presenza del distruttore ~Pila( ), il cui
scopo è di deallocare lo spazio in memoria occupato dai nodi presenti nella pila, una
volta che si esce dal programma principale. Per fare questo esso utilizza l’operatore
delete.

8 Classi nello stesso file o in file separati


Le classi Nodo e Pila dell’esempio 1 sono state inserite in un unico file come tutte
le semplici classi finora considerate. In un file sorgente, infatti, possono essere pre-
senti più classi ma una sola funzione main( ).

class Nodo
{
...
}; // fine classe Nodo
class Pila
{ Unico file con due classi
...
}; // fine classe Pila
int main ( )
{
...
return 0;
}

File Pila.cpp

Compilando tale file si ottiene contemporaneamente: il modulo oggetto (file Pi-


la.obj) e il file eseguibile (file Pila.exe).
Tuttavia è anche possibile salvare ogni classe in un file distinto, compilare separa-
tamente ciascun file ottenendo tanti moduli oggetto (*.obj) e, infine, linkare insieme
questi ultimi per ottenere il file eseguibile finale. Nel nostro caso avremmo potuto
avere:

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 149


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 150

class Nodo #include "Nodo.cpp"


{ class Pila
{
}; // fine classe Nodo
}; // fine classe Pila
File Nodo.cpp int main ( )
{
...
return 0;
}

File Pila.cpp

Come possiamo notare, occorre effettuare un include del file Nodo.cpp nel file Pi-
la.cpp.
Utilizzando, ad esempio, un compilatore a linea di comando, si possono compilare
separatamente i due sorgenti scrivendo:

cl –c Nodo.obj
cl –c Pila.obj

dove l’opzione –c indica che è richiesta la sola compilazione e non il linking.


Una volta ottenuti i due moduli oggetto, Nodo.obj e Pila.obj, occorrerà linkarli per
ottenere l’eseguibile. In un ambiente a linea di comando, bisognerà dunque scrivere:
link Pila.obj Nodo.obj

ottenendo il file eseguibile Pila.exe; in alternativa è possibile specificare un nome dif-


ferente per il file eseguibile, come ad esempio:
link Pila.obj Nodo.obj OUT:Pila2.exe

Così come già visto nell’Unità A4, il processo di compilazione e linkaggio, negli am-
bienti IDE quale Visual C++, viene ulteriormente agevolato effettuando il cosiddetto
build dei file .cpp definiti in un file speciale, detto file di progetto, aggiungendo o eli-
minando facilmente file al processo di compilazione e linkaggio.

9 Variabili e funzioni membro di classe


Oltre alle variabili istanza, in C++ è possibile definire le variabili di classe, cioè va-
riabili membro i cui valori sono condivisi da tutte le istanze di una classe. La modifi-
ca di una variabile di classe si ripercuote (nel senso che è visibile) su tutte le istan-
ze di quella classe.
Per definire una variabile di classe si utilizza la parola chiave static. La sintassi è la
seguente:
static <Tipo> <NomeVariabileMembro>;

Una variabile membro static può essere inizializzata una sola volta; pertanto, non
può venire inizializzata da un costruttore perché sarebbe inizializzata a ogni istanza
di quella classe. La si deve quindi necessariamente inizializzare all’esterno delle fun-
zioni e delle variabili membro della classe, come fosse una variabile globale, utiliz-
zando l’operatore di scope (“::”). La sintassi è:
<Tipo> <NomeClasse>::<NomeVariabileMembroStatic> = <Valore>

150 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 151

Per riferirci genericamente a una variabile membro di classe faremo quindi prece-
dere il suo nome da quello della classe e dall’operatore di scope, scrivendo:
<NomeClasse>::<NomeVariabileMembroStatic>;

Tornando all’esempio dell’automobile, possiamo aggiungere la variabile di classe:


static int NumeroRuote;

inserendo l’istruzione:
int Automobile::NumeroRuote = 4;

al di fuori della dichiarazione delle funzioni e delle variabili membro. Tale valore di
NumeroRuote si ripercuote in tutte le istanze della classe Automobile.

Auto1
Istanza Di
Automobile

OGGETTI
Auto2
int Automobile::NumeroRuote = 4 Istanza Di
Automobile

Auto3
Istanza Di
Automobile

Nell’esempio della classe Cerchio, se volessimo definire una famiglia di cerchi con
raggio uguale, ad esempio di raggio unitario, potremmo scrivere il codice seguente.

Esempio 2
Famiglia di cerchi con raggio unitario
class Cerchio
{
public:
double X; // prima coordinata del centro
double Y; // seconda coordinata del centro
static double Raggio;

Cerchio( ); // dichiarazione del costruttore


double Circonferenza( ); // funzione membro per il calcolo della circonferenza
double Area( ); // funzione membro per il calcolo dell’area
}; // fine della dichiarazione di classe

double Cerchio::Raggio=1; // inizializzazione esterna di una variabile membro static


int main( )
{
...
return 0;
}

Tutti gli oggetti di questa classe hanno coordinate del centro differenti ma stesso
raggio (unitario). Il raggio è pertanto variabile membro di classe.
Oltre alle variabili di classe, esistono le funzioni membro di classe, ossia quelle
funzioni membro che si possono richiamare indipendentemente dall’esistenza di

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 151


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 152

un’istanza della relativa classe. Le funzioni di classe possono gestire esclusivamente


variabili di classe e sono visibili anche all’esterno della classe. Per distinguerle dalle
funzioni membro di classe, le funzioni membro non di classe vengono chiamate fun-
zioni membro istanza.
Anche per definire una funzione membro di classe in C++ si utilizza la clausola static:
static <TipoValoreRestituito> <NomeFunzioneMembro>([<Parametri>])
{
<Istruzioni>
};

mentre per richiamarle si utilizza la stessa sintassi vista per le variabili di classe, ov-
vero:
<NomeClasse>::<NomeFunzioneMembroStatic>([<Parametri>]);

Nell’esempio della classe Cerchio, potremmo invece definire una funzione membro
di classe per la visualizzazione e una per la modifica della variabile membro di clas-
se Raggio, nel seguente modo:

Esempio 3
Funzioni membro di classe (vedi sito: CPP.C2.02.C)

#include <iostream.h>
class Cerchio
{
public:
double X; // prima coordinata del centro
double Y; // seconda coordinata del centro
static double Raggio;
Cerchio(double X1, double Y1)
{
X=X1;
Y=Y1;
}
double Circonferenza( ) // funzione per il calcolo della circonferenza
{
const double Pigreco=3.141593; // valore approssimato
return(2*Raggio*Pigreco);
}
static double VisualizzaRaggio( )
{
return(Raggio);
}
static void ModificaRaggio(double R)
{
Raggio=R;
}
}; // fine classe Cerchio
double Cerchio::Raggio = 1.0;
int main( )
{
Cerchio C1 =Cerchio(2,4);
cout << "Valore coordinata X: " << C1.X << endl;
cout << "Valore coordinata Y: " << C1.Y << endl;
cout << "Valore raggio: " << Cerchio::Raggio << endl;
Cerchio::ModificaRaggio(12);
cout << "Nuovo valore Raggio: " << Cerchio::VisualizzaRaggio( ) << endl;
return 0;
}

152 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 153

Richiamando la funzione membro ModificaRaggio( ), il nuovo valore del Raggio si


ripercuoterà su tutti gli oggetti della classe Cerchio.

10 Funzioni amiche di una classe


L’accesso alle variabili membro “private” di una classe è consentito solo alle fun-
zioni membro di quella classe. In questo modo si realizza l’information hiding e si può
modificare l’implementazione di una classe senza dover modificare la sua interfaccia
con l’esterno. Ci sono situazioni in cui la visibilità di alcuni membri “privati” di una
classe deve essere estesa solo a determinate funzioni esterne alla classe ma esplici-
tamente previste all’interno della classe. È questo un modo per “allentare” il vincolo
di accesso senza generalizzarlo a tutte le classi.
In C++ è possibile dichiarare all’interno di una classe funzioni membro che non so-
no implementate nella classe (non sono interne) ma sono definite all’esterno di que-
st’ultima. Tali funzioni sono dette funzioni amiche. Per dichiarare una funzione ami-
ca all’interno di una classe occorre far precedere il nome della funzione (indifferen-
temente nella parte privata o pubblica) dalla parola chiave friend, ovvero:
friend <NomeFunzioneEsterna>([<Parametri>]);

Possiamo dunque dire che le funzioni amiche ampliano l’interfaccia di una classe
andando ad aggiungersi alle funzioni membro pubbliche.
Ad esempio, definiamo la funzione amica TestVuota( ) della classe Pila definita nel
paragrafo 7.2 di questa Unità.

Esempio 4
Funzione amica di una classe
#include <iostream.h>
#include "Nodo.h"
class Pila
{
private:
Nodo *Testa; // testa della Pila
... // definizione delle funzioni membro della classe Pila
friend bool TestVuota(Pila *S); // dichiarazione della funzione amica TestVuota( )
definita esternamente alla classe */
}; // fine classe Pila
bool TestVuota(Pila *S) // funzione esterna alla classe Pila
{
return(S–>Testa == NULL); // accede alla testa della Pila: variabile membro privata
}
int main( )
{
Pila *P1 = new Pila( );
...
if (TestVuota(P1))
cout << "Pila vuota";
else
cout << "Pila non vuota";
return 0;
}

Come possiamo osservare, la funzione TestVuota( ), che verifica se la Pila passata


come parametro è o meno vuota, è definita all’esterno della classe ma è funzione ami-
ca, quindi può accedere alla variabile membro privata Testa che rappresenta la testa
della Pila.

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 153


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 154

10.1 Funzioni amiche di più classi


Definendo amica una stessa funzione in classi diverse si ottiene un modo per met-
tere in comunicazione gli oggetti di queste classi.
Supponiamo, ad esempio, di avere la seguente situazione. Una sede centrale di un
negozio che vende un solo articolo (facciamo questa ipotesi per semplicità) riceve in
tempo reale le vendite da parte degli altri punti vendita. Ogni vendita è caratterizza-
ta dalla quantità venduta di quell’articolo. La sede centrale contabilizza tutte le ven-
dite sommandole in una sua variabile membro privata: TotaleVendite. La struttura del
codice C++ per questa situazione può essere la seguente.

Esempio 5
Funzione amica di più classi (vedi sito: CPP.C2.03.C)

#include <iostream.h>
class PuntoVendita; // predichiarazione della classe PuntoVendita
class Sede
{
private:
int TotaleVendite;
public:
Sede( )
{
TotaleVendite=0;
}
friend void Aggiorna(PuntoVendita *P, Sede *S);
}; // fine classe Sede
class PuntoVendita
{
private:
int TotVenditaNegozio;
int CodiceNegozio;
public:
PuntoVendita(int Num)
{
CodiceNegozio=Num;
TotVenditaNegozio=0;
}
void Vendita(int Qta)
{
TotVenditaNegozio=TotVenditaNegozio+Qta;
}
friend void Aggiorna(PuntoVendita *P, Sede *S);
}; // fine classe PuntoVendita
void Aggiorna(PuntoVendita *P, Sede *S)
{
S–>TotaleVendite = S–>TotaleVendite + P–>TotVenditaNegozio;
cout << "Vendita del negozio " << P->CodiceNegozio << ": " << P–>TotVenditaNegozio << endl;
cout << "Totale vendite: “ << S–>TotaleVendite << endl;
}
int main()
{
Sede *S1 = new Sede( ); // crea una nuova sede centrale
PuntoVendita *P1 = new PuntoVendita(1); // crea il punto vendita 1
P1–>Vendita(5); // trasmette le vendite alla sede centrale
Aggiorna(P1,S1); // aggiorna le vendite totali della sede centrale
PuntoVendita *P2 = new PuntoVendita(2); // crea un punto vendita 2
P2–>Vendita(20); // trasmette le vendite alla sede centrale
Aggiorna(P2,S1); // aggiorna le vendite totali della sede centrale
return 0;
}

154 Modulo C: C++ e gli oggetti


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 155

La situazione creata nel main( ) è rappresentata dal seguente schema:

P1
Istanza Di AggiornaVendite(P1, S1)
PuntoVendita

S1
Istanza Di
SedeCentrale

P2
Istanza Di
PuntoVendita AggiornaVendite(P2, S1)

Il risultato a video dell’esecuzione del programma dell’esempio 5 è:

Il linguaggio C++ concede quindi accessibilità alle funzioni esterne su alcuni mem-
bri “privati” delle due classi correlate, senza doverli necessariamente dichiarare pub-
blici. Indica esplicitamente, insomma, chi può accedere a tali membri.

11 Classi amiche
Una classe A si dice amica di una classe B quando tutte le funzioni membro della
classe A sono friend nella classe B; in tal caso si scrive:

class A
{
friend class B
};

Possiamo rappresentare tale relazione con il seguente diagramma:

friend
B A

Si presti attenzione al fatto che la relazione di “amicizia” non è simmetrica: se una


classe è amica di una classe B non è detto che la classe B sia amica della classe A. Se
infatti A è amica di B vuol dire che tutte le funzioni membro di A accedono alle fun-
zioni membro di B (indipendentemente dalla loro visibilità), ma non è detto che tut-
te le funzioni membro di B possano accedere alle funzioni membro di A.

Unità didattica C2: Allocazione dinamica, oggetti e puntatori 155


p139-156_CeC_Unità C2 26-09-2007 16:49 Pagina 156

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Come si fa a dichiarare, in C++, una variabile e una 6. Qual è la differenza tra le seguenti istruzioni?
funzione membro di classe? a) Motocicletta M1 = Motocicletta( );
2. Che cosa indica il valore NULL? b) Motocicletta *M1 = new Motocicletta( );
3. Qual è l’utilizzo del puntatore predefinito this? 7. Come si fa a effettuare il passaggio di parametri di
4. Può una funzione essere contemporaneamente un oggetto per indirizzo?
static e private? 8. Che differenza c’è tra passare un oggetto come
5. Si può avere in C++ una variabile locale con lo parametro per indirizzo e passarlo per valore?
stesso nome di una variabile istanza?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI • SetDescrizione( )
• StampaEtichetta( )
1. Creare una classe Potenza con la quale si possa 6. Definire la classe Retta, utilizzando la classe Punto
interagire per elevare un numero a un dato esaminata in questa Unità e nelle precedenti.
esponente. 7. Aggiungere all’esercizio precedente le funzioni
2. Creare una classe Potenza come l’esercizio membro per:
precedente, ma con funzioni membro ricorsive. • il calcolo dell’equazione della retta;
3. Definire una classe per il calcolo del codice fiscale • il calcolo dell’intersezione con l’asse x;
di una persona. • il calcolo dell’intersezione con l’asse y.
4. Definire la classe Studente definendo le variabili 8. Date le classi Retta e Punto, aggiungere le seguenti
membro, i costruttori e le seguenti funzioni funzioni friend per:
membro: • verificare che un dato punto appartenga alla retta;
• GetNome( ) • verificare se una data retta passa per due punti
• AggiungiVoto( ) dati.
• GetMediaMateria( ) 9. Definire una classe NuovaMath che non permetta
• GetVotoMateria( ) sottoclassi e implementi almeno le seguenti
• SetVotoMateria( ) funzioni membro di classe:
5. Definire la classe Prodotto per un prodotto in un • int mcm(int M, int N)
supermercato, definendo variabili membro, • int MCD(int M, int N)
costruttori e le seguenti funzioni membro:
10. Definire una nuova classe TriangoloRettangolo.
• GetPrezzo( )
• SetPrezzo( ) 11. Utilizzando la classe Punto, creare una funzione
• GetDescrizione( ) amica che calcoli la retta passante per due punti.

Esercizio svolto
1. Altri metodi della classe Punto. Aggiungere alla Simmetrico( ), invece, deve restituire un nuovo
classe Punto (del piano) le seguenti funzioni oggetto Punto che dovrà essere creato e
membro: inizializzato con gli opposti delle coordinate del
• distanza tra due punti; punto corrente.
• simmetrico del punto corrente.
(Vedi sito: CPP.C2.04.C) double Distanza(Punto P)
{
Analisi del processo risolutivo return(sqrt(pow((X - P.X),2)+ pow((Y - P.Y),2)));
La funzione membro Distanza( ) deve calcolare la }
radice quadrata e l’elevamento al quadrato per Punto Simmetrico( )
determinare la distanza tra due punti. Per far {
questo utilizza sqrt( ) per il calcolo della radice Punto Simm = Punto(-X, -Y);
quadrata (funzione di libreria presente nel file return(Simm);
math.h), e pow( ) per l’elevamento a potenza (in
}
questo caso al quadrato). La funzione membro

156 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 157

Unità didattica

Ereditarietà
e polimorfismo C3
● Conoscenze e competenze di base necessarie a scrivere semplici programmi in C++ Prerequisiti
● Concetti di base per utilizzare in C++ classi, variabili membro e funzioni membro

● Saper implementare con costrutti C++ gli aspetti caratterizzanti la Obiettivi


programmazione a oggetti: incapsulamento, ereditarietà, polimorfismo

● Conoscere l’overriding di funzioni membro Conoscenze


● Conoscere le problematiche legate all’ereditarietà multipla da apprendere
● Conoscere il concetto di funzione virtuale e classe astratta

● Saper creare classi specializzate partendo da classi generiche Competenze


● Saper creare gerarchie di classi sfruttando il meccanismo dell’ereditarietà da acquisire
● Saper creare funzioni e classi astratte che possono essere un modello per funzioni
e classi concrete

1 Ereditarietà, classi e sottoclassi


Il linguaggio C++ permette a una classe di ereditare caratteristiche e comporta-
menti di altre classi. Questo meccanismo viene detto:
• ereditarietà singola quando la classe eredita solo da un’altra classe, che prende il
nome di superclasse;
• ereditarietà multipla quando la classe eredita da più classi (dette ancora super-
classi).
Il meccanismo dell’ereditarietà crea gerarchie di classi e sottoclassi in cui, con l’e-
reditarietà singola, una classe può avere al più una superclasse ma più sottoclassi,
mentre con l’ereditarietà multipla una classe può avere più superclassi e più sotto-
classi, come illustrato in figura C3.1.
✦ Figura C3.1
Superclassi e sottoclassi.

superclasse

Ereditarietà classe Ereditarietà


singola considerata multipla

sottoclasse

Unità didattica C3: Ereditarietà e polimorfismo 157


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 158

In C++, spesso si utilizza la dizione classe base per indicare una superclasse dalla
quale ereditano le sue sottoclassi, dette classi derivate (figura C3.2).

✦ Figura C3.2 Per creare una sottoclasse di una classe (che per
Classe base e classe classe base
derivata. quest’ultima è quindi una superclasse) si utilizza la se-
guente sintassi:

classe derivata class <NomeClasse> : [<Visibilità>] <NomeSuperclasse>


{
<CorpoClasse>
};

dove la categoria sintattica <Visibilità> (opzionale) può assumere i valori private, pu-
blic, protected, e specifica la modalità con cui la classe può accedere a variabili
membro e funzioni membro della superclasse illustrate nella seguente tabella. Se non
si indica alcuno specificatore, si intenderà lo specificatore per default private.

MEMBRI DELLA CLASSE DIVENTANO NELLA DIVENTANO NELLA DIVENTANO NELLA


BASE O SUPERCLASSE CLASSE DERIVATA CLASSE DERIVATA CLASSE DERIVATA
DEFINITA CON DEFINITA CON DEFINITA CON
SPECIFICATORE PUBLIC SPECIFICATORE SPECIFICATORE
PROTECTED PRIVATE

public public protected private


protected protected protected private
private non accessibili non accessibili non accessibili

Come possiamo notare dalla tabella, il principio che adotta il C++ per la visibilità
dei membri di una classe in una gerarchia di ereditarietà è il seguente:
• una sottoclasse non può ampliare la visibilità di un membro della sua superclasse;
• un membro protetto di una classe non potrà mai avere visibilità pubblica in alcuna
delle sue sottoclassi, cioè il livello di visibilità di un membro o rimane uguale scen-
dendo nelle sottoclassi della gerarchia, oppure può “restringersi”, ma mai aumen-
tare.
Possiamo riassumere lo scopo delle dichiarazioni private, public e protected con la
seguente tabella:

TIPO DI DICHIARAZIONE SCOPO

private Stabilisce quali sono i membri inaccessibili a tutte le classi.

protected Stabilisce quali sono i membri visibili solo alle sottoclassi.

public Stabilisce quali sono i membri visibili a tutte le classi.

Pertanto, nulla si può dire a priori sulla visibilità di un membro protetto X presen-
te in una superclasse di una classe A. Per stabilire se X è o meno visibile dalla classe
A bisognerà ripercorrere il cammino da A verso la superclasse.

2 Ereditarietà singola
Facciamo un esempio di gerarchia di classi con ereditarietà singola, rappresentan-
dole con il diagramma delle classi di figura C3.3. Si noti che nel diagramma è stato in-
serito anche lo specificatore di visibilità.

158 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 159

✦ Figura C3.3
VeicoloMotore Una gerarchia con la
classe VeicoloMotore
come superclasse
capostipite.

public public
Automobile Veicolo2Ruote

public public
Motocicletta Ciclomotore

La classe Motocicletta è una sottoclasse della classe Veicolo2Ruote. Quest’ultima è


superclasse della classe Motocicletta e sottoclasse della classe VeicoloMotore.
Ricordiamo che la classe derivata diventa una combinazione dei comportamenti
(metodi) di tutte le superclassi che la precedono nella gerarchia.
Quando invece la sottoclasse possiede una funzione membro con lo stesso nome,
stessi parametri di ingresso (cioè con lo stesso prototipo, escluso il valore di ritor-
no) della superclasse ma con codice (implementazione) differente, la funzione mem-
bro della superclasse non viene più ereditata: si dice che la sottoclasse espressa-
mente ridefinisce (con operazione di overriding) la funzione membro della super-
classe.
La nuova classe può differenziarsi dalla superclasse, oltre che per ridefinizione, an-
che per estensione, quando si aggiungono nuove funzioni membro e variabili mem-
bro non presenti nella superclasse.

Ricapitolando, una classe può:


• ereditare variabili e funzioni membro dalla superclasse;
• estendere nuove variabili e funzioni membro;
• ridefinire variabili e funzioni membro (overriding).
Inseriamo alcune funzioni estese e ridefinite nella precedente gerarchia di classi.

Esempio 1
Gerarchia con funzioni membro ereditate, ridefinite, estese

#include <iostream.h>

class VeicoloMotore
{
public:
char *Marca; // variabili membro ereditate dalle sottoclassi
char *Colore;
int Cilindrata;
bool StatoMotore;
VeicoloMotore( ) // costruttore
{
Marca = "Marca1";
Colore = "Bianco";
Cilindrata = "1000";
StatoMotore = true;
}

Unità didattica C3: Ereditarietà e polimorfismo 159


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 160

~VeicoloMotore( ) // distruttore
{
cout << "Distruttore di VeicoloMotore";
}
};

class Veicolo2Ruote : public VeicoloMotore


{
public:
bool SostegnoLaterale; // variabile membro estesa
Veicolo2Ruote( ) // costruttore
{
SostegnoLaterale = true;
}
~Veicolo2Ruote( ) // distruttore
{
cout << "Distruttore di Veicolo2Ruote";
}
void AvviaMotore( ) // funzione membro che sarà ereditata dalle sottoclassi
{
StatoMotore = true;
}
void MostraStato( ) // funzione membro che sarà ereditata dalle sottoclassi
{
cout << "Motocicletta: " << Marca << " " << Cilindrata << " " << Colore << endl;
if (StatoMotore == true)
cout << "Motore acceso" << endl;
else
cout << "Motore spento" << endl;
}
}; // fine classe Veicolo2Ruote

class Motocicletta : public Veicolo2Ruote // Motocicletta è sottoclasse di Veicolo2Ruote


{
public:
int Marcia; // nuova variabile membro non presente in Veicolo2Ruote
Motocicletta( ) // costruttore
{
Marcia=0; // in folle
}
~Motocicletta( ) // distruttore
{
cout << "Distruttore di Motocicletta";
}
void InserisciSostegnoLaterale( ) // funzione membro estesa
{
if (StatoMotore == false)
SostegnoLaterale = true;
else
cout << "Attenzione: spegnere prima il motore";
}
void MostraStato( ) // funzione membro ridefinita
{
cout << "Motocicletta: " << Marca << " " << Cilindrata << " " << Colore;
cout << "Marcia = " << Marcia ;
if (StatoMotore == true)
cout << "Motore acceso";

160 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 161

else
cout << "Motore spento";
if (SostegnoLaterale == true)
cout << "Sostegno laterale inserito" << endl;
else
cout << "Sostegno laterale non inserito" << endl;
} // fine MostraStato ridefinito
}; // fine classe Motocicletta

class Ciclomotore : public Veicolo2Ruote


// Ciclomotore è sottoclasse di Veicolo2Ruote
{
...
}; // fine classe Ciclomotore

class Automobile : public VeicoloMotore


// Automobile è sottoclasse di VeicoloMotore
{
...
}; // fine classe Automobile

Dall’esempio notiamo che la sottoclasse Motocicletta:


• eredita la funzione AvviaMotore( ) dalla superclasse Veicolo2Ruote;
• estende (definisce) una nuova funzione InserisciSostegnoLaterale( ) che serve per
la sosta del veicolo;
• ridefinisce la funzione MostraStato( ) per visualizzare se il sostegno laterale è stato
inserito o no.
Possiamo allora riscrivere il diagramma delle classi precedentemente introdotto
ma con un nuovo livello di dettaglio (figura C3.4).

✦ Figura C3.4
La gerarchia della
VeicoloMotore classe VeicoloMotore
+char *Marca con diverso livello di
+char *Colore dettaglio.
+int *Cilindrata
+bool *StatoMotore

public public
Automobile Veicolo2Ruote
+bool SostegnoLaterale
+ void AvviaMotore( ) modificatore
+ void MostraStato( ) query

public public
Motocicletta Ciclomotore
+int Marcia
+ void InserisciSostegnoLaterale( ) modificatore
+ void MostraStato( ) query

Unità didattica C3: Ereditarietà e polimorfismo 161


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 162

2.1 Un altro esempio di ereditarietà


Riprendendo l’esempio della classe Punto definita nelle Unità precedenti di questo
Modulo, possiamo estendere quest’ultima alla classe PuntoSpazio che definisce un
punto nello spazio. Il diagramma delle classi sarà:

Punto
+ float X
+ float Y
+ Punto( ) costruttore
+ Punto(IN float X1) costruttore
+ Punto(IN float A, IN float B) costruttore
+ int InQualeQuadrante( ) query
+ int Distanza(IN Punto P ) query

public
PuntoSpazio
+ float Z
+ Punto(IN float X1, IN float Y1, IN float Z1) costruttore
+ float Distanza(IN Punto P) query

Esempio 2
La classe PuntoSpazio (vedi sito: CPP.C3.01.C)

#include <iostream.h>
#include <math.h>
class PuntoSpazio: public Punto
public:
float Z; // si aggiunge la terza coordinata le altre due sono ereditate
Punto(float X1, float Y1, float Z1) // crea un punto nello spazio
{
X = X1;
Y = Y1;
Z = Z1;
}
float Distanza(P:Punto)
{
float R;
R = sqrt((X–P.X) * (X–P.X) + (Y–P.Y) * (Y–P.Y) + (Z–P.Z) * (Z–P.Z))
return(R)
}
}; // fine classe

Si noti che la classe PuntoSpazio eredita le coordinate X e Y dalla classe Punto: con-
seguentemente, tali variabili non devono essere ridefinite. Dovrà essere ridefinita, in-
vece, la funzione membro Distanza( ), per includere la terza coordinata.

2.2 Costruttori, distruttori ed ereditarietà


Supponiamo di avere una classe derivata e una sua superclasse. È possibile che en-
trambe contengano costruttori e distruttori. Vi sono due problemi che derivano dal-
l’uso di costruttori e distruttori: occorre innanzitutto comprendere l’ordine in cui
vengono eseguite tali funzioni (da parte del sistema) nel momento in cui si crea o si
distrugge un oggetto, e comprendere come sia possibile passare parametri alle fun-
zioni costruttore delle superclassi. Vedremo questo secondo problema nel paragrafo
successivo, mentre ora ci focalizziamo sull’ordine di chiamata dei costruttori e dei
distruttori.

162 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 163

Quando si crea un oggetto della classe derivata, se la superclasse contiene un co-


struttore questo verrà richiamato (dal sistema) per primo, seguito dal costruttore
della classe.
Quando si distrugge un oggetto della classe derivata, viene prima richiamato (dal
sistema) il suo distruttore e poi quello della sua superclasse, così come schematiz-
zato in figura C3.5.
✦ Figura C3.5
S (superclasse) Sequenza di chiamata
dei costruttori e dei
distruttori.
Chiamata al costruttore Chiamata al distruttore
della superclasse S della superclasse
(della classe derivata D)
D (classe derivata)
Chiamata al costruttore Chiamata al distruttore
della classe derivata D della classe derivata

Creazione di un Distruzione di un
Sequenza di Sequenza di
chiamata dei oggetto della classe oggetto della classe chiamata dei
costruttori derivata D derivata D distruttori

Generalizzando, possiamo dire che in una gerarchia di classi e sottoclassi, quando


si crea un oggetto di una classe il compilatore richiamerà automaticamente le funzio-
ni costruttore delle sue superclassi nell’ordine di derivazione (cioè dalla superclasse
capostipite verso i suoi discendenti). Quando invece si distrugge un oggetto di una
classe il compilatore richiamerà i distruttori in ordine inverso di derivazione.
Riprendendo l’esempio 1 sulla gerarchia che ha come superclasse capostipite Vei-
coloMotore, quando si crea un nuovo oggetto della classe derivata Motocicletta le
chiamate per i costruttori e per i distruttori avverranno nel seguente ordine:

Costruttori: Distruttori:
VeicoloMotore( ) ~VeicoloMotore( )
Veicolo2Ruote( ) ~Veicolo2Ruote( )
Motocicletta( ) ~Motocicletta( )

2.3 Costruttori delle sottoclassi con parametri


Nell’esempio precedente i costruttori delle superclassi non avevano parametri e
per questo motivo il sistema poteva richiamarli automaticamente secondo l’ordine
visto in precedenza. Nei casi in cui i costruttori delle superclassi abbiano parametri
formali, occorrerà comprendere come passare i relativi parametri attuali a tali co-
struttori. Ovviamente i parametri attuali devono essere passati dalle sottoclassi ver-
so le superclassi. Per fare questo, nella dichiarazione dei costruttori delle sottoclas-
si si utilizza la seguente forma estesa:
<NomeCostruttoreClasse>([<ParametriFormali>]):
<NomeCostruttoreSuperclasse1>([<ParametriPassati>]),
...
<NomeCostruttoreSuperclasseN>([<ParametriPassati>])
{
<CorpoCostruttoreClasse>
}

Unità didattica C3: Ereditarietà e polimorfismo 163


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 164

dove <ParametriFormali> sono i parametri veri e propri del costruttore della classe
derivata, mentre <ParametriPassati> sono i paramentri del costruttore della classe
derivata passati ai costruttori delle superclassi.
Vediamo meglio cosa ciò significhi riprendendo l’esempio 1.

Esempio 3
Gerarchia con costruttori e distruttori con parametri

class VeicoloMotore
{
public:
char *Marca;
char *Colore;
int Cilindrata;
bool StatoMotore;
VeicoloMotore(char *Mmarca, char *Mcolore, int Mcilindrata)
{
Marca = Mmarca;
Colore = Mcolore;
Cilindrata = Mcilindrata;
StatoMotore = true;
}
};

class Veicolo2Ruote : public VeicoloMotore


{
public:
bool SostegnoLaterale;

Veicolo2Ruote(char *Mmarca, char *Mcolore, int Mcilindrata, bool S):


VeicoloMotore(Mmarca,Mcolore,Mcilindrata)
{
SostegnoLaterale = S;
}
...
}; // fine classe Veicolo2Ruote

class Motocicletta : public Veicolo2Ruote // Motocicletta e’ sottoclasse di Veicolo2Ruote


{
public:
int Marcia; // nuova variabile membro non presente in Veicolo2Ruote

Motocicletta(char* Mmarca, char* Mcolore, int Mcilindrata, bool S, int Mmarcia):


Veicolo2Ruote(Mmarca,Mcolore,Mcilindrata,S)
{
Marcia=Mmarcia; // imposta la marcia
}
}; // fine classe Motocicletta

class Ciclomotore: public Veicolo2Ruote


// Ciclomotore è sottoclasse di Veicolo2Ruote
{
...
}; // fine classe Ciclomotore

class Automobile: public VeicoloMotore


// Automobile è sottoclasse di VeicoloMotore
{
...
}; // fine classe Automobile

164 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 165

Come possiamo notare, il costruttore della classe Motocicletta richiede cinque pa-
rametri formali, dei quali uno solo, Mmarcia, viene utilizzato dal suo costruttore, Mo-
tocicletta( ), per impostare la marcia. Gli altri parametri servono per richiamare espli-
citamente il costruttore della sua superclasse, Veicolo2Ruote( ), che richiede appun-
to quattro parametri formali. Iterando il ragionamento, anche il costruttore della
classe Veicolo2Ruote utilizza solo un parametro (quello relativo al SostegnoLaterale)
e richiama esplicitamente il costruttore della classe VeicoloMotore passandogli gli al-
tri tre parametri.
Quindi, rispetto ai costruttori senza parametri, questa volta è il programmatore
che deve richiamare esplicitamente i costruttori delle superclassi.

2.4 Chiamata della funzione membro della superclasse


Abbiamo detto che ridefinendo una funzione membro nascondiamo la definizione
di tale funzione nella superclasse. Per richiamare la funzione membro della super-
classe e non quella ridefinita nella classe si utilizza la seguente sintassi:
<NomePuntatoreOggettoDellaSottoclasse> –> <NomePuntatoreOggettoSuperclasse1>::
<NomeFunzioneSuperclasse>([<Parametri>]);

Ad esempio, relativamente alla gerarchia di classi dell’esempio 1 di questa Unità,


potremmo scrivere il seguente frammento di codice:
int main( )
{
Motocicletta *M = new Motocicletta("m1", "Giallo", 1000, true);
M–>Veicolo2Ruote::MostraStato( ); Viene richiamata la funzione
membro MostraStato( ) di
M–>MostraStato( ) Motocicletta

M–>Veicolo2Ruote:: MostraStato( ) Viene richiamata la funzione


return 0; membro MostraStato( ) di
Veicolo2Ruote
}

3 Funzioni virtuali
In C++ è possibile dichiarare in una classe particolari funzioni, dette funzioni vir-
tuali. Una funzione virtuale è una funzione che può essere ridefinita nelle sottoclas-
si solo con lo stesso prototipo della sua definizione. Una funzione virtuale viene
quindi definita una prima volta in una superclasse e, se presenti, tutte le sue succes-
sive definizioni nelle sottoclassi dovranno avere lo stesso prototipo della definizione
iniziale. Nelle sottoclassi, ogni ridefinizione continua a essere virtuale (anche se non
esplicitamente indicato).
Per definire una funzione virtuale basta far precedere il suo prototipo dalla paro-
la chiave virtual:
virtual <ValoreDiRitorno> <NomeFunzione>([<Parametri>])
{
<Istruzioni>
}

L’obiettivo delle funzioni virtuali è di non permettere l’overloading di quella fun-


zione nelle sottoclassi, ovvero di non consentire che le sottoclassi possano ridefini-
re tale funzione con diverso numero o tipo di parametri.
In sostanza, una funzione virtuale costringe l’implementatore della sottoclasse a
usare la stessa interfaccia (prototipo) della funzione definita la prima volta come virtual.
Costruttori e funzioni amiche non possono essere dichiarate virtual.

Unità didattica C3: Ereditarietà e polimorfismo 165


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 166

4 Funzioni e classi astratte


Una volta che si organizzano le classi in una gerarchia di ereditarietà, si presume
che le classi più “in alto” siano quelle più astratte e generali, mentre quelle più “in
basso” siano quelle più concrete e specifiche.
Spesso, quando si progetta un insieme di classi, si raggruppano variabili e funzio-
ni membro comuni in una superclasse condivisa. Se il motivo principale dell’esisten-
za di tale superclasse è quello di operare come centro di riferimento comune e con-
diviso e si pensa di ridefinire le funzioni membro nelle sue sottoclassi, tale super-
classe viene dichiarata astratta.
Nella gerarchia dell’esempio 3, notiamo che è facile trasformare la classe Veicolo-
Motore in una classe astratta. Le sue funzioni membro, infatti, come AvviaMotore( ),
hanno implementazioni completamente diverse nelle sottoclassi: in ognuna di esse,
tale funzione membro verrà ridefinita. Quella presente nella superclasse, invece,
darà le linee guida sulle funzioni e sulle variabili membro che dovranno essere pre-
senti nelle sottoclassi.
Le classi astratte non possono essere istanziate, ma possono contenere tutto quan-
to può contenere una normale classe, comprese variabili e funzioni membro istanza;
in più, possono contenere funzioni membro astratte, dette anche funzioni virtuali
pure. Le funzioni virtuali pure sono funzioni virtuali con prototipo ma prive di im-
plementazione: si suppone che quest’ultima sia fornita nelle sottoclassi. Le funzioni
virtuali pure, così come le funzioni virtuali, vengono dichiarate con la parola chiave
virtual, ma al posto dell’implementazione hanno la seguente sintassi:
virtual <TipoValoreRestituito> <NomeFunzioneMembro>([<Parametri>]) = 0;

Una classe astratta deve avere come funzione membro almeno una funzione vir-
tuale pura.

4.1 Un esempio di classi astratte


Un altro esempio di classi astratte può essere quello delle classi relative alle figu-
re geometriche. Supponiamo di avere la gerarchia di classi di figura C3.6. Nello sche-
ma possiamo notare la parola chiave abstract accanto al nome della classe astratta.
✦ Figura C3.6
Una gerarchia con una abstract FiguraGeometrica
classe astratta.

public public
Rettangolo Cerchio

Il codice C++ relativo a FiguraGeometrica e Rettangolo è il seguente.

Esempio 4
Un esempio di classi astratte

class FiguraGeometrica // classe astratta


{
private:
char *Nome;
public:
void setNome(char *Str)
{
Nome=Str;

166 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 167

}
void getNome( )
{
return(Nome);
}
virtual float Perimetro( ) = 0; // dichiarazione funzione virtuale pura
virtual float Area( )= 0; // dichiarazione funzione virtuale pura
}; // fine classe FiguraGeometrica

class Rettangolo : public FiguraGeometrica


{
private:
float B;
float H;
public:
Rettangolo(float B1, float H1)
{
B=B1;
H=H1;
}
float Perimetro( )
{
return(B*H)*2;
}
float Area( )
{
return(B*H);
}
}; // fine classe Rettangolo

class Cerchio : public FiguraGeometrica


{
public:
double X; // prima coordinata del centro
double Y; // seconda coordinata del centro
double Raggio;

Cerchio( ) // primo costruttore


{
X=0;
Y=0;
Raggio=1;
}
Cerchio(double VX, double VY, double VRaggio) // secondo costruttore
{
X=VX;
X=VY;
Raggio=VRaggio;
}
double Perimetro( )
{
const double Pigreco=3.141593; // valore approssimato
return(2*Raggio*Pigreco);
}
double Area( )
{
const double Pigreco=3.141593; // valore approssimato
return(Raggio*Raggio*Pigreco);
}
}; // fine classe Cerchio

Unità didattica C3: Ereditarietà e polimorfismo 167


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 168

Notiamo che le funzioni Perimetro( ) e Area( ) non possono essere implementate


nella classe FiguraGeometrica in quanto questa è ancora troppo generica: non è possi-
bile infatti calcolare il perimetro e l’area di una figura geometrica qualsiasi. Saranno
quindi le sottoclassi Rettangolo e Cerchio a fornire l’implementazione per tali funzioni.

5 Polimorfismo
Nell’esempio 1 della gerarchia di classi avente VeicoloMotore come classe caposti-
pite, se assegnassimo alla classe VeicoloMotore la funzione membro MostraStato( ),
questa verrebbe ridefinita (overriding) nelle sottoclassi Veicolo2Ruote, Motocicletta e
Automobile. Ciò vuol dire che si è adeguata tale funzione membro alle particolari esi-
genze delle due sottoclassi. Si dice in questo caso che la funzione membro Mostra-
Stato( ) denota, all’interno della gerarchia di classe, un aspetto polimorfo, nel senso
che cambia forma a seconda della classe in cui si trova.
✦ Figura C3.7 Quando un altro oggetto richiama la
La funzione membro VeicoloMotore
MostraStato( ) funzione membro MostraStato( ) di un
ridefinita nelle varie
MostraStato( ) oggetto facente parte della classe Motoci-
sottoclassi.
cletta, la funzione membro MostraStato( )
sarà cercata prima nella sottoclasse
public Motocicletta; se è trovata (stesso prototi-
public
Veicolo2Ruote po della chiamata) sarà eseguita, altri-
Automobile
menti si cercherà la funzione membro
MostraStato( )
MostraStato( ) risalendo nell’albero della gerarchia di
classe, nel nostro caso andandola a cer-
care nella superclasse Veicolo2Ruote.
public
Motocicletta
Nella figura C3.7 evidenziamo come la
funzione membro MostraStato( ) sia pre-
MostraStato( ) sente in ogni classe con una differente
definizione.

Un altro tipo di polimorfismo è quello che consente di inserire all’interno di una


classe delle funzioni membro che hanno lo stesso nome ma differente numero di para-
metri. Quando la funzione membro viene richiamata, la scelta corretta della funzione
da eseguire viene effettuata contando il numero e verificando il tipo dei parametri.
Questa situazione è nota come overloading (sovraccarico) delle funzioni membro.
Un chiaro esempio di overloading lo abbiamo già incontrato parlando delle funzio-
ni membro costruttore. Si possono avere, infatti, più funzioni costruttore con lo stes-
so nome, che si differenziano per il loro prototipo. Un ultimo caso, importantissimo,
di overloading è quello degli operatori, che esamineremo a breve.
Il concetto di polimorfismo è strettamente collegato a quello di binding dinamico
o associazione posticipata o ancora collegamento ritardato (late binding).

5.1 In che cosa consiste il binding dinamico?


Si chiama binding quel processo che lega la definizione, e perciò il codice, della
funzione appropriata a ogni chiamata di funzione.
Quando il legame avviene a tempo di compilazione si parla di binding statico (o
early binding). Il binding dinamico invece risolve l’impossibilità di stabilire, a tem-
po di compilazione, qual è la funzione, tra le tante con lo stesso nome, che deve es-
sere legata alla chiamata.
Facciamo subito un esempio di funzione membro da legare alla chiamata. Suppo-
niamo di avere la seguente situazione:
• definiamo tre classi VeicoloMotore, Automobile e Veicolo2Ruote. Automobile e Vei-
colo2Ruote sono sottoclassi di VeicoloMotore;

168 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 169

• VeicoloMotore possiede una funzione membro virtuale MostraStato( ), Automobile e


Veicolo2Ruote implementano tale funzione.

La situazione è mostrata di seguito:

class VeicoloMotore
{
public:
virtual void MostraStato( )
{
cout << "MostraStato( ) di VeicoloMotore";
}
}; // VeicoloMotore

class Automobile public: VeicoloMotore


{
public:
void MostraStato( )
{
cout << "MostraStato( ) di Automobile";
...
}
}; // Automobile

class Veicolo2Ruote public: VeicoloMotore


{
public:
void MostraStato( )
{
cout << "MostraStato( ) di Veicolo2Ruote";
...
}
}; // Veicolo2Ruote

Supponiamo ora di avere da qualche parte (nel programma principale o in qualche


altra funzione membro) il seguente spezzone di codice:

// definizione puntatore alla classe base della gerarchia


1 VeicoloMotore *Punt1;
2 int X;
3 cout<< "Inserisci un valore per X: "; cin >> X;
4 if (X==0)
// allocazione di un oggetto di classe Automobile al puntatore Punt1
5 Punt1 = new Automobile( );
else
// allocazione di un oggetto di classe Veicolo2Ruote al puntatore Punt1
6 Punt1 = new Veicolo2Ruote( );
// chiamata della funzione membro MostraStato( ) utilizzando il puntatore Punt1
7 Punt1–>MostraStato( );
8 delete Punt1;

Da questo frammento di codice notiamo come è possibile accedere alle funzioni


virtuali in fase di esecuzione di un programma.
L’istruzione 1 definisce un puntatore Punt1 alla classe base della gerarchia Veico-
loMotore.

Unità didattica C3: Ereditarietà e polimorfismo 169


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 170

Le istruzioni 5 e 6 allocano (tramite new) un oggetto di una sottoclasse in base al-


la scelta effettuata dall’operatore.
L’istruzione 7, mediante il puntatore Punt1, richiama la funzione membro Mostra-
Stato( ). Ma quale MostraStato( ) viene effettivamente chiamata, quella di Automobile
o quella di Veicolo2Ruote?
Come possiamo notare, il legame tra la chiamata a MostraStato( ) e la definizione
della funzione membro MostraStato( ) viene risolto solo al momento dell’esecuzione
dell’istruzione 4.
Analizzando tale istruzione possiamo concludere che verrà chiamata MostraSta-
to( ) di Automobile se X = 0 (poiché al puntatore Punt1 viene allocato un oggetto
di classe Automobile, figura C3.8), altrimenti sarà chiamata MostraStato( ) di Veico-
lo2Ruote se X ≠ 0 (poiché a Punt1 viene allocato un oggetto di classe Veicolo2Ruote,
figura C3.8).
In fase di compilazione non è quindi possibile stabilire quale delle due funzioni
MostraStato( ) verrà chiamata, poiché il valore di X sarà noto solo a tempo di esecu-
zione. Il legame tra il nome della funzione membro e il suo codice, pertanto, “sarà
posticipato” a tempo di esecuzione e non potrà essere risolto in anticipo a tempo di
compilazione.
Si parla in tal caso di binding dinamico o associazione posticipata.

✦ Figura C3.8
Un esempio di binding
dinamico. class VeicoloMotore
{
public:
virtual void MostraStato( )
{
cout << "MostraStato( ) di VeicoloMotore";
}
}; // VeicoloMotore

class Automobile public : VeicoloMotore


{
public: Se X = 0
void MostraStato( )
{
cout << "MostraStato( ) di Automobile";
...
} Punt1–>MostraStato( )
}; // Automobile

class Veicolo2Ruote public : VeicoloMotore


{
Se X != 0
public:
void MostraStato( )
{
cout << "MostraStato( ) di Veicolo2Ruote";
...
}
}; // Veicolo2Ruote

È evidente la maggiore flessibilità concessa al programmatore dal binding dinami-


co, ma anche la maggiore difficoltà qualora vi siano errori da correggere, proprio per-
ché non è esplicito l’oggetto destinatario del messaggio, oggetto che si rivelerà solo
durante l’esecuzione.

170 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 171

6 Ereditarietà multipla
Come abbiamo visto, una classe C++ può ereditare da più superclassi. In questo ca-
so si parla di ereditarietà multipla e si utilizza la seguente sintassi:

class <NomeClasse> : [<visibilità>] <NomeSuperclasse1>,


[<visibilità>] <NomeSuperclasse2>, ...,
[<visibilità>] <NomeSuperclasseN>
{
<CorpoClasse>
};

Dopo il nome della classe e i due punti ✦ Figura C3.9


Un esempio di
segue una lista di superclassi con il rispetti- VeicoloMotore Abitazione ereditarietà multipla.
vo indicatore di visibilità, separate dalla vir- +Superficie
MostraStato( ) +GetSuperficie( )
gola.
Facciamo subito un esempio, consideran- public public
do la gerarchia di classi di figura C3.9.
Come possiamo notare, la classe Caravan
Caravan
eredita sia dalla classe VeicoloMotore che
dalla classe Abitazione. Ciò accade perché MostraStato( )
un oggetto della classe Caravan possiede
caratteristiche e comportamenti della clas-
se VeicoloMotore e della classe Abitazione.
In C++ la gerarchia precedente si scrive nel modo illustrato dal seguente esempio.

Esempio 5
Un esempio di ereditarietà multipla

#include <iostream.h>
class VeicoloMotore
{
public:
char* Marca;
char* Colore;
int Cilindrata;
bool StatoMotore;
VeicoloMotore(char* Mmarca, char* Mcolore, int Mcilindrata)
{
Marca = Mmarca;
Colore = Mcolore;
Cilindrata = Mcilindrata;
StatoMotore = true;
}
void MostraStato( ) // funzione membro che verrà ereditata dalle sottoclassi
{
cout << "VeicoloMotore: " << Marca << " " << Cilindrata << " " << Colore << endl;
}
};
class Abitazione
{
public:
int Superficie;
int NumeroBagni;
Abitazione(int S)
{
Superficie=S;
}

Unità didattica C3: Ereditarietà e polimorfismo 171


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 172

int GetSuperficie( )
{
return(Superficie);
}
};
class Caravan : public VeicoloMotore, public Abitazione
{
public:
int PostiLetto;
Caravan(char* Mmarca, char* Mcolore, int Mcilindrata, int S, int P):
VeicoloMotore(Mmarca,Mcolore,Mcilindrata), Abitazione(S)
{
PostiLetto=P;
NumeroBagni=1;
}
void MostraStato( ) // funzione membro ridefinita proveniente dalla superclasse VeicoloMotore
{
cout << "Caravan: " << Marca << " " << Cilindrata << " " << Colore << endl;
cout << "Superficie: " << GetSuperficie( )<< endl; /* utilizza la funzione membro
definita in Abitazione */
cout << "Postiletto: " << PostiLetto << endl;
}
}; // fine classe Caravan
int main( )
{
Caravan *CV = new Caravan("Marca1", "Bianco", 2500, 20, 5);
CV–>MostraStato( );
CV–>VeicoloMotore::MostraStato( );
return 0;
}

Come possiamo notare, il costruttore della classe Caravan richiede cinque para-
metri, e ne utilizza uno solo per sé; gli altri quattro vengono utilizzati per richiamare
i costruttori delle superclassi Abitazione e VeicoloMotore da cui deriva direttamente.

VeicoloMotore

+MostraStato( ) 6.1 Il problema dell’ambiguità


nell’ereditarietà multipla
Veicolo2Ruote VeicoloCinese
L’ereditarietà multipla, anche se semplice e
+SostegnoLaterale +TassaImportazione
+GetTassa( )
intuitiva, comporta alcuni problemi di ambi-
guità che spesso ne consigliano un uso limi-
✦ Figura C3.10 tato.
La presenza di un MotoCinese
ciclo in una gerarchia +AvviamentoManuale
Consideriamo a titolo di esempio la gerar-
di classi con
+PerdeOlio( ) chia di classi di figura C3.10.
ereditarietà multipla.
A essa corrisponde il seguente codice C++:

Esempio 6
Un altro esempio di ereditarietà multipla

#include <iostream.h>
class VeicoloMotore
{
public:
char* Marca;
char* Colore;
int Cilindrata;

172 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 173

bool StatoMotore;
VeicoloMotore(char* Mmarca, char* Mcolore, int Mcilindrata)
{
Marca = Mmarca;
Colore = Mcolore;
Cilindrata = Mcilindrata;
StatoMotore = true;
}
void MostraStato( ) // funzione membro che verrà ereditata dalle sottoclassi
{
cout << "VeicoloMotore: " << Marca << " " << Cilindrata << " " << Colore <<endl;
}
};
class Veicolo2Ruote : public VeicoloMotore
{
public:
bool SostegnoLaterale;

Veicolo2Ruote(char* Mmarca, char* Mcolore, int Mcilindrata, bool S):


VeicoloMotore(Mmarca,Mcolore,Mcilindrata)
{
SostegnoLaterale = S;
}
}; // fine classe Veicolo2Ruote
class VeicoloCinese : public VeicoloMotore
{
public:
int TassaImportazione;
VeicoloCinese(char* Mmarca, char* Mcolore, int Mcilindrata, int T):
VeicoloMotore(Mmarca,Mcolore,Mcilindrata)
{
TassaImportazione = T;
}
int GetTassa( )
{
return(TassaImportazione);
}
};
class MotoCinese : public Veicolo2Ruote, public VeicoloCinese
{
public:
MotoCinese(char* Mmarca, char* Mcolore, int Mcilindrata, bool S, int T):
VeicoloMotore(Mmarca,Mcolore,Mcilindrata),
Veicolo2Ruote(Mmarca,Mcolore,Mcilindrata, S),
VeicoloCinese(Mmarca,Mcolore,Mcilindrata, T)
{
...
}
};
int main( )
{
MotoCinese *MC = new MotoCinese("Marca1", "Verde", 125, true, 500);
MC–>MostraStato( );
return 0;
}

Questo codice produce un errore a tempo di compilazione, in quanto la gerarchia


che implementa presenta un ciclo. Tale ciclo è responsabile dell’ambiguità di deri-
vazione della classe VeicoloMotore. Gli attributi e le funzioni membro di tale classe,
infatti, sono ereditati due volte dalla classe MotoCinese: una volta tramite il cammino

Unità didattica C3: Ereditarietà e polimorfismo 173


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 174

che passa per la classe Veicolo2Ruote, e un’altra volta tramite il cammino che passa
per la classe VeicoloCinese, così come evidenziato dallo schema di figura C3.11.
✦ Figura C3.11
primo VeicoloMotore secondo
cammino cammino
+MostraStato( )

Veicolo2Ruote VeicoloCinese
+SostegnoLaterale Ciclo +TassaImportazione
+GetTassa( )

MotoCinese
+AvviamentoManuale
+PerdeOlio( )

Per risolvere tale ambiguità e permettere quindi una corretta compilazione occorre
dichiarare virtuale la trasmissione dell’ereditarietà della superclasse in comune (la
classe VeicoloMotore), così come mostrato nel frammento di codice che segue:

#include <iostream.h>
class VeicoloMotore
{
...
};
class Veicolo2Ruote : public virtual VeicoloMotore
{
...
}; // fine classe Veicolo2Ruote
class VeicoloCinese : public virtual VeicoloMotore
{
...
};
class MotoCinese : public Veicolo2Ruote, public VeicoloCinese
{
...
};

6.2 Un’altra ambiguità risolta a tempo di compilazione


Consideriamo infine la situazione illustrata. La funzione membro MostraStato( ) è
presente nelle due superclassi (Veicolo2Ruo-
Veicolo2Ruote VeicoloCinese te e VeicoloCinese) della classe MotoCinese.
+TassaImportazione
+MostraStato( ) +MostraStato( )
Tale funzione presenta lo stesso numero e ti-
po di parametri. Se viene richiamato il meto-
do MostraStato( ) su un oggetto di classe Mo-
MotoCinese
toCinese, verrà generato un errore a tempo di
+AvviamentoManuale
+PerdeOlio( )
compilazione. L’errore evidenzia la difficoltà
del compilatore nel non saper quale Mostra-
Stato( ) richiamare, visto che il numero e il ti-
po dei parametri sono gli stessi per i due metodi. Tale ambiguità va risolta dal pro-
grammatore indicando espressamente quale funzione membro MostraStato( ) richia-
mare, ad esempio: VeicoloMotore::MostraStato( ).

174 Modulo C: C++ e gli oggetti


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 175

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Come si dichiarano, in C++, le sottoclassi?


2. Che cosa indicano gli specificatori di visibilità quando si crea una sottoclasse?
3. Quali sono le problematiche legate all’ereditarietà multipla?
4. In che cosa consiste il binding dinamico?
5. Che differenza c’è tra overriding e overloading?
6. Quali sono le caratteristiche di una funzione virtuale?
7. Che differenza c’è tra funzioni virtuali e funzioni virtuali pure?
8. In che cosa consiste una classe astratta?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Creare in C++ una gerarchia per le seguenti classi, d) Forno, forno a microonde, lavatrice, lavastoviglie.
e per ogni gerarchia fare un esempio di: funzione e) Autobus, camion, camion con rimorchio, caravan.
ridefinita; funzione estesa; funzione ereditata.
2. Definire la classe Nave (di Container) che trasporta
a) Veicolo, bicicletta, veicolo a propulsione umana,
Container. Ogni Container non può contenere più
veicolo a motore, caravan, abitazione.
di un certo numero di Pacco. Ogni Pacco è
b) Rettangolo, quadrato, poligono, esagono, cerchio, caratterizzato da una dimensione e un peso. Un
figure piane. Container, quindi, è caratterizzato da una
c) Mammiferi, rettili, uccelli, canidi, invertebrati, dimensione Max e un peso Max.
vertebrati, animali, pesci, felini.

Esercizi svolti
1. Creare la classe Cilindro come classe derivata della classe Cerchio (vedi sito: CPP.C3.02.C).
Analisi del processo risolutivo

Abbiamo già definito la classe Cerchio nel corso delle precedenti Unità. Di seguito è definita la classe Cilindro.
Si noti il metodo Area( ) del cilindro che utilizza la funzione membro Area( ) della superclasse Cerchio facendo
esplicito riferimento al nome della superclasse: Cerchio::Area( ). Sempre nella stessa funzione membro si
richiama la funzione membro Circonferenza( ), che è una funzione membro ereditata dalla superclasse Cerchio.

#include <iostream.h>
#include "Punto.h"
#include "Cerchio.h"

class Cilindro : public Cerchio


{
public:
double Altezza;
Cilindro(Punto C1, double R1, double H1);
double Area( ); // dichiarazione del metodo per il calcolo dell’area
}; // fine della dichiarazione di classe Cilindro

Unità didattica C3: Ereditarietà e polimorfismo 175


p157-176_CeC_Unità C3 26-09-2007 16:51 Pagina 176

Verifica
di fine unità

Cilindro::Cilindro(Punto C1, double R1, double H1): Cerchio(C1,R1)


{
Altezza = H1;
}
double Cilindro::Area( )
{
return (Cerchio::Area( ) *2 + Circonferenza( ) * Altezza);
}

int main( )
{
Punto P1 = Punto(2,3);
Cerchio Cer1 = Cerchio(P1,10);
Cilindro Cil1 = Cilindro (P1,10,20);
cout << "Valore coordinata X di Cil1: " << Cil1.Centro.X << endl;
cout << "Valore coordinata Y di Cil1: " << Cil1.Centro.Y << endl;
cout << "Valore raggio di Cil1: " << Cil1.Raggio << endl;
cout << "Valore altezza di Cil1: " << Cil1.Altezza << endl;
cout << "Valore Area di Cil1: " << Cil1.Area( ) << endl;
return 0;
}

2. Definire la classe Triangolo. Un oggetto di questa classe sarà caratterizzato dopo aver specificato i suoi
vertici, che sono oggetti della classe Punto già definita.

Analisi del processo risolutivo

In questo caso la classe Triangolo non è in relazione di ereditarietà con la classe Punto ma è in relazione di
aggregazione, cioè un triangolo è “composto da” oggetti della classe Punto. Si noti inoltre la presenza di una
funzione di classe che richiama una funzione istanza. Provare a completare l’esempio definendo la funzione
membro Area( ).

class Triangolo
private:
Punto:P1;
Punto:P2;
Punto:P3; // si definisce un triangolo tramite i suoi tre vertici
public:
Triangolo(Punto Point1, Punto Point2, Punto Point3) // costruttore
{
P1= Punto(Point1.X,Point1.Y);
P2= Punto(Point2.X,Point2.Y);
P3= Punto( Point3.X,Point3.Y);
}
float Perimetro( )
{
float D;
D = P1.Distanza(P2) + P2.Distanza(P3) + P3.Distanza(P1)
return(D)
}
static float Perimetro(Triangolo T) /* funzione di classe che richiama la
funzione istanza Perimetro( ) */
{
return(T.Perimetro)
}
} // fine classe

176 Modulo C: C++ e gli oggetti


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 177

Unità didattica

Overloading di operatori
e classi generiche C4
● Conoscenze e competenze di base necessarie a realizzare programmi in C++ Prerequisiti
● Concetti di base per utilizzare in C++ classi, variabili e funzioni membro
● Concetto di ereditarietà e polimorfismo

● Saper implementare l’overloading degli operatori tramite funzioni operatore Obiettivi


● Saper implementare classi generiche

● Conoscere la sintassi per poter sovraccaricare operatori binari Conoscenze


● Conoscere la sintassi per poter sovraccaricare operatori unari prefissi e postfissi, da apprendere
l’operatore di assegnazione, operatori relazionali
● Conoscere la sintassi per poter definire funzioni e classi generiche

● Saper strutturare un programma per utilizzare operatori sovraccaricati Competenze


da acquisire

1 Overloading di operatori
Nell’Unità precedente abbiamo parlato di un tipo di polimorfismo chiamato over-
loading delle funzioni membro, che abbiamo detto consistere nella possibilità di in-
serire all’interno di una classe funzioni membro aventi lo stesso nome ma differente
numero di parametri. In C++ esiste anche una importante caratteristica che consen-
te di avere, oltre all’overloading di funzioni membro, anche l’overloading (o sovrac-
carico) della maggior parte degli operatori.
Ad esempio, nella classe Vettore (vista nell’Unità C1), è possibile eseguire l’over-
loading dell’operatore ++ per effettuare l’incremento di una unità di ogni elemento
del vettore.

Eseguendo l’overloading di un operatore si estende lo spettro dei tipi ai quali può


essere applicato l’operatore senza perdere nessuno dei suoi utilizzi originali.
Un esempio di overloading di operatori lo abbiamo già introdotto quando abbiamo
parlato del comportamento polimorfo di alcuni operatori applicati sui tipi di dato
predefiniti. Nelle Unità precedenti, infatti, abbiamo visto che, ad esempio, l’operato-
re + può essere utilizzato sia per la somma aritmetica su operandi quali interi, float,
double, sia per la concatenazione di stringhe.
Vediamo ora l’overloading degli operatori applicati su oggetti definiti dall’utente.
Gli operatori soggetti a quest’ultimo tipo di overloading sono:
• operatori aritmetici (ad esempio: +, −, *, /, %)
• operatori di incremento e decremento (++, −−)
• operatori per allocazione dinamica (new, delete)
• operatori speciali (−>, −>*, <<=, >>=)

Unità didattica C4: Overloading di operatori e classi generiche 177


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 178

Gli operatori non soggetti a overloading sono invece:


• operatore punto (.)
• operatore di scope (::)
• operatore asterisco (*)
• operatore punto interrogativo (?)
L’overloading deve rispettare alcuni vincoli; infatti:
• non è possibile cambiare il numero di operandi di un operatore;
• non è possibile cambiare la precedenza di quell’operatore.

1.1 Definire una nuova operazione: le funzioni operatore


Per implementare l’overloading di operatori applicati a classi si utilizzano funzio-
ni speciali dette funzioni operatore (operator) che associano a tutti gli oggetti della
classe una nuova operazione. L’overloading di operatori è in realtà un overloading di
funzioni eseguito su funzioni particolari: le funzioni operator, appunto. Per creare un
overloading di operatore si utilizza la seguente sintassi:

• nella fase dichiarativa dell’interfaccia della classe


class <NomeClasse>
{
<CorpoClasse>
<SpecificatoreVisibilità>:
<TipoValoreRestituito> operator <SimboloOperatore>([<Parametri>])
};

• nell’implementazione della classe

<TipoValoreRestituito> <NomeClasse> :: operator <SimboloOperatore>([<Parametri>])


{
<Istruzioni>
}

Se <SpecificatoreVisibilità> è public, la nuova operazione è resa visibile all’esterno,


altrimenti, cioè se è private, è visibile solo all’interno della classe.

Nei paragrafi successivi considereremo la classe Vettore, già adoperata nell’Unità


C1, con l’obiettivo di definire:
• l’operatore binario + che calcoli la “somma di due vettori”;
• l’operatore unario prefisso ++ che effettui l’incremento di una unità per ogni ele-
mento del vettore;
• l’operatore unario postfisso −− che effettui il decremento di una unità per ogni ele-
mento del vettore;
• gli operatori di assegnazione = e += che effettuino rispettivamente la copia degli
elementi di un vettore e la somma degli elementi di un vettore;
• un operatore relazionale == che effettui un confronto tra due vettori.

1.2 Overloading di operatori binari


Per applicare un operatore binario infisso tra due oggetti di una stessa classe, scri-
veremo:
<Oggetto1> <SimboloOperatore> <Oggetto2>

178 Modulo C: C++ e gli oggetti


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 179

Quanto appena scritto equivale a richiamare la seguente funzione operatore:


<Oggetto1>. <SimboloOperatore>(<Oggetto2>)

che pertanto deve essere definita nella parte dichiarativa dell’interfaccia della classe.

Nella parte implementativa bisognerà invece seguire il seguente schema:


<TipoValoreRestituito> <NomeClasse> :: operator<SimboloOperatore>(<Oggetto2>)
{
<TipoValoreRestituito> <VariabileRisultato>;
<Istruzioni>
return(<VariabileRisultato>)
}

Ad esempio, per definire l’operatore binario di “somma di due vettori” effettuando


l’overloading dell’operatore “+”, scriveremo:

Esempio 1
Overloading dell’operatore “+”
class Vettore
{
private:
int *V; // variabili istanza
int MAXEL;
...
public:
Vettore(int N);
void Riempi( );
void Visualizza( );
...
Vettore operator+(Vettore V1); // dichiarazione della funzione operatore +
}; // fine classe Vettore
...
Vettore Vettore::operator+(Vettore V1)
// overloading di un operatore binario infisso
{
Vettore Risultato = Vettore(10);
int I=0;
while (I<MAXEL)
{
Risultato.V[I]= V[I] + V1.V[I];
I++;
}
return(Risultato);
} // fine operator+

Un programma main( ) di prova può essere il seguente:

int main( )
{
int S=0;
Vettore V1 = Vettore(10); // crea un vettore di 10 elementi interi
cout << "Riempimento del vettore " << '\n';
V1.Riempi( );
cout << "____________" << '\n';
cout << "Visualizza gli elementi inseriti" << '\n';
V1.Visualizza( );
cout << "____________" << '\n';

Unità didattica C4: Overloading di operatori e classi generiche 179


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 180

Vettore V2 = Vettore(10);
cout << "Riempimento del nuovo vettore " << '\n';
V2.Riempi( );
cout << "____________" << '\n';
V2.Visualizza( );
cout << "____________" << '\n';
Ris = V1 + V2;
Ris.Visualizza( );
return 0;
} // fine main

1.3 Overloading di operatori unari prefissi


Per applicare un operatore unario prefisso a un oggetto di una classe, scriveremo:
<SimboloOperatore> <Oggetto>

Quanto appena scritto equivale a richiamare la seguente funzione operatore:


<Oggetto>. <SimboloOperatore>( )

che pertanto deve essere definita nella parte dichiarativa dell’interfaccia della clas-
se. Come possiamo notare, viene richiamata una funzione operatore senza parametri
di ingresso; tale funzione può modificare l’oggetto al quale si applica, che dovrà per-
tanto essere restituito con una esplicita istruzione di return.
Nella parte implementativa bisognerà allora procedere come nel seguente schema:
<NomeClasse> <NomeClasse>:: operator <SimboloOperatore>( )
{
<Istruzioni>
return(*this)
}

Come possiamo notare, l’istruzione return(*this) restituisce il contenuto dello


stesso oggetto sul quale è stato invocato l’operatore.
Ad esempio, per definire l’operatore unario prefisso di “incremento del valore di
ogni elemento del vettore” effettuando l’overloading dell’operatore “++”, scriveremo:

Vettore operator++( ); // dichiarazione della funzione operator++


...
Vettore Vettore::operator++( ) // overloading di un operatore unario prefisso
{
int I=0;
while (I < MAXEL)
{
V[I]=V[I]+1;
I++;
}
return(*this);
} // fine operator++

Vediamo il frammento del programma main( ) per utilizzare tale operatore:

Vettore V1 = Vettore(10); // crea un vettore di 10 elementi interi



V1 ++;

180 Modulo C: C++ e gli oggetti


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 181

1.4 Overloading di operatori unari postfissi


Per applicare un operatore unario postfisso su un oggetto di una classe,
scriveremo:
<Oggetto> <SimboloOperatore>

Quanto appena scritto equivale a richiamare la seguente funzione operatore:


<Oggetto>. <SimboloOperatore>(0)

Il parametro di input 0 (per convenzione) serve per distinguere una chiamata a una
funzione operatore unario postfisso da una chiamata a funzione operatore prefisso
(senza parametri).
Nella parte implementativa bisognerà allora seguire il seguente schema:

<NomeClasse> <NomeClasse>:: operator <SimboloOperatore>(int)


{
<NomeClasse> <NomeOggettoTemporaneo> = *this;
<Istruzioni>
return(<NomeOggettoTemporaneo>)
}

Come possiamo notare, si inserisce un riferimento a un parametro formale di tipo


intero tra le parentesi tonde dopo il simbolo dell’operatore.
Ricordiamo che l’operatore postfisso restituisce il valore che l’operando aveva
prima dell’applicazione dell’operatore. Quindi, per l’operatore postfisso:
• viene salvato il contenuto dell’oggetto in un oggetto temporaneo;
• viene restituito l’oggetto temporaneo.
Ad esempio, per definire l’operatore unario postfisso di “decremento del valore di
ogni elemento del vettore” effettuando l’overloading dell’operatore “−−”, scriveremo:

Vettore operator−−(int); // dichiarazione della funzione operator++

Vettore Vettore::operator−−(int) // overloading di un operatore unario postfisso


{
int I;
Vettore VTemp = *this;
I=0;
while (I < MAXEL)
{
V[I]=V[I]-1;
I++;
}
return(VTemp);
} // fine operator−−

Vediamo il frammento del programma main( ) per utilizzare tale operatore:


Vettore V1 = Vettore(10); // crea un vettore di 10 elementi interi
...
−−V1;
...

1.5 Overloading dell’operatore di assegnazione


Per applicare l’operatore di assegnazione semplice o composta tra due oggetti,
scriveremo rispettivamente:

Unità didattica C4: Overloading di operatori e classi generiche 181


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 182

<Oggetto1> = <Oggetto2>

oppure
<Oggetto1> <SimboloOperatore> = <Oggetto2>

Quanto appena scritto equivale a richiamare la seguente funzione operatore:


<Oggetto1>.operator=(<Oggetto2>)

ovvero
<Oggetto1>.operator<SimboloOperatore> = (<Oggetto2>)

Nella parte implementativa bisognerà allora adottare il seguente schema:

<NomeClasse> <NomeClasse>:: operator=(<NomeClasse> <NomeOggetto> )


{
<Istruzioni>
return(*this)
}

ovvero
<NomeClasse> <NomeClasse>:: operator<SimboloOperatore>=
(<NomeClasse> <NomeOggetto>)
{
<Istruzioni>
return(*this)
}

Ad esempio, per definire l’operatore di “copia gli elementi di un vettore” (“=”) e di


“somma degli elementi di un vettore” (“+=”) scriveremo:
Vettore operator=(Vettore V1);
Vettore operator+=(Vettore V1);
...
Vettore Vettore::operator=(Vettore V1) // overloading per l’assegnazione semplice
{
int I;
I = 0;
while (I < MAXEL)
{
V[I] = V1.V[I];
I++;
}
return(*this);
} // fine operator=
Vettore Vettore::operator+=(Vettore V1) // overloading per assegnazione composta
{
int I;
I = 0;
while (I < MAXEL)
{
V[I]+= V1.V[I];
I++;
}
return(*this);
} // fine operator+=

182 Modulo C: C++ e gli oggetti


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 183

Vediamo il frammento del programma main( ) per utilizzare tale operatore:

Vettore V1 = Vettore(10); // crea un vettore di 10 elementi interi


Vettore V2 = Vettore(10); // crea un vettore di 10 elementi interi
...
V1 = V2;
V1 += V2;
...

1.6 Overloading di operatori relazionali


Per applicare un operatore relazionale tra due oggetti di una classe, scriveremo:
<Oggetto1> <SimboloOperatore> <Oggetto2>

Quanto appena scritto equivale a richiamare la seguente funzione operatore:


<Oggetto1>.operator<SimboloOperatore>(<Oggetto2>)

Nella parte implementativa bisognerà allora seguire il seguente schema:

bool <NomeClasse>:: operator <SimboloOperatore>(<NomeClasse> <NomeOggetto>)


{
<Istruzioni>
return(<Booleano>)
}

Ad esempio, per definire l’operatore relazionale di “confronto tra due vettori”


effettuando l’overloading dell’operatore “==”, scriveremo:

bool Vettore::operator==(Vettore V1); // dichiarazione dell’operatore ==


...
bool Vettore::operator==(Vettore V1) // overloading di un operatore relazionale
{
bool Ris = true;
int I;
I = 0;
while (I < MAXEL)
{
if (V[I] != V1.V[I])
return(false);
I++;
}
return(Ris);
} // fine operator==

Vediamo il frammento del programma main( ) per utilizzare tale operatore:

Vettore V1 = Vettore(10); // crea un vettore di 10 elementi interi


...
if (V1 == V2 )
cout << "Vettori uguali " << '\n';
else
cout << "Vettori non uguali" << '\n';

Unità didattica C4: Overloading di operatori e classi generiche 183


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 184

2 Funzioni e classi generiche


Il linguaggio C++ permette di creare funzioni e classi generiche. Una funzione ge-
nerica è una funzione che viene definita in modo da poterla applicare a dati di tipo
diverso. In sostanza, una funzione generica definisce una famiglia di funzioni tutte
con lo stesso comportamento, ma che operano su argomenti di tipo diverso. In una
funzione generica, quindi, il tipo di dato sul quale la funzione deve operare viene pas-
sato come parametro.
Analogamente, una classe generica è una classe che viene definita in modo tale
che le variabili e/o le funzioni membro della classe possano operare su dati di tipo
diverso. In sostanza, una classe generica definisce una famiglia di classi aventi le
stesse caratteristiche e gli stessi comportamenti, ma che operano su dati di tipo di-
verso, i quali vengono passati come parametri al momento della creazione di un nuo-
vo oggetto della classe generica.
È quindi possibile utilizzare funzioni e classi generiche passando loro, ogni volta,
vari tipi di dati, senza dover fare una esplicita nuova versione per ogni tipo di dato.
Per definire funzioni e classi generiche si ricorre al meccanismo dei template (o
modelli).

2.1 Funzioni generiche


Per definire una funzione generica si utilizza la parola chiave template (ossia “mo-
dello”: in effetti è come se stessimo definendo delle funzioni modello), secondo la se-
guente sintassi:
template <class Tipo1>, <class Tipo2>, ..., <class TipoN>
TipoValoreRestituito NomeFunzione([Parametri])
{
CorpoFunzione
}

Nella sintassi precedente le parole riservate class, finora utilizzate unicamente per
definire nuove classi, in questo contesto assumono invece il significato di “tipo pa-
rametrico” e sono inserite tra parentesi angolari.
Per evitare quindi ogni confusione di sorta, nel seguito di questa Unità non consi-
dereremo le parentesi angolari “<” e “>” come inizio e fine di una categoria sintattica,
poiché esse fanno parte della sintassi stessa del linguaggio. Scriveremo pertanto le
categorie sintattiche in corsivo.
Occorre sottolineare che i tipi parametrici, oltre ai tipi predefiniti in C++, possono
anche essere classi definite dall’utente.
Come esempio di funzione generica consideriamo una funzione che scambia i va-
lori delle due variabili passate come parametro. Analizziamo il seguente codice:
template <class T> void scambia(T &X, T &Y) // scambia i valori di X e Y
{
T Temp;
Temp = X;
X = Y;
Y = Temp;
}

Come possiamo osservare dalla prima riga, T è il nome simbolico per un tipo ge-
nerico utilizzato per i parametri formali X e Y. Sempre di tipo T è la variabile locale
Temp utilizzata come variabile di appoggio all’interno della funzione. In altri termini,
T è solo un “segnaposto” che verrà sostituito con il tipo di dato reale passato al tem-
po della chiamata della funzione.
Per richiamare una funzione generica si utilizza la normale chiamata di funzione
con il solito passaggio di parametri attuali. Il tipo dei parametri attuali passati deter-

184 Modulo C: C++ e gli oggetti


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 185

mina il tipo di dato sul quale la funzione generica dovrà operare. Ad esempio, per ri-
chiamare la precedente funzione scambia( ) su parametri attuali di tipo diverso pos-
siamo ricorrere al seguente programma principale:
int main( )
{
int V1=12, V2=35;
char V3='C'; V4='B';
cout << " Prima dello scambio V1 = " << V1 << " V2= " << V2 << endl;
cout << " Prima dello scambio V3 = " << V3 << " V4= " << V4 << endl;
scambia(V1, V2);
scambia(V3, V4);
cout << " Dopo lo scambio V1 = " << V1 << " V2= " << V2 << endl
cout << " Dopo lo scambio V3 = " << V3 << " V4= " << V4 << endl
return 0;
}

Le funzioni generiche sono una possibile alternativa in alcuni casi di overloading


di funzioni. Nell’esempio precedente, infatti, invece di una funzione generica avrem-
mo potuto realizzare due funzioni con lo stesso nome, e con lo stesso numero di pa-
rametri ma con tipo di parametri diverso:
void scambia(int &V1, int &V2);
void scambia(char &V3, char &V4);

L’esempio precedente era relativo a soli due tipi, ma lo avremmo potuto utilizzare
anche per scambiare due variabili di tipo qualsiasi.
I vantaggi della funzione generica sono in conclusione riconducibili al dover svi-
luppare un solo codice per la funzione invece che due, tre o più funzioni del tutto si-
mili.

2.2 Classi generiche


Anche per definire una classe generica si utilizza la parola chiave template, se-
condo la sintassi:
template <class Tipo1>, <class Tipo2>, ..., <class TipoN>
class NomeClasse
{
CorpoClasse
};

dove CorpoClasse conterrà variabili membro e funzioni membro definite utilizzando


i tipi generici Tipo1, Tipo2, …, TipoN.
In particolare, le funzioni membro generiche avranno la seguente sintassi:

template <class Tipo1>, <class Tipo2>, ..., <class TipoN>


TipoValoreRestituito NomeClasse <Tipo1,Tipo2,...,TipoN>::NomeFunzione
(Tipo1 Parametro1, Tipo2 Parametro2,..., TipoN ParametroN)
{
CorpoFunzioneMembro
}

Per creare un esempio di classe generica riconsideriamo la nostra classe Vettore.


Tale classe è stata definita per operare su elementi di tipo intero. Se volessimo crea-
re una classe generica che operi su un vettore il cui tipo di elementi è passato di vol-
ta in volta come parametro, dovremmo scrivere:

Unità didattica C4: Overloading di operatori e classi generiche 185


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 186

Esempio 2
Definizione della classe Vettore come classe generica
#include <iostream.h>
const int MAXEL=10;
template <class T>
class Vettore
{
private:
T* V; // variabili istanza
public:
Vettore( );
void Riempi( );
void Visualizza( );
bool Ricerca(T Num);
T Somma( );
}; // fine classe Vettore
template <class T>
Vettore<T>::Vettore( )
{
V = new T[MAXEL];
}
template <class T>
void Vettore<T>::Riempi( )
{
int I=0;
while (I<MAXEL)
{
cout << "Inserire il nuovo elemento:"; cin >> V[I];
I++;
}
} // fine Riempi
template <class T>
void Vettore<T>::Visualizza( )
{
int I=0;
while (I<MAXEL)
{
cout << I << " elemento: " << V[I] << '\n';
I = I +1;
}
}
template <class T>
bool Vettore<T>::Ricerca(T Num)
{
int I=0;
while (I<MAXEL)
{
if (V[I] == Num)
return(true);
I = I + 1;
}
return(false);
}
template <class T>
T Vettore<T>::Somma( )
{
T S=0;
int I=0;
while (I<MAXEL)
{
S = S + V[I];
I = I +1;
}
return(S);
}

186 Modulo C: C++ e gli oggetti


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 187

Come possiamo osservare, analogamente a quanto visto per le funzioni generiche,


il tipo T è utilizzato come tipo parametrico, nel senso che è un segnaposto per il ti-
po reale che verrà passato come parametro al momento della creazione di un ogget-
to di classe Vettore.
Per creare un oggetto di una classe generica si utilizza la seguente sintassi:
NomeClasse <Tipo1>, <Tipo2>, ..., <TipoN> NomeOggetto;

Ad esempio, il seguente programma main( ) crea due vettori, uno di interi e l’altro
di double, e richiama le sue funzioni membro.

int main( )
{
Vettore<int> V1; // crea un vettore di 10 elementi interi
V1.Riempi( );
V1.Visualizza( );
cout << "Somma degli elementi:" << V1.Somma( ) << '\n';
Vettore<double> V2; // crea un vettore di 10 elementi reali
V1.Riempi( );
V1.Visualizza( );
cout << "Somma degli elementi:" << V1.Somma( ) << '\n';
return 0;
} // fine main

È possibile definire una classe generica estendendo la sua sintassi in modo da pas-
sare argomenti che non siano necessariamente tipi. Rimandiamo al sito di riferimen-
to di questo volume per gli esempi di quanto appena affermato.

Unità didattica C4: Overloading di operatori e classi generiche 187


p177-188_CeC_Unità C4 26-09-2007 16:53 Pagina 188

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. A che cosa servono le funzioni operatore?


2. Quali sono gli operatori che non si possono sovraccaricare?
3. Quali sono gli operatori che si possono sovraccaricare?
4. Come si fa a ridefinire un operatore binario?
5. In alcuni casi, a che cosa possono essere alternative le funzioni generiche?
6. Con quale sintassi è possibile definire una funzione generica?
7. Con quale sintassi è possibile definire una classe generica?
8. A che cosa serve la parola chiave template?
9. Quale tipo di parametri ricevono i template?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Ridefinire le funzioni membro di Push( ) e di Pop( ) con due operatori a scelta (ad esempio ++ e −−).
2. Definire una funzione generica di ordinamento (ad esempio bubblesort) di un vettore.
3. Definire una funzione generica di ricerca sequenziale in un vettore.
4. Definire la classe generica Pila.
5. Definire la classe Frazione (di interi) e ridefinire gli operatori + e ∗ in modo che calcolino la somma e il prodotto
tra frazioni.
6. Definire la classe Stringa effettuando l’overloading degli operatori:
• assegnazione (=);
• somma (+);
• lunghezza (~).
7. Definire la classe NumeroComplesso effettuando l’overloading degli operatori:
• modulo (!);
• somma (+);
• prodotto (∗).
8. Definire la classe generica MatriceQuadrata.
9. Data la classe MatriceQuadrata, ridefinire le operazioni di:
• somma tra due matrici (+);
• prodotto tra due matrici (∗);
• incremento (++);
• decremento (−−).

188 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 189

Unità didattica

Flussi e database
C5
● Concetti di base per utilizzare in C++ classi, variabili e funzioni membro Prerequisiti
● Concetti di ereditarietà e polimorfismo
● Linguaggio SQL
● Modello relazionale per la progettazione di basi di dati

● Saper implementare classi che gestiscano i flussi da e su file Obiettivi


● Saper implementare classi che gestiscano l’interazione con un database

● Conoscere il concetto di flusso Conoscenze


● Conoscere le classi per creare, modificare, interrogare un database da apprendere

● Saper leggere e scrivere su file di testo e su file binari attraverso i flussi Competenze
● Saper utilizzare le classi opportune per visualizzare, inserire, cancellare, modificare da acquisire
i dati presenti in un database

1 Gli stream
Per implementare le funzioni relative alla gestione dei file, il linguaggio C++ utiliz-
za il concetto astratto di stream. Lo stream (chiamato anche flusso) può essere im-
maginato come un canale tra la sorgente di una certa informazione e la sua destina-
zione.
Gli stream vengono suddivisi in:
• stream di input, se il punto di vista è quello del consumatore dell’informazione, os-
sia di chi riceve l’informazione attraverso il flusso (figura C5.1);
• stream di output, se il punto di vista è quello del produttore dell’informazione, os-
sia di chi deve inviare le informazioni in uno stream (figura C5.2);
• stream di input/output, se il punto di vista è sia quello del produttore dell’infor-
mazione, sia quello del consumatore dell’informazione.
La potenza dell’astrazione dello stream consiste nel fatto che non è necessario co-
noscere la sorgente delle informazioni quando si legge un flusso di input, così come
non è necessario conoscere tutto riguardo alla relativa destinazione quando si scrive
un flusso di output.
Ogni linguaggio che supporta gli stream definirà proprie funzioni e proprie classi
per il trattamento degli stream stessi.

Unità didattica C5: Flussi e database 189


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 190

✦ Figura C5.1
Lo stream visto dal Stream di input
consumatore
dell’informazione. ? Ha funzioni membro e classi
messe a disposizione
? Riceve: da C++ per ricevere i byte o i caratteri o
• byte gli oggetti dallo stream indipendentemente
? • caratteri da chi li ha inviati
• .........
? CONSUMATORE O DESTINAZIONE

PRODUTTORE O SORGENTE

✦ Figura C5.2
Lo stream visto dal Stream di output
produttore
dell’informazione. ? Ha funzioni membro e classi
messe a disposizione
? Invia: da C++ per inviare i byte o i caratteri o
• byte gli oggetti allo stream indipendentemente
? • caratteri da chi li dovrà ricevere
• oggetti
PRODUTTORE
?

CONSUMATORE

Nelle figure che seguono riportiamo due esempi di stream. Il primo (figura C5.3) è
uno stream di caratteri in cui il produttore (la sorgente) è un file (di caratteri) e il con-
sumatore (la destinazione) è il monitor del computer. Il secondo (figura C5.4) è uno
stream di interi in cui la sorgente è la tastiera e la destinazione è un file (di interi).
✦ Figura C5.3
Un esempio di stream
di caratteri con
sorgente e
destinazione. File Stream di caratteri
di
caratteri

Produttore: file Consumatore: monitor

✦ Figura C5.4
Un esempio di stream
di interi con sorgente
e destinazione.
Stream di interi File
di
interi

Produttore: tastiera Consumatore: file

2 Stream e file
In uno stream possiamo considerare una qualsiasi sorgente e una qualsiasi desti-
nazione dei dati. In questa Unità considereremo stream in cui la sorgente o la desti-
nazione dei dati sono file.
In C++ i flussi possono operare su:
• file binari;
• file di testo.

190 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 191

Prima di proseguire, è dunque opportuno ricordare la differenza di memorizzazio-


ne di dati in formato testo e di dati in formato binario. Nel formato testo i dati sono
rappresentati in forma leggibile dall’uomo. Ogni carattere è identificato tramite il suo
valore ASCII. Ad esempio, il numero 234 è rappresentato da una sequenza di tre ca-
ratteri: “2”, “3”, “4”. Aprendo tale file con un normale word processor è possibile os-
servare il numero 234 in questa forma, ossia comprensibile.
Nel formato binario, invece, i dati non sono più leggibili con un generico word pro-
cessor. Essi vengono infatti visti come una sequenza di byte il cui contenuto può es-
sere un qualsiasi valore compreso tra 0 e 255.
Per gestire questi differenti tipi di file in C++ si utilizzano apposite classi che per-
mettono di controllare le operazioni di lettura e scrittura su file tramite stream.

3 Le classi per gestire i file


Il C++ standard fornisce il supporto al proprio sistema di I/O a oggetti attraverso il
file header iostream.h. Tale file contiene le classi necessarie per la gestione degli
stream; organizzate in un insieme complesso di gerarchie, esse partono da un siste-
ma di classi template utilizzate per definire i flussi di input, di output e di input/out-
put che sono rispettivamente: istream, ostream, iostream.
Gli oggetti predefiniti cout e cin introdotti nell’Unità A3 appartengono a queste
classi; per utilizzarli, occorre importare il file header iostream.h. In particolare:
• cin è un oggetto predefinito della classe istream ed è utilizzato per i flussi di input
da tastiera;
• cout è un oggetto predefinito della classe ostream ed è utilizzato per i flussi di out-
put su unità video.
Anche gli operatori << e >>, detti rispettivamente inseritore ed estrattore, appar-
tengono a queste classi.
Essendo istream, ostream e iostream classi template, esse descrivono la struttura di
una classe senza specificarla completamente; non è possibile istanziare direttamen-
te un oggetto flusso di input o un oggetto flusso di output. Perciò, per creare tali og-
getti, aventi i file come sorgente o come destinazione, si utilizzano le loro sottoclas-
si presenti nel file header fstream.h, che va quindi incluso:
• ifstream per leggere da un flusso con sorgente file;
• ofstream per scrivere su flussi con destinazione file;
• iostream per leggere e scrivere su flussi con sorgente o destinazione file.
Il tipo di accesso ai file (sorgenti o destinazioni di un flusso) può essere:
• sequenziale: per accedere a un oggetto memorizzato nel file, chiamato record lo-
gico, è necessario accedere sequenzialmente ai record che lo precedono;
• diretto o relativo: è possibile accedere direttamente a un record senza accedere a
tutti i record che lo precedono fisicamente.
Per interagire con un flusso che utilizza i file occorre necessariamente:
• creare un oggetto di classe istream oppure ostream oppure iostream, cioè un oggetto
flusso di input da file, flusso di output su file, flusso di input/output da e su file;
• aprire un file mediante la funzione open( ) presente nelle librerie di I/O del file
fstream.h;
• chiudere un file utilizzando la funzione close( ) presente nelle librerie di I/O del fi-
le fstream.h.
La funzione open( ) per l’apertura di un file segue la sintassi:
<NomeOggettoFlusso>.open(<File> [, <Modalità>])

Unità didattica C5: Flussi e database 191


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 192

dove:
• <NomeOggettoFlusso> indica il nome di un oggetto di classe ifstream, ofstream op-
pure fstream precedentemente creato;
• <File> specifica il percorso e il nome del file da aprire;
• <Modalità> (opzionale) può assumere uno dei seguenti valori predefiniti:

VALORE PREDEFINITO SIGNIFICATO


ios::in Input (lettura): crea un file dal quale è possibile leggere i record.
ios::out Output (scrittura): crea un file sul quale è possibile scrivere record.
ios::app Append: apre un file sul quale è possibile aggiungere record solo alla fine del file.
ios::trunc Troncamento: apre un file già esistente eliminando il suo contenuto.
ios::ate AtEnd: apre un file posizionandosi alla sua fine.
os::nocreate NoCreate: apre solo file già esistenti. Se il file non esiste, viene generata
un’eccezione.
ios::noreplace NoReplace: apre solo nuovi file. Se il file già esiste, viene generata un’eccezione.
ios::binary Binary: apre un file binario (la modalità di default apre un file di testo).

Questi valori predefiniti possono essere usati in combinazione tra di loro utiliz-
zando il connettivo logico OR mediante il simbolo “|”.
Ad esempio, per aprire un file binario già esistente possiamo scrivere:
ifstream FlussoDiInput;
FlussoDiInput.open("C:\\archivi\primo.exe", ios::binary | ios::nocreate);

Per aprire invece un file di testo di output in modalità append scriveremo:


ofstream FlussoDiOutput;
FlussoDiOutput.open("C:\\archivi\secondo.txt", ios::app);

Notare come il carattere “\” che indica la sequenza di directory è stato codificato
tramite la sequenza di escape \\ .
Le istruzioni precedenti possono essere riscritte nella seguente forma abbreviata:

ofstream FlussoDiOutput("C:\\archivi\secondo.txt", ios::app);


La funzione open( ) restituisce true se lo stream è connesso a un file aperto, altri-
menti ritorna false. Possiamo allora controllare se tutto è andato a buon fine con il
seguente frammento di codice:
if(!FlussoDiOutput)
{
cout << "File non aperto " << endl;
return -1; // restituire –1 significa che si è verificato un errore
}

La chiusura di un file è un’operazione che può avvenire:


• in automatico, quando si esce dal programma o dal blocco che ha creato l’oggetto
flusso da o su file;
• da programma, utilizzando la già citata funzione close( ), la cui sintassi è:
<NomeOggettoFlusso>.close( )

Essa non richiede alcun parametro e non restituisce alcun valore di ritorno.
Ad esempio, per chiudere il flusso di output aperto con le istruzioni precedenti
scriveremo:
FlussoDiOutput.close( );

L’associazione tra l’oggetto FlussoDiOutput e il file fisico “C:\\archivi\\secondo.txt”


viene eliminata.

192 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 193

4 Flussi sequenziali
Come abbiamo accennato, i flussi che operano su file sequenziali, che chiameremo
flussi sequenziali, scrivono un record speciale alla fine del file caratterizzato dal va-
lore EOF (EndOf File). La lettura avviene a partire dal primo record fisicamente regi-
strato sul file o dal record attivo sul quale si trova un ipotetico puntatore che indica
l’ultimo record letto nel file (figura C5.5).
✦ Figura C5.5
Flussi che operano
Per accedere al quinto record occorre su file sequenziali.
accedere ai primi quattro che lo precedono

Inizio N N N Fine
del file caratteri caratteri caratteri del file

4.1 Leggere e scrivere flussi sequenziali di byte


I dati contenuti in un file binario vengono visti come una sequenza di byte. Per di-
stinguerli dagli altri, si dice che file di questo tipo sono non formattati.
Una volta creato un oggetto stream per l’input e un oggetto stream per l’output si
possono utilizzare le funzioni get( ) e put( ) per leggere e scrivere sui file. Quando la
lettura o la scrittura è terminata viene chiamata la funzione close( ) per chiudere il
flusso.
Analizziamo la sintassi delle funzioni appena introdotte; iniziamo da get( ), che ha
sintassi:
<NomeOggetto>.get(<Carattere>)

dove <NomeOggetto> è il nome di un oggetto di classe ifstream e <Carattere> è la va-


riabile di tipo char che conterrà il carattere letto.
Invece per put( ) la sintassi è:
<NomeOggetto>.put(<Carattere>)

dove <NomeOggetto> è il nome di un oggetto di classe ofstream e <Carattere> è la va-


riabile di tipo char che conterrà il carattere da scrivere sul file.
Essendo file ad accesso sequenziale, a ogni get( ) e a ogni put( ) si sposta il punta-
tore interno al record successivo nel file. Non esistono funzioni membro che per-
mettono lo spostamento di tale puntatore a uno specifico record.
Proponiamo un esempio, in cui utilizziamo le classi precedenti per realizzare un
programma che effettui una copia, byte per byte, di un file sorgente di un flusso di in-
put in un altro file, che è destinazione di un flusso di output. La situazione è illustra-
ta dalla figura C5.6:
✦ Figura C5.6
Esempio di
programma “copia
Stream sequenziale Stream sequenziale file” byte per byte.
(di output) di byte (di input) di byte
File main( )
nuovo.exe 01011100 10010110 01011100 10010110

Unità didattica C5: Flussi e database 193


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 194

Esempio 1
Copia (byte per byte) di un file binario (vedi sito: CPP.C5.01.C)

#include <fstream.h>
int main( )
{
ifstream IN("esempio.exe", ios::in | ios::binary);
if(!IN)
{
cout << "File in lettura non aperto " << endl;
return -1;
}
ofstream OUT("nuovo.exe", ios::out | ios::binary);
if(!OUT)
{
cout << "File in scrittura non aperto " << endl;
return -1;
}
char NuovoByte;
while IN.get(NuovoByte) // IN varra' falso (-1) solo quando si sarà giunti alla fine del file
{
OUT.put(NuovoByte);
}
IN.close( );
OUT.close( );
return 0;
}

Dall’esempio notiamo quanto segue:


• un flusso (di input) di byte avente un file come sorgente è creato con l’istruzione:
ifstream IN("esempio.exe", ios::in | ios::binary);

• una volta creato l’oggetto IN della classe ifstream è possibile leggere da tale flusso
tramite la funzione get( ). Questa funzione restituisce un numero intero che rap-
presenta il contenuto del byte successivo del flusso. Se restituisce –1 vuol dire che
si è giunti alla fine del file.
Discorsi analoghi valgono per l’oggetto OUT di classe ofstream e la funzione put( ),
che creano un flusso (di output) di byte e scrivono su tale flusso. Nell’esempio, il con-
tenuto precedente del file “nuovo.exe” viene cancellato quando il file viene aperto.
Avremmo anche potuto aggiungere in coda al file (senza quindi perdere il suo conte-
nuto precedente) utilizzando la modalità append:
ofstream OUT("nuovo.exe", ios::out | ios::binary | ios::app);

4.2 Leggere e scrivere flussi sequenziali di oggetti


Un altro modo per leggere e scrivere file binari consiste nel ricorrere alle funzioni
read( ) e write( ) che consentono di leggere e scrivere blocchi di dati binari. Vediamo
la loro sintassi, iniziando da read( ):
<NomeOggetto>.read((unsigned char *) <PuntRecord>, sizeof(<TipoDiDato>))

dove:
• <NomeOggetto> è il nome di un oggetto di classe ifstream;
• <PuntRecord> è la variabile puntatore a carattere ma di tipo unsigned, cioè caratte-
ri cui non è associato alcun codice. È perciò un blocco di dati binari senza una in-
terpretazione; <PuntRecord> prenderà quindi l’indirizzo della variabile che con-
terrà i dati provenienti dal file;

194 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 195

• <TipoDiDato> è il tipo di dato dell’oggetto cui punta <PuntRecord> nel quale inse-
rire i dati provenienti dal file.
Per write( ) la sintassi è:
<NomeOggetto>.write((unsigned char *) <PuntRecord>, sizeof(<TipoDiDato>))

dove:
• <NomeOggetto> è il nome di un oggetto di classe ofstream;
• <PuntRecord> è la variabile puntatore a carattere ma di tipo unsigned, cioè caratte-
ri cui non è associato alcun codice. È quindi un blocco di dati binari senza una in-
terpretazione. <PuntRecord> prenderà dunque l’indirizzo della variabile che con-
terrà i dati da scrivere sul file;
• <TipoDiDato> è il tipo di dato dell’oggetto cui punta <PuntRecord> che vogliamo
scrivere sul file.
Anche in questo caso, trattandosi di un file ad accesso sequenziale, a ogni read( ) e
write( ) viene spostato un puntatore interno al record successivo.
Utilizzando queste funzioni è possibile leggere e scrivere oggetti dai e sui file. Vedia-
mo subito un esempio. Consideriamo la classe Dipendente, con le sue variabili mem-
bro e le sue funzioni membro. Creiamo un nuovo oggetto di classe Dipendente, inseria-
✦ Figura C5.7
molo su un file di output e leggiamolo dal file appena creato. La situazione è illustrata Esempio di flusso
dalla figura C5.7. Il relativo codice è riportato nell’esempio 2. di input e di output
di oggetti.

Stream sequenziale Stream sequenziale


(di output) di oggetti (di input) di oggetti
File di classe Dipendente main( ) di classe Dipendente File
nuovo.exe esempio.exe
Dip Dip

Codice=124 Codice=124
Cognome="Rossi" Cognome="Rossi"
Nome="Paolo" Nome="Paolo"

Esempio 2
Lettura e scrittura di oggetti da e verso un file (vedi sito: CPP.C5.02.C)

#include <fstream.h>
class Dipendente
{
public:
int Codice;
char *Cognome;
char *Nome;
int Stipendio;
Dipendente(int C, char *Co, char * No, int Stip)
{
Codice = C;
Cognome = Co;
Nome = No;
Stipendio = Stip;
}
// altre funzioni membro della classe Dipendente
};
int main( )
{
ofstream OUT("dip.dat", ios::out | ios::binary);
if(!OUT)
{
cout << "File in scrittura non aperto " << endl;

Unità didattica C5: Flussi e database 195


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 196

return -1;
}
Dipendente D1=Dipendente(10, "Rossi", "Nome", 1000);
Dipendente *PuntD1=&D1;
OUT.write((unsigned char*) PuntD1, sizeof(Dipendente));
OUT.close( );
Dipendente D2=Dipendente(0," "," ",0);
Dipendente *PuntD2=&D2;
ifstream IN("dip.dat", ios::in | ios::binary);
if(!IN)
{
cout << "File in lettura non aperto " << endl;
return -1;
}
IN.read((unsigned char *) PuntD2, sizeof(Dipendente));
cout << PuntD2 –>Codice << endl;
cout << PuntD2 –>Cognome << endl;
cout << PuntD2 –>Nome << endl;
cout << PuntD2 –>Stipendio << endl;
IN.close( );
return 0;
}

4.3 Leggere e scrivere flussi sequenziali di testo


Per leggere e scrivere da e su un file di testo basta utilizzare gli operatori << e >>
già introdotti. Tali operatori vengono impiegati nello stesso modo già visto per le ap-
plicazioni da consolle, ma invece di utilizzare lo standard output cout (il video) e lo
standard input cin (la tastiera) si devono specificare i nomi degli oggetti di tipo flus-
so da file da input e flusso su file per l’output creati in precedenza. Per utilizzare quin-
di l’operatore di input >> si adopera la seguente sintassi:
<NomeOggettoFlussoInput> >> <NomeVariabile>

dove <NomeOggettoFlussoInput> è il nome dell’oggetto di classe ifstream preceden-


temente creato, <NomeVariabile> è il nome relativo alla variabile di qualsiasi tipo che
vogliamo leggere dal file.
L’operatore >> legge dal file una sequenza che termina con un delimitatore quale:
“\t” oppure “\n” oppure “\0”. Tale sequenza è assegnata alla variabile <NomeVaria-
bile> dopo essere stata convertita nel tipo della variabile.
Analogamente, per utilizzare l’operatore di output << si adopera la seguente sin-
tassi (nella sua forma più semplice):
<NomeOggettoFlussoOutput> << <NomeVariabile>

dove <NomeOggettoFlussoOutput> è il nome dell’oggetto di classe ofstream prece-


dentemente creato, <NomeVariabile> è il nome relativo alla variabile di qualsiasi tipo
che vogliamo memorizzare nel file.
L’operatore << inserisce nel file la variabile NomeVariabile così come avrebbe fatto
se si fosse trattato di scriverla su video. Ovviamente è anche possibile utilizzare “\n”,
“\t” ecc.
Per entrambi gli operatori esistono le seguenti forme generiche:
<NomeOggettoFlussoOutput> << <NomeVariabile1> << <NomeVariabile2> << ...
<NomeVariabileN>

dove <NomeOggettoFlussoOutput> è il nome dell’oggetto di classe ofstream prece-


dentemente creato, <NomeVariabile1> … <NomeVariabileN> sono i nomi relativi alle
variabili di qualsiasi tipo che vogliamo memorizzare nel file.

196 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 197

Analogamente, per l’operatore >> esiste la sintassi generica:


<NomeOggettoFlussoInput> >> <NomeVariabile1> >> <NomeVariabile2> >> ...
<NomeVariabileN>

dove <NomeOggettoFlussoInput> è il nome dell’oggetto di classe ifstream preceden-


temente creato, <NomeVariabile1> … <NomeVariabileN> sono i nomi relativi alle va-
riabili di qualsiasi tipo che vogliamo leggere dal file.
Vediamo un esempio in cui vengono memorizzate su un file di testo informazioni
riguardanti il codice, il nome e il prezzo di un prodotto. Tali informazioni vengono poi
lette e visualizzate a video.

Esempio 3
Leggere e scrivere un file di testo (vedi sito: CPP.C5.03.C)

#include <fstream.h>
#include <math.h>
class Prodotto
{
public:
int Codice;
char *Nome;
double Prezzo;
Prodotto(int ParCodice, char *ParNome, double ParPrezzo)
{
Codice= ParCodice;
Nome=ParNome;
Prezzo=ParPrezzo;
}
};
int main( )
{
ofstream OUT("prodotti.txt");
if(!OUT)
{
cout << "File in scrittura non aperto " << endl;
return -1;
}
Prodotto P1 = Prodotto(1, "P1", 100.70);
Prodotto P2 = Prodotto(2, "P2", 200.80);
Prodotto P3 = Prodotto(3, "P3", 300.90);
OUT << P1.Codice << '\t' << P1.Nome << '\t' << P1.Prezzo << endl;
OUT << P2.Codice << '\t' << P2.Nome << '\t' << P2.Prezzo << endl;
OUT << P3.Codice << '\t' << P3.Nome << '\t' << P3.Prezzo << endl;
OUT.close( );
ifstream IN("prodotti.txt");
if(!IN)
{
cout << "File in lettura non aperto " << endl;
return -1;
}
int VCodice;
char VNome[30];
double VPrezzo;
cout << "Codice" << '\t' << "Nome" << '\t' << "Prezzo:" << endl ;
while (IN >> VCodice >> VNome >> VPrezzo)
{
cout << VCodice << '\t' << VNome << '\t' << VPrezzo << endl;
}
IN.close( );
return 0;
}

Unità didattica C5: Flussi e database 197


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 198

5 Manipolatori per la formattazione dell’output


Quando si scrive sullo standard output (cout) oppure su di un file di testo è possi-
bile formattare l’output ovvero modificarne l’aspetto in visualizzazione.
Per fare questo il C++ ricorre ai manipolatori, particolari variabili o funzioni mem-
bro, molti dei quali definiti nel file header iomanip.h, che possono essere inseriti in
un flusso di output. Riassumiamo di seguito i manipolatori più utilizzati:

MANIPOLATORE SIGNIFICATO
endl Invia nel flusso di output il carattere speciale “nuova linea”.
ends Invia nel flusso di output il carattere speciale NULL.
hex, oct, dec Imposta il formato per i numeri interi inviati nel flusso di output (vale anche
per un flusso di input). hex = esadecimale, oct = ottale, dec = decimale.
left, right Allinea l’output rispettivamente a sinistra o a destra.
setfill(<Carattere>) Riempie con il <Carattere> specificato gli (eventuali) spazi bianchi presenti
in un campo.
setprecision(<NumeroCifre>) Imposta il numero di cifre decimali per la visualizzazione di un numero
reale.

Come esempio consideriamo il seguente codice:


Esempio 4
Utilizzo dei modificatori per l’output (vedi sito: CPP.C5.04.C)

#include <fstream.h>
#include <math.h>
#include <iomanip.h>
int main( )
{
double X = sqrt(2);
cout << "Radice quadrata di 2: " << setprecision(4) << X << '\t' << endl;
cout << "Radice quadrata di 2: " << setprecision(6) << X << '\t' << endl;
cout << "Radice quadrata di " << hex << 234 << " (234 in esadecimale):" << sqrt(234) << endl;
return 0;
}

Esso produce a video il seguente output:

6 Interazione con i database


6.1 Basi di dati, DBMS e linguaggio SQL
Tutti i più recenti linguaggi di programmazione a oggetti mettono a disposizione
del programmatore classi e oggetti per l’interazione con le basi di dati, organizza-
zioni strutturate di archivi in relazione tra di loro per la memorizzazione efficiente ed
efficace delle informazioni.
Le basi di dati che consideriamo in questa Unità sono progettate utilizzando il mo-
dello relazionale, che organizza i dati in tabelle (chiamate relazioni), nelle quali cia-
scuna riga corrisponde a un record logico e ciascuna colonna rappresenta i campi di
tale record.
Il modello relazionale permette di porre in relazione più tabelle, eventualmente crean-
do apposite tabelle di supporto, in modo da memorizzare dati e associazioni tra dati.

198 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 199

Le basi di dati create secondo il modello relazionale vengono gestite da particola-


ri software, detti DBMS (Database Management Systems). Un DBMS è dunque un in-
sieme di programmi che serve per gestire gli archivi che contengono le tabelle (e per-
ciò i dati e le relazioni tra dati) provenienti dalla progettazione di una base di dati tra-
mite modello relazionale.
Anche in C++ esistono classi appositamente definite per interagire con i principali
DBMS presenti sul mercato, come ad esempio Oracle, MS Access, MySQL, Sybase ecc.
Con “interazione con un DBMS” intendiamo il poter:
• inserire nuovi dati nelle tabelle;
• modificare e cancellare i dati inseriti nelle tabelle;
• interrogare i dati inseriti nelle tabelle.
Tutti i nuovi linguaggi di programmazione a oggetti utilizzano l’SQL (Structured
Query Language) quale linguaggio standard di interrogazione (query) e manipolazio-
ne delle tabelle presenti in una base di dati. L’SQL è un linguaggio di tipo dichiarati-
vo, le cui istruzioni possono essere inglobate in un altro linguaggio detto linguaggio
ospite (nel nostro caso il C++): in tal caso si parla di SQL Embedded.
Nel seguito di questa Unità daremo per scontata la conoscenza delle principali
istruzioni SQL che ci consentiranno l’interazione con il DBMS, soffermandoci invece
sulle primitive, classi e funzioni che consentono l’integrazione dell’SQL con il lin-
guaggio C++.
La situazione è illustrata in figura C5.8.
✦ Figura C5.8
PROGRAMMA C++ Base di dati, DBMS
Progettazione della e linguaggio SQL.

Utilizzo di SQL per base di dati


l’interazione con il DBMS (utilizzando il
modello relazionale)

DBMS
Tabelle che
(relazionale)
formano la
base di dati

ARCHIVI
che contengono le tabelle

6.2 ODBC e ADO


L’ODBC (Open DataBase Connectivity) è un’interfaccia software standard, sia in am-
biente Windows sia in altri ambienti, che consente ai programmatori di collegarsi e
interagire con qualsiasi DBMS purché siano disponibili i driver ODBC per quel DBMS.
I driver per un DBMS sono un insieme di programmi che permettono a un’applica-
zione di trasferire dati da e verso un DBMS. Un’interfaccia software standard è quin-
di un insieme di funzioni che consentono di interagire con il DBMS.
In sostanza, se si vuole accedere a un DBMS qualsiasi, si possono sfruttare i driver
ODBC per quel DBMS e utilizzarli all’interno delle applicazioni per connettersi e in-
terrogare la base di dati in esso contenuta. Vediamo in figura C5.9 uno schema per
questo approccio di interazione con i database.
Per associare e configurare i driver ODBC relativi a un particolare database cui si
vuole accedere bisogna aggiungere ciò che si chiama un DSN (Data Source Name), ov-
vero il nome dell’origine dati; ciò consiste nello specificare il tipo di database che si in-
tende utilizzare e i driver ODBC per quel database, come mostrato in figura C5.10 (in
ambiente Windows).

Unità didattica C5: Flussi e database 199


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 200

Driver ODBC DBMS Access


per Access
Programma O
C++ con
D Driver ODBC Rete DBMS Oracle
istruzioni
per Oracle qualsiasi
SQL B

C
DBMS MySQL
Driver ODBC
per MySQL

Interfaccia standard: sono note le primitive con cui si interagisce

✦ Figura C5.9 Cliccando sul pulsante Aggiungi si definisce una nuova DSN. Se si vuole invece modi-
Approccio basato
su ODBC. ficare le impostazioni per un DSN già esistente, occorrerà selezionare l’origine dati
dall’elenco e successivamente, tramite il pulsante Seleziona, si potrà impostare il per-
corso del database da collegare.
L’approccio basato su ODBC è particolarmente adatto all’uti-
lizzo in un ambiente client/server, anche se occorre effettuare al-
cune operazioni di installazione direttamente sul computer
client. In particolare, il client deve:
• conoscere il tipo di DBMS che è stato installato sul server;
• reperire i driver ODBC per il collegamento a quel particolare
DBMS;
• installare sul proprio sistema operativo tali driver.
In molti altri approcci client/server, invece, il client non deve
effettuare alcuna delle precedenti operazioni di configurazione.
In ambiente Windows, un altro approccio per connettersi a un
database consiste nel ricorrere alla tecnologia ADO (ActiveX Da-
✦ Figura C5.10 ta Object), che fa ricorso a oggetti ActiveX per accedere ai dati;
Nuova DNS l’ADO è infatti definito come un modello a oggetti di accesso ai dati indipendente dal
nell’approccio ODBC.
linguaggio di programmazione utilizzato.
La tecnologia ADO, che utilizza una serie di classi e oggetti per l’interazione con un
DBMS, offre numerosi vantaggi, i più importanti dei quali sono:
• non è necessario definire preventivamente un DSN (si parla di connessioni DSN-less);
• è possibile accedere a DBMS che contengono oggetti multimediali;
• è possibile accedere in modo semplice e sicuro a database presenti in Internet.
Indipendentemente dall’uno o dall’altro approccio, vediamo gli oggetti e le principa-
li classi che ci permettono di effettuare le operazioni più importanti sulla base di dati.
In particolare, nell’interazione con un DBMS per visualizzare i dati contenuti in una
tabella, possiamo individuare le seguenti fasi che dettaglieremo singolarmente:
1. Importare le librerie necessarie per utilizzare gli oggetti ADO.
2. Inizializzare l’ambiente COM.
3. Creare una connessione
3.1. Creare un oggetto di classe Connection.
3.2. Attivare la connessione.
4. Impostare la query SQL.
4.1. Creare un oggetto di classe Command.
4.2. Legare l’oggetto Command alla query e alla connessione.
5. Eseguire la query SQL e memorizzare il suo risultato.
5.1. Creare un oggetto di classe Recordset.
5.2. Memorizzare il risultato della query nell’oggetto di classe Recordset.
5.3. Prelevare dati e metadati dall’oggetto di classe Recordset.
6. Chiudere la connessione.

200 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 201

Per poter meglio comprendere le varie fasi precedenti, introduciamo un esempio


di interazione in cui supponiamo di avere a disposizione un database di nome Nego-
zio costituito da varie tabelle relazionali, tra cui una tabella relativa ai prodotti in
vendita nel negozio. La tabella Prodotti ha la seguente struttura:
TABELLA PRODOTTI
Campo Descrizione
Cprod codice del prodotto
Cat categoria del prodotto
DescP descrizione del prodotto
Prezzo prezzo unitario
Qres quantità residua

Vediamo negli esempi seguenti il codice relativo alla versione C++ di Microsoft ne-
cessario a visualizzare i dati contenuti nella tabella Prodotti.

6.3 Esempio di visualizzazione dei dati di una tabella mediante ODBC


Nell’approccio ODBC, supponiamo di aver creato un’entrata DNS collegando il no-
me e il cammino necessario a rintracciare il nostro database Negozio.mdb creato uti-
lizzando come DBMS il programma Microsoft Access.
Fase 1: Importare le librerie a collegamento dinamico (dll) necessarie per utilizzare
oggetti ADO contenuti nel file msado.dll e solitamente presente (nei sistemi Windows)
nella cartella C:\Programmi\File Comuni\System\ADO. L’istruzione è la seguente:
#import "C:\Programmi\File Comuni\System\ADO\msado15.dll"
no_namespace rename("EOF", "EndOfFile")

È necessario specificare l’attributo no_namespace per non dover riferirsi ai name-


space quando si inizializzano o definiscono variabili i cui tipi sono definiti dai file hea-
der importati. Un namespace è semplicemente una regione di dichiarazioni, avente
lo scopo di localizzare i nomi delle variabili per evitare conflitti; questo perché, quan-
do si include un qualsiasi file di header nel programma, il contenuto di tale file si tro-
verà per default nello spacename standard del linguaggio.
È inoltre necessario rinominare la fine del file EOF con EndOfFile, perché in una ti-
pica applicazione C++, EOF è già stata definita dalla costante “–1”.
Fase 2: Inizializzare l’ambiente COM (Component Object Model) di Microsoft, ovve-
ro l’ambiente dove possono essere gestiti gli oggetti ADO. L’istruzione è la seguente:
::CoInitialize(NULL);

Tale istruzione appartiene alle API (Application Programming Interface), cioè all’in-
terfaccia software che mette in comunicazione un programma con il sistema opera-
tivo (Windows, in questo caso).
Sotto-fase 3.1: Creare un oggetto di classe Connection. Si opera creando prima un
puntatore a una connessione, cioè un oggetto di classe _ConnectionPtr, poi si crea
un’istanza vera e propria utilizzando la funzione CreateInstance( ) chiamata sull’og-
getto puntatore. Ciò avviene secondo la sintassi:

_ConnectionPtr <NomePuntatoreAConnessione>;
<NomePuntatoreAConnessione>.CreateInstance(__uuidof(Connection));

Nel nostro esempio le istruzioni sono:


_ConnectionPtr PuntConn;
PuntConn.CreateInstance(__uuidof(Connection));

Unità didattica C5: Flussi e database 201


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 202

La funzione CreateInstance( ) utilizza come parametro il GUID (Global Unique Iden-


tifier) che il compilatore associa alla classe Connection. Tale identificatore è restitui-
to dalla funzione __uuidof( ), che in realtà può essere applicata a qualsiasi tipo o clas-
se che utilizzeremo nel seguito secondo la sintassi:
__uuidof(<NomeClasse>);

dove <NomeClasse> indica il nome di una qualsiasi delle classi che utilizzeremo e re-
stituisce il GUID associato dal compilatore a quella classe. Come vedremo, __uui-
dof( ) verrà utilizzata anche su altri oggetti.
Si faccia attenzione alla sintassi: davanti a ConnectionPtr c’è un solo underscore,
mentre davanti a __uuidof( ) ce ne sono due.

Sotto-fase 3.2: Attivare la connessione utilizzando la funzione Open( ) richiamata


sull’oggetto puntatore a connessione. La sintassi di Open( ) è:
Open(<NomeDSN>, <NomeUtente>, <PasswordUtente>, NULL);

dove:
• <NomeDSN> specifica il nome della sorgente dei dati (DSN) precedentemente im-
postata, contenente il nome e il percorso del database;
• <NomeUtente> e <PasswordUtente> specificano il nome utente e la password di ac-
cesso per quella sorgente.

Nel nostro esempio avremo:


PuntConn −> Open("DBNegozio"," "," ",NULL);

Sotto-fase 4.1: Creare un oggetto di classe Command. Per oggetto di classe Com-
mand si intende una definizione di un comando specifico da eseguire su una fonte da-
ti. Si utilizza un oggetto Command per poter eseguire query sul database e restituire
i record in un oggetto di classe Recordset.
Per creare un oggetto Command si dichiara prima un oggetto puntatore, poi si crea
un’istanza di tale oggetto (così come abbiamo visto per l’oggetto Connection), se-
condo la sintassi:

_CommandPtr <NomePuntatoreAComando>;
<NomePuntatoreAComando>.CreateInstance(__uuidof(Command));

Nel nostro esempio le istruzioni sono:

_CommandPtr PuntCmd;
PuntCmd.CreateInstance(__uuidof(Command));

Sotto-fase 4.2: Impostare la query SQL e legarla all’oggetto di classe Command. Una
volta creato l’oggetto di classe Command si impostano le sue proprietà (ActiveCon-
nection e CommandText) associandolo alla connessione e al comando SQL con le se-
guenti istruzioni:

PuntCmd –> ActiveConnection = PuntConn;


PuntCmd –> CommandText = "SELECT * FROM Prodotti";

Fase 5: Eseguire la query e memorizzare il suo risultato. Il risultato della query è


un insieme di t-uple provenienti dall’interrogazione tramite select dal database.
Sotto-fase 5.1: Creare un oggetto di classe Recordset. Per oggetto Recordset si in-
tende un oggetto in grado di accogliere il risultato di una query. Tale oggetto sarà

202 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 203

quindi strutturato in modo da poter memorizzare al suo interno le righe della tabel-
la risultato. Per creare un oggetto Recordset si dichiara prima un oggetto puntatore,
poi si crea un’istanza di tale oggetto (così come abbiamo visto per l’oggetto Connec-
tion e l’oggetto Command) secondo la sintassi:
_RecordsetPtr <NomePuntatoreARecordset>;
<NomePuntatoreARecordset>.CreateInstance(__uuidof(Recordset));

Nel nostro esempio le istruzioni sono:


_RecordsetPtr PuntRs;
PuntRs.CreateInstance(__uuidof(Records));

Sotto-fase 5.2: Memorizzare il risultato della query nell’oggetto di classe Recordset.


Si richiama il metodo PutRefSource( ) dell’oggetto appena creato passandogli come
parametro l’oggetto di classe Command secondo la sintassi:
<NomePuntatoreARecordset> –> PutRefSource(<NomePuntatoreACommand>);

Nel nostro esempio avremo:


PuntRs –> CPutRefSource(PuntCmd);

PuntRset punterà a un oggetto che conterrà il risultato della query.


PuntRs CProd Cat DescP Prezzo Qres
oggetto
A002 CS casalinghi 2000 15 di classe
A014 CS casalinghi 3000 65 Recordset

L’oggetto di classe Recordset deve ora essere aperto tramite il metodo Open( ) nel
seguente modo:

_variant_t VNull;
VNull.vt = VT_ERROR;
VNull.scode = DISP_E_PARAMNOTFOUND;
PuntRs –> Open(VNull, VNull, adOpenDynamic, adLockOptimistic, adCmdUnknown);

Come possiamo notare, viene creato un oggetto di classe _variant_t. I dati dei Re-
cordset di ADO sono in formato variant, per garantire la corretta interpretazione in
ambienti software diversi.

Sotto-fase 5.3: Prelevare dati e metadati dall’oggetto di classe Recordset per la for-
mattazione del risultato della query. Una volta aperto l’oggetto di classe Recordset si
possono prelevare i dati (e i metadati) in esso contenuti utilizzando ad esempio il se-
guente ciclo while:
while (!PuntRs –> EndOfFile)
{
cout << (char *) _bstr_t(PuntRs –> Fields –> GetItem("CProd") –> Value);
cout << ", " ;
cout << (char *) _bstr_t(PuntRs –> Fields –> GetItem("DescP") –> Value);
cout << ", " << endl;
PuntRs –> MoveNext( );
}

Come possiamo notare:


• la condizione di terminazione del while è stata creata utilizzando la proprietà
EndOfFile dell’oggetto di classe Recordset;

Unità didattica C5: Flussi e database 203


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 204

• il metodo MoveNext( ) ci permette di spostarci al record successivo contenuto nel-


l’oggetto di classe Recordset;
• per selezionare il valore di un campo attraverso il suo nome si utilizza la sintassi:
<NomePuntatoreARecordset> –> Fields −> GetItem("<NomeCampo>") –> Value

Altri metodi della classe Recordset sono:

METODO SCOPO
MoveFirst ( ) Posizionarsi sul primo record.
MoveLast ( ) Posizionarsi sull’ultimo record.
Move Previous ( ) Posizionarsi sul record precedente rispetto al record corrente.

Fase 6: Chiudere la connessione. L’ultima fase è quella della chiusura degli oggetti
di classe Recordset e Connection e dell’ambiente COM tramite le seguenti istruzioni:

PuntRs –> Close( );


PuntConn –> Close( );
CoUninitialize( );

Vediamo di seguito il codice completo per visualizzare l’elenco dei prodotti del no-
stro negozio:

Esempio 5
Visualizzazione dei prodotti presenti in un database (vedi sito: CPP.C5.05.C)

#import "C:\Programmi\File Comuni\System\ADO\msado15.dll" \


no_namespace rename("EOF", "EndOfFile") // librerie necessarie per utilizzare gli oggetti ADO
#include <iostream.h>
int main( )
{
::CoInitialize(NULL); // inizializza l’ambiente COM
_ConnectionPtr PuntConn;
PuntConn.CreateInstance(__uuidof(Connection)); // crea una connessione
PuntConn –> Open(“DBNegozio"," "," ",NULL); // attiva la connessione con il DB Negozio
_CommandPtr PuntCmd;
PuntCmd.CreateInstance(__uuidof(Command)); // crea un oggetto di classe Command
PuntCmd –> ActiveConnection = PuntConn;
PuntCmd –> CommandText = " SELECT * FROM Prodotti"; // imposta le query
_RecordsetPtr PuntRs;
PuntRs.CreateInstance(__uuidof(Recordset)); // crea un oggetto di classe Recordset
PuntRs –> PutRefSource(PuntCmd); // memorizza il risultato della query nel Recordset
_variant_t VNull;
VNull.vt = VT_ERROR;
VNull.scode = DISP_E_PARAMNOTFOUND;
PuntRs –> Open(VNull, VNull, adOpenDynamic, adLockOptimistic, adCmdUnknown);
while (!PuntRs –> EndOfFile) // preleva i dati dall’oggetto Recordset
{
cout << (char *) _bstr_t(PuntRs –> Fields –> GetItem("CProd") –> Value)<< ", " ;
cout << (char *) _bstr_t(PuntRs –> Fields –> GetItem("DescP") –> Value)<< ", " ;
PuntRs –> MoveNext( );
}
PuntRs –> Close( );
PuntConn –> Close( ); // chiude la connessione
CoUninitialize( );
return 0;
}

204 Modulo C: C++ e gli oggetti


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 205

6.4 Esempio di inserimento e modifica dei dati di una tabella


mediante ODBC
Il codice per le operazioni di inserimento di un nuovo prodotto e modifica di un
prodotto esistente è del tutto analogo a quello visto nel paragrafo precedente, tran-
ne che per la fase 5. Questa volta infatti non c’è bisogno di creare un oggetto di clas-
se Recordset ma, una volta impostata, la query può essere eseguita utilizzando la fun-
zione Execute( ) direttamente su un oggetto di classe Connection; la sintassi è:
<NomePuntatoreAConnessione> –> Execute(<StringaSQL>, NULL, adExecuteNoRecords);

dove <StringaSQL> è una stringa di tipo _bstr_t, contenente la query SQL da eseguire;
adExecuteNoRecords definisce <StringaSQL> come un comando che non restituisce
alcuna riga (le eventuali righe trovate vengono scartate).

Ad esempio, per inserire un record della nostra tabella Prodotti possiamo scrivere
il seguente frammento di codice:

_bstr_t StrQuery = "INSERT INTO Prodotti (CProd, Cat, DescP, Prezzo)


VALUES('A003', 'CS', 'televisore 21 pollici', 350)";
PuntConn –> Execute(StrQuery, NULL, adExecuteNoRecords);

Notiamo che la stringa contenente la query è stata dichiarata di tipo _bstr_t in


quanto Execute( ) utilizza oggetti di tipo variant.

Invece, per modificare un record della tabella Prodotti, possiamo ad esempio scri-
vere il seguente frammento di codice:

_bstr_t StrQuery = "UPDATE Prodotti SET Descrizione = 'Lavastoviglie'


WHERE CProd = 'A001'";
PuntConn –> Execute(StrQuery, NULL, adExecuteNoRecords);

Unità didattica C5: Flussi e database 205


p189-206_CeC_Unità C5 26-09-2007 16:55 Pagina 206

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Che cos’è un flusso o stream?


2. Che differenza c’è tra file e stream?
3. Che differenza c’è tra file in formato testo e file binari?
4. Quali librerie si devono utilizzare per scrivere e leggere su di un flusso?
5. Che differenza c’è tra flussi sequenziali e flussi relativi?
6. In che cosa consiste l’approccio ODBC?
7. Che cos’è un DNS? Come si fa a creare un DNS?
8. Come vengono utilizzati le funzioni MoveFirst( ) e MoveLast( )?
9. A che cosa serve la classe Recordset?
10. Quali sono le fasi da eseguire per connettersi a un database?
11. Su quale oggetto agiscono le funzioni Open( ) e Close( )?
12. Come viene utilizzato il metodo Execute( )?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI 9. Creare un programma C++ per l’inserimento dei
dati relativi a un ContoCorrente in un database di
1. Creare un programma C++ che, presa una stringa ContiCorrente.
di testo, la inserisca in un file di caratteri. 10. Creare un programma C++ per la modifica dei dati
relativi a un ContoCorrente da un database di
ContiCorrente.
11. Utilizzando la classe Punto già definita nel Modulo
Stringa qualsiasi B, costruire un’applicazione in pseudocodice che
“salvi” su file di oggetti i valori relativi alle
file in formato testo coordinate di alcuni punti relativi a un certa
superficie.
2. Modificare l’esercizio precedente in modo che a 12. Ripetere l’esercizio precedente salvando le
ogni trasferimento di stringa venga visualizzato il coordinate dei punti in un database di Punti.
numero di caratteri letti o scritti.
13. Utilizzando le classi Punto e Triangolo (da definire
3. Creare un programma C++ che visualizzi il numero utilizzando la classe Punto), costruire
di occorrenze di un carattere, preso in input, in un un’applicazione C++ che “salvi” su file i valori
dato file di testo. relativi a 10 triangoli.
4. Creare un programma C++ che concateni due file di
14. Ripetere l’esercizio precedente salvando i triangoli
testo, ovvero crei un nuovo file che abbia il
in un database di Triangoli.
contenuto del primo e poi il contenuto del secondo.
15. Data la classe Sciatore (Nome, Cognome, Numero,
5. Creare un file di testo che sia ottenuto da un altro
TempoPrimaProva, TempoSecondaProva), registrare
file di testo eliminando tutti i caratteri spazio.
su due file i tempi relativi alle due prove di discesa
6. Creare un programma C++ che inverta ogni riga di libera.
un file di testo. (Ad esempio: la riga “Questa è una
riga” viene invertita nella riga: “agir anu è 16. Stilare la classifica finale e determinare il vincitore
atseuQ”). dell’esercizio precedente.
7. Creare un programma C++ per l’inserimento dei 17. Data la classe Libro, creare un programma C++ per
dati relativi a un’automobile in un database di la gestione di una semplice biblioteca in modo che
Automobili. sia possibile: inserire un nuovo libro, cancellare un
vecchio libro, modificare i dati di un libro.
8. Creare un programma C++ per il prelievo dei dati
relativi a una particolare Automobile da un 18. Aggiungere all’esercizio precedente le funzionalità
database di Automobili. di: prestito di un libro, ricerca di un libro.

206 Modulo C: C++ e gli oggetti


p207-217_CeC_Unità D1 26-09-2007 16:56 Pagina 207

Ambiente di Modulo D
programmazione
visuale per C++
• U.D. D1. Interfaccia grafica ed eventi
in Visual C++
• U.D. D2. Creare applicazioni visuali
in Visual C++

Prerequisiti
● Costrutti di base della programmazione a oggetti in C++ necessari per utilizzare classi, variabili
membro e funzioni membro
● Elementi caratteristici dell’interfaccia utente in ambiente Windows

Obiettivi
● Considerare gli elementi di una GUI come oggetti con proprietà e metodi
● Individuare i componenti standard di una GUI organizzandoli in modo da creare GUI strutturate
● Realizzare un modello a eventi per collegare un evento qualsiasi a un gestore

Conoscenze da apprendere
● Conoscere il concetto di oggetto GUI
● Conoscere le librerie MFC
● Conoscere le parti più importanti dell’ambiente Visual C++
● Conoscere i principali controlli di Visual C++
● Conoscere come utilizzare AppWizard e ClassWizard
● Conoscere come utilizzare i menu
● Conoscere come inserire controlli in una finestra di dialogo
● Conoscere come legare eventi ai controlli

Competenze da acquisire
● Saper inserire oggetti GUI all’interno di contenitori
● Saper disporre oggetti GUI a video
● Saper collegare eventi a oggetti GUI
● Saper scrivere semplici applicazioni in Visual C++ che utilizzino l’interfaccia utente grafica
● Saper inserire e opportunamente collegare a un progetto le classi definite dall’utente

207
p207-217_CeC_Unità D1 26-09-2007 16:56 Pagina 208

Unità didattica

D1 Interfaccia grafica
ed eventi in Visual C++

Prerequisiti ● Costrutti di base della programmazione a oggetti in C++


● Elementi caratteristici dell’interfaccia utente in ambiente Windows

Obiettivi ● Considerare gli elementi di una GUI come oggetti con proprietà e metodi
● Individuare i componenti standard di una GUI organizzandoli in modo da creare
GUI strutturate
● Realizzare un modello a eventi per collegare un evento qualsiasi a un gestore

Conoscenze ● Conoscere il concetto di oggetto GUI


da apprendere ● Conoscere le librerie MFC
● Conoscere le parti più importanti dell’ambiente Visual C++
● Conoscere i principali controlli di Visual C++

Competenze ● Saper disporre oggetti GUI a video


da acquisire ● Saper collegare eventi a oggetti GUI

1 La Microsoft Foundation Class


Le classi utilizzate dal Visual C++ per la gestione degli elementi dell’interfaccia gra-
fica e per la gestione degli eventi non fanno parte dello standard del linguaggio C++:
appartengono al linguaggio Visual C++ di Microsoft, ovvero sono state create e inse-
rite nelle librerie di questo ambiente, che pertanto le contiene in aggiunta a quelle
standard del C++. Una delle librerie utilizzate dal Visual C++ è la Microsoft Foundation
Class (MFC).
La libreria MFC comprende editor di risorse sofisticati per l’implementazione di fi-
nestre di dialogo molto complesse, barre degli strumenti, menu, immagini e moltis-
simi altri elementi che compaiono quotidianamente in tutte le applicazioni Windows.
La MFC permette di programmare l’interfaccia Windows in modo molto semplice ma
efficace, sgravando il programmatore dall’onere di consultare le API (Application Pro-
gramming Interface) di Win32 (ossia di ogni sistema operativo della Microsoft a 32
bit) ogniqualvolta desidera richiamare una funzione del sistema operativo. (Le API di
Windows sono le funzioni che il sistema operativo mette a disposizione del pro-
grammatore.)
La libreria MFC si pone come strato software collocato tra le API di Windows e l’ap-
plicazione Visual C++, così come schematizzato in figura D1.1.

208 Modulo D: Ambiente di programmazione visuale per C++


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 209

La MFC è implementata come un insieme di ✦ Figura D1.1


classi C++, molte delle quali rappresentano og- Utente
getti comuni quali: finestre, finestre di dialogo,
documenti ecc.; altre servono per definire la
Applicazione Visual C++
struttura di un’applicazione. Tutte le classi utiliz-
zano le funzioni primitive di Win32.
MFC
Analizziamo meglio come sono strutturate le
classi di MFC. Esse si dividono in:
API di Win 32

1. Classi per la gestione dell’architettura dell’ap-


plicazione: S.O.
– CWinApp rappresenta l’applicazione nel ve- Windows
ro senso del termine e comprende funzioni
molto utili, come ProcessShellCommand che
elabora gli argomenti della linea di comando,
e OnFileOpen, che implementa il comportamento del comando Apri del menu Fi-
le. CWinApp è derivata da CWinThread, che rappresenta il flusso di esecuzione
e che gestisce il suo comportamento (priorità, Suspend, Resume...);
– CDocument contiene funzioni membro per poter accedere ai file e alle viste a es-
si collegate.
2. Classi per la gestione della memoria atte a facilitare la programmazione (CArray,
CString).
3. Classi per la gestione delle eccezioni: CException.
4. Classi per la gestione delle finestre: CWind, che contiene le proprietà: barra del ti-
tolo, barre di scorrimento, bordi ecc., e le funzioni membro per la personalizza-
zione del comportamento di una finestra. Sue sottoclassi importanti sono:
– CDialog, la classe relativa alle finestre di dialogo;
– CFrameWind, che descrive il comportamento della finestra principale di livello
superiore di un’applicazione (quella che include la barra del titolo, il menu prin-
cipale, la barra degli strumenti, la barra di stato ecc.);
– CView, la base per le classi di vista di livello inferiore;
– CFormView, la classe base per le classi con vista di tipo controllo di dialogo.
5. Classi per la gestione dei file: CFile.
6. Classi per interagire con i database:
– CDataBase e CRecordSet e CRecordView per accedere ai database ODBC;
– CDaoWorkspace e CDaoDatabase, per accedere ai database DAO (Data Access
Object). Tali classi permettono di accedere ai record delle basi di dati registrate
nel sistema (tramite DSN). Dopo l’accesso al database è possibile aggiungere o
eliminare record, o semplicemente esplorare il database con tutte le funzionalità
di ricerca e selezione cui siamo abituati.
7. Classi per la gestione della grafica: CDC (Class Device Control), una classe impor-
tante che implementa le funzionalità del contesto di dispositivo di Windows; il con-
testo del dispositivo è rappresentato dalla classe GDI (Graphic Device Interface),
che si occupa dell’output dell’applicazione verso video, stampanti e altri disposi-
tivi di uscita.
8. Nuove classi vengono aggiunte costantemente, ad esempio: CHtmlView, che im-
plementa le funzioni per un browser Internet come Internet Explorer.
Esse derivano tutte da una classe particolare molto generica, detta CObject. La po-
tenza di questa classe è data dalla sua genericità. È come se volessimo creare un ele-
mento da poter inserire in un array senza preoccuparci della sua struttura interna.
Per includere le classi MFC la procedura è molto semplice: sono divise in gruppi di
appartenenza a seconda della funzione che assolvono. Per alcune non è necessaria
un’inclusione esplicita (CString), per altre – le meno utilizzate – è necessario dichia-
rarne a volte l’utilizzo (CSocket).

Unità didattica D1: Interfaccia grafica ed eventi in Visual C++ 209


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 210

2 La programmazione visuale
L’ambiente Visual C++ è un ambiente di sviluppo integrato (IDE, Integrated Deve-
lopment Environment) di tipo RAD (Rapid Application Development). Queste sigle in-
dicano che è un ambiente in cui sono presenti tutti gli strumenti necessari allo svi-
luppo di un’applicazione, come editor, compilatore, linker, debugger ecc., e che tali
strumenti vengono utilizzati per lo sviluppo semplice e rapido di un’applicazione.
Per costruire l’applicazione si utilizza la possibilità offerta dal Visual C++ di com-
porre l’interfaccia grafica (e non solo grafica) in modo visivo mediante la scelta visi-
va degli oggetti e la loro composizione e interazione con l’utente. Tale modo di pro-
grammare prende il nome di programmazione visuale.

3 L’ambiente Visual C++


In figura D1.2 è mostrata la finestra principale di Visual C++ 6.0, che compare a vi-
deo non appena Visual C++ va in esecuzione.
✦ Figura D1.2
L’ambiente Microsoft Barra Barra degli WizardBar
Visual C++. dei menu strumenti standard

BuildMiniBar

Casella
dei controlli

Workspace Finestra Spazio di lavoro per la composizione dell’interfaccia grafica


per l’output e per la progettazione dell’applicazione in generale

Analizziamo le principali zone in cui tale finestra è suddivisa. In figura D1.2 indivi-
duiamo:
• la barra dei menu (simile in tutti gli ambienti visuali) in cui sono riportati i co-
mandi che vengono impiegati per la realizzazione delle applicazioni;
• la barra degli strumenti standard (simile in tutti gli ambienti visuali e applicativi
Windows): consente un accesso rapido ai comandi utilizzati più frequentemente
nell’ambiente di programmazione. Tutti questi strumenti possono comunque esse-
re attivati dalla barra dei menu;
• la WizardBar che consente un accesso veloce alla creazione e alla modifica delle
classi e dei metodi;
• la BuildMiniBar contenente le icone per l’accesso rapido alla compilazione, alla
creazione dell’eseguibile e all’esecuzione del debugger;

210 Modulo D: Ambiente di programmazione visuale per C++


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 211

• il pannello del workspace, contenente tre schede: ClassView, ResourceView e Fi-


leView. Queste schede permettono di spostarsi in qualunque parte del progetto
per vedere rispettivamente: le classi da cui è composto il nostro progetto, le risor-
se grafiche quali finestre di dialogo, icone ecc., e infine i file del progetto: sorgenti
C++, header, librerie ecc., organizzati in modo gerarchico;
• lo spazio di lavoro che occupa la zona più grande all’interno della finestra princi-
pale (è quello dedicato alla composizione dell’interfaccia utente grafica e alla mo-
difica del codice);
• la finestra per l’output che visualizza gli errori del compilatore, del linker e i mes-
saggi del debugger;
• la casella dei controlli che contiene tutti gli elementi grafici od oggetti grafici od
oggetti GUI, detti controlli, che permettono l’interazione dell’utente con l’applica-
zione.

Finestre e barre possono essere spostate a piacimen- ✦ Figura D1.3


Un esempio di menu
to dal programmatore in modo da ottenere un ambiente di contesto.
ergonomico e personalizzato.
Si possono operare scelte, oltre che da menu, dalle fi-
nestre o dalle barre precedenti, anche dal menu di con-
testo che compare premendo il tasto destro del mouse
(figura D1.3). Molto utile è la presenza dei comandi Pro-
perties ed Events per impostare le proprietà di un ogget-
to grafico e per legare a esso gli eventi, come vedremo
nella prossima Unità.

4 I modelli di un’applicazione
Per realizzare un’applicazione Visual C++ con interfaccia grafica è possibile utiliz-
zare un progetto di tipo MFC AppWizard (exe) che fornisce “modelli di interfaccia
predefiniti” (figura D1.4).
✦ Figura D1.4

Unità didattica D1: Interfaccia grafica ed eventi in Visual C++ 211


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 212

In particolare, questo tipo di progetto for-


nisce modelli con interfaccia a singolo do-
cumento (SDI, Single Document Interface), in-
terfaccia a documenti multipli (MDI, Multiple
Document Interface), interfaccia basata su
dialog box (dialog based), come mostrato in
figura D1.5.
In base al modello scelto, bisognerà se-
guire determinati schemi di interazione. Ad
esempio, se selezioniamo Single document il
sistema creerà una finestra principale con
una barra di menu, una barra degli strumen-
ti e un’area di visualizzazione per un docu-
mento. Invece, se scegliamo Dialog based
verrà proposta una finestra di dialogo, al cui
interno è possibile inserire qualsiasi con-
trollo.

✦ Figura D1.5
4.1 Classi e oggetti grafici
Il modello di interfaccia predefinito serve solo a impostare la struttura principale
di un’applicazione. Indipendentemente dal modello di interfaccia scelto, il sistema
creerà una sua struttura di classi relative alla scelta operata. A ogni aggiunta di ele-
menti grafici a questa struttura di base, su indicazione del programmatore, il sistema
creerà nuove classi relative a ciascun oggetto grafico. Sarà cura del programmatore
collegare opportunamente tali oggetti tra di loro per farli interagire.
Se ad esempio vogliamo aggiungere alla nostra applicazione, creata con un model-
lo di interfaccia qualsiasi, un nuovo pulsante, il sistema creerà tramite ambiente IDE
un oggetto di nome (ad esempio) Premimi di classe CButton, e lo inserirà all’interno
di una classe contenitore come un CDialog o, meglio, una classe creata sempre dal si-
stema che eredita da CDialog.
La scheda ClassView comprende anche la struttura delle classi impostata dal siste-
ma e che scaturisce dalla costruzione di un’interfaccia grafica, oltre alla struttura
delle classi “di lavoro”, non legate cioè a elementi grafici, definite dal programmatore.

4.2 I controlli
Ogni controllo può essere posizionato all’interno di una finestra di dialogo (dialog
box) tenendo premuto il tasto sinistro del mouse e trascinando tale controllo fino al-
la posizione stabilita.
✦ Tabella D1.1 In tabella D1.1 vediamo un elenco dei principali controlli Visual C++, con la speci-
I principali controlli di ficazione del nome, l’icona mostrata nella casella controlli, la relativa descrizione e
Visual C++.
la visualizzazione del controllo all’interno di un contenitore.

NOME ICONA DESCRIZIONE VISUALIZZAZIONE


CONTROLLO
Etichetta o Static Text Un’etichetta è una stringa di testo utilizzata per “etichettare” al-
tre componenti, ovvero assegnare loro un nome o una descrizione
visibile nella GUI. Non può essere modificata dall’utente; è adatta
per visualizzare titoli, descrizioni e informazioni di carattere stati-
co per l’utente.
Casella di testo o Edit Box Una casella di testo è un elemento grafico costituito in genere da
una singola riga priva di barre di scorrimento, che consente all’u-
tente di immettere testo al suo interno. È quindi adatta per acco-
gliere l’input da parte dell’utente. Una casella di testo possiede
proprietà e metodi per la sua modifica nell’aspetto e nella dimen-
sione da parte del programmatore

212 Modulo D: Ambiente di programmazione visuale per C++


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 213

NOME ICONA DESCRIZIONE VISUALIZZAZIONE


CONTROLLO
Pulsante o Button I pulsanti sono semplici componenti dell’interfaccia utente che av-
viano una determinata azione quando vengono premuti. Sono ca-
ratterizzati dalla presenza di una stringa di testo, detta etichetta
del pulsante.
Casella di controllo Le caselle di controllo sono componenti a due stati: attivato o disat-
o Check Box tivato (oppure selezionato/deselezionato, vero/falso e così via).
All’interno di un gruppo di caselle è possibile selezionarne quante
se ne desidera: le scelte consentite non sono mutuamente esclusi-
ve. È quindi possibile attivarne contemporaneamente più di una.
Pulsante di opzione I pulsanti di opzione sono una variante delle caselle di controllo.
o Radio Button Consentono di effettuare delle scelte alternative: pertanto la sele-
zione di un pulsante provocherà automaticamente la disattivazio-
ne di quello già attivato.

Casella immagine o Picture La casella immagine è un contenitore di immagini in formato:


BMP, GIF, ICO, WMF, e JPG. È possibile inserire anche immagini
animate.

Casella combinata La casella combinata è composta da un campo di testo e da una li-


o Combo Box sta di opzioni da cui scegliere quella da inserire nella casella.

Controllo Rich Edit Un controllo Rich Edit si presenta come una grande casella di te-
sto nella quale oltre a inserire, modificare e cancellare il testo, è
possibile applicare tutte le funzionalità classiche di applicazioni
Windows, quali: impostazioni di font, dimensione e colore dei ca-
ratteri, formattazione del testo, evidenziazione del testo, operazio-
ni per blocchi di testo (taglia, copia, elimina ecc.).

Ognuno dei precedenti controlli, ma anche gli altri non descritti, possiede un
insieme di proprietà predefinite, delle quali è possibile impostare i valori ini-
ziali. Per impostare le proprietà di un controllo si deve selezionare Properties
dal menu di contesto (Figura D1.6).

La finestra delle proprietà, mostrata in


figura D1.7, è composta da tre schede –
General, Styles, Extended Styles – che ser-
vono per impostare proprietà relative a
caratteristiche generali o specifiche dello
stile della visualizzazione del controllo.
✦ Figura D1.7

✦ Figura D1.6

In tabella D1.2 sono elencate le proprietà comuni a molti controlli.

NOME PROPRIETÀ DESCRIZIONE


ID Imposta il nome del controllo con il quale lo si individua dall’interno di un pro-
gramma Visual C++.
Caption Imposta il testo visualizzato all’interno di un controllo. Per un pulsante, ad esem-
pio, imposta il testo visualizzato su di esso.
Visible Determina se il controllo è visibile quando l’applicazione è in esecuzione.
Disabled Disabilita il controllo, nel senso che esso non risponderà agli eventi collegati.
Quando un controllo è disattivato assume una visualizzazione sfumata.
Tab stop Indica se l’utente può o meno muoversi sul controllo utilizzando il tasto di tabula-
zione.
Default Determina se il controllo è quello di default in una finestra di dialogo. Un control- ✦ Tabella D1.2
lo di default è disegnato con bordi più marcati. Alcune proprietà
Align text Determina l’allineamento del testo all’interno del controllo. Left è l’allineamento comuni a diversi
controlli.
di default.

Unità didattica D1: Interfaccia grafica ed eventi in Visual C++ 213


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 214

5 Gestione degli eventi in Visual C++


Un evento è un avvenimento asincrono che può verificarsi mentre l’applicazione è
in esecuzione. Sono eventi: l’apertura o la chiusura di finestre, i movimenti o i clic del
mouse, la pressione di tasti ecc. Sono asincroni in quanto non è prevedibile quando
tali avvenimenti possano verificarsi: il programmatore non sa, infatti, in che momen-
to si scateneranno.
Quando si verifica un evento qualsiasi il sistema operativo lo intercetta e indivi-
✦ Figura D1.8
Sorgente,
dua l’oggetto che ha determinato l’evento, detto origine dell’evento, ad esempio il
intercettatore e Pulsante1 oppure il CampoTesto2 ecc. Successivamente, una funzione Visual C++ ge-
gestore di un evento.
store dell’evento risponderà allo specifico evento che si è verificato (figura D1.8).

Clic del mouse


Tasto del mouse premuto Intercettazione Funzione
Tasto del mouse rilasciato Evento:
del clic Visual C++
Movimento del mouse clic del
del mouse di gestione
Pressione di un tasto (da tastiera) mouse
da parte del clic
Clic su casella di controllo del sistema del mouse
operativo

Sorgente dell’evento Intercettatore di evento Gestione dell’evento

5.1 Un modello di gestione degli eventi


Ogni linguaggio di programmazione basato sugli eventi adotta un proprio sistema
di gestione degli eventi, ovvero un proprio modo di manipolarli e rispondere a quel-
li che possono verificarsi. Si dice quindi che ogni linguaggio adotta un proprio “mo-
dello di eventi”. È da ricordare a questo proposito che le classi e il modello di ge-
stione degli eventi del C++ non fanno parte dello standard del linguaggio C++, per-
tanto ogni ambiente visuale può avere il proprio modello di gestione degli eventi.
Per meglio capire il modello di eventi del Visual C++ occorre introdurre la gestio-
ne dei messaggi del sistema operativo Windows.
Windows basa il suo funzionamento sull’invio e la ricezione di messaggi prodotti
dall’interazione tra utente e la macchina fisica. Ad esempio, la pressione di un tasto
invierà un messaggio alla finestra attiva in quel momento, e analogamente per i movi-
menti e i clic del mouse. Alcuni messaggi vengono anche mandati da un programma al
sistema operativo; ciò si verifica ad esempio quando l’utente nasconde una finestra e,
una volta riapparsa, questa ha bisogno di essere ridisegnata, oppure quando un pro-
gramma richiede dei dati relativi allo schermo o allo screensaver.
Distinguiamo allora tra sorgente del messaggio (nel caso precedente: il tasto) e
destinatario del messaggio (nel caso precedente: la finestra corrente). Ogni mes-
saggio è quindi caratterizzato da:
• un puntatore alla finestra destinataria;
• un identificativo del messaggio (message ID);
• altri parametri che serviranno per interpretare il comportamento del messaggio.
Tutti i messaggi prodotti dall’interazione con la GUI dell’applicazione vengono in-
seriti in apposite code con priorità, in attesa di essere recuperati. Come ciò avvenga
non ci interessa, ma è importante sapere che i messaggi accodati “aspettano” finché
non vengono richiamati; è per questo motivo che talvolta, mentre il computer è mol-
to occupato in qualche operazione, risponde in ritardo ai nostri input (clic del mou-
se, digitazione da tastiera e via dicendo).
Windows mette a disposizione circa 200 tipi di messaggi diversi, e a questi se ne
aggiungono circa una ventina a ogni nuova versione. Nei prossimi paragrafi descri-

214 Modulo D: Ambiente di programmazione visuale per C++


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 215

veremo come tali messaggi/eventi vengono collegati a un opportuno gestore definito


dall’utente (utilizzeremo i termini “messaggio” ed “evento” come sinonimi). Nella
prossima Unità, invece, illustreremo due esempi di creazione di applicazioni Visual
C++ con interfaccia utente grafica.

5.2 Come creare un gestore per un evento


Un’applicazione Visual C++ intercetta dunque i messaggi del sistema operativo. Do-
po aver scelto il componente GUI desiderato, selezionando Events dal menu di con-
testo si apre la finestra di dialogo di figura D1.9, in cui distinguiamo le seguenti aree:
• New Windows messages/events: è l’elenco dei messaggi che supporta l’oggetto se-
lezionato nella casella Class or object to handle illustrata di seguito;
• Existing message/event handlers: è l’elenco dei messaggi per i quali è stato definito
un manipolatore (handler);
• Class or object to handle: indica gli oggetti GUI della finestra considerata (ad esem-
pio una casella di controllo, una casella di testo o un pulsante). Ogni oggetto è iden-
tificato da un object ID.

L’handler o funzione manipola-


tore o funzione gestore è la funzio-
ne che l’utente deve definire per ri-
spondere al messaggio. Tutti i nomi
di funzione manipolatore che il Wi-
zard genera sono in corrisponden-
za con i nomi dei messaggi. Il nome
del gestore sarà così suggerito (il
programmatore può decidere di
modificare tale nome proposto dal
sistema): prefisso On, cui segue il
nome del messaggio in minuscolo e
dell’elemento che lo ha generato,
ma con le iniziali di ogni parola in
maiuscolo. Ad esempio, la funzione
✦ Figura D1.9
OnClickedButton1( ) è in risposta al
messaggio BN_CLICKED che è gene-
rato dal clic sull’oggetto di nome Button1. Tale funzione è vista dal sistema Visual C++
come una funzione membro della classe relativa alla finestra contenitore di quel con-
trollo.

Riassumendo, nel modello di eventi che adotteremo:


• un evento è intercettato dal sistema operativo, il quale produce un messaggio me-
morizzato in opportune code;
• l’oggetto messaggio contiene informazioni sulla posizione e sul momento in cui si è
verificato l’evento, sul suo tipo e altro ancora, ad esempio: un puntatore alla fine-
stra destinataria, un identificativo del messaggio, altri parametri che serviranno
per interpretare il comportamento del messaggio;
• l’ambiente VisualC++ identificherà per ogni elemento GUI i possibili messaggi cui
può rispondere;
• se l’elemento GUI deve rispondere a quel messaggio, l’utente deve definire un ge-
store per quel messaggio, dal nome proposto: On<NomeMessaggio><NomeOggetto-
GUI>( );
• l’utente crea ed edita il codice relativo al manipolatore che si occuperà della ge-
stione del messaggio. Tale manipolatore è visto come una funzione membro della
classe relativa alla finestra di dialogo considerata.

Unità didattica D1: Interfaccia grafica ed eventi in Visual C++ 215


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 216

✦ Figura D1.10 Possiamo riepilogare il nostro modello nella figura D1.10, che mette in evidenza l’e-
Intercettatore e
gestore di evento. vento “clic del mouse”.

Ambiente Visual C++


Evento: Intercetta il messaggio di
Clic del mouse Generazione Windows conoscendo: OnClickedIncrementa( )
sul pulsante di un • finestra obiettivo; funzione membro della
Incrementa messaggio • oggetto sorgente; classe relativa alla
da parte • altri parametri del messaggio. finestra contenitore
di Windows Richiama il manipolatore del pulsante
definito dall’utente

Possibile tipo di evento Intercettatore di evento Gestore dell’evento

5.3 Raggruppare gli eventi


Ma quali sono gli eventi che possono verificarsi e che dovremo trattare? Negli ul-
timi tempi gli eventi che un linguaggio di programmazione deve gestire si sono mol-
tiplicati ed è così diventato sempre più difficile tenere traccia di tutti. Per fare un po’
di chiarezza sui tipi di eventi che possono verificarsi proviamo a raggruppare in Ta-
bella D1.3 per sezioni omogenee i principali messaggi di Windows. Per avere una li-
sta completa di tutti i messaggi possibili di Windows basterà andare nell’help del Vi-
sual C++ e nella scheda dell’indice scrivere “WM_”. Verrà visualizzata la lista di tutti
i messaggi, con relativa descrizione e parametri associati.

✦ Tabella D1.3
Principali messaggi di TIPI DI MESSAGGIO NOME MESSAGGIO DESCRIZIONE
Windows.
Provenienti dai tasti WM_CLICKED Clic su un oggetto
del mouse WM_DOUBLECLICKED Doppio clic su un oggetto
WM_LBUTTONDOWN Tasto sinistro del mouse premuto
WM_RBUTTONDOWN Tasto destro del mouse premuto
WM_LBUTTONUP Tasto sinistro del mouse rilasciato
WM_RBUTTONUP Tasto destro del mouse rilasciato
Provenienti dal WM_MOUSEMOVE Movimento del mouse
movimento del mouse WM_MOUSEWHEEL Movimento della rotella del mouse (se presente)
Provenienti WM_MOVE Movimento di una finestra
dalle finestre WM_CREATE Creazione di una finestra
WM_CLOSE Chiusura di una finestra
WM_VSCROLL Scrolling della barra di scorrimento verticale
WM_HSCROLL Scrolling della barra di scorrimento orizzontale
Legati al fuoco WM_SET FOCUS Dopo aver guadagnato il fuoco
(selezione o messa WM_KILLFOCUS Immediatamente prima che una finestra perda il fuoco
in primo piano
di un oggetto)
Legati alla tastiera WM_KEYUP Tasto (non di sistema) premuto
WM_KEYDOWN Tasto (non di sistema) rilasciato
Legati ai menu WM_MENUSELECT Non appena si seleziona una voce da un menu
il messaggio è mandato al proprietario del menu
WM_COMAND Non appena si seleziona una voce da un menu
o una combinazione di tasti legata a quel menu
WM_NEXTMENU Non appena la freccia destra o sinistra è adoperata
per selezionare da un menu
Legati alle modifiche EN_CHANGE Video aggiornato con il contenuto di una casella di testo
dell’utente EN_UPDATE Contenuto di una casella di testo modificato

216 Modulo D: Ambiente di programmazione visuale per C++


p207-217_CeC_Unità D1 26-09-2007 16:57 Pagina 217

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Quali sono gli aspetti caratterizzanti di un’interfaccia utente grafica rispetto


a un’interfaccia utente a carattere?
2. Che cosa sono le classi MFC? Perché vengono utilizzate nell’ambiente Visual C++?
3. Che cosa indicano le sigle IDE e RAD? Visual C++ è un ambiente IDE e RAD?
4. Che cos’è un controllo? Come si fa a visualizzare le proprietà di un controllo?
5. Che cosa imposta la proprietà Caption?
6. Che cosa sono i messaggi del sistema operativo Windows?
7. Come avviene il legame tra evento e gestore dell’evento?
8. A che cosa servono i Wizard?
9. Quali sono le parti principali da cui è composto l’ambiente Visual C++?
10. Che differenza c’è tra Visual C++ e C++?
11. A che cosa serve la casella dei controlli?
12. Che cos’è un Combo Box?
13. Che cos’è un Radio Button?
14. Quali sono i principali eventi provenienti dal movimento del mouse?
15. Quali sono i principali eventi provenienti dalle finestre?
16. A che cosa serve l’area di lavoro del Visual C++?
17. Quali eventi possono essere legati a un Edit Box?
18. Che differenza c’è tra un Radio Button e un Check Box?
19. Qual è il prefisso di ogni funzione di gestione di un evento, proprio del Visual C++?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Elencare almeno sei controlli diversi.


2. Elencare almeno otto tipi diversi di eventi.
3. Inserire un controllo a scelta nell’area di lavoro di Visual C++.
4. Creare un nuovo progetto dandogli un nome e scegliendo SDI come modello di interfaccia predefinita.
5. Creare un nuovo progetto che contenga un Dialog Box.
6. Aggiungere un pulsante a un Dialog Box e individuare i possibili eventi che a tale pulsante
possono essere legati.
7. Creare un’interfaccia utente grafica contenente un pulsante, due Check Box e un Edit Box.
8. Modificare il nome e le dimensioni di un Edit Box precedentemente inserito.

Unità didattica D1: Interfaccia grafica ed eventi in Visual C++ 217


p218-238_CeC_Unità D2 26-09-2007 16:58 Pagina 218

Unità didattica

D2 Creare applicazioni
visuali in Visual C++

Prerequisiti ● Caratteristiche di base del linguaggio C++


● Conoscenze e competenze di base necessarie a scrivere semplici programmi C++
● Concetti di base per utilizzare in C++ classi, variabili membro e funzioni membro
● Conoscenza dell’interfaccia tipica di un’applicazione Windows

Obiettivi ● Saper scrivere semplici applicazioni in Visual C++ che utilizzino la GUI
● Saper inserire e opportunamente collegare a un progetto le classi definite dall’utente

Conoscenze ● Conoscere come utilizzare AppWizard


da apprendere ● Conoscere come utilizzare ClassWizard
● Conoscere come inserire controlli in un Dialog Box
● Conoscere come impostare applicazioni basate su menu
● Conoscere come legare eventi ai controlli

Competenze ● Saper creare semplici interfacce grafiche in Visual C++ disponendo


da acquisire opportunamente gli oggetti GUI
● Saper collegare eventi a oggetti GUI di un’interfaccia utente grafica in Visual C++
● Saper scrivere semplici applicazioni con interfaccia grafica e menu
utilizzando i Wizard dell’ambiente Visual C++
● Saper inserire e opportunamente collegare a un progetto le classi definite
dall’utente in ambiente Visual C++
● Saper creare semplici applicazioni che si interfaccino con un database in ambiente
Visual C++

1 Creare la prima applicazione visuale:


uso di pulsanti e caselle di testo
È infine giunto il momento di creare semplici applicazioni Visual C++ inserendo op-
portunamente e in modo visuale gli elementi GUI visti nell’Unità D1. Costruiremo
quindi applicazioni che utilizzino oggetti GUI sempre diversi, vedendo in questo mo-
do esempi di utilizzo dei principali oggetti GUI disponibili in ambiente Visual C++.
Iniziamo con una prima applicazione il cui obiettivo è
di creare un’interfaccia composta da un pulsante e una
casella di testo, così come mostrato in figura D2.1.
Il pulsante Incrementa provvederà, a ogni clic del
mouse, a incrementare di 1 il contenuto numerico del-
la casella di testo alla sua destra. Nei paragrafi se- ✦ Figura D2.1
guenti vedremo le singole fasi della progettazione per L’interfaccia grafica della nostra prima
applicazione di esempio.
arrivare a tale risultato finale.

218 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 219

1.1 Creare un nuovo progetto


Per poter dare inizio a una nuova applicazione è innanzitutto necessario creare un
nuovo progetto. Un progetto viene utilizzato per gestire tutti gli elementi che costi-
tuiscono un programma Visual C++ e che producono un’applicazione Windows-like
(cioè simile a quelle presenti in ambiente Windows).
Operativamente, occorre effettuare i passi illustrati di seguito.
• Selezionare New dal menu File.
Apparirà immediatamente la finestra di dialogo New che consente di selezionare il
tipo di progetto adatto all’applicazione.
• Specificare il tipo di progetto che si desidera creare.
Dalla finestra di dialogo New, cliccando sulla scheda Projects è possibile visualizza-
re un elenco di tutti i tipi di progetto cui si può dar vita. Per questo nostro primo
esempio selezioniamo MFC AppWizard (exe); ciò significa che il progetto utilizzerà
le Microsoft Foundation Class di Windows creando un file eseguibile.
• Assegnare un nome ad un progetto.
Questa fase è obbligatoria, e in questo caso attribuiremo il nome Incrementa.
• Specificare la directory di lavoro.
Sempre sulla scheda Projects della finestra di dialogo New, la casella contrassegna-
ta dall’etichetta Location ci permette di specificare la directory nella quale verran-
no salvati i file del progetto; la collocazione predefinita si basa sul nome del pro-
getto: per esempio, C:\Programmi\Microsoft Visual Studio\Progetti\Incrementa, che
ovviamente può essere modificata, magari con C:\C++ esempi\Incrementa. Si veda
la figura D2.2.
Nome del progetto

Directory di lavoro

Tipo di progetto

Premendo il pulsante OK si dà
inizio alla creazione del progetto,
che è guidata da un Wizard detto
AppWizard (Application Wizard);
✦ Figura D2.2
esso comincerà a creare il codice Videata iniziale
relativo alle scelte fatte dal pro- per un nuovo
progetto.
grammatore.

1.2 Creare la struttura dell’applicazione visuale con AppWizard


Il compito di AppWizard è di creare la struttura principale di un programma che
va poi estesa e ampliata. A tal fine AppWizard consente di specificare il modello di
interfaccia che vogliamo utilizzare così
come visto nell’Unità precedente, quindi uti-
lizza la libreria MFC per generare dei file che
nel loro insieme costituiscono un progetto
Visual C++.
La prima finestra di dialogo AppWizard
permette di scegliere fra tre modelli di inter-
faccia per l’applicazione, come mostrato
nella figura D2.3.
✦ Figura D2.3
Per il progetto Incrementa scegliamo un’in- La scelta del
terfaccia a finestre di dialogo, quindi selezio- modello di
interfaccia.
niamo il pulsante di opzione Dialog based.

Unità didattica D2: Creare applicazioni visuali in Visual C++ 219


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 220

È possibile specificare gli altri parametri passando alle videate successive; ciò av-
viene premendo il pulsante Next. Nel nostro caso, facendo clic direttamente sul pul-
sante Finish, l’AppWizard può creare subito il nuovo progetto.
A questo punto compare la finestra di dialogo New Project Information, utilizzata
per riassumere le impostazioni effettuate. È possibile vedere i nomi delle classi C++
del progetto e i nomi dei file da creare. Nelle Features è anche possibile vedere le fun-
zionalità che verranno fornite da AppWizard. Confermiamo le scelte effettuate clic-
cando su OK in questa finestra di dialogo.
AppWizard ha così completato il suo compito: ora il progetto Incrementa appena
creato è aperto in Visual Studio e consiste in un’applicazione Windows completa e
funzionante.
A questo punto viene visualizzato lo spazio di lavoro, che contiene le tre schede:
ClassView, ResourceView e FileView viste nell’Unità precedente.
Pur non avendo ancora scritto neppure una riga di codice, se clicchiamo sulla prima
voce Incrementa classes della scheda ClassView compaiono alcune voci, tra le quali:
CAboutDlg, CMainFrame, CIncrementaApp, CIncrementaDoc, CIncrementaView. Queste
sono le classi create automaticamente dal sistema in base alle nostre scelte, contenu-
te nel nostro progetto e si riferiscono, rispettivamente: alla classe per la finestra About,
alla classe Finestra principale, alla classe Applicazione, alla classe Documento e alla
classe Vista.
Tutte queste classi sono contenitori per funzioni che hanno un ben preciso signi-
ficato. Ad esempio: la classe Finestra principale contiene tutti i gestori dei menu, del-
la barra degli strumenti, del titolo e di stato; la classe Vista si occupa dell’output gra-
fico della finestra e della stampa, la classe Documento si occupa del salvataggio e ca-
ricamento dei file. Questa metodologia di programmazione si chiama documento-vi-
sta e permette di scrivere programmi molto ordinati, leggibili e di facile correzione.

1.3 La prima compilazione e il primo linking dei file dell’applicazione


Giunti a questo punto è già possibile eseguire il processo di costruzione per pro-
durre il file eseguibile per il progetto. Nel nostro esempio produrremo il file chiama-
to Incrementa.exe, che rappresenterà un’applicazione Windows eseguibile da Esplo-
ra risorse o da qualsiasi altra locazione del nostro computer.
Il primo passo consiste nella compilazione dei singoli file C++ all’interno di un pro-
getto e nel loro collegamento per realizzare il file eseguibile. Occorre selezionare
Build Incrementa.exe, che compare direttamente come voce al menu Build (oppure
premere F7 per utilizzare una scorciatoia).
I messaggi del compilatore e del linking compariranno nella finestra della scheda
Build nella parte inferiore della videata principale dell’ambiente Visual C++
Il file Incrementa.exe è situato nella sottodirectory \Debug della directory di pro-
getto \Incrementa che contiene anche i file oggetto da cui l’eseguibile è composto.

1.4 La prima esecuzione dell’applicazione


Per eseguire l’applicazione bisogna selezionare dal menu Build la voce Execute
Incrementa.exe oppure premere la combinazione di tasti Ctrl+F5 oppure, ancora, l’i-
cona con il punto esclamativo presente nel BuildMiniBar (si veda l’Unità preceden-
te). Apparirà, così, la finestra principale dell’applicazione, che include già alcune
delle funzioni di interfaccia standard che appaiono praticamente in tutti i program-
mi per Windows.
La finestra dell’applicazione ha, infatti, due pulsanti, OK e Annulla, e visualizza del
testo. Ha anche una barra del titolo con associata un’icona, il nome dell’applicazio-
ne e un pulsante di chiusura (figura D2.4).
La barra del titolo dispone anche di un menu di sistema e può essere utilizzata per
trascinare la finestra sullo schermo facendo clic su di essa e tenendo premuto il ta-
sto sinistro del mouse.

220 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 221

Facendo clic sull’immagine MFC nell’angolo


superiore sinistro della finestra di dialogo Incre-
menta e selezionando About Incrementa dal me-
nu di sistema che appare, vedremo che il pro-
gramma visualizza informazioni sull’applicazio-
ne stessa in una finestra denominata About In-
crementa. Cliccando su OK nella finestra About
Incrementa questa viene chiusa; un analogo di-
scorso vale per la finestra di dialogo Incrementa.

1.5 Modificare l’interfaccia


dell’applicazione
Finestre di dialogo, icone e menu sono deno-
minate risorse. Visual C++ possiede un editor
molto potente per progettare i vari tipi di risor- ✦ Figura D2.4
La prima videata del
se e inserirli nell’applicazione. Dovendo realizzare l’interfaccia grafica mostrata in fi- nostro nuovo progetto.
gura D2.1, occorrerà inserire i seguenti due elementi GUI:
• una casella di testo;
• un pulsante.
Vediamo nei successivi sottoparagrafi, in dettaglio, ognuna delle due operazioni.
Prima di poter aggiungere questi due elementi della GUI, è innanzitutto necessario
aprire il modello di dialogo. Per far questo si può utilizzare la seguente procedura:
1) selezionare la scheda Resource View dello spazio di lavoro del progetto: appare l’e-
lenco delle risorse del progetto;
2) espandere l’elenco delle risorse facendo clic sul segno + sulla sinistra di Incre-
menta Resources ed espandere la cartella Dialog: appaiono i due identificatori di dia-
logo IDD_ABOUTBOX e IDD_INCREMENTA_DIALOG;
3) fare doppio clic sull’identificatore IDD_INCREMENTA_DIALOG: appare così il mo-
dello della finestra di dialogo principale dell’applicazione Incrementa. Nell’editor di ri-
sorse la risorsa di dialogo viene visualizzata come quando si esegue il programma.
A questo punto è possibile modificare il modello di dialogo.
Innanzitutto, occorrerà rimuovere l’etichetta FARE che appare al centro del mo-
dello di dialogo e il pulsante Annulla poiché non fanno parte della nostra applicazio-
ne. Per far ciò, basta cliccare sul testo FARE: disponi i comandi della finestra di dialo-
go qui per far apparire un rettangolo di formattazione attorno al testo, e premere il
tasto Canc sulla tastiera. Così facendo, il controllo viene rimosso. La stessa proce-
dura va applicata al pulsante Annulla e più in generale a qualsiasi altro elemento gra-
fico che vogliamo rimuovere.

1.5.1 Aggiungere una casella di testo


Per aggiungere un nuovo campo testo, occorrerà procedere come indicato di seguito. Edit Box

1) Selezionare il controllo Edit Box dalla casella dei controlli, come mostra-
to nella figura a lato.
2) Spostare il mouse sul modello di dialogo; il puntatore cambierà in una
croce per indicare dove dev’essere collocata la nuova casella di testo. Si
sistema, quindi, la croce nel punto desiderato e si fa clic con il mouse.
Apparirà immediatamente una nuova casella di testo, nella quale sarà
stato inserito il testo di default: Edit.
3) Selezionando il controllo appena creato e premendo il tasto destro del
mouse, si aprirà il menu di contesto, dal quale si selezionerà Properties,
ovvero le proprietà dell’oggetto GUI selezionato, come mostrato nella fi-
gura a lato, in cui si individua l’identificativo di default dato alla casella
di testo: IDC_EDIT1.

Unità didattica D2: Creare applicazioni visuali in Visual C++ 221


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 222

4) Cambiare l’identificativo di default in TXT1, clic-


cando nel campo di testo e digitando il nuovo iden-
tificativo.

1.5.2 Aggiungere un pulsante


I passaggi per aggiungere un nuovo pulsante sono molto semplici: basta selezio-
nare il controllo OK già presente sul modello di dialogo e spostarlo tenendo premu-
to il tasto del mouse, per trascinarlo nel punto desiderato oppure cancellare que-
st’ultimo e aggiungere un nuovo controllo Button scelto dalla casella dei controlli.
Per cambiare l’etichetta del pulsante
occorre selezionarlo con un clic: attorno a
esso deve apparire un rettangolo di format-
tazione. A questo punto, cliccando con il
tasto destro del mouse apparirà la finestra
di dialogo Push Button Properties, come
mostrato nella figura a lato.
Digitiamo Incrementa nell’apposita casella di testo con etichetta Caption e scrivia-
mo poi IDC_Incrementa nella casella di testo con etichetta ID, andando così a sosti-
tuire il valore predefinito IDOK. In tal modo l’identificatore del pulsante diventa più
significativo.
Dobbiamo assicurarci che la finestra di
dialogo Push Button Properties abbia le ca-
selle di controllo Visibile e Tab stop selezio-
nate, e che nelle caselle di testo ID e Caption
compaiano rispettivamente i testi IDC_In-
crementa e Incrementa.
Infine, chiudiamo la finestra di dialogo Push Button Properties. L’etichetta del nuo-
vo pulsante sul modello IDD_Incrementa_DIALOG deve ora indicare Incrementa.

1.6 Gestire l’evento clic sul pulsante


A questo punto dobbiamo legare l’evento “clic sul pulsante Incrementa” a una fun-
zione di gestione di questo evento. Il comportamento che tale funzione deve assu-
mere consiste nell’incrementare di 1 il contenuto numerico della casella di testo. I
passi da seguire sono i seguenti.
1) Aprire il modello di dialogo Incrementa nell’editor di risorse. Per questo è neces-
✦ Figura D2.5 sario seguire i passaggi del paragrafo precedente.
L’MFC ClassWizard.

2) Definire le Member Variables. Bisogna definire


per ogni controllo una variabile detta Member
Variable destinata a memorizzare i valori assunti
dai controlli durante l’esecuzione dell’applica-
zione. Tali variabili sono dette membro poiché in
effetti sono le variabili membro dell’oggetto fine-
stra di dialogo di classe CIncrementaDlg, la classe
creata dal sistema che contiene una variabile
membro per ogni elemento grafico che stiamo
inserendo.
Per inserire le Member Variables ci serviamo
del Wizard delle classi MFC (MFC ClassWizard,
figura D2.5) raggiungibile con il tasto destro del
mouse cliccando nello spazio di lavoro della
nostra interfaccia grafica.

222 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 223

Per ogni controllo si deve cliccare sul pulsante Add Variable per assegnare un no-
me alla variabile, una categoria e un tipo. Nel nostro caso avremo:
• m_ICD_Incrementa per il controllo del pulsante;
• m_TXT1 per il controllo della casella di testo.
Attribuiremo quindi le seguenti categorie e tipi:

NOME ELEMENTO GUI (CONTROLLO) NOME VARIABILE MEMBRO CATEGORIA TIPO

IDC_Incrementa m_IDC_Incrementa Control CButton


TXT1 m_TXT1 Value int

I tipi delle variabili membro sono i tipi primitivi consentiti dal C++ visualizzabili
cliccando sulla casella combinata di nome Variable type.
La casella combinata Category viene ✦ Figura D2.6
utilizzata per selezionare: Value o Con- Le variabili membro
della nostra
trol. Value si utilizza nei casi in cui quel- applicazione.
la variabile deve contenere un valore,
ad esempio una casella di testo che
deve prendere un valore intero, come
nella nostra situazione.
I nomi consentiti per le variabili mem-
bro rispettano lo standard Visual C++; il
sistema propone nomi che cominciano
con i caratteri m_. Il risultato finale sarà
quello mostrato in figura D2.6.

In figura D2.7 sono riassunte alcune proprietà degli oggetti GUI e delle variabili
membro a essi legate.
✦ Figura D2.7

Identificatore ID_Incrementa Identificatore TXT1


Caption Incrementa Align Text Left
Visible Sì Border Sì
Tab_Stop Sì Nome variabile membro m_TXT1
Nome variabile membro m_IDC_Incrementa Categoria variabile membro Value
Categoria variabile membro Control Tipo variabile membro CString
Tipo variabile membro CButton

Unità didattica D2: Creare applicazioni visuali in Visual C++ 223


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 224

3) Associare l’evento al pulsante. Con quest’ultimo passo aggiungiamo la funzione


membro (sempre dell’oggetto finestra di dialogo di classe CIncrementaDlg) che descri-
ve il comportamento del controllo pulsante. Per far questo dobbiamo:
• fare clic con il tasto destro del mouse sul pulsante Incrementa nel modello di dialo-
go, e selezionare dal menu di contesto l’opzione Events; cosi facendo verrà aperta la
finestra di dialogo New Windows Message and Event Handlers per la classe di dialogo;
• selezionare BN_CLICKED nell’elenco New Windows Messages/Events;
• fare clic sul pulsante Add and Edit. Apparirà la finestra di dialogo Add Member Func-
tion, nella quale viene denominata la funzione del programma che verrà eseguita
ogni volta che la finestra di dialogo riceverà il messaggio BN_CLICKED per il pul-
sante Incrementa;
• fare clic su OK per accettare il nome predefinito OnIncrementa( ). Il corpo della nuo-
va funzione appare nella finestra dell’editor. La funzione viene aggiunta come mem-
bro della classe CIncrementaDlg, che è stata creata e denominata automaticamente
quando AppWizard ha dato vita al progetto. La funzione così com’è non fa nulla: è
necessario aggiungere del codice al posto del commento in inglese inserito auto-
maticamente dal sistema:

void CIncrementaDlg::OnIncrementa( )
{
// TODO: Add your control notification handler code here
}

• Modificare la funzione OnIncrementa( ) in modo che contenga il seguente codice:

void CIncrementaDlg::OnIncrementa( )
{
UpdateData(true); /* aggiornare le variabili membro con i valori inseriti
dall’utente */
m_TXT1 = m_TXT1 +1;
UpdateData(false); /* aggiornare la finestra dell’applicazione
con i nuovi valori delle variabili membro */
}

Nel codice precedente notiamo che:


• per incrementare il contenuto della casella di testo TXT1 basterà assegnare il nuo-
vo valore utilizzando la variabile membro associata, in questo caso m_TXT1;
• l’istruzione UpdateData(true) serve per aggiornare le variabili membro con i valori
inseriti dall’utente. Invece, l’istruzione UpdateData(false) serve per aggiornare la fi-
nestra dell’applicazione con i nuovi valori delle variabili membro.
Analogamente alle variabili membro, la funzione OnIncrementa( ) altro non è che
una funzione membro dell’oggetto finestra di dialogo di classe CIncrementaDlg crea-
ta automaticamente dal sistema. Pertanto, quel che fa automaticamente il sistema è
generare una classe MFC di nome CIncrementaDlg relativa alla finestra di dialogo
principale e aggiungere a essa una variabile membro per ogni controllo e una fun-
zione membro per ogni evento che si vuole legare ai controlli.

1.7 Verifica dell’applicazione appena modificata


Per verificare che le modifiche apportate all’applicazione abbiano sortito gli effet-
ti desiderati, è necessario ricompilare i file e rigenerare l’eseguibile Incrementa.exe.
Occorrerà, quindi, selezionare Build Incrementa.exe (viene così ricostruito il file ese-
guibile includendo tutte le modifiche apportate); se non ci sono messaggi d’errore,
sarà possibile eseguire l’applicazione premendo Ctrl+F5.

224 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 225

In questo caso, cliccando sul pulsante Incrementa viene incrementato il valore nu-
merico (in quanto la variabile membro associata è stata definita come variabile mem-
bro di tipo int) presente nella casella di testo TXT1.

1.8 Salvare e chiudere il progetto


Visual C++ ha il grosso pregio di salvare automaticamente qualunque modifica ven-
ga apportata prima di costruire il progetto. In ogni caso è sempre possibile salvare
un file specifico premendo il pulsante Save oppure tutti i file cliccando su SaveAll.
Per salvare un progetto non è necessario compiere alcuna operazione particolare:
quando si è finito di lavorare con esso è sufficiente chiuderlo selezionando la voce
Close Workspace dal menu File oppure uscendo semplicemente da Visual Studio.

2 Utilizziamo le finestre di dialogo predefinite


In un’applicazione Windows l’interazione con l’utente spesso si manifesta attra-
verso finestre di dialogo predefinite. Tali finestre servono per trasmettere semplici
messaggi di errore oppure per segnalare situazioni che possono verificarsi o che si
stanno già verificando. Le finestre di dialogo predefinite possono essere create at-
traverso la funzione MessageBox, la cui sintassi è:

MessageBox(<Messaggio>, <Titolo> [,<ListaProprietà>])

dove:
• <Messaggio> indica il messaggio che verrà visualizzato all’interno della finestra di
dialogo;
• <Titolo> indica il titolo che comparirà sulla barra del titolo della finestra di dialogo;
• <ListaProprietà> indica una o più proprietà (opzionali) che possono essere specifi-
cate per determinare l’aspetto grafico dei pulsanti aggiuntivi o delle icone presenti
nella finestra di dialogo. I valori di tali proprietà iniziano tutti per MB_. Per default,
l’unica proprietà obbligatoria è MB_OK che indica la visualizzazione del pulsante di
OK per terminare la visualizzazione del messaggio.
I più importanti valori di tali proprietà sono riassunti in tabella D2.1. Per aggiun-
gere più valori si utilizza come carattere separatore “|”.
✦ Tabella D2.1
VALORE TIPO DESCRIZIONE

MB_OK Pulsante Visualizza il pulsante predefinito OK


MB_OKCANCEL Pulsanti Visualizza i pulsanti predefiniti OK e Annulla
MB_YESNO Pulsanti Visualizza i pulsanti predefiniti SI e NO
MB_YESNOCANCEL Pulsanti Visualizza i pulsanti predefiniti SI, NO, Annulla
MB_RETRYCANCEL Pulsanti Visualizza i pulsanti Riprova e Annulla
MB_ABORTRETRYIGNORE Pulsanti Visualizza i pulsanti Termina, Riprova e Ignora
MB_ICONSTOP Icona Visualizza l’icona di messaggio critico
MB_ICONESCLAMATION Icona Visualizza l’icona di messaggio di avvertimento
MB_ICONQUESTION Icona Visualizza l’icona di messaggio interrogativo
MB_ICONINFORMATION Icona Visualizza l’icona di messaggio di informazione
MB_APPLMODAL Modalità di scelta Occorre obbligatoriamente rispondere al messaggio
obbligatoria per proseguire l’elaborazione
di applicazione
MB_SYSTEMMODAL Modalità di scelta Occorre obbligatoriamente rispondere al messaggio
obbligatoria di sistema per proseguire con qualsiasi altra attività del sistema
MB_DEFBUTTON1 Imposta il pulsante Imposta il focus sul primo, secondo o terzo pulsante
MB_DEFBUTTON2 con focus
MB_DEFBUTTON3

Unità didattica D2: Creare applicazioni visuali in Visual C++ 225


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 226

Creiamo ora un’applicazione che presenta un pulsante Visualizza il quale, se pre-


muto, crea un MessageBox con la visualizzazione di un messaggio di pericolo, come
mostrato in figura D2.8, dove sono anche rappresentate per alcuni oggetti grafici le
proprietà e le variabili membro con le loro caratteristiche più importanti.
✦ Figura D2.8
Messaggio Possibile perdita dati
Titolo Attenzione
Pulsanti OK, Annulla
Icone Messaggio di avvertimento

Identificatore ID_Visualizza
Caption Visualizza
Visible Sì
Tab_Stop Sì
Nome variabile membro m_Visualizza
Categoria variabile membro Control
Tipo variabile membro CButton

I passi per creare il pulsante Visualizza, come già visto nel paragrafo precedente,
sono:
• impostare le proprietà, in particolare la Caption e l’identificatore;
• legare la funzione membro OnVisualizza( ) all’evento BN_CLICKED.
Il codice relativo a tale funzione membro è il seguente:

void CVisualizzaDlg::OnVisualizza( )
{
MessageBox("Possibile perdita dati", "Attenzione",
MB_ICONEXCLAMATION | MB_OKCANCEL | MB_APPLMODAL);
}

L’effetto finale è quindi l’ottenimento del messaggio “Possibile perdita dati” all’in-
terno di una finestra di dialogo dal titolo Attenzione, con un’icona rappresentante un
messaggio di avvertimento e la presenza di due pulsanti OK e Annulla.

3 Utilizzare GUI e classi definite dall’utente


Finora abbiamo analizzato come costruire un’applicazione visuale utilizzando
Wizard e autocomposizioni tipiche dell’ambiente Visual C++. Durante la costruzione
di un progetto, le uniche classi, le uniche variabili e funzioni membro utilizzate erano
quelle generate automaticamente dal sistema. Vediamo ora come inserire nell’ambito
di un’applicazione visuale classi create dall’utente durante un’analisi e una progetta-
zione a oggetti, che vadano ad aggiungersi e
a integrare le classi generate dal Visual C++.
La nostra nuova applicazione di esempio
consisterà nel creare un’applicazione visua-
le che, sfruttando la classe Punto già analiz-
zata nel Modulo C, permetta l’inserimento
delle coordinate di due punti del piano car-
tesiano e calcoli la distanza tra di essi. La
GUI per questo esempio si presenterà come
mostrato a lato.

226 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 227

Per creare questa GUI si proce-


derà così come visto nel para-
grafo precedente. Le variabili
membro relative agli oggetti grafi-
ci sono raffigurate a lato.

Abbiamo inserito quattro varia-


bili di tipo float collegate ai quat-
tro campi di testo per l’inserimen-
to delle coordinate, e una variabi-
le di tipo double collegata alla ca-
sella di testo che conterrà il valo-
re della distanza tra i due punti.

3.1 Includere classi definite dall’utente


L’operazione che dobbiamo ora compiere è di inserire nel progetto il file Punto-
DelPiano.h contenente la definizione della classe Punto vista nell’Unità C1 e comple-
tata nell’esercizio svolto di fine Unità C2. Tale file header, essendo già stato scritto e
quindi già disponibile, va inserito nel progetto nel seguente modo.
1) Selezioniamo dal menu Project la voce Add to Project e, dal suo sottomenu, il co-
mando Files.
2) Nella finestra di dialogo che si apre (Insert Files into Project) occorrerà seleziona-
re il percorso e il nome del file che deve essere aggiunto al progetto: nel nostro caso
è il file PuntoDelPiano.h.
3) Una volta premuto il pulsante OK, tale file comparirà nell’elenco FileView.
4) L’ultimo passo da compiere è l’inserimento della seguente riga di include:

#include "PuntoDelPiano.h"

nel file Distanza.h contenente tutti gli include del progetto Distanza. A questo punto
il file è stato incluso completamente nel progetto Distanza e la classe Punto in esso
contenuta può essere utilizzata.

3.2 Associamo l’evento al pulsante


Per completare la nostra applicazione associamo l’evento “clic del mouse” al pul-
sante Distanza. Procediamo allo stesso modo visto nell’esempio precedente, accet-
tando questa volta il nome predefinito OnDistanza per la funzione membro di classe
CDistanzaDlg, legata all’evento “clic del mouse”. Nel corpo di questa nuova funzione
aggiungeremo il seguente codice (vedi sito: CPP.D2.01.C):

void CDistanzaDlg::OnDistanza( )
{
double D=0;
UpdateData(true);
Punto *pP1 = new Punto(m_IDC_X1, m_IDC_Y1);
// crea un nuovo punto con coordinate X1 e Y1
Punto *pP2 = new Punto(m_IDC_X2, m_IDC_Y2);
// crea un nuovo punto con coordinate X2 e Y2
D= pP1–>Distanza(*pP2); // calcola la distanza tra due punti
m_TXT_DISTANZA= D;
UpdateData(false);
}

Unità didattica D2: Creare applicazioni visuali in Visual C++ 227


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 228

Dal codice precedente notiamo che:


• vengono creati due nuovi punti utilizzando i valori delle variabili membro legate ai
campi di testo IDC_X1, IDC_Y1, IDC_X2, IDC_Y2;
• viene richiamato il metodo Distanza( ) della classe Punto e il suo risultato viene in-
serito nella variabile membro m_TXT_DISTANZA.

4 Utilizzare caselle di controllo


e pulsanti di opzione
Le caselle di controllo (Check Box) e i pulsanti di opzione (Radio Button) sono mol-
to utilizzati nelle applicazioni in ambiente Windows. Vediamo l’esempio di un loro uti-
lizzo. L’obiettivo è di realizzare un’applicazione che, dato un numero intero inserito
in input, verifichi se è multiplo di 3, multiplo di 5, pari o dispari.
La GUI per questo esempio si presenterà come in figura D2.9, dove sono anche rap-
presentate le variabili membro con le loro caratteristiche più importanti.

✦ Figura D2.9
Identificatore Radio1 Identificatore Radio2
Caption Pari Caption Dispari
Group Sì Group No
Tab_Stop Sì Tab_Stop Sì
Nome variabile membro m_Radio Nome variabile membro m_Radio
Categoria variabile membro Value Categoria variabile membro Value
Tipo variabile membro Int Tipo variabile membro Int

Identificatore Edit1
Nome variabile membro m_ Edit1
Categoria variabile membro Value
Tipo variabile membro Int

Identificatore Check1 Identificatore Check1

Caption Multiplo di 3 Caption Multiplo di 5

Nome variabile membro m_Check1 Nome variabile membro m_Check2

Categoria variabile membro Value Categoria variabile membro Value

Tipo variabile membro Bool Tipo variabile membro Bool

Per quanto riguarda le caselle di controllo, viene assegnata loro una variabile mem-
bro per ogni casella, e possono essere selezionate o deselezionate impostando a true
o a false il valore della variabile membro associata (che è di tipo booleano).
Ai pulsanti di opzione, invece, viene associata un’unica variabile membro; per poter
utilizzare tali pulsanti in modo esclusivo li si deve raggruppare. Per creare un gruppo
di radio button occorre assegnare loro un ordine di tabulazione a partire dal primo
controllo del gruppo. Per far questo si seleziona dal menu Layout la scelta TabOrder :
si vedranno apparire nella finestra di dialogo i numeri indicanti l’ordine di tabulazio-
ne. Al primo pulsante del gruppo (quello che presenta il numero più basso) deve esse-
re impostata la proprietà Group, selezionabile dal menu di contesto Properties. Per gli
altri pulsanti tale proprietà deve essere deselezionata.

228 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 229

In questo modo si associa un’unica variabile membro al primo Radio Button se-
condo l’ordine di tabulazione. Tale variabile assume valore numerico 0 se si selezio-
na il primo Radio Button, valore numerico 1 per il secondo e così via. Assume infine
il valore –1 se nessun Radio Button è stato selezionato.
Ritornando al nostro esempio, dobbiamo ora legare all’interfaccia grafica l’evento
relativo all’immissione dell’input da parte dell’utente. Per fare questo selezioniamo
la casella di testo e scegliamo Events dal menu di contesto visualizzato tramite clic
con il tasto destro del mouse. Tra gli eventi disponibili per la casella di testo sce-
gliamo: EN_UPDATE che come sappiamo indica l’evento relativo alla modifica di una
casella di testo. Scelto l’evento, inseriamo nello scheletro della funzione membro
proposta dal sistema il seguente codice (vedi sito: CPP.D2.02.C), che stabilisce se il
numero inserito sia divisibile per 3, per 5 e se sia pari o dispari.

void CParidispariDlg::OnUpdateEdit1()
{
UpdateData(true);
if (m_Edit1 !=0)
{
if((m_Edit1 % 3) ==0) // verifica se il numero è divisibile per 3
m_Check1=true; // seleziona il primo Check Box
else
m_Check1=false;

if((m_Edit1 % 5) ==0) // verifica se il numero è divisibile per 5


m_Check2=true; // seleziona il secondo Check Box
else
m_Check2=false; // deseleziona il secondo Check Box

if((m_Edit1 %2 )==0) // verifica se il numero è pari


m_Radio = 0; // seleziona il primo Radio Button (Radio1)
else
m_Radio = 1; // seleziona il secondo Radio Button (Radio2)
}
UpdateData(false);
}

5 Finestre definite dal programmatore


e caselle combinate
Finora abbiamo esaminato applicazioni che si basavano sull’utilizzo di una sin-
gola finestra vista come un oggetto di classe CDialog (una sola finestra di dialogo),
o applicazioni che facevano uso di piccole finestre predefinite: i MessageBox. Le
applicazioni di uso comune in ambiente Windows, però, spesso necessitano di
un’interazione attraverso più finestre definite dal programmatore.
Nell’esempio che segue l’obiettivo è di realizzare un’applicazione che apra una o
l’altra finestra tra due, a seconda della scelta effettuata in una casella combinata o
Combo Box (anch’essa già vista nell’Unità D1). La GUI di questa applicazione è
quindi formata da una finestra principale che contiene un Combo Box con due pos-
sibili scelte: “prima finestra”, “seconda finestra”, come mostrato in figura D2.10,
dove sono anche rappresentate le variabili membro con le loro caratteristiche più
importanti.
È importante ricordare che quando si aggiunge un Combo Box bisogna lasciare
un’area verticale sufficiente a mostrare le varie opzioni quando si fa scendere la
tendina.

Unità didattica D2: Creare applicazioni visuali in Visual C++ 229


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 230

✦ Figura D2.10

Identificatore Combo1
Data Finestra1
Finestra2
Sort Sì Identificatore Button1
Vertical Scroll Sì Caption Apri
Nome variabile membro m_Combo1 Nome variabile membro m_Button1
Categoria variabile membro Control Categoria variabile membro Control
Tipo variabile membro CComboBox Tipo variabile membro CButton

Si noti la proprietà Data,


che viene impostata sem-
pre attraverso la finestra di
dialogo Properties, che per i
combo box assume la forma
a lato.
Ogni voce, per risultare
inserita, deve terminare
con Ctrl+Invio.
Per selezionare la scelta attualmente impostata in un Combo Box si utilizza la fun-
zione:
GetCurSel()

Analogamente, per impostare in un Combo Box una particolare scelta si utilizza il


metodo:
SetCurSel(<NumeroScelta>)

dove <NumeroScelta> è un intero che rappresenta il numero d’ordine delle scelte e


parte da 0.
Occorre ora creare le due nuove finestre (due nuovi Dialog Box). Per fare questo
selezioneremo dal menu Insert la voce Resource e sceglieremo Dialog, con un clic di
conferma sul pulsante New. Ovviamente ripeteremo questa operazione per due volte.
È necessario ora dare un nome alle due nuove finestre ricorrendo ancora al menu
Properties.
Le due finestre si presenteranno come illustrato nella figura seguente:

Per poterci riferire alle due nuove finestre richiamandole dalla finestra principale
dobbiamo innanzitutto creare due nuove classi, una per ogni finestra: per far questo
si seleziona una finestra alla volta e si utilizza ClassWizard. Il sistema riconosce la vo-
lontà di attribuire una nuova classe a ogni nuovo Dialog Box selezionato; basterà
quindi confermare quanto proposto.
Ritornando ora sulla finestra principale, occorrerà associare al pulsante Button1
l’evento relativo al clic del mouse e aggiungere al gestore che si sta creando il se-
guente codice (vedi sito: CPP.D2.03.C):

230 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 231

void CFinestreDlg::OnButton1( )
{
CFinestra1 F1;
CFinestra2 F2;

if (m_Combo1.GetCurSel( ) == 0)
F1.DoModal( );
else
F2.DoModal( );
}

La funzione DoModal( ) permette l’interazione dell’utente con la finestra di dialo-


go. In questo caso si creano due nuovi oggetti finestra di nome F1 e F2 di classe CFi-
nestra1 e CFinestra2. Si applica quindi la funzione DoModal( ) a tali oggetti.
Infine, occorre inserire le seguenti direttive di inclusione nel file principale (quello
della classe relativa alla finestra principale), nel nostro caso FinestreDlg.cpp:

#include "Finestra1.h"
#include "Finestra2.h"

6 Finestre di uso comune


Oltre alle semplici finestre derivate direttamente dalla classe CDialog, in Visual C++
le finestre sono anche rappresentate dalla classe CCommonDialog (finestre di uso co-
mune), anch’essa sottoclasse di CDialog.
La classe CCommonDialog si presenta graficamente come le classiche applicazioni
Windows, ovvero con un menu principale contenente le tipiche voci: File, Modifica,
Visualizza ecc., che permettono di salvare un file, modificare il testo, impostare la pa-
gina per la stampa ecc.
Per realizzare tali funzionalità Visual C++ utilizza soprattutto le seguenti classi, che
offrono sempre le interfacce a finestra tipiche delle applicazioni Windows:

CLASSE DESCRIZIONE

CFileDialog Apertura e salvataggio di un file


CColorDialog Impostazione del colore
CFontDialog Impostazione del font di un carattere
CPageSetupDialog Impostazione del formato della pagina di stampa
CPrintDialog Impostazioni per la stampa
COleDialog Impostazione e gestione oggetti OLE

Nel prossimo paragrafo vedremo come costruire un oggetto di classe CCommonDia-


log (una finestra con una barra di menu a livello principale) che utilizzi altri oggetti
finestra ma di classe CDialog.

7 Lavoriamo con i menu


I menu sono tra gli elementi di un’interfaccia utente più semplici e intuitivi da uti-
lizzare anche da parte dei meno esperti, e sono inoltre un ottimo strumento per ac-
cedere alla maggioranza delle funzioni dei programmi. (Anni fa, in ambiente DOS e
Linux, i menu non esistevano: si era costretti a imparare a memoria molte sequenze
di tasti per accedere alle varie funzioni che l’applicativo metteva a disposizione.)

Unità didattica D2: Creare applicazioni visuali in Visual C++ 231


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 232

I menu sono elementi a struttura gerarchica. Ciò sta a significare che man mano
che si scende di livello nei sottomenu, si accede a funzioni più specifiche del menu
di livello superiore.
I menu si suddividono in due categorie principali: quelli a discesa o top-down e
quelli a comparsa o pop-up. I primi sono quelli standard che compaiono sotto la bar-
ra del titolo, i secondi appaiono di solito per visualizzare i menu di contesto, cioè i
menu associati alla pressione del tasto destro del mouse. Per il resto le due catego-
rie sono perfettamente identiche, tranne semmai per il fatto che per i menu pop-up
è possibile scegliere il punto in cui visualizzarli.
Le MFC forniscono molte funzioni per trattare i menu; tali funzioni coprono anche
le esigenze più particolari, come la visualizzazione di bitmap al posto delle voci te-
stuali oppure la visualizzazione di testo assieme alle bitmap, e via dicendo.
Per la creazione dei menu è possibile usare vari approcci; il più semplice è sicura-
mente quello di utilizzare il Wizard, che è anche quello utilizzato nella maggioranza
delle situazioni. Seguiremo i passi dell’autocomposizione per impostare il menu prin-
cipale secondo le nostre esigenze.
Un menu particolare è quello di sistema, ed è quello che appare in tutte le appli-
cazioni Windows quando si fa clic col tasto sinistro del mouse sull’icona dell’appli-
cazione della barra del titolo o quando con il tasto destro si clicca su tale barra o, an-
cora, quando si preme Alt+Barra spaziatrice. Tale menu contiene le voci per sposta-
re, ridimensionare, chiudere, ingrandire, ridurre a icona e ripristinare l’applicazione.
Tutto ciò è fornito di default da Windows.
Le voci di menu presenti sotto la barra del titolo sono dette voci di menu princi-
pali o di livello superiore; le voci che compaiono selezionandole sono dette voci di
menu di livello inferiore, e possono contenere a loro volta altre voci di menu di li-
vello ancora inferiore. Tutte insieme, possono formare una struttura nidificata a più
livelli; naturalmente conviene non esagerare e cercare di non superare i due livelli di
nidificazione.
Generalmente, nelle applicazioni Windows le voci del menu principale sono rag-
gruppate per funzionalità simili, ad esempio: File, Modifica, Visualizza…
Ogni voce, inoltre, per consentire un accesso più rapido può contenere delle scor-
ciatoie (shortcut) che si presentano sotto forma di lettere sottolineate da premere as-
sieme al tasto Alt oppure sotto forma di combinazioni di tasti (ad esempio, Alt+F5,
Ctrl+Y ecc.).

7.1 Creiamo un’applicazione con menu e finestre


Nell’esempio che segue l’obiettivo è di realizzare un’applicazione che si basi su un
menu principale che possieda una voce Apri a questo livello; a sua volta, tale voce
deve essere composta dai sottomenu Finestra1 e Finestra2. Le finestre sono simili a
quelle dell’esempio del paragrafo 5, cioè appena compariranno visualizzeranno al-
l’interno il loro nome.
La GUI di questa applicazione è mostrata nella figura D2.11.
Innanzitutto, quando creiamo il nuovo progetto scegliamo come modello di appli-
✦ Figura D2.11
cazione quello basato su interfaccia a singolo documento SDI (Single Document In-
terface). Andando avanti con l’autocomposizione,
poiché vogliamo un’applicazione con un menu molto
semplice e priva di toolbar e delle voci File, Modifica,
Visualizza, opereremo nel seguente modo:
• al passo 4 dell’autocomposizione deselezioniamo:
Docking ToolBar, Printing e Print preview;
• selezioniamo Menu dalla scheda delle risorse e can-
celliamo le singole voci principali File, Modifica, Vi-
sualizza, selezionandole con il mouse e premendo
il tasto Canc.

232 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 233

Per aggiungere una nuova voce a livello principa-


le andiamo nella scheda Risorse e dalla sottoscheda
Menu selezioniamo tale menu principale, che appe-
na creato è chiamato IDR_MAINFRAME.
Un rettangolo tratteggiato indica la posizione in
cui inserire una nuova voce, così come mostrato a
lato.
Facendo doppio clic con il mouse su tale rettan-
golo si apre la finestra di dialogo Menu Item Proper-
ties, nella quale dobbiamo inserire il nome della
nuova voce (nella casella di testo Caption).
Scriviamo dentro alla casella di testo Caption la voce F&inestre. Il carattere & è uti-
lizzato per creare una scorciatoia sulla lettera seguente: si potrà dunque aprire il me-
nu Finestre premendo Alt+I.
Una volta confermata la nuova voce Finestre l’am-
biente visuale prospetta l’inserimento delle sotto-
voci di Finestre. Clicchiamo su ognuna delle sotto-
voci prospettate per editarne le proprietà; tra que-
ste, le più importanti sono ID, Caption e Prompt.
• ID è l’identificatore che contraddistingue la voce
di menu: il suo nome deve essere diverso da ogni
✦ Figura D2.12
altro nel progetto al fine di evitare strani conflitti;
• Caption rappresenta il vero e proprio testo della voce di menu. È possibile anche
specificare il tasto scorciatoia, cioè il tasto che richiama la voce una volta che si è
aperto il menu; questo è da non confondere con la combinazione di tasti (accele-
ratore da tastiera). Il tasto da premere è contraddistinto dal simbolo di sottolinea-
tura sotto al carattere in questione per la scorciatoia. In un menu i simboli scorcia-
toia devono essere univoci, per evitare conflitti imprevedibili. Vale a dire: se vi so-
no ad esempio le due voci File e Finestra, non si deve specificare per entrambe co-
me carattere scorciatoia la “F”, ma devono essere scelti due caratteri diversi.
• Talvolta in questo campo c’è anche il simbolo per ricordare l’acceleratore di tastie-
ra; lo si può riconoscere dal fatto che dopo la voce c’è il simbolo \t seguito dall’ac-
celeratore. Nel nostro caso, per la voce Finestra1 possiamo inserire come caption il
seguente valore: Finestra&1\tCtrl+1 Questo sta a significare che 1 è il tasto scorcia-
toia e Ctrl+1 l’acceleratore di tastiera. Il simbolo \t serve per allineare a destra il te-
sto che viene dopo: in tal modo le indicazioni di tutti gli acceleratori saranno cor-
rettamente allineate. Si noti che per specificare l’acceleratore di tastiera non basta
mettere il simbolo nel campo Caption: questo è infatti solo un appunto visivo per
l’utente. Per specificarlo occorre cliccare sulla voce Accelerator della scheda delle
risorse e qui indicare per l’ID di interesse la combinazione di tasti che si vuole usa-
re (seguendo le indicazioni dell’help contestuale). Si veda la figura D2.13.

✦ Figura D2.13

Unità didattica D2: Creare applicazioni visuali in Visual C++ 233


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 234

• Prompt: qui va specificato il testo che appare sulla barra di stato quando si passa
il mouse sulla voce di menu. Spesso vengono indicate due stringhe separate dal
simbolo \n. In tal caso la prima stringa è quella che appare sulla barra di stato,
mentre le seconda è quella del tooltip. Il tooltip è la piccola finestrella dei sugge-
rimenti che compare quando ci si sofferma per qualche istante con il mouse su
una voce della barra degli strumenti. In genere la si posiziona vicino al puntatore
del mouse e ha uno sfondo tendente al giallo.
Altre proprietà dei menu sono:

CLASSE DESCRIZIONE

Grayed Rende “grigia” una voce, disattivandola (non è cliccabile)


Inactive Rende non cliccabile una voce ma la lascia del colore originario
Checked Aggiunge un segno di spunta alla voce
Pop-up Inserisce una piccola freccia verso destra per segnalare la presenza di un sottomenu
Separator Inserisce una linea di separazione orizzontale

7.2 Colleghiamo il menu alla funzione di gestione


Come già visto in precedenza, per collegare due nuove finestre all’applicazione
occorrerà:
• creare due nuove classi, una per ogni finestra: per far questo si seleziona una fine-
stra alla volta e si utilizza ClassWizard. Le classi saranno: CFinestra1 e CFinestra2;
• inserire le seguenti direttive di inclusione nel file principale (quello della classe
relativa alla finestra contenente il menu principale), nel nostro caso MainFrm.cpp,
contenente l’implementazione della classe CMainFrame:
#include "Finestra1.h"
#include "Finestra2.h"

Una volta create le classi per queste nuove finestre, opportunamente linkate alla
nostra applicazione, ricorriamo al ClassWizard richiamandolo con il tasto destro
del mouse.
Nel campo Object Ids delle schede Message Maps vedremo l’identificatore delle
nostre voci; nel campo Messages vedremo invece i possibili messaggi che si posso-
no legare ai menu. Se facciamo doppio clic su COMMAND il Wizard ci chiederà il
nome della funzione membro (il gestore di questo evento) che dovrà essere richia-
mata quando si seleziona quella voce di menu.
Aggiungiamo un gestore alla volta relativamente a ogni sottovoce, quella relativa
alla Finestra1 e quella relativa alla Finestra2, cliccando sul pulsante Add Function e
dando loro il nome di OnF1( ) e OnF2( ). Il codice relativo è il seguente (vedi sito:
CPP.D2.04.C).

void CMainFrame::OnF1( )
{
CFinestra1 F1;
F1.DoModal( );
}

void CMainFrame::OnF2( )
{
CFinestra2 F2;
F2.DoModal( );
}

234 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 235

8 Strumenti visuali per interagire


con i database
Visual C++ offre la possibilità di creare un’interfaccia grafica con
impostazioni standard per la visualizzazione dei dati inseriti in un
database. Il nostro obiettivo è di utilizzare tale interfaccia standard,
aggiungendo alcuni elementi che consentano di interagire con un
database per inserire, modificare, eliminare e visualizzare i dati pre-
senti in una tabella.
Riconsideriamo il database Negozio.mdb già visto nell’Unità C5, in
particolare la sua tabella Prodotti. La GUI della nostra applicazione
è mostrata in figura D2.14.
I toolbar buttons messi in evidenza in figura fanno parte dell’in-
terfaccia standard che ci accingiamo a costruire, e ci permettono di
spostarci da un record all’altro della tabella Prodotti. La visualizza-
zione dei campi di ogni record verrà effettuata nei campi di testo
con etichetta Codice, Descrizione e Prezzo, che andremo a posizio- ✦ Figura D2.14
nare nell’interfaccia grafica.
Il Wizard che effettua l’autocomposizione tramite la classe CRecordView utilizza un
oggetto di classe CFormView per connettersi direttamente a un oggetto recordset.
L’oggetto CFormView usa un dialog data exchange (DDX) per spostare i valori dei
campi del record corrente del Recordset ai controlli del form inseriti dal program-
matore e per muoverli, modificati, dai controlli al Recordset. L’oggetto Recordset uti-
lizza infine un dialog field exchange (RFX) per muovere i dati dai suoi campi alle ap-
propriate colonne delle tabelle della sorgente di dati DNS.
La situazione è rappresentata nella figura D2.15.
✦ Figura D2.15

DDX

Cprod Descrp Prezzo


02 faro anteriore 200
14 batteria 300

oggetto Recordset

RFX

Vediamo i passi da seguire.


• Dopo aver scelto di creare un’applicazione di tipo MFC AppWizard (exe), al passo 1
dell’autocomposizione scegliamo un’applicazione basata su singolo documento
(SDI).
• Al passo 2 selezioniamo la scelta che propone di includere il database con una vi-
sta senza il supporto del menu File (Apri, Salva ecc.). Premendo il pulsante Data
Source occorre scegliere:
• – il tipo di database (per quanto visto nell’Unità C5): ODBC, DAO oppure OLE DB;
• – il tipo di Recordset: Snapshot, Dynaset.
Nel nostro caso sceglieremo come sorgente dati ODBC con database di Micro-
soft Access e come tipo di Recordset Dynaset. Un Recordset di tipo Dynaset
(set di record dinamico) può essere costituito da un insieme di dati prelevati
temporaneamente da una o più tabelle, da un’intera tabella o da un suo sot-
toinsieme o, ancora, da una query definita in un database di Access. Il nome

Unità didattica D2: Creare applicazioni visuali in Visual C++ 235


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 236

“set di record dinamico” deriva dal fatto che i dati presenti nel Recordset ver-
ranno aggiornati automaticamente ogni volta che si apportano modifiche ai dati
del database.
• Specifichiamo il percorso del database Negozio.mdb e la tabella scelta, Prodotti.
• Andando avanti fino al passo 6 si può notare come la classe base proposta dal si-
stema sia di tipo CRecordView.
• Una volta terminata l’autocomposizione, nel Dialog Box che appare occorrerà in-
serire i controlli necessari a creare la nostra GUI di figura D2.10.
Questa volta, quando andremo a scegliere il nome delle variabili membro per ogni
controllo le selezioneremo tra quelle proposte, che hanno la forma:
m_pSet –>m_<NomeCampo>

dove m_pSet è la variabile puntatore al Recordset e <NomeCampo> è il nome asso-


ciato a ogni campo della tabella Prodotti.
Nel nostro caso avremo le seguenti variabili membro:
m_pSet –>m_CProd
m_pSet –>m_DescP
m_pSet –>m_Prezzo

Se terminassimo a questo punto la nostra applicazione, avremmo un


programma in cui, tramite i toolbar automaticamente inseriti dall’auto-
composizione, potremmo scorrere tutti i record della tabella Prodotti e
anche modificarli. La nostra idea iniziale era però di personalizzare l’ap-
plicazione con l’aggiunta dei pulsanti: Aggiungi, Modifica, Elimina.
Per far ciò, dopo aver creato i singoli pulsanti associamo loro il se-
guente codice, già visto nell’Unità C5, che ci permette di effettuare le
operazioni di aggiunta di un nuovo record, modifica o eliminazione di
uno già esistente (vedi sito: CPP.D2.05.C).

void CDb3View::OnInserisci( )
{
char Codice[4];
char Descrizione[30];
char Prezzo[5];
::CoInitialize(NULL);
_ConnectionPtr PuntConn;
PuntConn.CreateInstance(__uuidof(Connection));

PuntConn–>Open("DBNegozio","","",NULL);
UpdateData(true);
_bstr_t strCodice = (_bstr_t) m_pSet–>m_CProd;
_bstr_t strDescrizione = (_bstr_t) m_pSet–>m_DescP;
_bstr_t strPrezzo = (_bstr_t) m_pSet–>m_Prezzo;
_bstr_t StrQuery = "INSERT INTO Prodotti (CProd, DescP, Prezzo) VALUES ("+
strCodice + ", ‘" + strDescrizione + "‘," + strPrezzo + ")";
PuntConn–>Execute(StrQuery , NULL, adExecuteNoRecords);
MessageBox("Prodotto inserito", "Attenzione", MB_OK | MB_ICONEXCLAMATION);
PuntConn–>Close( );
CoUninitialize( );
}
void CDb3View::OnModifica( )
{
char Codice[4];

236 Modulo D: Ambiente di programmazione visuale per C++


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 237

char Descrizione[30];
char Prezzo[5];
::CoInitialize(NULL);
_ConnectionPtr PuntConn;
PuntConn.CreateInstance(__uuidof(Connection));
PuntConn–>Open("DBNegozio","","",NULL);
UpdateData(true);
_bstr_t strCodice = (_bstr_t) m_pSet–>m_CProd;
_bstr_t strDescrizione = (_bstr_t) m_pSet–>m_DescP;
_bstr_t strPrezzo = (_bstr_t) m_pSet–>m_Prezzo;
_bstr_t StrQuery = "UPDATE Prodotti SET DescP = ‘" + strDescrizione + "‘,
Prezzo = " + strPrezzo + " WHERE CProd = ‘" + strCodice + "‘";
PuntConn–>Execute(StrQuery, NULL, adExecuteNoRecords);
MessageBox("Prodotto modificato", "Attenzione", MB_OK | MB_ICONEXCLAMATION);
PuntConn–>Close( );
CoUninitialize( );
}
void CDb3View::OnElimina( )
{
char Codice[4];
char Descrizione[30];
char Prezzo[5];
::CoInitialize(NULL);
_ConnectionPtr PuntConn;
PuntConn.CreateInstance(__uuidof(Connection));
PuntConn–>Open("DBNegozio","","",NULL);
UpdateData(true);
_bstr_t strCodice = (_bstr_t) m_pSet–>m_CProd;
_bstr_t strDescrizione = (_bstr_t) m_pSet–>m_DescP;
_bstr_t strPrezzo = (_bstr_t) m_pSet–>m_Prezzo;
_bstr_t StrQuery = "DELETE FROM Prodotti WHERE CProd = ‘" + strCodice + "‘";
PuntConn–>Execute(StrQuery, NULL, adExecuteNoRecords);
MessageBox("Prodotto eliminato", "Attenzione", MB_OK | MB_ICONEXCLAMATION);
PuntConn–>Close( );
CoUninitialize( );
}

Occorre infine effettuare un ultimo passo: l’import per le librerie .dll necessarie
per utilizzare gli oggetti ADO presenti nel codice precedente. Per far questo dob-
biamo inserire la seguente istruzione nel file header legato alla classe derivata da
CRecordView:
#import "C:\Programmi\File Comuni\System\ADO\msado15.dll"\
no_namespace rename("EOF", "EndOfFile")

Ad esempio, se il nostro progetto si chiama DB1 la classe derivata da CRecordView


sarà DB1View (file DB1View.cpp) e il file header relativo sarà: DB1View.h.

Unità didattica D2: Creare applicazioni visuali in Visual C++ 237


p218-238_CeC_Unità D2 26-09-2007 16:59 Pagina 238

Verifica
di fine unità

Verifica delle conoscenze


■ RISPONDI AI SEGUENTI QUESITI

1. Come viene utilizzato AppWizard?


2. Come si fa a compilare e poi a eseguire un’applicazione a interfaccia grafica?
3. Che cosa contiene la scheda Resource View?
4. A che cosa servono le variabili membro? Perché si chiamano così?
5. Come si fa ad associare un evento a un controllo pulsante?
6. Come si fa ad aggiungere al progetto un file contenente classi definite dall’utente?

Verifica delle competenze


■ SVOLGI I SEGUENTI ESERCIZI

1. Modificare l’applicazione di esempio Incrementa 6. Definire la classe NumeroBinario in modo che,


vista in questa Unità aggiungendo un pulsante che utilizzando un’interfaccia utente grafica, si possa:
decrementi il valore presente nella casella di testo • convertire un numero in numero intero;
TXT1. • eseguire l’operazione di AND bit a bit;
2. Creare un’applicazione grafica che simuli una • eseguire l’operazione di OR bit a bit.
calcolatrice. 7. Creare un programma Visual C++ che permetta la
3. Creare un’applicazione grafica che simuli una gestione di una piccola biblioteca consentendo le
calcolatrice utilizzando la classe Calcolatrice operazioni di:
definita dall’utente. • inserimento di un libro;
• cancellazione di un libro;
4. Creare un programma Visual C++, che presa una
• modifica di un libro;
stringa di testo da un TextField, la salvi in un file di
• visualizzazione dei libri presenti nella biblioteca.
caratteri.
8. Utilizzando le classi Punto e Triangolo, creare
un’applicazione Visual C++ che memorizzi in un
Stringa qualsiasi database di triangoli i valori relativi ai vertici di
ogni nuovo triangolo creato tramite interfaccia
utente grafica.

file in formato testo

5. Creare un programma Visual C++ che, a ogni clic


del mouse su un pulsante, prenda una riga da un
file in formato testo e la visualizzi nel TextField.

Stringa qualsiasi

Pulsante
Prendi

file in formato testo

238 Modulo D: Ambiente di programmazione visuale per C++


p239-240_CeC_Indice analitico 26-09-2007 17:00 Pagina 239

Indice analitico

ADO (ActiveX Data Object), 200 dichiarative di precompilazione, 15 late binding, 168
alfabeto, 21 direttive al preprocessore, 15 linkage, 62
allocazione dinamica, 107, 139 distruttore, 126 linker, 17
analisi lessicale, 22 do..while, 48 main, 14
apertura di un file, 191 DSN (Data Source Name), 200 make file, 65
API (Application Programming Dynaset (set di record dinamico), manipolatori, 198
Interface), 201, 208 235 Mascitti Rick, 12
AppWizard (Application Wizard), 219 early binding (v. binding statico) matrice, 76
array, 75 enum, 89 Member Variable, 222
array multidimensionali, 76 ereditare, 159 memoria dinamica, 106
assegnazione multipla, 30 ereditarietà multipla, 157 memory leak, 108
attributi, 115 ereditarietà singola, 157 menu, 231
auto, 64 espressione, 23 messaggi, 127
binding dinamico, 168 estendere, 159 metodi, 115
binding statico, 168 estensione, 159 MFC, 208
blocco, 43 etichetta, 42 Microsoft Foundation Class (MFC),
blocco di codice, 43 evento, 214 208
BNF (Backus-Naur Form), 12 fall-through, 46 modulo software, 62
break, 42, 46, 51-52 file di header, 16 new, 107, 140
campi, 85 file di intestazione, 16 ODBC (Open DataBase Connectivity),
casting esplicito, 37 file di progetto, 65 199
chiusura di un file, 192 flussi sequenziali, 193 oggetto, 114
ciclo infinito, 52 for, 49 OOP (Object Oriented Programming),
cin, 16 friend, 153 114
classe, 114 funzioni, 56 operandi, 23
classe base, 158 funzioni amiche, 153 operator, 178
classe generica, 184 funzioni generiche, 184 operatore “?”, 36
classi amiche, 155 funzioni membro, 115 operatore di complemento a 1, 34
classi derivate, 158 funzioni membro astratte, 166 operatore di input, 41
COM (Component Object Model), 201 funzioni membro di classe, 151 operatore virgola, 36
commento a blocchi, 22 funzioni operatore, 178 operatori, 23, 30
compilatori, 15 funzioni virtuali, 165 operatori di shift, 34
compilazione condizionale, 69 funzioni virtuali pure, 166 operatori sui bit, 33
connessione, 201 garbage, 108 overloading, 168
const, 27 gets, 81 overloading delle funzioni membro,
“contenuto di”, 95 goto, 42 177
continue, 42, 51, 52 GUID (Global Unique Identifier), 202 overloading di operatori, 177
controlli, 211, 212 heap, 106 overriding, 159
conversione tra tipi, 36 IDE (Integrated Development parametro attuale, 60
costante stringa, 26 Environment), 210 parametro formale, 60
costanti, 24 if annidate, 44 parametro reference, 61
costruttore, 122 include, 15 passaggio di parametri, 60
cout, 16 indentazione, 15 polimorfismo, 168
DBMS (Database Management indice, 75 precompilazione, 15
Systems), 199 “indirizzo di”, 95 preprocessore, 15
deallocazione dinamica, 108 information hiding, 65, 118 printf, 81
define, 15 interfaccia del modulo, 65 private, 118
delete, 108, 146 istruzione iterativa universale, 50 procedure, 56
diagrammi delle classi, 120 iterazione postcondizionale, 48 progetto, 219

Indice analitico 239


p239-240_CeC_Indice analitico 26-09-2007 17:00 Pagina 240

programma oggetto, 15 specificatore di “classe di UML (Unified Modelling Language),


programma sorgente, 15 memorizzazione”, 64 120
proprietà, 115 SQL (Structured Query Language), unione, 88
prototipo, 116 199 variabili di classe, 150
prototipo della funzione, 57 stack, 106 variabili globali, 58
public, 118 static, 64, 150 variabili istanza, 115
puntatore, 94 stream di input-output, 189 variabili locali, 58
qualificatore, 28 string.h, 82 variabili membro, 115
RAD (Rapid Application Stroustrup Bjarne, 11 variabili puntate, 94
Development), 210 strutture, 85 variabili static, 70
Recordset, 203 strutture iterative, 47 variabili statiche, 106
register, 64 superclasse, 157 variant, 203
regole di visibilità, 59 switch, 45 vettore, 75
return, 14, 42, 57, 119 tabelle, 87 virtual, 166
risolutore di scope, 59 template, 184 visibilità intermodulo, 62
Ritchie Dennis, 10 terminatore di stringa, 80 void, 15, 56, 119
segnatura (signature), 116 this, 146 warning, 17
shadowing, 59 tipo base, 94 while, 47
sizeof, 35 tipo puntatore, 94 wizard, 219
sottoclasse, 158 tipo stringa, 79

240 Indice analitico