Sei sulla pagina 1di 60

Corso Base di Programmazione in C/C++ - Lezione 1

Lezione 1 – Introduzione
In questa prima lezione verranno descritti gli strumenti che si hanno a disposizione durante la scrittura del
codice C++, si prenderà familiarità con i termini di uso comune come Funzioni, Procedure, Compilatore e
Linker nonché la differenza esistente tra i primi due termini e tra gli ultimi due.

Cos’è il C++
Il C++ è un linguaggio di programmazione, quindi un linguaggio per calcolatori o computer, (abbreviato in
seguito come “L. di P.” o semplicemente “linguaggi”) ad alto livello nato dall’evoluzione di un altro linguaggio
di programmazione: il C. Si noti nella precedente frase l’uso dei termini “alto livello”. Il mondo della
programmazione è costellato di molti linguaggi ognuno con delle proprie caratteristiche e come si dice
spesso: “vicinanza alla macchina”. Proprio questa vicinanza permette di fare una suddivisione dei L. di P. in
tre gruppi (linguaggi di esempio sono riportati a fianco):

1. Basso Livello: Assembler.


2. Medio Livello: C, Basic,…
3. Alto Livello: C++, JAVA, Pascal, MS Visual Basic,…

In realtà esiste anche un altro livello composto da un solo linguaggio: il Linguaggio Macchina, composto da
due soli simboli, lo zero (0) e l’uno (1). Ovviamente oggi nessuno è così folle da scrivere un intero
programma come un lunga sequenza di 0 ed 1 ma si ricorre, appunto, ad un L. di P. di più alto livello, dei
linguaggi molto più vicini al linguaggio umano anche se molto semplificati.
Passando da un livello ad un altro più alto vengono introdotti dei concetti che si astraggono dalla macchina e
si avvicinano di più al modo di pensare dell’uomo. Proprio questa astrazione ci introduce nella tipologia dei
Linguaggi ad Oggetti e nella Programmazione Orientata agli Oggetti (OOP – Object Oriented Programming).
In seguito si studierà cos’è effettivamente un Oggetto, per ora è sufficiente sapere che un Oggetto è un
modo di racchiudere ed organizzare pezzi di programma in modo da poterli utilizzare in futuro anche in altri
progetti. Per ora pensiamo ad un Oggetto come ad una scatola chiusa nella quale inserisco delle informazioni
e prendo altre informazioni correlate a quelle inserite.
Il C++ fa ampio utilizzo di questo concetto ed è stato tra i primi L. di P. ad utilizzare questa astrazione ed è
per questo che fa parte dei linguaggi ad alto livello mentre il suo antenato, il C, fa parte del livello medio.

Differenze generali con gli altri linguaggi


Come per le diverse lingue parlate dall’uomo, anche per le macchine sono stati creati diversi linguaggi
ognuno dei quali utilizza un proprio “vocabolario” ed una propria “grammatica”. Un qualunque concetto può
essere scritto in tutti i linguaggi ma ciò che cambia è la forma. Ad esempio, traduciamo nei diversi L. di P.
sopra elencati la frase: Incrementare la variabile X di 2.

MACCHINA ASSEMBLER PASCAL C++

01011010 mov ax, X X = X + 2;


10101011 add ax, 2 X := X + 2;
10110111 mov X, ax oppure
10101011
11101011... X += 2;

Inoltre più il livello a cui appartiene un linguaggio è alto, più è indipendente dal tipo di macchina in cui viene
scirtto. Ad esempio, il linguaggio macchina è completamente dipendente dal tipo di processore in cui viene
eseguito: Intel® o AMD®, Motorola®,… . L’Assembler potrebbe differire in un diverso insieme di istruzioni, il
C++ come pure JAVA invece sono completamente indipendenti dalla macchina e dal Sistema Operativo
usato (PC, Apple, Linux, Windows, MAC OS,…), ciò che cambia è il Compilatore ed il Linker.

1
Corso Base di Programmazione in C/C++ - Lezione 1

Il Compilatore
Il procedimento di creazione di un programma scritto in C++ o altro è composto di 3 parti che utilizzano 3
strumenti diversi, tutte con un unico scopo: creare la famosa sequenza di 1 e 0 utili al microprocessore per
portare a termine il proprio lavoro. Il primo strumento necessario per poter scrivere un programma è l’Editor
di Testo nel quale il programmatore scrive le istruzioni come se fosse un testo qualunque. Quindi il primo
procedimento è: la scrittura delle istruzioni.
Il secondo strumento è il Compilatore. Quest’ultimo è un particolare tipo di programma che prende in input il
file di testo scritto nella prima fase, va alla ricerca di eventuali errori sintattici e se tutto il programma è
corretto produce un particolare file binario chiamato File Oggetto (con estensione .obj). Se il programma non
è corretto il compilatore segnala la riga o le righe sbagliate e si ferma. Oggigiorno un compilatore include gia
un editor di testo con la particolarità di mettere in evidenza con colori diversi le differenti strutture
grammaticali del linguaggio. Il processo che utilizza il compilatore è detto: Compilazione.

Il Linker
Il terzo ed ultimo strumento da utilizzare nel processo di creazione di un programma eseguibile è il Linker.
Questo è un altro particolare programma che prende in input il file oggetto prodotto dal compilatore e crea il
file binario comprensibile al computer e che può essere eseguito. Esempi di file binari prodotti dal linker sono
i .exe, .dll, .ocx, .bin
Anche durante questa fase possono esserci degli errori ed il linker deve essere pronto a segnalarli ed a
fermare il processo. Tipici errori del linker sono l’incapacità di trovare le librerie esterne da includere nel
programma eseguibile. In questi casi per correggere gli errori bisogna indicare al linker i percorsi corretti nei
quali si trovano le librerie.

Funzioni e Procedure
Sia il linguaggio C che il C++ sono linguaggi composti da un cero numero di funzioni. Prima di andare avanti
è necessario chiarire quale differenza esiste tra una funzione ed una procedura. Entrambe sono dei pezzi di
codice C/C++ ai quali si accede tramite un nome, il nome della funzione/procedura, ed alla quale possono
essere passati dei valori detti parametri. La presenza di questi ultimi non è obbligatoria. Per una funzione
potrebbe sembrare strano ma in effetti non lo è, si pensi ad esempio ad una funzione che restituisce un
numero casuale. La differenza fondamentale tra una funzione ed una procedura e che la prima deve
restituire un valore, mentre la seconda no. Esempi di funzioni sono: Massimo(x,y), Minimo(x,y,z),…
Esempi di procedure sono: CancellaSchermo(), ScriviFile(fd,x),…

2
Corso Base di Programmazione in C/C++ - Lezione 1

Come vedremo in seguito in C/C++ è possibile scrivere sia funzioni che procedure ma la distinzione è solo a
livello concettuale infatti una funzione termina con una riga del tipo return X; mentre una procedura può
non contenere l’istruzione return oppure contenere una riga del tipo return;

3
Corso Base di Programmazione in C/C++ - Lezione 2

Lezione 2 – Composizione di un programma


C/C++
In questa lezione gettiamo le basi su come deve essere strutturato un programma C/C++. Individuare le
varie parti in cui è logicamente diviso. La divisione è logica e serve solo al programmatore a tenere ben
distinte le diverse sezioni in modo da poterle rintracciare subito, al compilatore con interessa tale divisione
(anzi, non ne è nemmeno a conoscenza!). Vedremo anche alcune differenze sintattiche tra il C e C++.

Composizione schematica di un programma C/C++


E’ comune abitudine iniziare il programma C/C++ con la lista dei file esterni da includere nel proprio
programma. Un file esterno può contenere di tutto: definizione di variabili, costanti, funzioni e per il C++
anche oggetti.
La sintassi usata in C/C++ per includere un file esterno è la seguente:

#include <file esterno>

oppure

#include “file esterno”

Ogni riga può contenere una sola direttiva #include, ed ogni direttiva può contenere un solo file. Il primo
modo si utilizza per includere file di librerie contenuti nelle cartelle del compilatore, mentre il secondo modo
è usato quando si devono includere file scritti da noi stessi. Il numero di #include può essere grande a
piacere, ovviamente più funzioni vengono incluse nel proprio programma più il file eseguibile prodotto ed il
tempo di compilazione saranno grandi.
Vediamo qualche esempio:

#include <iostream.h>
#include <stdio.h>
#include <stdlib.h>
#include “mio_file.h”

Subito dopo la lista delle direttive #include compare la lista delle direttive #define. Tale direttiva viene
usata per definire le costanti (valori che non cambiano mai). Un utilizzo avanzato di questa direttiva è quello
di definire con un nome anche una o più istruzioni.
4
Corso Base di Programmazione in C/C++ - Lezione 2

La sintassi usata in C/C++ per utilizzare la direttiva #define è:

#define nome_della_costante valore

#define nome_insieme_di_istruzioni istruzioni_su_una_riga

Vediamo subito qualche esempio:

#define PI_GRECO 3.1415926535897932384626433832795


#define EURO_in_LIRE 1936.27

#define _2_ALLA_16 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2*2


#define _2_ALLA_32 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2*2 \
*2*2*2*2*2*2*2*2*2*2*2*2*2*2*2*2

E’ pratica comune scrivere i nomi delle costanti in maiuscolo in modo da essere ben evidenti nel codice. Le
#define utilizzate con istruzioni C/C++ devono estendersi su una sola riga, se si intende scrivere su più
righe bisogna terminare la riga con il carattere ‘\’ (backslash). Infine si noti come nella definizione del nome
non sono ammessi gli spazi.

Dopo le direttive #define vengono dichiarate le variabili globali. Non preoccupatevi se non capite cosa
vogliono significare le diverse righe che seguiranno perché la discussione sulle variabili sarà ripresa più in là.
Un esempio completo sulla prima parte di un programma è il seguente:

#include <string.h>
#include <iostream.h>
#include <conio.h>

#define _2_ELEVATO_ALLA_17 2*2*2*2*2*2*2*2*2*2*2*2*2*2*2*2 \


*2
#define GETCH getch();

int i,j;
char nome_e_cognome[50];

E’ possibile definire un numero qualunque di variabili ed eventualmente quelle che hanno lo stesso tipo
possono essere scritte separate da una virgola. Il ‘;’ alla fine di ogni definizione di variabile è obbligatorio.
Il nome di una variabile può essere lungo al massimo 255 caratteri, può contenere lettere (‘a’,… ,’z’)
maiuscole o minuscole, numeri ed il carattere ‘_’ (underscore) ma deve iniziare con una lettera oppure con il
carattere ‘_’.

Funzioni
Subito dopo le variabili vengono scritte le funzioni. Come gia detto in precedenza, una funzione è una entità
che prende in input un insieme di parametri (eventualmente vuoto) e ritorna un valore che possiede una
certa relazione con i dati in ingresso. La sintassi da seguire è la seguente:

tipo_dato_ritornato Nome_della_funzione(tipo_dati1 dato1, tipo_dato2 dato2,...)


{
istruzioni tra cui compare return d;
}

Ad esempio, una funzione che esegue la quarta potenza del numero intero dato come input ha il seguente
aspetto:

int X_alla_4(int x)

5
Corso Base di Programmazione in C/C++ - Lezione 2

{
return x*x*x*x;
}
In un programma C++ prima di implementare la funzione, bisogna scrivere il prototipo. Quest’ultimo è
semplicemente una riga di codice che stabilisce gli input e gli output della funzione. Esso non esegue alcuna
azione diversa da quella da segnalare al compilatore quanto si deve aspettare successivamente nella
definizione della funzione. Per i prototipi ci sono due regole da rispettare:

1. Il prototipo deve essere definito prima che il programma principale inizi.


2. Le definizioni delle funzioni devono essere poste dopo la fine del programma principale.

Quindi il precedente esempio va corretto nel seguente modo:

int X_alla_4(int x);

... //Altri prototipi, segue il Programma principale, il main(), che vedremo poi

int X_alla_4(int x)
{
return x*x*x*x;
}

Per richiamare una funzione è sufficiente scrivere il suo nome ed elencare tra parentesi tonde i valori da
assegnare ai suoi parametri. Per l’esempio di prima:

y = X_alla_4(23); //y assumerà il valore 279841

Commenti
E’ buona norma scrivere delle righe di commento nel programma. Un commento altro non è che una
spiegazione di cosa verrà eseguito nelle prossime istruzioni o nella istruzione a sinistra del commento.
Mentre state scrivendo un programma sapete cosa state facendo, ma supponete di dover riutilizzare una
parte di codice gia scritto, chi vi dirà cosa fanno le righe scritte o meglio, chi vi spiegherà come utilizzare
quella porzione di codice? Nessuno! A meno che non siete stati furbi a scrivere qualche commento.
Il C++ ha due modi di scrivere i commenti:

1. Su riga singola.
2. Su più righe.

Il commento a riga singola si introduce scrivendo i caratteri ‘//’ seguiti dal testo del commento che può
includere qualunque carattere. Ad esempio:

return x*x*x*x; //Ritorna il numero ‘x’ moltiplicato 4 volte per se stesso ;-)

Il commento su più righe invece si scrive così:

/*
La funzione che segue
calcola x elevato alla quarta
*/

int X_alla_4(int x)
{
return x*x*x*x;
}

Ovvero il commento viene iniziato con i caratteri ‘/*’ e concluso con ‘*/’. Quest’ultimo tipo di commento è
l’unico accettato dai compilatori C.

6
Corso Base di Programmazione in C/C++ - Lezione 2

Oggetti
Passiamo ora alla descrizione, tra l’altro introduttiva, sugli oggetti. Un oggetto è una entità che permette di
racchiudere insieme varabili e funzioni detti rispettivamente Membri Dati e Funzioni Membro. Questo tipo di
entità è stato introdotto nella programmazione alcuni anni fa ed ha raccolto subito un enorme successo
tant’è che sulla scia del C++ sono nati altri linguaggi ad oggetti come il JAVA, ADA e l’Object Pascal
evoluzione del Pascal utilizzato nel Borland Delphi. Ma cosa ha di tanto potente la programmazione ad
oggetti? Pensiamo un po’ alla dimensione dei programmi di oggi, composti ad milioni di righe di codice,
gestirle come un unico grande insieme di funzioni è praticamente impossibile. La potenza della OOP risiede
nella capacità degli oggetti di:

1. Ereditarietà: Un oggetto, con dei propri dati e funzioni membro, può ereditare (ovvero farli diventare
propri) i dati e le funzioni membro di un’altra classe diventando così ancora più potente.

2. Generalizzazione: Questo concetto è legato alla ereditarietà. Un oggetto viene progettato in modo da
poter soddisfare una certa classe di problemi. Tale classe è composta da molti sotto-problemi più
specifici, un oggetto può specializzarsi nella risoluzione di una sotto-classe del marco-problema iniziale e
quindi eredita le proprietà dell’oggetto “padre” in questo modo mettendosi in un’ottica relativa
all’oggetto più specializzato, si dice che il padre generalizza il problema.
3. Incapsulamento: L’incapsulamento non deve essere visto come il semplice raggruppamento di dati e
funzioni in un’unica entità, quale l’oggetto. La potenza di questo concetto sta nella capacità di poter
definire 3 tipi di incapsulamenti usabili anche contemporaneamente. I 3 modi sono:
 Pubblico: All’esterno dell’oggetto possono essere utilizzati solo i dati e le funzioni definite come
pubbliche.
 Privato: Con questo tipo di incapsulamento si realizza l’Hiding dell’informazione ovvero, i dati e le
funzioni membro vengono nascoste al mondo esterno. Solo le funzioni appartenenti all’oggetto
stesso potranno utilizzare le funzioni e i dati privati.
 Protetto: Quest’ultima modalità è legato alla ereditarietà. Come vedremo più avanti, anche un
oggetto può essere ereditato in modo pubblico, privato e protetto. A seconda di come viene
ereditato un oggetto i dati e le funzioni incapsulati in modo protetto saranno visibili o no al figlio.

4. Polimorfismo: Questo concetto è tra i più importanti nella programmazione ad oggetti. Alcuni linguaggi
di programmazione supportano gli oggetti ma non il polimorfismo: sono dei falsi linguaggi ad oggetti.
Polimorfismo significa “molte forme”, nei linguaggi di programmazione vuol dire che ad una stessa
funzione posso assegnare compiti diversi a seconda dell’oggetto che ha ereditato una particolare
funzione detta virtuale. Questo concetto sarà esaminato meglio più avanti quando si conoscerà meglio la
programmazione ad oggetti.

7
Corso Base di Programmazione in C/C++ - Lezione 2

Differenza tra Espressione ed Istruzione


Parlando delle righe di codice C/C++ o di un altro linguaggio di programmazione molto spesso di sente
parlare di espressione e di istruzione.
Anche se non ancora abbiamo imparato a scrivere un programma C/C++ la distinzione tra i due termini può
essere compresa facendo degli esempi sul linguaggio umano e che potrà essere poi applicato al C/C++
senza modifiche.

Linguaggio umano C/C++

Espressioni: Espressioni:
 La variabile A è maggiore della variabile B?  A>B
 A maggiore di B e C uguale a D?  (A>B) && (C==D)

Istruzioni: Istruzioni:
 Metti in Y il valore di X più 1  Y = X + 1;
 Se A maggiore è di B incrementa X  if(A>B) x++; else x--;
altrimenti decrementa X

Funzione main()
Qualunque programma esso sia deve avere un inizio ed una fine. In C/C++ l’inizio (o come si dice in gergo
tecnico, il punto di ingresso) è dato da una particolare funzione denominata main. Vediamo subito un
esempio di programma C++ vuoto ma sintatticamente corretto:

main()
{
//Dichiarazioni delle variabili locali

//istruzioni

return; //Non è obbligatorio mettere questa istruzione


}

Il punto di uscita dal programma, ovvero l’ultima istruzione eseguita è nell’istruzione return, se presente, o
nella parentesi graffa che termina la funzione main.
Ci sono diversi modi di scrivere la funzione main, inoltre quest’ultima ha anche dei parametri, ma non è
obbligatorio utilizzarli e quindi metterli al contrario del C che invece bisogna inserirli anche se non vengono
usati. Vediamo un modo alternativo di scrittura del main:

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


{
//Dichiarazioni delle variabili locali

//istruzioni

return 0; //Adesso è obbligatorio mettere questa istruzione


}

Questo tipo di scrittura permette di scrivere programmi che accettano un input dal prompt dei comandi della
shell (DOS, UNIX,…). Supponiamo che il precedente esempio faccia parte di un file chiamato maxnum.cpp,
il cui file eseguibile dopo la compilazione si chiamerà maxnum.exe, nella shell sarà possibile scrivere:

C:\miei_programmi\maxnum 2 –2 8

La variabile argc contiene il numero di elementi nella riga di comando (4 nell’esempio precedente), mentre
l’altra variabile, argv, contiene le 4 stringhe della riga di comando (“maxnum”, “2”, “-2” e “8”).

8
Corso Base di Programmazione in C/C++ - Lezione 2

Riprendiamo per un attimo il discorso sulle variabili. Nell’esempio precedente c’è il commento che accenna
alle variabili locali. Una variabile si dice locale quando è dichiarata all’interno di una funzione f1. La funzione
f1 che dichiara delle proprie variabili locali, può utilizzare sia quelle globali che quelle locali. Ovviamente le
variabili locali di un’altra funzione f2 non sono accessibili all’interno di f1. Quanto detto vale sia per il C che
per il C++ ed in generale vale per qualunque L. di P.
Il valore zero restituito dall’ultimo esempio dovrebbe essere utilizzato dal sistema operativo o dal programma
che ha mandato in esecuzione un altro processo. Di solito si usa lo zero per indicare la normale terminazione
del processo ed un codice diverso da zero per le condizioni di errore.

9
Corso Base di Programmazione in C/C++ - Lezione 3

Lezione 3 – Tipi di Dati Semplici


La lezione che stiamo per iniziare ci permetterà di imparare a definire le diverse variabili di cui è composto
un programma. Definire una variabile significa allocare memoria sufficiente a contenere l’informazione
desiderata. Una variabile può essere vista per semplicità come una scatola aperta nella quale posso inserire
una sola cosa per volta. Così come ci sono scatole di diversa forma (tipo) e dimensione, così esistono
variabili di tipo e dimensioni diverse.
I tipi di dati possono essere divisi in due insiemi:

 Tipi semplici
 Tipi complessi

Tipi semplici
I tipi di dati semplici sono i mattoncini elementari con i quali costruire i tipi più complessi i quali a loro volta
permettono di creare complesse le strutture dati utilizzate nei database, nei sistemi operativi, ecc.
In C/C++ i tipi semplici sono i seguenti:

 int: Permette di rappresentare interi positivi e negativi di 32bit (4 byte), ovvero valori compresi in un
intervallo tra –2147483648 e 2147483647. Esistono alcune varianti di questo tipo di dato:
o Unsigned: Valore intero senza segno a 32bit. L’intervallo dei numeri rappresentabili e compreso
tra 0 e 4294967296.
o Short: Permette di rappresentare interi positivi e negativi di 16bit (2 byte), ovvero valori
compresi in un intervallo tra –32768 e 32767.
o Unsigned Short: Valore intero senza segno a 16bit. L’intervallo dei numeri rappresentabili e
compreso tra 0 e 65535.
o DWORD: Uguale al tipo Intero, utilizzato nella programmazione per Windows e assembler.
o WORD: Uguale al tipo Short, utilizzato nella programmazione per Windows e assembler.

 char: Permette di memorizzare un carattere. Ha la dimensione di 8bit (1 byte).

 bool: Tipo di dato utilizzato nella valutazione delle espressioni logiche. Ha la dimensione di 8bit (1 byte)
e memorizza 0 per la condizione false (falso) ed 1 per true (vero).

 float: Tipo di dato per la memorizzazione di valori in virgola mobile a singola precisione. Ha la
dimensione di 32bit (4 byte) e l’intervallo dei valori è compreso tra 3.4x10-38 e 3.4x1038.

 double: Tipo di dato per la memorizzazione di valori in virgola mobile a doppia precisione. Ha la
dimensione di 64bit (8 byte) e l’intervallo dei valori è compreso tra 5.0x10–324 e 1.7x10308.

 Puntatore: Un puntatore è un tipo di dato che contiene l’indirizzo di memoria di un’altra variabile. I
computer di oggi hanno uno spazio di indirizzamento di 32bit pari a 4GB, un puntatore quindi occupa 4
byte di memoria.

 void: Non è un vero e proprio tipo di dato, ma in C/C++ è utilizzato per indicare che ad esempio la
funzione non restituisce nulla oppure per indicare un puntatore generico (void *). Per questo ultimo
caso, il puntatore è sempre di 4 byte.

Tipi Complessi
Se qualche lettore ha gia familiarità con qualche linguaggio di programmazione si sarà accorto della
mancanza del tipo di dati per le stringhe. In effetti in C non esiste un tipo di dato per le stringhe, mentre in
C++ è stato creato l’oggetto String che permette di lavorare con le stringhe come con qualsiasi altro tipo di
dato. Il motivo per cui in C non esiste il tipo stringa è semplice: una stringa di caratteri altro non è che una
sequenza finita di caratteri memorizzati uno di seguito all’altro.

10
Corso Base di Programmazione in C/C++ - Lezione 3

Una sequenza è possibile realizzarla con qualunque tipo di dato. Per i caratteri si parla di stringhe, mentre
per gli altri tipi di parla di vettore o array. Quindi è possibile realizzare array di caratteri, interi, double, ecc.
La sintassi utilizzata per dichiarare una variabile di tipo array è semplicissima:

tipo_base_array nome_variabile[num_elem];

Come possiamo osservare, si scrive prima il tipo di base dell’array, poi si mette il nome da assegnare
all’array ed infine racchiuso tra le parentesi quadre si mette il numero massimo di elementi che l’array potrà
contenere. Il ‘;’ è obbligatorio.
Vediamo qualche esempio di dichiarazione di array:

double Punto_3D[3];
int Top10[10];
char Nome_Cognome[60];
float *Coordinate2D[2]; //Array di 2 puntatori a float

E’ possibile realizzare un array di array? Certamente! Anzi è possibile creare array di array di array… di array
fino a 32 dimensioni. Un array di array è detto matrice o anche array bi-dimensionale. La dichiarazione di
una matrice o di un vettore multi-dimensionale in generale si fa nel seguete modo:

tipo_base_matrice nome_variabile[num_elem1][num_elem2]...[num_elemN];

ovvero:

double TavolaPitagorica[12][12];

Il numero di elementi di ogni dimensione non deve essere necessariamente uguale, ovvero avrei potuto
anche scrivere:

double TavolaPitagorica[20][10];

int Matrice[3][4];

1 2 3 4
1
2
3

La tabella di sopra mostra l’organizzazione degli elementi, ovvero nella dicitura M[i][j], il numero i indica
il numero di righe totali mentre il numero j rappresenta il numero di colonne.
Prima di proseguire il discorso con altri tipi di dati complessi, è obbligatorio anticipare il discorso sugli
operatori matematici e vedere come si legge un elemento di un array o matrice anche perché questo
argomento è fonte di molti errori logici durante la scrittura di un programma.
Per poter selezionare un elemento dell’array si deve utilizzare la scrittura seguente:

nume_array[num_elem_desiderato-1]

ad esempio

primo = Top10[0];

La precedente riga non è sbagliata! L’indicizzazione di un vettore inizia da zero (0), quindi avremo
Top10[0], Top10[1], ..., Top10[9]; per un totale di 10 elementi. Lo stesso discorso si fa per gli
array multi-dimensionali. L’ultimo elemento della precedente matrice è Matrice[2][3].
Concludiamo questa lezione con la descrizione del tipo di dato che racchiude (o può racchiudere) tutti i tipi
visti finora: la Struttura – Struct (o record come viene denominata in altri linguaggi).

11
Corso Base di Programmazione in C/C++ - Lezione 3

struct TModello3D
{
double Posizione[3];
char NomeOggetto[50];
int ColoreRGB[3];
};

TModello3D Modello;
TModello3D ListaModelli[1000];
TModello3D *ModelloTemporaneo;

Quello appena visto è un semplice esempio di struttura e dichiarazione di variabili che hanno tipo struttura.
Una struttura viene dichiarata scrivendo la parola riservata struct:

struct nome_struttura
{
//dichiarazioni variabili
};

un altro modo di scrivere una variabile di tipo struttura è il seguente:

struct
{
//dichiarazioni variabili
} nome_var_struttura;

ovvero la variabile è di tipo struttura ma quest’ultima non ha nome e quindi non è possibile dichiarare
nessun altra variabile in modo da utilizzare la stessa struttura. Per poter utilizzare le variabili dichiarate
all’interno di una struttura si ricorre alla cosiddetta notazione puntata.
Ad esempio:

Modello.Posizione[0] = 2;
Modello.Posizione[1] = 0;
Modello.Posizione[2] = 5;

ovvero si utilizza la seguente sintassi:

nome_var_Struttura.Nomecampo;

se invece abbiamo il puntatore ad una variabile di tipo struttura si utilizza la notazione freccia ‘->’:

nome_var_Struttura->Nomecampo;

ModelloTemporaneo->Posizione[0] = 2;
ModelloTemporaneo->Posizione[1] = 0;
ModelloTemporaneo->Posizione[2] = 5;

Vediamo ora come si utilizza un array di strutture. Riprendiamo l’esempio all’inizio della pagina:

TModello3D ListaModelli[1000];

il suo uso è il seguente:

ListaModelli[57].Posizione[2] = 3;

12
Corso Base di Programmazione in C/C++ - Lezione 4

Lezione 4 – Operatori Matematici


In questa lezione conosceremo gli operatori del C/C++. Gli operatori sono delle vere e proprie funzioni che
prendono dei valori in ingresso e restituiscono altri valori. Gli operatori sono divisi in 2 categorie: aritmetici e
logici.

Operatori Aritmetici
Il C/C++ forniscono gli operatori matematici fondamentali: addizione (+), sottrazione (-), moltiplicazione (*),
divisione (/) e modulo (%). Vediamo subito un esempio:

void main()
{
int a = 10, b = 5, c = 3, r;

r = a+b; // r contiene 15
r = a-b; // r contiene 5
r = a*b; // r contiene 50
r = a/b; // r contiene 2
r = a/c; // r contiene 3
r = a%b; // r contiene 0 (10/5 = 2 con resto 0)
r = a%c; // r contiene 1 (10/3 = 3 con resto 1)
}

Nella riga:

r = a/c;

ad r viene assegnato il risultato della divisione intera 10/3, troncato all’intero inferiore, cioè 3. I risultati delle
divisioni intere vengono troncati, non arrotondati.
L’operatore di modulo restituisce il resto della divisione.

E’ molto frequente avere la stessa variabile sia a sinistra che a destra dell’uguale, cioè:

r = r op x

dove op è uno degli operatori matematici, r e x sono gli operandi e il risultato viene memorizzato in r. Il C
(e quindi il C++) fornisce una forma compatta di questo tipo di operazione:

r op= x

Vediamo un esempio:

int x = 0;
x += 5; // x == 5
x -= 3; // x == 2
x *= 10; // x == 20
x /= 2; // x == 10
x %= 3; // x == 1

Oltre agli operatori binari, il C mette ovviamente a disposizione anche gli operatori unari + e -, che
permettono di attribuire un segno ai numeri, secondo il senso comune. Ad esempio:

x = -y;

ha il significato ovvio di assegnare a x l’opposto di y.

13
Corso Base di Programmazione in C/C++ - Lezione 4

Operatori di incremento e decremento


Il C/C++ mette a disposizione del programmatore una forma compatta di incremento e decremento delle
variabili. Nessun programmatore “serio” scriverà mai:

x += 1;

ma piuttosto:

x++;

che significa: incrementa x di 1.


Analogamente:

x--;

significa decrementa x di 1.
Ci sono due versioni degli operatori di incremento e decremento: la versione prefissa e la versione postfissa.
Nel pre-incremento l’operatore ++ appare prima della variabile mentre nel post-incremento l’operatore ++
appare dopo la variabile; analogamente per il decremento.
Nel pre-incremento e pre-decremento:

++x;
--x;

l’operazione è effettuata prima della valutazione dell’espressione risultante.


Nel post-incremento e post-decremento:

x++;
x--;

l’operazione è effettuata dopo la valutazione dell’espressione.


Per capire meglio la differenza, proviamo il seguente programma:

#include <iostream>

void main()
{
int x = 0, y = 5;
cout << “x: “ << x << endl;
cout << “++x: “ << ++x << endl;
cout << “x++: “ << x++ << endl;
cout << “x: “ << x << endl;
cout << “y: “ << y << endl;
cout << “--y: “ << --y << endl;
cout << “y--: “ << y-- << endl;
cout << “y: “ << y << endl;
}

In esecuzione si ottiene:

x: 0
x: 1
x: 1
x: 2
y: 5
y: 4
y: 4
y: 3
14
Corso Base di Programmazione in C/C++ - Lezione 4

Per concludere, come molti hanno fatto, ci piace interpretare il nome C++ come “un passo oltre il C”.

Operatori ternari
Sempre nell’ottica della scrittura di codice compatto, esiste un operatore ternario che permette di scegliere
tra due espressioni sulla base della valutazione di una terza espressione. La forma generale è la seguente:

espressione1 ? espressione2 : espressione3

Se l’espressione1 viene valutata vera, allora il risultato dell’intera espressione è espressione2,


altrimenti è espressione3. Ad esempio:

void main()
{
int x = 3;
// se x è uguale a 3, assegna 4 a d, altrimenti assegnagli 5
int d = (x==3) ? 4 : 5;
}

Operatori logici
Gli operatori logici possono essere suddivisi in due gruppi: quelli normalmente usati nei confronti tra valori e
quelli utilizzati per collegare i risultati di due confronti. Ecco una breve serie di esempi relativi al primo
gruppo:

(a == b) // VERA se a è UGUALE a b
(a != b) // VERA se a è diversa da b
(a < b) // VERA se a è strettamente minore di b
(a > b) // VERA se a è strettamente maggiore di b
(a <= b) // VERA se a è minore o uguale a b
(a >= b) // VERA se a è maggiore o uguale a b

La sintassi di questi operatori ed il loro significato appaiono scontati, ad eccezione, forse, dell'operatore di
uguaglianza "==": in effetti i progettisti del C, constatato che nella codifica dei programmi i confronti per
uguaglianza sono, generalmente, circa la metà degli assegnamenti, hanno deciso di distinguere i due
operatori "raddoppiando" la grafia del secondo per esprimere il primo. Ne segue che

a = b;

assegna ad a il valore di b, mentre

(a == b)

esprime una condizione che è vera se le due variabili sono uguali.


Veniamo al secondo gruppo. Gli operatori logici normalmente usati per collegare i risultati di due o più
confronti sono due: si tratta del prodotto logico ("&&", o and) e della somma logica ("||", o or).

(a < b && c == d) // AND: vera se entrambe sono VERE


(a < b || c == d) // OR: vera se ALMENO UNA e' VERA
!(a == b) // NOT: vera se l’espressione è false

E' possibile scrivere condizioni piuttosto complesse, ma vanno tenute presenti le regole di precedenza e di
associatività. Ad esempio, poiché tutti gli operatori del primo gruppo hanno precedenza maggiore di quelli
del secondo, la

(a < b && c == d)

15
Corso Base di Programmazione in C/C++ - Lezione 4

è equivalente alla

((a < b) && (c == d)

Nelle espressioni in cui compaiono sia "&&" che "||" va ricordato che il primo ha precedenza rispetto al
secondo, perciò

(a < b || c == d && d > e)

equivale a

((a < b) || ((c == d) && (d > e)))

Se ne trae, se non altro, che in molti casi usare le parentesi, anche quando non indispensabile, è
sicuramente utile, dal momento che incrementa in misura notevole la leggibilità del codice e abbatte la
probabilità di commettere subdoli errori logici.

Precedenza degli Operatori


Consideriamo ora una serie di assegnamenti:

a = b = c = d;

Il compilatore C/C++ la esegue assegnando il valore di d a c; poi il valore di c a b; infine, il valore di b ad


a. Il risultato è che il valore di d è assegnato in cascata alle altre variabili; in pratica, che l'espressione è
stata valutata da destra a sinistra, cioè che l'operatore di assegnamento gode di associatività da destra a
sinistra.

In altre parole, la precedenza si riferisce all'ordine in cui il compilatore valuta gli operatori, mentre
l'associatività concerne l'ordine in cui sono valutati operatori aventi la stessa precedenza (non è detto che
l'ordine sia sempre da destra a sinistra).
Le parentesi tonde possono essere sempre utilizzate per definire parti di espressioni da valutare prima degli
operatori che si trovano all'esterno delle parentesi. Inoltre, quando vi sono parentesi tonde annidate, vale la
regola che la prima parentesi chiusa incontrata si accoppia con l'ultima aperta e che vengono sempre
valutate per prime le operazioni più interne. Così, ad esempio, l'espressione

a = 5 * (a + b / (c - 2));

è valutata come segue: dapprima è calcolata la differenza tra c e 2, poi viene effettuata la divisione di b per
tale differenza. Il risultato è sommato ad a ed il valore ottenuto è moltiplicato per 5. Il prodotto, infine, è
assegnato ad a. In assenza delle parentesi il compilatore avrebbe agito in maniera differente, infatti:

a = 5 * a + b / c - 2;

è valutata sommando il prodotto di a e 5 al quoziente di b diviso per c; al risultato è sottratto 2 ed il valore


così ottenuto viene assegnato ad a.
Vale la pena di presentare l'insieme degli operatori C/C++, riassumendone in una tabella le regole di
precedenza ed associatività; gli operatori sono elencati in ordine di precedenza decrescente.

Operatore Descrizione Associatività


() chiamata di funzione da sx a dx
[] indici di array
. appartenenza a struttura
-> appartenenza a struttura refernziata da
puntatore
16
Corso Base di Programmazione in C/C++ - Lezione 4

! NOT logico da dx a sx
~ complemento a uno
- meno unario (negazione)
++ autoincremento
-- autodecremento
& indirizzo di
* indirezione
(tipo) cast (conversione di tipo)
sizeof() dimensione di
* moltiplicazione da sx a dx
/ divisione
% resto di divisione intera
+ addizione da sx a dx
- sottrazione
<< scorrimento a sinistra di bit da sx a dx
>> scorrimento a destra di bit
< minore di da sx a dx
<= minore o uguale a
> maggiore di
>= maggiore o uguale a
== uguale a da sx a dx
!= diverso da (NOT uguale a)
& AND su bit da sx a dx
^ XOR su bit da sx a dx
| OR su bit da sx a dx
&& AND logico da sx a dx
|| OR logico da sx a dx
? : espressione condizionale da dx a sx
operatori di assegnamento (semplice e
=, etc. da dx a sx
composti)
, virgola (separatore di espressioni) da sx a dx

Come si vede, alcuni operatori possono assumere significati diversi. Il loro modo di agire sugli operandi è
quindi talvolta desumibile senza ambiguità solo conoscendo il contesto di azione, cioè le specifiche
espressioni in cui sono utilizzati.

17
Corso Base di Programmazione in C/C++ - Lezione 5

Lezione 5 – Istruzioni di Condizione


Iniziamo, con questa lezione, ad entrare nel vivo del linguaggio C/C++. Al termine della lezione saremo in
grado di far prendere al programma delle semplici decisioni deterministiche ovvero delle scelte fatte sulla
base della risoluzione di una espressione logica di controllo.

Istruzione if-else
Nella precedente lezione si è gia potuto osservare una esempio di istruzione decisionale:

d = (x==3) ? 4 : 5;

nella quale se x vale 3 allora alla variabile d viene dato il valore 4 altrimenti gli viene assegnato 5. Notare la
terminologia utilizzata nella frase precedente: Se ‘espressione’ allora ‘espressione1’ altrimenti ‘espressione2’.
Una istruzione if-else funziona allo stesso modo, con l’unica differenza che al posto delle espressione1 e 2 ci
sono 2 gruppi di istruzioni. La sintassi di questa istruzione è la seguente:

if(espressione)
{
//istruzioni separate da ;
}
else
{
//istruzioni separate da ;
}

Schematicamente questa istruzione può essere rappresentata nel modo che seguente e proprio per questa
schematizzazione si parla spesso di ramo if e ramo else:

La valutazione della condizione tra parentesi tonde, in generale, deve tornare un valore del tipo bool che
assume quindi i valori true o false. Se la condizione è vera viene eseguito il primo blocco di istruzioni
racchiuse tra le parentesi graffe, se la condizione e falsa vengono eseguite le istruzioni del secondo blocco.
Vediamo subito qualche esempio:

18
Corso Base di Programmazione in C/C++ - Lezione 5

#include <iostream.h>

int main()
{
int anni;

cout << "Inserire l’età: "; //Stampa a video la stringa tra gli apici
cin >> anni; //Legge da tastiera un numero e lo memorizza nella var. anni

if(anni >= 18)


{
cout << "Puoi prendere la patente B." << endl;
}
else
{
cout << "Sei troppo giovane per portare l’auto!!!" << endl;
cout << "L’età minima e 18. " << endl;
}
return 0;
}

In questo semplice esercizio viene stampato prima un messaggio, subito dopo il programma si mette in
attesa di un input da tastiera e finalmente viene eseguito il controllo sulla variabile anni. Se quest’ultima è
maggiore o uguale a 18 viene eseguita la stampa a video "Puoi...", altrimenti vengono eseguite le altre 2
stampe del ramo else.
Quando il ramo if o quello else o entrambi contengono una sola istruzione, le parentesi graffe possono
essere omesse:

...

if(anni >= 18)


cout << "Puoi prendere la patente B." << endl;
else
{
cout << "Sei troppo giovane per portare l’auto!!!" << endl;
cout << "L’età minima e 18. " << endl;
}
...

Inoltre il ramo else non è obbligatorio. Ad esempio:

#include <iostream.h>

int main()
{
int anni;

cout << " Inserire l’età: ";


cin >> anni;

if(anni >= 18)


{
cout << "Puoi prendere la patente B." << endl;
return;
}
cout << "Sei troppo giovane per portare l’auto!!!" << endl;
cout << "L’età minima e 18. " << endl;
return 0;
}

19
Corso Base di Programmazione in C/C++ - Lezione 5

Istruzioni if-else annidate


Sia il ramo if che il ramo else possono contenere altre istruzioni if-else, in questi casi si parla di istruzioni if-
else annidate. Valgono le stesse regole viste in precedenza, ovvero else non obbligatorio e parentesi graffe
eliminabili se il ramo in considerazione contiene una sola istruzione.

#include <iostream.h>

#define MyAge 21

main()
{
int anni;

cout <<"Indovina la mia età: ";


cin >> anni;
if(anni == MyAge)
{
cout << endl << "BRAVISSIMO, ai indovinato!"<< endl;
}
else
{
if(anni < MyAge)
{
cout << "No, sono più vecchio." << endl;
}
else
{
cout << " No, sono più giovane " << endl;
}
}
return 0;
}

Istruzione switch-case
Ci sono casi in cui è necessario annidare molte istruzioni if-else:

resto = a % b;
if(resto==0) cout << "Resto nullo" << endl;
else
if(resto==1) cout << "Resto = uno" << endl;
else
if(resto==2) cout << "Resto = due" << endl;
else
if(resto==3) cout << "Resto = tre" << endl;
...

In simili situazioni la lettura del codice diventa anche più complicata, specialmente se le istruzioni nei rami if
ed else sono molte. Per fortuna il linguaggio C/C++ mette a disposizione una istruzione equivalente che
permette di evitare di dover annidare tanti if-else, tale istruzione è lo switch-case:

switch(espressione)
{
case val1: //istruzioni
break;
case val2: //istruzioni
break;

20
Corso Base di Programmazione in C/C++ - Lezione 5

case valn: //istruzioni


break;
default: //istruzioni
}

L’espressione tra le parentesi tonde dello switch deve avere come risultato un valore (di tipo int, float,
char,…) che sarà poi confrontato con tutti i valori costanti valn dei diversi case. Il primo valore che
risulterà uguale con quello tornato dall’espressione iniziale causerà l’esecuzione delle istruzioni che seguono i
‘:’ del relativo case. Se nessuno dei case soddisfa l’espressione dello switch allora saranno eseguite le
istruzioni della sezione default. Quest’ultima sezione è opzionale.

Vediamo qualche esempio:

switch(a % b)
{
case 0: cout << "Resto nullo" << endl;
break;
case 1: cout << "Resto uno" << endl;
break;
...
case 9: cout << "Resto nove" << endl;
break;
}

switch(nome[i])
{
case ‘a’:
case ‘e’:
case ‘i’:
case ‘o’:
case ‘u’: cout << "La lettera" << i << "del nome è una vocale." << endl;
break;
default : cout << "La lettera" << i << "del nome è una consonante." << endl;
}

Osserviamo l’ultimo esempio. Se ci sono due o più case per i quali si deve eseguire la stessa o le stesse
istruzioni è possibile utilizzare la forma abbreviata, ovvero mettere le istruzioni solo nell’ultimo case e
lasciare vuoti tutti quelli precedenti.

Esempio completo:

#include <iostream.h>
#include <stdlib.h>

int main()
{
21
Corso Base di Programmazione in C/C++ - Lezione 5

char choice;

cout << "FLIGHT BOOKING SYSTEM" << endl << endl;


cout << "1..New York to London Heathrow" << endl;
cout << "2..New York to Vancouver" << endl;
cout << "3..New York to Sydney" << endl;
cout << "4..New York to Cape Town" << endl;
cout << "Q..Quit" << endl;

cout << endl << endl << "Enter your choice : ";
cin >> choice;

switch(choice)
{
case '1': cout << endl << "New York to London booked"<< endl << endl;
break;
case '2': cout << endl << "New York to Vancouver booked" << endl << endl;
break;
case '3': cout << endl << "New York to Sydney booked" << endl << endl;
break;
case '4': cout << endl << "New York to Cape Town booked" << endl << endl;
break;
case 'Q':
case 'q': exit(0);
default: cout << endl << "Bad choice!" << endl << endl;
}
return 0;
}

22
Corso Base di Programmazione in C/C++ - Lezione 6

Lezione 6 – Istruzioni di ciclo


In questa lezione impareremo come far eseguire al calcolatore le stesse istruzioni un numero determinato di
volte oppure fin quando non si verifica una certa condizione. In C/C++ i cicli sono divisi in due classi:
incondizionati o iterativi e cicli condizionati.

Il ciclo iterativo for


Numerose operazioni di elaborazione richiedono che determinate istruzioni vengano ripetute varie volte, un
procedimento noto come iterazione. Il ciclo for segue un tracciato simile alla figura che segue:

Come vedremo tra breve, nella definizione del for viene specificato il numero di volte che il ciclo deve
ripetersi tramite una variabile contatore. E’ anche possibile creare cicli infiniti dai quali è possibile uscire
utilizzando l’istruzione break vista nell’istruzione switch-case. La sintassi del ciclo for è la seguente:

for(valore iniziale; valore finale; incremento)


{
//istruzioni
}

Esaminiamo una alla volta le 3 espressioni tra le parentesi tonde:


 Valore iniziale: in realtà è un assegnamento del tipo i=0; oppure i=-10; ma che comunque
stabilisce il valore iniziale della variabile contatore.

 Valore finale: il valore finale che la variabile contatore assumerà viene specificato dalla
valutazione di una espressione di controllo del tipo i<50; oppure i<100; ovvero la variabile
contatore viene incrementata fino a che l’espressione precedente diventa falsa (i==50 o i==100).

 Incremento: è composto da una istruzione di assegnamento o incremento o ancora decremento


del tipo i++; o i--; o infine i+=3; L’ultima istruzione fa procedere l’incremento a passi di 3. Per
l’assegnamento di decremento fare attenzione a che il valore iniziale sia maggiore di quello finale
altrimenti si cade in un ciclo infinito!!!

Vediamo qualche esempio:

#include <iostream.h>

int main()

23
Corso Base di Programmazione in C/C++ - Lezione 6

{
int x;

for(x = 0; x < 10; x++)


{
cout << x << endl;
}
return 0;
}

Questo esempio stampa a video uno sotto l’altro i numero da 0 a 9.


Come visto nella Lezione 5 per l’if-else ad una istruzione, anche per il for vale la regola che permette di
eliminare le parentesi graffe solo se il corpo del ciclo è composto da una sola istruzione.

for(x = 0; x < 10; x++)


cout << x << endl;

Vediamo un altro esempio che esegue la somma di 5 numeri immessi dall’utente:

#include <iostream.h>

main()
{
int loop;
int total = 0; // declare and initialize variables
int number = 0;

for(loop = 1; loop <= 5; loop++)


{
cout << "Enter a number : ";
cin >> number;
total += number; // keep running total
}
cout << endl << "The total is " << total;
return(0;
}

L’esempio che segue scrive in lettere maiuscole il nome (o la stringa) inserita da tastiera:

#include <iostream.h>
#include <string.h>
#include <ctype.h>
#define MAX 20

int main()
{
char name[MAX];
int x;

cout << "Enter a name : ";


cin >> name;
for (x = 0; x < strlen(name); x++)
{
name[x] = toupper(name[x]);
}
cout << endl << name << endl;
return 0;
}

Concludiamo gli esempi con un ciclo infinito particolare:

24
Corso Base di Programmazione in C/C++ - Lezione 6

...
for(;;) //Realizza il ciclo infinito
{
//istruzioni
}
...

oppure

...
for(;;)
{
if(condizione_uscita) break; //permette di uscire dal ciclo
...
}
...

NOTA: Anche se non è espressamente vietato dalla sintassi del linguaggio, si consiglia di evitare
assolutamente di modificare la variabile contatore all’interno del ciclo perché si potrebbe incorrere in errori
logici e cadere quindi in un ciclo infinito o eseguire il ciclo un numero inferiore rispetto a quando il calcolo ne
richiede. Se si ha la necessità di modificare la variabile contatore, utilizzare uno dei cicli condizionati che
seguono.

Ciclo condizionato while


L’istruzione di ciclo while è utilizzata per ripetere un gruppo di istruzioni fino a che rimane vera una
espressione di controllo. Lo schema logico di questa istruzione è il seguente:

La sintassi di questa istruzione è la seguente:

while(condizione)
{
//istruzioni che modificano le variabili della condizione
}

25
Corso Base di Programmazione in C/C++ - Lezione 6

dove condizione è una qualunque espressione (complicata quanto basta) che ha come risultato true o
false. Fino a che la condizione è vera le istruzioni tra le parentesi graffe vengono eseguite, in caso di
falsità si esce fuori dal ciclo. Nella condizione è quindi possibile inserire una espressione contenente
qualunque operatore di confronto (<, <=, >, >=, ==, !=) e/o logico (&&, ||, !).
Anche qui, come visto nella Lezione 5 per l’if-else ad una istruzione, vale la regola che permette di
eliminare le parentesi graffe solo se il corpo del ciclo è composto da una sola istruzione.
Vediamo qualche esempio di utilizzo.

#include <iostream.h>

int main()
{
int counter = 0; //set initial value

while(counter < 10)


{
cout << counter << " "; //display
counter++; //increment
}
return 0;
}

Il precedente listato di programma C++, stampa a video i numeri da 0 a 9.

#include <iostream.h>

int main()
{
char lettera = ‘b’;
while(lettera > ‘a’ && lettera < ‘z’)
{
cout << lettera << " ";
lettera ++;
}
return 0;
}

Il precedente listato di programma C++, stampa a video le lettere minuscole dell’alfabeto dalla ‘b’ alla ‘y’.
Anche con il ciclo while è possibile creare cicli infiniti. Un tipico esempio è il seguente:

...
while(true) //Realizza il ciclo infinito
{
//istruzioni
}
...

Ciclo condizionato do-while


L’istruzione di ciclo do-while è utilizzata per ripetere un gruppo di istruzioni mentre rimane vera una
condizione di controllo. Al contrario del while, le istruzioni sono eseguite almeno una volta. Lo schema
logico è il seguente nel quale risulta evidente che se la condizione di controllo è vera si ripetono le istruzioni
altrimenti si esce dal ciclo:

26
Corso Base di Programmazione in C/C++ - Lezione 6

La sintassi è simile al while:

do{
//istruzioni che modificano le variabili della condizione
}while(condizione);

Nella condizione è quindi possibile inserire una espressione contenente qualunque operatore di confronto
(<, <=, >, >=, ==, !=) e/o logico (&&, ||, !).
Anche qui, come visto nella Lezione 5 per l’if-else ad una istruzione, vale la regola che permette di
eliminare le parentesi graffe solo se il corpo del ciclo è composto da una sola istruzione.
Vediamo un esempio di utilizzo:

#include <iostream.h>

int main()
{
int counter = 0; //set initial value

do{
cout << counter << " "; //display
counter++; //increment
}while(counter < 10); //test condition
return 0;
}

27
Corso Base di Programmazione in C/C++ - Lezione 7

Lezione 7 – Funzioni
Nel mondo reale i programmi tendono ad essere molto grandi e complicati. Per gestire una simile
complessità, sono disponibili un certo numero di tecniche di programmazione note come progettazione top-
down. La progettazione top-down è l’arte della decomposizione di un problema complesso in attività di
complessità ridotta e più facilmente gestibili.
Queste piccole attività formano la base della scrittura di un insieme di moduli che possono essere collegati
fra loro a formare un programma completo. In questo modo si ottengono numerosi vantaggi: codice breve,
ricerca degli errori più semplice, possibilità di lavorare in gruppo, riutilizzo del codice, librerie.
Un modulo in C++ è noto come funzione e consiste di un prototipo e di una definizione. Come accennato
nella lezione 2, ci sono due regole che bisogna rispettare:

 Il prototipo deve essere definito prima che il programma principale inizi.

 Le definizioni devono essere poste dopo il termine del programma principale.

Dichiarazione di una funzione: il Prototipo


Come accennato il prototipo deve essere posto prima che il programma principale inizi ed è composto da tre
elementi (visibili nel disegno sottostante):

 Input: Gli input della funzione sono racchiusi tra parentesi tonde e sono noti come parametri di
input, ma in realtà sono delle vere e proprie variabili locali alla funzione (vedi Lezione 2 – main()) e
quindi visibili solo ed esclusivamente dalla funzione in questione. Si deve sempre specificare il tipo di
ogni parametro uno alla volta, ovvero non è possibile utilizzare la forma abbreviata per parametri
che hanno lo stesso tipo (ad es.: int i,j,k; per le funzioni non è ammesso).

 Nome Funzione: Il nome della funzione deve essere unico e non deve coincidere con le parole
chiavi del C/C++ (ad es.: for, switch,…). Inoltre, per il nome vale la stessa regole delle variabili:
deve iniziare con una lettera o con ‘_’ e contenere poi solo lettere, numeri o ‘_’, tutto il resto è
vietato.

 Output: Viene specificato solo il tipo di dati che la funzione deve restituire: int, float, double,
char, struct e puntatori ad uno qualunque dei tipi visti. Quando si parlerà più in dettaglio della
OOP si vedrà come una funzione può tornare anche un oggetto.

Definizione della funzione: implementazione


Il corpo di una funzione è esattamente come quello di un normale programma C/C++ ovvero del main() e
può includere proprie variabili locali ed istruzioni. Ripetiamo ancora una volta che tutte le variabili definite
all’interno delle parentesi graffe del corpo della funzione sono invisibili all’esterno della funzione stessa.
Ora che la funzione è dichiarata e implementata, come si utilizza? L’operazione è semplicissima, basta
scrivere il suo nome ed inserire tra le parentesi tonde i diversi valori da assegnare ai rispettivi parametri e
terminare la riga con il ‘;’.
Passiamo ora ad esaminare un primo semplice esempio completo:

#include <iostream.h>

// Prototipo
void Show(int x);

28
Corso Base di Programmazione in C/C++ - Lezione 7

int main()
{
int number;

cout << "Enter a number: ";


cin >> number;
Show(number);
return 0;
}

// Implementazione
void Show(int x)
{
cout.setf(ios::right); //***** - vedi sotto
cout << "The number is: ";
cout.width(6); //***** - vedi sotto
cout << x << endl;
}

Si noti dal definizione del prototipo, il main e l’implementazione della funzione. Si provi al calcolatore il
risultato ottenuto cercando anche di capire cosa eseguono le righe commentate con gli asterischi (un buon
programmatore deve cimentarsi da subito a capire le istruzioni che non ha mai visto).

Passaggio per Valore e per Riferimento


I due concetti esposti in questo paragrafo sono importantissimi e permettono di risolvere diversi problemi di
ottimizzazione della memoria utilizzata e velocità di esecuzione dei programmi.
Quando un valore viene passato ad una funzione come parametro di input, il contenuto di quella variabile
viene copiato nella variabile interna dichiarata nell’intestazione della funzione. All’interno della funzione, solo
il valore copiato viene manipolato; il valore originale rimane inalterato. L’esempio precedente è tipico di un
passaggio di parametro per valore nel quale avrei anche potuto modificare il contenuto della variabile x
senza avere influenza sulla variabile number.
Il secondo importante concetto che bisogna capire perfettamente è il passaggio dei parametri per
Riferimento. Ricordiamo quanto detto nella lezione 3 a proposito del tipo di dato puntatore: un puntatore è
un tipo di dato che contiene l’indirizzo di memoria di un’altra variabile.

Quando una variabile puntatore esterna viene passata come parametro di input a una funzione, il contenuto
di quella variabile, costituito da un indirizzo, viene copiato nella variabile puntatore interna dichiarata
nell’intestazione della funzione. Poiché un indirizzo punta ad una locazione di memoria all’interno del
computer, questo significa che entrambe le variabili puntatore, interna ed esterna, puntano alla stessa
posizione. Quest’area di memoria può essere quindi manipolata dall’interno della funzione o dal programma
principale. Questa è la cosiddetta memoria condivisa.
Vediamo subito un esempio:

#include <iostream.h>

void Twice(int* x);

int main()
{
int* number; //puntatore ad intero

29
Corso Base di Programmazione in C/C++ - Lezione 7

int Num1 = 77;

number = &Num1; //number prende l’indirizzo della variabile Num1


cout << *number << endl;
Twice(number);
cout << *number << endl;
return 0;
}

void Twice(int* x)
{
*x = *x * 2; //modifica la locaz. di memoria il cui indirizzo è contenuto in x
}

Nel precedente esempio facciamo attenzione a due particolari metodi di scrittura che finora non abbiamo
visto:

 &nome_var: questa scrittura va letta come “indirizzo della variabile nome_var”. Quindi il risultato di
questa espressione è un puntatore che ha lo stesso tipo della variabile nome_var.

 *nome_var: questa scrittura è leggermente più complicata da capire. Prima di tutto la variabile
nome_var è di tipo puntatore (a intero, carattere,…). L’asterisco va letto come “contenuto della
locazione di memoria”, quindi tutta l’espressione si legge così: “contenuto della locazione di memoria
puntata da nome_var”. Nei casi in cui potrebbe crearsi confusione, è possibile usare le parentesi tonde
ovvero scrivere, per l’esempio precedente,(*x) = (*x) * 2;.

Ricorsione
Una definizione che adoperasse il concetto stesso che intende definire sarebbe considerata circolare e
dunque vuota, per le stesse ragioni per cui non accetteremmo come concludente una “dimostrazione” che
facesse uso della tesi da stabilire. Tuttavia esistono forme di circolarità che non sono considerate vuote, ma
anzi accettate come definizioni e ragionamenti validi: sono quelli in cui si ricorre all’induzione matematica.
Come in generale si possono definire induttivamente insiemi, così, in particolare, si può fare per le funzioni:

Possiamo tuttavia calcolare i valori di una funzione definita per ricorsione interpretando la definizione
implicita come una regola di calcolo. Questo è quanto avviene nel caso di funzioni ricorsive in C++ (ed in
ogni linguaggio che accetti la ricorsione: ad esempio il PASCAL, il C, Java). La funzione fattoriale si può
infatti implementare:

int Fattoriale(int n)
{
if(n == 0) return 1;
return n * Fattoriale (n-1);
}

La valutazione di Fattoriale(3) si può descrivere nel seguente modo: poiché 3 ≠ 0 il valore sarà
3*Fattoriale(2); il calcolo di Fattoriale(3) viene allora sospeso, per valutare Fattoriale(2),
che a sua volta richiede di calcolare 2*Fattoriale(1) e dunque viene anch’esso sospeso in attesa che la
chiamata Fattoriale(1) ritorni un valore. Analogamente Fattoriale(1) richiede di calcolare
1*Fattoriale(0), e viene sospeso per calcolare Fattoriale(0). Finalmente l’argomento è 0, dunque
sappiamo esplicitamente che il valore di Fattoriale(0) è 1. Da questo momento si riprendono una

30
Corso Base di Programmazione in C/C++ - Lezione 7

dopo l’altra le computazioni delle chiamate sospese: troviamo allora che Fattoriale(1) ritorna 1; che
Fattoriale(2) ritorna 2, ed infine che Fattoriale(3) ritorna 6.
Come questo semplice esempio suggerisce, non è agevole eseguire a mente i calcoli che una definizione
ricorsiva di una funzione comporta. Al contrario abbiamo un’idea molto più chiara di cosa faccia la versione
iterativa della stessa funzione fattoriale:

int Fattoriale_iterativo(int n)
{
int fact=1;

for(int i=2; i<=n; i++) fact *= i;


return fact;
}

Concludiamo questo discorso un pò articolato con una semplice frase che racchiude tutto il senso del
paragrafo: una funzione C/C++ è ricorsiva quando all’interno del suo corpo viene richiamata se stessa!

31
Corso Base di Programmazione in C/C++ - Lezione 8

Lezione 8 – Strutture dati Complesse


Qualunque tipo di programma, semplice o complesso, poggia le sue basi su un insieme di dati di un certo
tipo (tali tipi possono essere anche più d’uno) organizzati secondo le proprie esigenze. L’insieme organizzato
e tipizzato di dati prende nome di struttura dati. Facciamo un paragone con un esempio preso della vita
quotidiana. Pensiamo ad una libreria, quest’ultimo altro non è che un grande contenitore diviso in ripiani
ognuno dei quali può contenere un numero finito di libri. Se ci serve altro spazio possiamo comprare un’altra
libreria. Un libro a sua volta può essere visto come un insieme di pagine contenenti a loro volta testo.
Schematicamente abbiamo una simile situazione:

Dalla figura si evince che la macro entità Libreria è composta da 4 sotto-tipi di dati ognuno dei quali con un
proprio tipo. Una simile organizzazione forma la struttura dati “Libreria”.
In questa lezione esamineremo alcune strutture dati molto comuni ed anche molto utili in quanto
l’organizzazione dei dati della quasi totalità dei programmi può essere riconducibile ad esse. Tali strutture
dati sono: Lista, Lista bi-direzionale, Pila, Albero.

Allocazione dinamica della Memoria


Prima di passare in rassegna le strutture dati sopra menzionate, bisogna prima sapere come è possibile
“chiedere al sistema operativo di darci altra memoria”. Questa azione è chiamata in gergo Allocazione
Dinamica, all’opposto c’è il rilascio della memoria che si chiama Deallocazione. Pensiamo all’esempio della
libreria. Come facciamo a sapere quante librerie ci serviranno per tutta la vita (in programmazione è la vita
del programma)? Non potremmo mai saperlo! Quando ne abbiamo bisogno andiamo al negozio e ne
compriamo un’altra (chiediamo altra memoria per il programma al S.O.). Ma il negoziante avrà sempre una
libreria della grandezza richiesta (ci sarà nel computer una quantità di memoria sufficiente a soddisfare la
nostra richiesta)? Non è detto! In casi simili dobbiamo gestire questa condizione di ERRORE.
La memoria libera presente nel calcolatore viene chiamata Heap.
Il modo di allocare e deallocare memoria in C e C++ è diverso. Il C fa uso di funzioni di libreria mentre il
C++ ha degli operatori che fanno parte direttamente nel linguaggio. Il motivo di una simile differenza sta nel
fatto che il C++ deve poter allocare memoria anche per gli Oggetti e ricordiamo che un oggetto è composto
da dati e funzioni e quindi deve essere trattato in un certo modo.
Esaminiamo prima l’allocazione dinamica in C. Il prototipo della funzione di allocazione è:

void *malloc(unsigned size);

32
Corso Base di Programmazione in C/C++ - Lezione 8

Semplice no? La funzione prende in ingrasso un intero senza segno che rappresenta il numero di byte da
allocare e restituisce un puntatore alla memoria allocata. Il puntatore restituito non ha tipo, ma come
vedremo tra breve può essere convertito in un puntatore standard.
Per deallocare la memoria che non è più necessaria si utilizza invece la seguente funzione:

void free(void *block);

ovvero una funzione che prende in ingresso un puntatore e che non restituisce nulla.
Esempio completo in C:

#include <stdio.h>
#include <string.h>
#include <alloc.h>
#include <process.h>

int main(void)
{
char *str;

/* allocate memory for string */


if((str = (char *) malloc(10)) == NULL)
{
printf("Not enough memory to allocate buffer\n");
exit(1); /* terminate program if out of memory */
}

/* copy "Hello" into string */


strcpy(str, "Hello");

/* display string */
printf("String is %s\n", str);

/* free memory */
free(str);

return 0;
}

Esaminiamo le parti che più ci interessano:


 Conversione del puntatore void: Ciò che a noi serve è un puntatore a carattere, la conversione del
puntatore tornato dalla malloc viene fatta in questo modo: (char *) malloc(10). Questo tipo di
conversione si chiama Conversione Cast a run-time ed è una delle capacità del C/C++ che li rende dei
potenti linguaggi di programmazione.
 Deallocazione: free(str); esaminando il prototipo delle funzione è necessaria una conversione cast.
Siccome un puntatore void è più generale un (char *), la conversione viene fatta automaticamente
dal compilatore.
 Puntatore nullo: un particolare valore che può assumere un puntatore è il valore nullo indicato con
NULL. Tale valore deve essere controllato in modo da sapere se il contenuto della variabile puntatore è
valido.

Passiamo al C++. L’operatore di allocazione è il new, mentre quello di deallocazione è il delete o


delete[].

Puntatore = new tipo(num_byte);


delete[] Puntatore; oppure delete Puntatore;

Il precedente programma convertito in C++ diventa:

#include <iostream.h>
33
Corso Base di Programmazione in C/C++ - Lezione 8

#include <stdio.h>
#include <string.h>
#include <process.h>

int main()
{
char *str;

//allocate memory for string


if((str = new char(10)) == NULL)
{
cout << "Not enough memory to allocate buffer\n" << endl;
exit(1); //terminate program if out of memory
}

//copy "Hello" into string


strcpy(str, "Hello");

//display string
cout << "String is " << str << endl;

//free memory
delete[] str;

return 0;
}

Visto che il C++ include il C, in un programma C++ è possibile usare il sistema di allocazione-deallocazione
del C. E’ importante però rispettare la seguente regola: un puntatore allocato con l’operatore new deve
essere deallocato con l’operatore delete, mentre un puntatore allocato con la funzione malloc deve
essere deallocato con la funzione free.

Liste
Iniziamo la descrizione delle strutture dati complesse più comuni con le liste. Queste ultime non sono altro
che una concatenazione logica di una struttura, struct, di base. La rappresentazione schematica di una
struttura dati a Lista è la seguente:

Prima di andare avanti bisogna dare una avvertenza. Attenzione a non cancellare il Puntatore Principale. Se
ciò accade, tutta la struttura dati andrà persa! Anzi, rimarrà anche allocata inutilmente memoria nel sistema!
La lista è governata dalla seguente regola: Un elemento della lista viene aggiunto sempre alla fine.
Ovviamente secondo le proprie esigenze sarà possibile modificare la struttura e comportamento. Ad esempio
una ottimizzazione nella velocità di inserimento potrebbe essere quella di mantenere un altro puntatore
ausiliario che punta all’ultimo elemento della lista.
L’importante è però definire la struttura e le sue regole di utilizzo e rispettarle sempre nel programma onde
evitare errori logici.
Vediamo come si realizza in C/C++ la definizione della struttura dati lista:

struct TLista;

struct TLista
{
34
Corso Base di Programmazione in C/C++ - Lezione 8

int dato1;
int dato2;
TLista *next;
};

TLista *Lista; //Il puntatore principale della lista

Esaminiamo il codice. La prima riga definisce l’esistenza di una struct chiamata TLista. Siccome la
definizione di una lista è ricorsiva ovvero all’interno della struttura c’è una variabile puntatore dello stesso
tipo della lista, la prima riga indica al compilatore di mantenere traccia del nome TLista e la sua definizione
sarà esplicitata in seguito.
Subito dopo segue la struct che definisce il contenitore dei dati mantenuti da una lista. Nel record è
possibile inserire qualunque tipo di dato standard o complesso definito prima della struct in esame. Notare
la presenza del puntatore *Lista di tipo TLista. Questo puntatore è la variabile che lega logicamente uno
dopo l’altro i vari record della lista.
Scriviamo adesso il codice C++ utile alla gestione della lista, nello specifico: inserimento di un elemento alla
fine della lista, stampa del contenuto della lista e cancellazione totale della lista:

#include <iostream.h>
#include <conio.h>

struct TLista;

struct TLista
{
int dato1;
int dato2;
TLista *next;
};

TLista *Lista;

void AggiungiElem(TLista *L, int d1, int d2);


void StampaLista(TLista *L);
void CancellaLista(TLista *L);

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


{
Lista = NULL;
Lista = new TLista;
Lista->dato1 = 0;
Lista->dato2 = 0;
Lista->next = NULL;

AggiungiElem(Lista,1,10);
AggiungiElem(Lista,2,20);
AggiungiElem(Lista,3,30);
AggiungiElem(Lista,4,40);
AggiungiElem(Lista,5,50);
StampaLista(Lista);
CancellaLista(Lista);
getch();
return 0;
}

void AggiungiElem(TLista *L, int d1, int d2)


{
TLista *temp, *ultimo;
temp = L;
//esploro la lista fino alla fine.

35
Corso Base di Programmazione in C/C++ - Lezione 8

while(temp!=NULL)
{
ultimo = temp;
temp = temp->next;
}
temp = new TLista;
temp->dato1 = d1;
temp->dato2 = d2;
temp->next = NULL;
ultimo->next = temp;
}

void StampaLista(TLista *L)


{
TLista *temp;
temp = L;
while(temp!=NULL)
{
cout << "(" << temp->dato1 << ", " << temp->dato2 << ")";
temp = temp->next;
}
}

void CancellaLista(TLista *L)


{
TLista *temp;
temp = L;
while(L!=NULL)
{
temp = L->next;
delete L;
L = temp;
}
}

La funzione AggiungiElem, aggiunge appunto un elemento alla lista. L’elemento è costituito da 2 numeri
interi. Il ciclo while non fa altro che esplorare la lista fino alla fine e mantenere un puntatore che al termine
del ciclo punterà all’ultimo elemento della lista, alloca la memoria per un nuovo record e lo collega alla lista,
precisamente all’ultimo.
La funzione StampaLista, tramite il ciclo while, scorre l’intera lista e per ogni record stampa tra parentesi
tonde i due campi del record.
L’ultima funzione, CancellaLista, è simile alla precedente. Il ciclo while scorre tutta la lista e mantiene
un puntatore ausiliario che al momento della cancellazione del record punta al record successivo nella lista.

Lista Bidirezionale
La precedente struttura dati ha un problema: la scansione può essere fatta solo in un senso. Volendo creare
una struttura dati lista che può essere esplorata in entrambi i sensi, è necessario apportare solo una piccola
modifica, visibile schematicamente nella figura seguente:

Come si può vedere, è sufficiente aggiungere un altro puntatore di tipo “lista” che punta all’elemento
logicamente precedente a quello in cui ci si trova.

36
Corso Base di Programmazione in C/C++ - Lezione 8

L’esempio del precedente paragrafo può essere trasformato in lista bidirezionale apportando pochissime
modifiche:

#include <iostream.h>
#include <conio.h>

struct TLista;

struct TLista
{
int dato1;
int dato2;
TLista *next;
TLista *prev;
};

TLista *Lista;

void AggiungiElem(TLista *L, int d1, int d2);


void StampaLista(TLista *L);
void CancellaLista(TLista *L);

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


{
Lista = NULL;
Lista = new TLista;
Lista->dato1 = 0;
Lista->dato2 = 0;
Lista->next = NULL;
Lista->prev = NULL;

AggiungiElem(Lista,1,10);
AggiungiElem(Lista,2,20);
AggiungiElem(Lista,3,30);
AggiungiElem(Lista,4,40);
AggiungiElem(Lista,5,50);
StampaLista(Lista);
CancellaLista(Lista);
getch();
return 0;
}

void AggiungiElem(TLista *L, int d1, int d2)


{
TLista *temp, *ultimo;
temp = L;
//esploro la lista fino alla fine.
while(temp!=NULL)
{
ultimo = temp;
temp = temp->next;
}
temp = new TLista;
temp->dato1 = d1;
temp->dato2 = d2;
temp->next = NULL;
temp->prev = ultimo;
ultimo->next = temp;
}

void StampaLista(TLista *L)

37
Corso Base di Programmazione in C/C++ - Lezione 8

{
TLista *temp;
temp = L;
while(temp!=NULL)
{
cout << "(" << temp->dato1 << ", " << temp->dato2 << ")";
temp = temp->next;
}
}

void CancellaLista(TLista *L)


{
TLista *temp;
temp = L;
while(L!=NULL)
{
temp = L->next;
delete L;
L = temp;
}
}

Le modifiche fatte sono:


 Aggiunta del puntatore TLista *prev; nella definizione della struttura TLista.
 Aggiunta della riga Lista->prev = NULL; nel main per l’inizializzazione del primo record.
 Aggiunta della riga temp->prev = ultimo; nella funzione AggiungiElem. Questa riga è quella che
realizza la connessione logica tra i record consecutivi della lista.

Tutto il resto rimane inalterato.

Pila
Quando si è discusso sulla lista mono-direzionale, si è detto che gli elementi devono essere aggiunti sempre
alla fine della lista. Ma se vogliamo invece inserirli all’inizio?? Ebbene, nessuno ci vieta di inserirli all’inizio, in
questo modo però stiamo creando un’altra struttura dati! Più precisamente stiamo creando una struttura dati
a Pila, nella quale gli elementi (i record) sono inseriti sempre “in testa”. Questa organizzazione dei dati è
anche detta LIFO dall’acronimo inglese Last In, First Out ovvero il primo ad essere inserito sarà l’ultimo ad
essere tolto. Lo schema logico di una simile struttura dati è identico in ogni sua parte a quello dalla lista
mono-direzionale, quindi non lo ripetiamo. Ciò che invece cambiano sono le operazioni di gestione.
Fondamentalmente le operazioni possibili sono 2 ma nessuno vieta di aggiungerne altre di aiuto. Le
operazioni principali si chiamano PUSH e POP. La prima inserisce un record in testa alla struttura dati mentre
la seconda cancella la testa della pila restituendo come valore di output il contenuto della dell’elemento
cancellato.
Riportiamo come di consueto un listato di esempio con l’aggiunta delle funzioni ausiliarie EMPTY che
controlla se la pila è vuota e CLEAR_STACK che svuota tutta la pila liberando anche la memoria:

#include <iostream.h>
#include <conio.h>

#define DIMBUF 50

struct PILA;
struct PILA
{
char strdir[DIMBUF];
struct PILA *next;
} *Pila;

void PUSH(char *s);


38
Corso Base di Programmazione in C/C++ - Lezione 8

void POP(char *s);


bool EMPTY(void);
void CLEAR_STACK(void);

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


{
char msg[DIMBUF];

PUSH("uno");
PUSH("DUE");
PUSH("tre");
PUSH("QUATTRO");
PUSH("--- fine ---");
while(!EMPTY())
{
POP(msg);
cout << msg << endl;
}
CLEAR_STACK();
getch();
return 0;
}

void PUSH(char *s)


{
struct PILA *t = new PILA;
if(t==NULL) return;
else
{
strcpy(t->strdir,s);
t->next = Pila;
Pila = t;
}
}

void POP(char *s)


{
struct PILA *t;

if(Pila!=NULL)
{
strcpy(s,Pila->strdir);
t = Pila;
Pila = Pila->next;
delete t;
}
}

bool EMPTY(void)
{
if(Pila==NULL) return true;
return false;
}

void CLEAR_STACK(void)
{
char *buf=new char[DIMBUF];
while(!EMPTY()) POP(buf);
}

39
Corso Base di Programmazione in C/C++ - Lezione 8

Struttura dati ad Albero


L’ultima struttura dati che esamineremo prende il nome di struttura dati ad Albero in quanto la sua
schematizzazione assomiglia ad un albero capovolto o ancora meglio ad un albero genealogico. Proprio sulla
base di quest’ultima analogia, quando si parla di struttura dati ad albero, si usano i termini padre, figlio,
ecc…, per i record che terminano la struttura si usa il termine foglia e in generale, un qualunque record della
struttura viene indicato come Nodo. Il primo nodo della struttura prende il nome di Radice. Lo schema logico
si presenta così:

Un albero che ha solo 2 figli si chiama Albero Binario, uno con 3 figli Albero ternario,…, uno con n figli si
chiama Albero n-ario.
Pensate un attimo, ma non vi sembra di aver gia visto da qualche parte nel vostro computer una
organizzazione simile? Di esempi ce ne sono tanti, ma quello più evidente è l’organizzazione dei file e cartelle
del sistema operativo nel quale i nodi sono le singole cartelle e le foglie i file veri e propri (documenti,
immagini,…). Altri esempi di impiego massiccio di queste strutture sono i database nei quali le righe delle
tabelle sono organizzate ad albero ordinato per una maggiore velocità di ricerca rispetto ad una
organizzazione sequenziale ordinata. Sugli alberi è stata fatta molta ricerca e sono nati tanti libri. Per lo
scopo di queste dispense è sufficiente sapere come sono organizzati gli alberi, come si inserisce e cancella
un nodo e come si può testare la presenza di un cero elemento nell’albero supponendo che sia ordinato.
Di seguito riportiamo il codice di un possibile esempio di utilizzo della struttura ad albero binario detto di
ricerca in quanto è governato dalla seguente regola: dato un nodo contenente il valore X, il figlio sinistro
contiene un dato minore di X mentre il figlio destro contiene un dato maggiore di X.

#include <iostream.h>
#include <conio.h>

struct TAlbero;
struct TAlbero
{
int dato;
TAlbero *Padre;
TAlbero *FiglioDS;
TAlbero *FiglioSN;
};

TAlbero *Albero;

void InOrder(TAlbero *A);


void PreOrder(TAlbero *A);
void PostOrder(TAlbero *A);
void TreeInsert(TAlbero *A, int v);
40
Corso Base di Programmazione in C/C++ - Lezione 8

TAlbero *Minimo(TAlbero *A);


TAlbero *Successore(TAlbero *A);
TAlbero *DeleteNode(TAlbero *A, int key);
TAlbero *TreeSearch(TAlbero *A, int v);

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


{
Albero=NULL;
TreeInsert(Albero,5); TreeInsert(Albero,1);
TreeInsert(Albero,10); TreeInsert(Albero,3);
TreeInsert(Albero,6); TreeInsert(Albero,4);
TreeInsert(Albero,0);
TreeInsert(Albero,7); TreeInsert(Albero,2);
TreeInsert(Albero,8);
cout << "Radice: " << Albero->dato << endl;
cout << "InOrder: ";
InOrder(Albero);
cout<<endl;
cout << "PreOrder: ";
PreOrder(Albero);
cout<<endl;
cout << "PostOrder: ";
PostOrder(Albero);
getch();
return 0;
}

void InOrder(TAlbero *A) //Detto anche Ordine Simmetrico


{
if(A!=NULL)
{
InOrder(A->FiglioSN);
cout << A->dato << " ";
InOrder(A->FiglioDS);
}
}

void PreOrder(TAlbero *A) //Detto anche Ordine Anticipato


{
if(A!=NULL)
{
cout << A->dato << " ";
PreOrder (A->FiglioSN);
PreOrder (A->FiglioDS);
}
}

void PostOrder(TAlbero *A) //Detto anche Ordine Differito


{
if(A!=NULL)
{
PostOrder (A->FiglioSN);
PostOrder (A->FiglioDS);
cout << A->dato << " ";
}
}

void TreeInsert(TAlbero *A, int val)


{
TAlbero *x, *y, *z;

41
Corso Base di Programmazione in C/C++ - Lezione 8

z = new TAlbero;
z->FiglioSN = NULL;
z->FiglioDS = NULL;
z->Padre = NULL;
z->dato = val;
y = NULL;
x = A;
while(x!=NULL)
{
y = x;
if(z->dato<x->dato) x = x->FiglioSN;
else x = x->FiglioDS;
}
z->Padre = y;
if(y==NULL) Albero = z;
else
if(z->dato<y->dato) y->FiglioSN = z;
else y->FiglioDS = z;
}

TAlbero *Minimo(TAlbero *A)


{
TAlbero *x;
x = A;
while(x->FiglioSN!=NULL) x = x->FiglioSN;
return x;
}

TAlbero *Successore(TAlbero *A)


{
TAlbero *x,*y;
x = A;
if(x->FiglioDS!=NULL) return Minimo(x->FiglioDS);
else
{
y = x->Padre;
while(y!=NULL && x==y->FiglioDS)
{
x = y;
y = y->Padre;
}
}
return y;
}

TAlbero *DeleteNode(TAlbero *A, TAlbero *z)


{
TAlbero *x,*y;
if(z->FiglioSN==NULL || z->FiglioDS==NULL) y = z; else y = Successore(z);
if(z->FiglioSN!=NULL) x = y->FiglioSN; else x = y->FiglioDS;
if(x!=NULL) x->Padre = y->Padre;
if(y->Padre==NULL) Albero = x;
else
if(y==(y->Padre)->FiglioSN) (y->Padre)->FiglioSN = x;
else (y->Padre)->FiglioDS = x;
if(y!=z) z->dato = y->dato;
return y;
}

TAlbero *TreeSearch(TAlbero *A, int v)


{

42
Corso Base di Programmazione in C/C++ - Lezione 8

TAlbero *temp;

temp = A;
while(temp!=NULL && v!=temp->dato)
{
if(v<temp->dato) temp = temp->FiglioSN;
else temp = temp->FiglioDS;
}
return temp;
}

Iniziamo la spiegazione delle funzioni. Supponiamo di avere in memoria una albero come in figura:

 InOrder(): La funzione richiama ricorsivamente se stessa sul figlio sinistro del nodo attuale. Quando il
nodo passato è nullo la ricorsione termina, si torna all’ultima funzione chiamante, si stampa il contenuto
del nodo e comincia la ricorsione sul figlio destro. Questo processo ricorsivo termina quando si raggiunge
l’ultima foglia figlia destra di un certo nodo. Il risultato stampato da questa funzione è: 10, 20, 30, 35,
40, 50, 70, 90, 80, 99.
 PreOrder(): La funzione stampa prima il contenuto del nodo e inizia il processo ricorsivo sul figlio
sinistro e poi su quello destro. Il risultato stampato da questa funzione è: 50, 30, 20, 10, 40, 35, 70, 80,
90, 99.
 PostOrder(): La funzione inizia il processo ricorsivo sul figlio sinistro, poi su quello destro ed infine
stampa il contenuto del nodo. Il risultato stampato da questa funzione è: 10, 20, 35, 40, 30, 90, 99, 80,
70.
 Minimo(): Ritorna il nodo che contiene il dato più piccolo ovvero, in base alla regola che governa un
albero binario di ricerca, ritorna la foglia più a sinistra seguendo il disegno precedente (10).

Per esercizio provare a descrivere il comportamento delle altre funzioni.

Puntatore a Funzione
Concludiamo questa lezione con la descrizione di un particolare tipo di puntatori che non tutti i linguaggi
possiedono: i puntatori a funzione. Così come abbiamo un puntatore ad intero, carattere, … così è possibile
avere un puntatore il cui contenuto sarà l’indirizzo di memoria nel quale una certa funzione inizia. Per coloro
i quali dopo questo corso vorranno imparare a programmare per Windows® senza utilizzare il framework VCL
o MFC si troveranno subito a dover utilizzare almeno un puntatore a funzione. Senza addentrarci negli
aspetti della programmazione base di Windows, diciamo soltanto che il sistema operativo utilizzerà questo
puntatore in modo da poter richiamare il gestore dei messaggi che Windows manda all’applicazione.
La sintassi di dichiarazione di un puntatore a funzione è la seguente:

tipo_restituito (*nome_funz)(parametri);

43
Corso Base di Programmazione in C/C++ - Lezione 8

dove parametri è un elenco di parametri di input della funzione dichiarato come di consueto. Vediamo un
esempio:

...
double (*f1)(double x1);
double (*f2)(double x2,double x3);
...
cout << “2 elevato alla 32= ” << pow(2,32) << endl;
f2 = pow;
cout << “2 elevato alla 32= ” << f1(2,32) << endl;
...
cout << “Coseno di 0.321 radianti= ” << cos(0.321) << endl;
f1 = cos;
cout << “Coseno di 0.321 radianti= ” << f1(0.321) << endl;
...

44
Corso Base di Programmazione in C/C++ - Lezione 9

Lezione 9 – Oggetti
In passato, i programmatori scrivevano codice che manipolava i dati, i dati stessi e il codice che li
manipolava venivano trattati come due elementi separati. La programmazione orientata agli oggetti (OOP),
invece, tratta dati e codice come una singola entità, nota come classe. Il concetto della OOP introduce nuove
parole di uso comune durante la programmazione in C++ (tutto ciò che verrà detto da qui in poi non sarà
più applicabile al C):

CLASSE
ISTANZA
OGGETTO
INCAPSULAMENTO
MEMBRO DATI
FUNZIONE MEMBRO
COSTRUTTORE e DISTRUTTORE

La OOP è un nuovo modi di pensare la programmazione. Non si deve pensare più al codice lineare e alla
manipolazione di alcuni dati esterni con una funzione. Si deve adottare l’impostazione mentale che i dati e il
codice sono raggruppati all’interno dello stesso corpo.

Classe: Incapsulamento
Una classe è una struttura di dati che contiene tutto quanto è necessario per memorizzare e manipolare i
dati. Nella OOP, ogni variabile definita all’interno di una classe è denominata membro dati. Le funzioni che
manipolano i dati sono dette invece funzioni membro. Dovrebbe essere possibile manipolare i dati membro
solo tramite le funzioni membro. Le funzioni esterne non possono accedere ad un membro dati. Questo
modo di agire realizza quello che in gergo si chiama incapsulamento dell’informazione.
Vediamo come si crea una classe in C++:

class nome_classe
{
//dati e funzioni membro
};

ad esempio:

class shape
{
//dati e funzioni membro dichiarati alla solita maniera
};

int main()
{
shape forma; //esempio di dichiarazione di una var.
return 0;
}

Quindi la classe viene dichiarata usando la parola riservata class seguita dal nome da dare alla classe.
L’incapsulamento dell’informazione è utile quando non si vuole far accedere a dati critici per il corretto
funzionamento del programma. Come vedremo in seguito, il divieto di accesso diretto ai dati non è
comunque controllato, ovvero, è possibile rendere visibili all’esterno dati non critici.
Un ultimo concetto che bisogna avere ben chiaro in mente è la distinzione tra classe ed oggetto. La prima è
un qualcosa di astratto che non esiste fisicamente e l’unico modo per poterla vedere è leggere la sua
definizione (class x {...};), un oggetto è fisicamente presente nella memoria di un calcolatore, ha un
proprio spazio di memoria e cosa più importante può essere utilizzato ovvero creato, distrutto accedere ai
dati e alle funzioni membro, copiato e passato come parametro di input ad una funzione oppure restituito
come parametro di output da un’altra funzione.
45
Corso Base di Programmazione in C/C++ - Lezione 9

Membri Dati e Funzioni Membro


A questo punto dovrebbe essere gia ben chiaro la distinzione tra un membro dati ed una funzione membro.
Ma che differenza c’è tra una funzione membro ed una normalissima funzione esterna? E tra un membro dati
ed una variabile generale? Per entrambe le domande la risposta è: Nessuna! Esiste solo una distinzione
concettuale, un dato o funzione membro è dichiarata all’interno di una classe ed esiste solo se viene creato
un oggetto, mentre le variabili e funzioni generali esistono all’esterno della classe/oggetto. Infatti
estendendo l’esempio precedente possiamo scrivere:

class shape
{
public: //Vedremo dopo cosa significa
double Larghezza, Altezza;
double area;

double CalcolaArea(double l, double h);


};

Semplice no?
Volendo ora scrivere l’implementazione della funzione CalcolaArea(); come bisogna procedere? La
sintassi da utilizzare nella definizione delle funzioni membro è la seguente:

tipo_restituito nome_classe::funzione_membro(parametri)
{
//istruzioni
}

Ovvero

double shape::CalcolaArea(double l, double h)


{
Larghezza = l;
Altezza = h;
area = l*h;
return area;
}

Come si utilizza una classe? Estendiamo il main visto prima.

int main()
{
shape forma;

forma.Larghezza = 10.9;
forma.Altezza = 100.0;
cout << forma.CalcolaArea(10.0, 12.5) << endl;
return 0;
}

Il programma precedente stamperà 125. Quindi come possiamo osservare si utilizza la stessa notazione
utilizzata per le struct. Facciamo lo stesso esempio ma questa volta i puntatori:

int main()
{
shape *forma;

forma = new shape;


forma->Larghezza = 10.9;
forma->Altezza = 100.0;
46
Corso Base di Programmazione in C/C++ - Lezione 9

cout << forma->CalcolaArea(10.0, 12.5) << endl;


delete forma;
return 0;
}

Visibilità esterna dei dati e funzioni


Senza addentrarci in discussioni filosofiche universitarie sulla visibilità dei dati e funzioni membro, veniamo
subito al dunque! Ci sono 3 modi possibili di “nascondere” l’informazione che si realizzano utilizzando le 3
parole riservare public, private e protected. Più precisamente la sintassi è la seguente:

class nome_classe
{
public:
//dati e funzioni membro pubbliche
private:
//dati e funzioni membro private
protected:
//dati e funzioni membro protette
};

Notare i ‘:’ dopo le parole chiavi. Non è necessario utilizzarle tutte, inoltre l’ordine può essere come più si
ritiene giusto ed infine è possibile avere più sezioni private, pubbliche o protette:

class classe1
{
public:
//dati e funzioni membro pubbliche
private:
//dati e funzioni membro private

public:
...
};

Descriviamo ora con più precisione il significato di questi 3 modi di proteggere l’informazione.

Public
Qualunque membro dati e funzione membro dichiarata dopo la parola chiave public è detta essere una
funzione pubblica o dato pubblico. Essere pubblico significa che all’esterno della classe si può accedere
direttamente a quella funzione o variabile. Ad esempio, è lecito scrivere:

class classe1
{
public:
int d1;
char d2;
char LeggiCarattere();
private:
//dati e funzioni membro private
};

int main()
{
classe1 c;
c.d2 = c.LeggiCarattere();
}

47
Corso Base di Programmazione in C/C++ - Lezione 9

Private
Qualunque membro dati e funzione membro dichiarata dopo la parola chiave private è detta essere una
funzione privata o dato privato. Essere privato significa che all’esterno della classe non è possibile accedere
direttamente a quella funzione o variabile. Ad esempio:

class classe1
{
public:
int d1;
char d2;
char LeggiCarattere();
private:
int priv1;
int leggiIntero();
};

int main()
{
int i;
classe1 c;
c.d2 = c.LeggiCarattere();
i = c.leggiIntero(); //ERRORE
}

Il precedente programma è sbagliato in quanto la funzione leggiIntero(); è privata ovvero non visibile
dall’esterno. Una qualunque funzione dichiarata come pubblica ha comunque accesso, nella sua
implementazione, ai dati e funzioni membro privati.

Protected
Lo status di dato o funzione protetta è simile a quello di privato ma ha un comportamento particolare
quando una classe eredita i dati e funzioni membro di un’altra classe. Per ora sospendiamo il discorso e lo
riprenderemo più in là quando parleremo della ereditarietà.

Costruttore e Distruttore
Nella creazione di una classe è possibile dichiarare 2 funzioni particolari che non vengono chiamate
direttamente nel codice C++, queste sono il Costruttore e il Distruttore.
Il costruttore ha il nome uguale a quello della classe, può avere parametri di input e non restituisce niente e
viene chiamato automaticamente quando la variabile oggetto viene creata. Lo scopo del costruttore è quello
di inizializzare i dati membro ed allocare eventualmente memoria dinamica. Non utilizzate mai una chiamata
diretta al costruttore di una classe! Il costruttore è opzionale, ovvero, non è necessario che la classe abbia
un costruttore.
Il distruttore ha un nome uguale a quello della classe ma è preceduto dal carattere tilde ‘~’, non ha
parametrici input e non restituisce niente. Viene chiamato automaticamente quando viene distrutta (o
deallocata) la variabile oggetto. Lo scopo principale è quello di deallocare la memoria dinamica utilizzata
nella classe. Anche per il distruttore e consigliabile evitare di richiamarlo direttamente ed inoltre non è
necessario che sia presente.
Per entrambi infine è obbligatorio che siano definiti nella sezione pubblica della classe, in caso contrario il
compilatore darà errore.
Facciamo qualche esempio concreto

class shape
{
public: //Vedremo dopo cosa significa
shape(); //costruttore
~shape(); //distruttore

48
Corso Base di Programmazione in C/C++ - Lezione 9

double Larghezza, Altezza;


double area;

double CalcolaArea(double l, double h);


};

shape::shape()
{
cout << ″Variabili inizializzate″ << endl;
Larghezza = Altezza = 0;
}

shape::~shape()
{
cout << ″Distruzione dell’oggetto″ << endl;
}

double shape::CalcolaArea(double l, double h)


{
Larghezza = l;
Altezza = h;
area = l*h;
return area;
}

int main()
{
shape *forma;

forma = new shape;


forma->Larghezza = 10.9;
forma->Altezza = 100.0;
cout << forma->CalcolaArea(10.0, 12.5) << endl;
delete forma;
return 0;
}

Il programma precedente produrrà il seguente output sullo schermo:

Variabili inizializzate
125
Distruzione dell’oggetto

Ereditarietà
Con l’introduzione della OOP è stato introdotto un nuovo e potente metodo di sviluppo dei programmi:
l’ereditarietà. Come il nome suggerisce, con il C++ (ovvero con qualunque linguaggio ad oggetti), è possibile
creare una classe che eredita i dati e le funzioni membro di un’altra classe (la classe padre). Ereditare vuol
dire che tutto ciò che era contenuto nella classe padre diventa parte integrante della classe figlio e
all’esterno sembra un’unica classe.
Ci sono 3 modi di ereditare i dati e membri da una classe padre: pubblico, privato o protetto. Inoltre l’eredità
può essere multipla ovvero una classe eredita da più classi padre.
In gergo più tecnico si parla di classe derivata e classe base. In questo modo, la creazione di più livelli di
derivazione da luogo ad un sistema gerarchico di classi. Alcuni di questi sistemi sono noti come framework
VCL (di Borland®) ed MFC (di Microsoft®).

49
Corso Base di Programmazione in C/C++ - Lezione 9

Riprendiamo il concetto di dato o membro protetto (protected) di una classe, è giunto il momento di
spiegare meglio il suo significato. Qualsiasi cosa protetta si comporta come privata all’interno della propria
classe. Questo significa che non è possibile accedere ad essa dal mondo esterno. Gli elementi protetti
possono essere comunque ereditati da una classe derivata e nella classe derivata essi si comportano come
se fossero privati. Una classe derivata non può ereditare gli elementi privati della classe base.
La tabella che segue mostra come sono ereditati i dati e funzioni membri in base a come la classe base viene
ereditata (colonne).

Definizione del dati e membri della classe

Public Protected Private


Public Pubblico privato Non acc. dalla classe base
Protected Privato Privato Non acc. dalla classe base
Private Privato Privato Non acc. dalla classe base

Vediamo come si scrive in C++ il codice che permette di eseguire l’ereditarietà.

class nome_classe_derivata: public nome_classe_base


{
//dati e funzioni membro
};

class nome_classe_derivata: private nome_classe_base


{
//dati e funzioni membro
};

class nome_classe_derivata: protected nome_classe_base


{
//dati e funzioni membro
};

Per una eredità multipla si scrive:

class nome_classe_derivata: public nome_classe_base1, nome_classe_base2


{
//dati e funzioni membro
};

50
Corso Base di Programmazione in C/C++ - Lezione 9

Concludiamo il discorso sulla ereditarietà descrivendo un ultimo importante concetto. Se un classe base ha
un costruttore allora la classe derivata deve esporre un costruttore che abbia gli stessi parametri del
costruttore della classe base più eventuali altri parametri. Il passaggio dei parametri dal costruttore della
classe derivata a quello della classe base avviene in un modo diverso dal solito:

class base
{
public:
base(int p1, int p2);
...
};

class derivata : public base


{
public:
derivata(double a, int b, int c);
...
};

derivata::derivata(double a, int b, int c) : base(b, c)


{
...
}

Vediamo un semplice esempio funzionante di ereditarietà:

#include <iostream.h>

class shape
{
protected:
double length;
double height;
double area;
public:
void CalcArea();
void ShowArea();
shape(double l = 0, double h = 0);
};

class ThreeD : public shape


{
protected :
double depth;
double volume;
public:
void CalcVol();
void ShowVol();
ThreeD(double z=0,double x=0,double y=0);
};

ThreeD::ThreeD(double z, double x, double y):shape(x,y)


{
depth = z;
}

shape::shape(double l, double h)
{
length = l;
height = h;
}
51
Corso Base di Programmazione in C/C++ - Lezione 9

void shape::CalcArea()
{
area = length * height;
}

void shape::ShowArea()
{
cout << "THE AREA IS : " << area;
}

void ThreeD::CalcVol()
{
volume = depth * length * height;
}

void ThreeD::ShowVol()
{
cout << "THE VOLUME IS : " << volume << endl;
}

main()
{
double x, y, z;

cout << "ENTER THE LENGTH : ";


cin >> x;
cout << "ENTER THE HEIGHT : ";
cin >> y;
cout << "ENTER THE DEPTH : ";
cin >> z;
ThreeD box(z,x,y);
box.CalcVol();
box.ShowVol();
return 0;
}

Polimorfismo
Prima di definire il concetto di Polimorfismo è fondamentale che il concetto di ereditarietà sia ben chiaro ed
inoltre bisogna apprenderne un altro: classe astratta. La classe astratta è uno strumento di progettazione
che consente di definire funzionalità di base, lasciando che le funzionalità specifiche del programma vengano
definite successivamente. Le classi astratte hanno le seguenti caratteristiche:

 Hanno almeno una funzione membro virtuale pura (che vedremo tra poco).
 Le classi astratte vengono usate come classe base per creare classi derivate.
 Ogni classe che contiene una funzione virtuale pura, non può creare un oggetto.

Una funzione è definita virtuale anteponendo alla sua definizione la parola chiave virtual. Questo significa
che tutte le classi derivate condividono la stessa funzione per evitare l’ambiguità.

52
Corso Base di Programmazione in C/C++ - Lezione 9

Una classe derivata può sovrascrivere la definizione di una funzione membro virtuale ridefinendo la sua
funzionalità. La nuova definizione verrà utilizzata in tutte le istanze degli oggetti della classe derivata.
Quando una funzione viene dichiarata virtuale nella classe base, essa rimane virtuale in tutte le classi
derivate. Si è liberi di includere od omettere la parola virtual nella ridefinizione della funzione membro.
Una funzione virtuale pura, è una funzione virtuale impostata a zero e non viene specificata alcuna
definizione per essa. Sembra un po’ strano. La sintassi è la seguente:

virtual tipo_restituito nome_funzione(parametri) = 0;

ad esempio

virtual void f1()=0;

Essa non esegue nulla eccetto impedire alla classe di creare un’istanza dell’oggetto e occupare memoria per
assegnargli una definizione quando eredita una classe.
A questo punto si può dire definire il concetto di polimorfismo: Il polimorfismo è la capacità di una funzione
membro di avere differenti funzionalità in vari punti dell’albero gerarchico. La funzionalità utilizzata è quella
più appropriata per l’oggetto al quale appartiene.

Diamo un esempio:

#include <iostream.h>
#include <stdlib.h>

#define TAB '\t'

class SEQUENCE
{
protected:
int back;
char data[10];

public:
virtual void POKE(char ch);
virtual void POP(void) = 0;
virtual void PEEK(void) = 0;
SEQUENCE();
};

SEQUENCE::SEQUENCE()
{
back = 0;
}

void SEQUENCE::POKE(char ch)


53
Corso Base di Programmazione in C/C++ - Lezione 9

{
if(back < 9)
{
back++;
data[back] = ch;
cout << endl;
}
else
cout << endl << "SORRY - FULL" << endl << endl;
}

class MyDEQUE : public SEQUENCE


{
public:
MyDEQUE();
void POP(void);
void PEEK(void);
};

void MyDEQUE::POP(void)
{
int index;
char item;

if(back > 0)
{
cout << "LEAVE DEDUE FROM FRONT OR BACK (f/b) : ";
cin >> item;
if((item == 'b') || (item == 'B'))
{
back--;
}
if((item == 'f') || (item == 'F'))
{
for(index = 0; index < back; index++)
{
data[index] = data[index + 1];
}
back--;
}
}
else
{
cout << endl << "DEDUE IS EMPTY";
cout << endl << endl;
}
}

void MyDEQUE::PEEK(void)
{
int x;

if (back == 0)
{
cout << endl << "DEDUE IS EMPTY";
cout << endl << endl;
}
else
{
for (x = 1; x <= back; x++)
{

54
Corso Base di Programmazione in C/C++ - Lezione 9

cout << data[x] << TAB;


}
cout << endl << endl;
}
}

MyDEQUE::MyDEQUE() : SEQUENCE()
{
cout << "LIST CREATED" << endl << endl;
}

char menu(void);

main()
{
char ch;
char poker;

MyDEQUE D;
while (1)
{
ch = menu();
switch(ch)
{
case '1' : cout <<
"Enter the character : ";
cin >> poker;
D.POKE(poker);
break;
case '2' : D.POP();
break;
case '3' : D.PEEK();
break;
case '4' : exit(0);
}
}
return(0);
}

char menu(void)
{
char choice;

cout << "1...Join the DEQUE" << endl;


cout << "2...Leave the DEQUE" << endl;
cout << "3...Show the DEQUE" << endl;
cout << "4...Quit the program" << endl << endl;
cout << "Enter your choice : ";
cin >> choice;
return choice;
}

55
Corso Base di Programmazione in C/C++ - Lezione 10

Lezione 10 – Gestione dei File


Nelle precedenti lezioni, tutti gli input o output avvenivano tramite la tastiera e lo schermo. In questa lezione
impareremo ad effettuare letture e scritture su file presenti sulla memoria di massa (hard disk). Senza
addentrarci troppo su come è fatto un calcolatore, i diversi tipi di dispositivi di memorizzazione e velocità di
accesso, descriviamo subito quali sono le operazioni di base che bisogna fare per poter utilizzare il file.
La gestione di un file, di qualunque tipo esso sia, è fatta in tre passi:

1. Apertura: Questa operazione crea una associazione tra il file su disco, individuato tramite il suo
percorso e nome, ed una variabile locale al programma chiamata in gergo file descriptor o Handle. In
questa operazione viene anche fatta la scelta della modalità di apertura: lettura, scrittura, append
(scrittura che inizia alla fine del file), binario o ASCII.
2. Lettura/Scrittura: Queste operazioni permettono l’effettiva scrittura e lettura dei dati su e da file. Il
contenuto da scrivere/leggere ovviamente è memorizzato in una variabile locale al programma. In base
al tipo di apertura del file (binario o ASCII) sarà possibile leggere/scrivere dati direttamente in formato
testo o binario.
3. Chiusura: Quando non è più necessario l’utilizzo del file è consigliabile chiudere il file. Al contrario
dell’apertura, questa operazione distrugge il collegamento che si era creato prima. Questa operazione
non è obbligatoria ma siccome il numero di file che un programma può mantenere aperti
contemporaneamente è limitato, per evitare la generazione di un errore di apertura file è consigliabile
chiudere un file dopo il suo utilizzo.

Handle – Apertura e chiusura di un file


Per capire cos’è un handle osserviamo il disegno sottostante:

Dal disegno si evince che un handle non è altro che un puntatore ad una struttura dati interna al sistema
operativo, alla quale ovviamente il programma ha accesso, contenente delle informazioni sul file aperto:
dimensione, attributi, posizione attuale nel file per le operazioni di lettura, data di creazione ed ultima
modifica ed infine un puntatore alla posizione fisica del file nell’HDD che un programmatore non userà mai.
Questa struttura potrebbe essere nascosta al programmatore è visibile solo parzialmente tramite una
struttura intermedia fornita dal linguaggio di programmazione. Ad esempio in Visual Basic l’handle di un file
è un numero intero a cominciare da zero.
Il modo di aprire e chiudere un file in C e C++ è diverso, esaminiamoli separatamente a partire dal
linguaggio C (come al solito in un programma C++ è possibile utilizzare le chiamate fatte in C).
L’esempio che segue include anche operazioni lettura e scrittura che vedremo in seguito, questo esempio
apre 2 file, uno in lettura e l’altro in scrittura. Il secondo file viene riempito con lo stesso contenuto del primo
ed infine chiusi. Notare la gestione dell’errore di apertura file.

#include <stdio.h>

int main(void)
56
Corso Base di Programmazione in C/C++ - Lezione 10

{
FILE *in, *out;

if((in = fopen("TESTFILE.DAT", "rt")) == NULL)


{
fprintf(stderr, "Cannot open input file.\n");
return 1;
}

if((out = fopen("TESTFILE.BAK", "wt")) == NULL)


{
fprintf(stderr, "Cannot open output file.\n");
return 1;
}

while(!feof(in))
fputc(fgetc(in), out);

fclose(in);
fclose(out);

return 0;
}

La prima riga di codice all’interno del main dichiara 2 variabili puntatore alla struttura FILE. Viene subito
aperto il file di input tramite l’operazione in = fopen("TESTFILE.DAT", "rt"), nella quale viene
specificato il nome del file e la modalità lettura (r) testo (t). Se l’operazione fallisce la variabile ‘in’ conterrà
NULL. La chiusura dei file è fatta nelle 2 istruzioni precedenti il return: fclose(in); e fclose(out);.
Una funzione interessante è la feof() prensente nella condizione di test del ciclo while. Questa funzione
controlla che il puntatore al file, nella struttura descritta all’inizio del paragrafo, non sia arrivato alla fine del
file stesso. In altre parole questa funzione ci consente di determinare quando il file termina.
La lettura di un carattere dal file di input si esegue con la funzione fgetc(in) che ritorna il carattere letto,
mentre la scrittura di un carattere viene fatta con fputc() la quale ha come parametri di input il carattere
da scrivere e l’handle del file di output.

Passiamo al C++. Un semplice esempio di gestione file è il seguente:

#include <fstream.h>

main()
{
int loop = 0;
int x;

char filename[12] = "a:xtest.dat";


int mode = (ios::in | ios::binary);

fstream fin( filename, mode );


if (!fin) cerr << "Unable to open file";
while (fin >> x)
{
cout << x << endl;
loop++;
}
fin.close();
return(0);
}

57
Corso Base di Programmazione in C/C++ - Lezione 10

Ricordiamo che il C++ è un linguaggio ad oggetti, quindi quale migliore esempio di utilizzo degli oggetti
quello di incapsulare in una classe tutta la gestione dei file! Infatti la classe che permette di gestire i file è
chiamata fstream e nell’esempio l’oggetto di questo tipo e fin.
L’inizializzazione avviene passando al costruttore dell’oggetto i parametri filename e mode. In questo
esempio il file è aperto in lettura (ios::in) binaria (ios::binary). La chiusura del file è fatta tramite
l’ultima istruzione: fin.close(), mentre la lettura con l’istruzione fin >> x. Se quest’ultima istruzione
non legge alcun intero dal file ha come risultato zero e quindi si esce dal ciclo while.

File di testo e file binari


La distinzione tra un file di testo o binario è utile solo al programmatore, per il calcolatore non esiste alcuna
differenza. Un file di testo è un file che contiene solo ed esclusivamente lettere, segni di punteggiatura e
numeri. Un file di testo se aperto con un qualunque editor di testo sarà possibile leggere chiaramente il suo
contenuto. Attenzione: Se nel nostro programma abbiamo una variabile di tipo intero e dobbiamo scriverla in
un file di testo, è necessario convertire il valore intero nella sua rappresentazione stringa. Lo stesso discorso
vale per i valori in virgola mobile. Se invece il file è binario, ovvero un file contenente un serie di byte
qualunque, l’operazione di conversione non è necessaria.
In C le operazioni utilizzate sono:

fprintf
Il prototipo è

int fprintf(FILE *stream, const char *format[, argument, ...]);.

ed un altro esempio di utilizzo è: fprintf(stream, "%d %c %f", i, c, f);


Dopo il segno “%” è possibile mettere una stringa composta nel seguente modo:

% [flags] [width] [.prec] [F|N|h|l|L] type_char

Dove:
[flags] : Segno dei valori numerici e può assumere il carattere ‘-‘, ‘+’ o la stringa “blank”.

[width]: Numero minimo di caratteri da utilizzare si può scrivere “%0n” ad indicare che se il numero di
caratteri è minore di n allora la saranno stampati degli zeri.

[.prec] : Precisione, ovvero il numero di caratteri da stampare dopo la virgola ad es. “%.3f”.

[F|N|h|l|L] : Modificatore del tipo di caratteri che segue.

type_char : Vedi figura seguente

58
Corso Base di Programmazione in C/C++ - Lezione 10

fscanf
Il prototipo è

int fscanf(FILE *stream, const char *format[, address, ...]);

I parametri utilizzati sono gli stessi della fprintf. L’unica differenza alla quale bisogna fare attenzione è la
modalità di passaggio delle variabili che conterranno i valori letti. Questa regola vale non solo per la fscanf
ma anche per la scanf.

scanf(“%i”, &val);

nella quale è evidente la & davanti al nome della variabile val. Questa notazione indica che bisogna passare
alla funzione l’indirizzo della variabile.

Per i file binari in C è possibile utilizzare la funzione open il cui prototipo è:

int open(const char *path, int access [, unsigned mode]);

nella quale la variabile access permette di impostare oltre che la modalità testo anche quella binaria.
Vediamo un esempio applicativo:

#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <io.h>

int main(void)
{
int handle;
char msg[] = "Hello world";

if ((handle = open("TEST.$$$", O_CREAT | O_TEXT)) == -1)


{
perror("Error:");

59
Corso Base di Programmazione in C/C++ - Lezione 10

return 1;
}
write(handle, msg, strlen(msg));
close(handle);
return 0;
}

I possibili valori che il parametro access può assumere sono divisi in 2 gruppi:

Read/Write Flags
O_RDONLY Apertura in sola lettura.
O_WRONLY Apertura in sola scrittura.
O_RDWR Apertura in lettura e scrittura.

Other Access Flags


O_NDELAY Non è usato; per compatibilità con UNIX.
O_APPEND Se impostato, il puntatore al file punta alla fine del file.
O_CREAT Se il file esiste, questo flag non ha effetto, se invece non esiste il file viene creato.
O_TRUNC Se il file esiste, la sua lunghezza viene impostata a zero.
O_EXCL Usato solo con O_CREAT. Se il file esiste, viene generato un errore.
O_BINARY Apre il file in modalità binaria.
O_TEXT Apre il file in modalità testo.

Le operazioni di lettura e scrittura avvengono tramite le funzioni read e write.

int read(int handle, void *buf, unsigned len);

int write(int handle, void *buf, unsigned len)

In C++ invece è possibile utilizzare un unico oggetto che gestisce sia la lettura/scrittura binaria che quella
testuale. Per la lettura/scrittura binaria è sufficiente specificare la costante ios::binary vista nell’esempio
sul C++. Nel caso in cui sia necessaria la lettura/scrittura testuale basta omettere tale costante. Le
operazioni di lettura e scrittura avvengono gli usuali operatori >> e <<.

60

Potrebbero piacerti anche