Il linguaggio
C++
Dalla programmazione imperativa
alla programmazione a oggetti
Minerva Italica
p001-004_CeC_ 26-09-2007 16:06 Pagina 2
ISBN 88-298-2328-7
Edizioni:
2 3 4 5 6 7 8 9 10
2004 2005 2006 2007 2008
Presentazione
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
5
p005-008_CeC 26-09-2007 16:07 Pagina 6
6
p005-008_CeC 26-09-2007 16:07 Pagina 7
7
p005-008_CeC 26-09-2007 16:07 Pagina 8
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++
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.
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:
C=0
• 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>
Facoltativo Funzione 1
Facoltativo Funzione 2
...
Facoltativo Funzione N
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;
}
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.
#include <iostream.h>
#include "iostream.h"
Ad esempio, l’istruzione:
cout << "Ciao a tutti";
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.
int main( )
{
cout << "Ciao mondo!";
return 0;
}
che dopo essere stato digitato, compilato e linkato visualizza sul video il seguente ri-
sultato:
File File
Preprocessore Compilatore Linker
sorgente eseguibile
Ese1.cpp Ese1.exe
File
oggetto
Ese1.obj
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...:
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.
Verifica
di fine unità
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";
}
Unità didattica
Elementi lessicali
ed espressioni A2
● Compilare ed eseguire un’applicazione C++ Prerequisiti
● 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
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 */
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;
}
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
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.
6 Gli operandi
Nello studio degli operandi, particolare importanza assumono le variabili, le co-
stanti e il tipo che può essere loro assegnato.
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.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.
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";
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.
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.
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:
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:
Tipi di dato
bool array
int union
float
double
enum
OPERAZIONE OPERATORE
Addizione +
Sottrazione –
Moltiplicazione *
Divisione intera /
Resto della divisione tra interi (modulo) %
Assegnamento =
Incremento ++
Decremento ––
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;
}
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.
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.
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.
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;
• 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 = 11 e X = 11.
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
> maggiore
>= maggiore o uguale
< minore
<= minore o uguale
== uguale
!= diverso
|| 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
X Y X&&Y X||Y !X
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;
}
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 | :
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
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:
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:
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;
}
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-
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>
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.
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).
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.
e avremo come risultato la perdita delle cifre decimali, cioè Y varrà 15. Attenzione: in
questo caso vi sarebbe stato comunque un casting implicito.
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;
}
Verifica
di fine unità
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
X = X + Y;
X = X – Y;
X = X * Y;
X = X / Y;
X = X % Y;
int main( )
{
float a, b; /* questo è un commento
corretto.
a = 100 // questo commento è valido // + 5;
b = 10 // questo commento è valido
return 0;
}
Verifica
di fine unità
#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;
}
#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;
}
!(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:
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++
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> };
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;
}
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>;
Ad esempio:
if (Voto == 10) goto ESITO;
...
ESITO : cout << "Hai raggiunto il massimo dei voti!";
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>;
[}]
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;
}
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.
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;
}
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.
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;
...
}
...
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.
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.
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;
}
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: ";
cin >> N;
}
while (N < 10 || N > 20);
cout << "Il numero inserito e' " << N << endl;
return 0;
}
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;
}
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.
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;
}
Esempio 10
Un esempio dell’istruzione continue (vedi sito: CPP.A3.10.C)
#include <iostream.h>
int main( )
{
int I;
for (I = 0; I < 100; I++)
{
if (I % 2)
continue;
cout << "Il quadrato di " << I << " e' " << I * I << endl;
}
return 0;
}
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
}
...
Verifica
di fine unità
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?
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;
if (k==5) {
cout << "Numero uguale a cinque " << endl;
k++;
}
cout << "Il nuovo valore di k e' " << k << endl;
return 0;
}
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
}
#include <iostream.h>
char c;
int Z = 0;
int main( )
{
for (c = 'z'; c < 'a'; c++)
Z++;
cout << Z << endl;
return 0;
}
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
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.
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.
#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;
}
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:
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.
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;
}
La tabella A4.1 riepiloga le differenze principali tra i due passaggi di parametri ana-
lizzati.
✦ Tabella A4.1
PASSAGGIO PER VALORE RISULTATO
✦ Figura A4.1
La composizione di un Progetto software: programma di Gestione aziendale
progetto software in
più moduli.
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-
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):
il nome dell’eseguibile sarà uguale a quello del primo modulo oggetto, nel nostro ca-
so Principale.exe.
✦ Figura A4.4
Organizzazione di un
modulo software.
Modulo software
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.
✦ Figura A4.5
File Principale.h
#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
LINKER
File di libreria
È 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>
...
if (X <= 40)
{
Y = 30;
Z = 50;
}
...
// file Data.h
typedefunsigned short dd; // giorno
typedefunsigned short mm; // mese
typedefunsigned yy; // anno
void StampaData(Data);
// file Data.cpp
#include "Data.h"
#include <iostream.h>
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.
// 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:
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:
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);
}
#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);
}
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>
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
Verifica
di fine unità
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
di fine unità
Esercizio svolto
Ricevuta in input una data fornita come giorno, mese e anno in forma numerica, stamparla in forma letterale.
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;
}
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
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).
✦ 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
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
#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:
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>]
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);
}
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
{
...
}
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( )
{
int Elementi;
do
{
cout << "Quanti elementi si vogliono inserire? ";
cin >> Elementi;
}
while ((Elementi < 1) || (Elementi > MASSIMO));
return(Elementi);
}
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>[ ];
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
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 ' ':
È 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:
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;
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;
}
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;
}
È doveroso ricordare che la funzione gets( ) può essere utilizzata anche in asso-
ciazione con cout e non necessariamente con la funzione printf.
FUNZIONE DESCRIZIONE
#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( ).
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;
}
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.
Per assegnare a una variabile X la temperatura delle ore 7,00 di sabato scriveremo:
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
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>;
};
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;
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-
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;
};
struct Data
{
unsigned short Giorno, Mese;
unsigned Anno;
};
struct Automobile
{
char Marca[15];
char Modello[20];
char Targa [7];
unsigned Cilindrata;
} Concessionaria[20];
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>;
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
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!
Verifica
di fine unità
Verifica
di fine unità
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>
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];
}
Verifica
di fine unità
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
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:
Memoria centrale
valore
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;
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
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;
• 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:
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
Punt1 200.75 X
Punt2 50.00 Y
Punt3 50 Z
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;
}
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!!
int A = 20;
int *Punt = NULL;
...
...
Punt = &A;
cout << *Punt << endl; // ora e' corretto
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)
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*/
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)
Come esempio di legame tra array di caratteri, puntatori e stringhe, scriviamo due
versioni di un programma che effettua la copia tra due stringhe.
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;
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>
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.
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.
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>
Verifica
di fine unità
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?
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
B2 Allocazione e deallocazione
dinamica della memoria
Prerequisiti ● Puntatori
● Nozioni di base sulla struttura della memoria centrale
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
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.
int A = 20;
int *PuntA = NULL;
PuntA = new int; // si poteva scrivere direttamente: int *PuntA = new int;
*PuntA = A;
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);
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;
}
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:
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
int K = 20;
int *PuntK = NULL;
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;
#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;
}
#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);
}
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.
Verifica
di fine unità
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?
C++ Modulo C
e gli oggetti
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
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.
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 “;”.
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:
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;
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:
La parte implementativa consiste nella definizione del corpo di ogni sua funzione
membro, ciascuna delle quali dovrà essere implementata con la seguente sintassi:
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;
È 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)
class Automobile
{
int Serbatoio; // dichiarazione di variabili membro
char* Marca, Colore;
int Cilindrata, Marcia;
#include "Automobile.h"
Esempio 4
Utilizzo degli specificatori di visibilità
class Automobile
{
public: // dichiarazione di variabili membro visibili all’esterno
char* Marca, Colore;
int Cilindrata, Marcia;
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:
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:
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).
int Rifornimento(int B)
{
int Temp;
Temp = Serbatoio + B;
Serbatoio = Temp;
return(Temp);
}
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 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.
Ad esempio:
Automobile A1 = Automobile( );
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
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(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);
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;
}
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:
~Cerchio( ); // dichiarazione di una funzione membro distruttore per la classe Cerchio
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.
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);
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);
Get<NomeVariabileMembro>([<Parametri>])
Set<NomeVariabileMembro>([<Parametri>])
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;
}
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;
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';
}
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:
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
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;
}
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::Cerchio( )
{
Centro.X = 0;
Centro.Y = 0;
Raggio = 1;
}
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);
}
int main( )
{
Punto P1 = Punto(2,3);
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.
Il codice C++ a oggetti relativo alla classe Vettore è riportato nell’esempio 11.
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( )
{
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
È 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.
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).
Verifica
di fine unità
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.
Verifica
di fine unità
Esercizio svolto
1. Classe Calcolatrice
Definire la classe Calcolatrice che possa svolgere le quattro operazioni elementari.
#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
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
<NomeClasse> *<NomePuntatoreAOggetto>;
Punto P1;
Punto *pP1;
pP1 = &P1;
Nel secondo modo occorre introdurre la sintassi per l’allocazione di un oggetto at-
traverso l’operatore new:
pP1
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:
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);
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:
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:
garbage
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:
void Origine(Punto P)
{
P.X=0;
P.Y=0;
}
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:
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( )
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;
}
Viene passato
l’indirizzo iniziale Indirizzo iniziale di pP0
di pP0
Spazio di indirizzamento
della funzione Origine( )
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
class Automobile
{
public:
int Serbatoio;
private:
int Marcia;
...
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.
Pila
– Nodo* Testa
+ Pila ( ) costruttore
+ void Push (IN N : Nodo) modificatore
+ Nodo* Pop ( ) modificatore
– void Stampa ( ) query
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;
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.
class Nodo
{
...
}; // fine classe Nodo
class Pila
{ Unico file con due classi
...
}; // fine classe Pila
int main ( )
{
...
return 0;
}
File Pila.cpp
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
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.
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>
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>;
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;
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
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;
}
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;
}
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;
}
P1
Istanza Di AggiornaVendite(P1, S1)
PuntoVendita
S1
Istanza Di
SedeCentrale
P2
Istanza Di
PuntoVendita AggiornaVendite(P2, S1)
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
};
friend
B A
Verifica
di fine unità
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?
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
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
superclasse
sottoclasse
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:
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.
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:
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à.
✦ Figura C3.3
VeicoloMotore Una gerarchia con la
classe VeicoloMotore
come superclasse
capostipite.
public public
Automobile Veicolo2Ruote
public public
Motocicletta Ciclomotore
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;
}
~VeicoloMotore( ) // distruttore
{
cout << "Distruttore di VeicoloMotore";
}
};
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
✦ 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
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.
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
Costruttori: Distruttori:
VeicoloMotore( ) ~VeicoloMotore( )
Veicolo2Ruote( ) ~Veicolo2Ruote( )
Motocicletta( ) ~Motocicletta( )
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;
}
};
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.
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>
}
Una classe astratta deve avere come funzione membro almeno una funzione vir-
tuale pura.
public public
Rettangolo Cerchio
Esempio 4
Un esempio di classi astratte
}
void getNome( )
{
return(Nome);
}
virtual float Perimetro( ) = 0; // dichiarazione funzione virtuale pura
virtual float Area( )= 0; // dichiarazione funzione virtuale pura
}; // fine classe FiguraGeometrica
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.
class VeicoloMotore
{
public:
virtual void MostraStato( )
{
cout << "MostraStato( ) di VeicoloMotore";
}
}; // VeicoloMotore
✦ Figura C3.8
Un esempio di binding
dinamico. class VeicoloMotore
{
public:
virtual void MostraStato( )
{
cout << "MostraStato( ) di VeicoloMotore";
}
}; // VeicoloMotore
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:
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;
}
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
Esempio 6
Un altro 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 Veicolo2Ruote : public VeicoloMotore
{
public:
bool SostegnoLaterale;
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
{
...
};
Verifica
di fine unità
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"
Verifica
di fine unità
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.
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
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
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.
che pertanto deve essere definita nella parte dichiarativa dell’interfaccia della classe.
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+
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';
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
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)
}
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:
<Oggetto1> = <Oggetto2>
oppure
<Oggetto1> <SimboloOperatore> = <Oggetto2>
ovvero
<Oggetto1>.operator<SimboloOperatore> = (<Oggetto2>)
ovvero
<NomeClasse> <NomeClasse>:: operator<SimboloOperatore>=
(<NomeClasse> <NomeOggetto>)
{
<Istruzioni>
return(*this)
}
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-
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;
}
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.
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);
}
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.
Verifica
di fine unità
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 (−−).
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 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.
✦ 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
✦ Figura C5.4
Un esempio di stream
di interi con sorgente
e destinazione.
Stream di interi File
di
interi
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.
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:
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);
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:
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( );
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
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;
}
• 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);
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;
• <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.
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;
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;
}
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;
}
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.
#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;
}
DBMS
Tabelle che
(relazionale)
formano la
base di dati
ARCHIVI
che contengono le tabelle
C
DBMS MySQL
Driver ODBC
per MySQL
✦ 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.
Vediamo negli esempi seguenti il codice relativo alla versione C++ di Microsoft ne-
cessario a visualizzare i dati contenuti nella tabella Prodotti.
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));
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.
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.
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));
_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:
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));
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( );
}
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:
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)
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:
Invece, per modificare un record della tabella Prodotti, possiamo ad esempio scri-
vere il seguente frammento di codice:
Verifica
di fine unità
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++
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
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.
BuildMiniBar
Casella
dei controlli
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;
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
✦ 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.
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).
✦ Figura D1.6
✦ 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”.
✦ 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
Verifica
di fine unità
Unità didattica
D2 Creare applicazioni
visuali in Visual C++
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
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.
È 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) 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.
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:
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
void CIncrementaDlg::OnIncrementa( )
{
// TODO: Add your control notification handler code here
}
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 */
}
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.
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
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.
#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.
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);
}
✦ 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
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.
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;
✦ 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
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):
void CFinestreDlg::OnButton1( )
{
CFinestra1 F1;
CFinestra2 F2;
if (m_Combo1.GetCurSel( ) == 0)
F1.DoModal( );
else
F2.DoModal( );
}
#include "Finestra1.h"
#include "Finestra2.h"
CLASSE DESCRIZIONE
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.).
✦ Figura D2.13
• 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
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( );
}
DDX
oggetto Recordset
RFX
“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>
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];
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")
Verifica
di fine unità
Stringa qualsiasi
Pulsante
Prendi
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