Professional Documents
Culture Documents
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):
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.
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
oppure
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
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>
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:
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:
... //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:
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 ;-)
/*
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
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
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:
//istruzioni
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
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.
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
};
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;
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];
ListaModelli[57].Posizione[2] = 3;
12
Corso Base di Programmazione in C/C++ - Lezione 4
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;
13
Corso Base di Programmazione in C/C++ - Lezione 4
x += 1;
ma piuttosto:
x++;
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;
x++;
x--;
#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:
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;
(a == b)
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
Nelle espressioni in cui compaiono sia "&&" che "||" va ricordato che il primo ha precedenza rispetto al
secondo, perciò
equivale a
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.
a = b = c = d;
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;
! 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
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
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:
...
#include <iostream.h>
int main()
{
int anni;
19
Corso Base di Programmazione in C/C++ - Lezione 5
#include <iostream.h>
#define MyAge 21
main()
{
int anni;
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
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.
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 << 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
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:
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).
#include <iostream.h>
int main()
23
Corso Base di Programmazione in C/C++ - Lezione 6
{
int x;
#include <iostream.h>
main()
{
int loop;
int total = 0; // declare and initialize variables
int number = 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;
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.
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
#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
}
...
26
Corso Base di Programmazione in C/C++ - Lezione 6
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:
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.
#include <iostream.h>
// Prototipo
void Show(int x);
28
Corso Base di Programmazione in C/C++ - Lezione 7
int main()
{
int number;
// 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).
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>
int main()
{
int* number; //puntatore ad intero
29
Corso Base di Programmazione in C/C++ - Lezione 7
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;
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
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.
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:
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;
/* display string */
printf("String is %s\n", str);
/* free memory */
free(str);
return 0;
}
#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;
//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;
};
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;
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;
}
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;
}
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;
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;
}
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;
}
}
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;
PUSH("uno");
PUSH("DUE");
PUSH("tre");
PUSH("QUATTRO");
PUSH("--- fine ---");
while(!EMPTY())
{
POP(msg);
cout << msg << endl;
}
CLEAR_STACK();
getch();
return 0;
}
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
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;
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;
}
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).
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
class shape
{
public: //Vedremo dopo cosa significa
double Larghezza, Altezza;
double area;
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
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;
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
shape::shape()
{
cout << ″Variabili inizializzate″ << endl;
Larghezza = Altezza = 0;
}
shape::~shape()
{
cout << ″Distruzione dell’oggetto″ << endl;
}
int main()
{
shape *forma;
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).
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);
...
};
#include <iostream.h>
class shape
{
protected:
double length;
double height;
double area;
public:
void CalcArea();
void ShowArea();
shape(double l = 0, double h = 0);
};
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;
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:
ad esempio
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>
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;
}
{
if(back < 9)
{
back++;
data[back] = ch;
cout << endl;
}
else
cout << endl << "SORRY - FULL" << endl << endl;
}
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
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;
55
Corso Base di Programmazione in C/C++ - Lezione 10
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.
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;
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.
#include <fstream.h>
main()
{
int loop = 0;
int x;
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.
fprintf
Il prototipo è
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”.
58
Corso Base di Programmazione in C/C++ - Lezione 10
fscanf
Il prototipo è
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.
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";
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.
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