Complementi Di Programmazione - Appunti
Complementi Di Programmazione - Appunti
programmazione in C
Appunti delle lezioni di
Complementi di Programmazione
Giorgio Grisetti
Luca Iocchi
Daniele Nardi
Fabio Patrizi
Alberto Pretto
Edizione 2023/2024
2024
Indice
1. Da Python a C 1
1.1. Architettura del calcolatore 1
1.1.1. Architettura della CPU 1
1.1.2. Architettura della memoria 2
1.1.3. Programmazione del calcolatore 2
1.2. Linguaggi di programmazione 3
1.2.1. Linguaggio macchina e linguaggi alto livello 3
1.2.2. Interpreti e compilatori 4
1.2.3. Esempio programma Python 4
1.2.4. Paradigmi di programmazione 5
1.3. Il linguaggio C 5
1.3.1. Tecniche di programmazione in C 6
1.3.2. Scrivere, compilare ed eseguire un programma C 8
1.4. Variabili 12
1.4.1. Dichiarazione di variabili 12
1.4.2. Notazione grafica per la rappresentazione di variabili 14
1.4.3. Assegnazione 14
1.4.4. Inizializzazione delle variabili 15
1.5. Funzioni 16
1.5.1. Intestazione di una funzione 17
1.5.2. Parametri di una funzione 17
1.5.3. Risultato di una funzione 18
1.5.4. Funzioni definite in math.h 18
1.5.5. La funzione exit 19
vi Introduzione alla programmazione in C
3. Puntatori 93
3.1. Memoria, indirizzi e puntatori 93
3.1.1. Operatore indirizzo-di 94
3.1.2. Operatore di indirizzamento indiretto 94
3.1.3. Variabili di tipo puntatore 95
3.1.4. Esempio: uso di variabili puntatore 96
3.1.5. Condivisione di memoria 97
3.1.6. Assegnazione 98
3.1.7. Uguaglianza tra puntatori 98
viii Introduzione alla programmazione in C
4. Funzioni 111
4.1. Astrazione sulle operazioni: funzioni 111
4.2. Definizione di funzioni 111
4.2.1. Risultato di una funzione: l’istruzione return 113
4.2.2. Esempi di definizione di funzioni 114
4.2.3. Funzioni: istruzioni o espressioni? 115
4.2.4. Segnatura di una funzione 115
4.2.5. Dichiarazione delle funzioni 116
4.3. Passaggio dei parametri 117
4.3.1. Passaggio di parametri per valore 117
4.3.2. Passaggio di parametri tramite puntatori 119
4.3.3. Passaggio di parametri per riferimento 123
4.3.4. Valori restituiti di tipo puntatore 124
4.3.5. Parametri di tipo puntatore a funzione 125
4.4. Variabili locali di una funzione 126
4.4.1. Campo d’azione delle variabili locali 126
4.4.2. Variabili locali definite static 127
4.5. Variabili globali 128
4.6. Tempo di vita delle variabili 129
4.7. Modello run-time 130
4.7.1. Record di attivazione 131
4.7.2. Pila dei record di attivazione 132
Indice ix
8. Ricorsione 223
8.1. Funzioni ricorsive 223
Indice xi
a = b + c
print ( a )
C:
int a = b + c ;
printf ( " % d \ n " ,a ) ;
• Compilatore
L’interprete (es. Python) considera le istruzioni del linguaggio di
programmazione una alla volta traducendole direttamente in istruzioni
in linguaggio macchina ed eseguendole direttamente.
Il compilatore di un linguaggio effettua la traduzione in linguaggio
macchina dell’intero programma producendo un programma in lin-
guaggio macchina. L’esecuzione del programma avviene separatamente
dal processo di traduzione. C è un linguaggio compilato.
Principali vantaggi dei linguaggi compilati (ad es. C):
• Velocità di esecuzione
calcololungo.py
import time
s = 0
for i in range (0 ,100) :
s += 2** i
time . sleep (0.1)
1.3. Il linguaggio C
Queste dispense trattano il linguaggio di programmazione C. C è un
linguaggio di programmazione di alto livello, compilato, che supporta i
paradigmi di programmazione imperativo e funzionale. La sua estensione
C++ supporta anche il paradigma di programmazione orientata agli
oggetti.
Caratteristiche generali di C:
• estremamente efficiente;
primo.c
int main () {
printf ( " Il mio primo programma C \ n " ) ;
printf ( " ... e non sara ’ l ’ ultimo .\ n " ) ;
return 0;
}
int main () {
...
}
1. Da Python a C 7
return 0;
1.3.1.3. Commenti
È possibile annotare il testo del programma con dei commenti.
C dispone di due tipi di commento:
SI
Errori di
Scrittura Compilazione
Begin compilazione?
programma programma
SI
Errori di
Verifica NO
esecuzione?
programma
NO
End
nome.c
dove:
Esempio: primo.c
g ++ -o primo primo . c
NomeE seguibil e
./ NomeEs eguibile
./ primo
equasecondo_err.c
int main () {
equasecondo.c
int main () {
double a ,b ,c , root , root1 , root2 ;
printf ( " Inserisci coefficienti a b c : \ n " ) ;
scanf ( " % lf " , & a ) ;
scanf ( " % lf " , & b ) ;
scanf ( " % lf " , & c ) ;
root = sqrt ( b * b - 4.0 * a * c ) ;
root1 = 0.5 * ( root - b ) / a ;
root2 = -0.5 * ( root + b ) / a ;
printf ( " radici di % f x ^2 + % f x + % f \ n " , a , b , c ) ;
printf ( " % f e % f \ n " , root1 , root2 ) ;
return 0;
}
1.4. Variabili
Al contrario di Python, in C tutti gli identificatori, in particolare le
variabili, devono essere dichiarati prima dell’uso!
La dichiarazione di una variabile richiede di specificare il tipo del-
la variabile (ad esempio, int, double, char,...) e il nome. Il tipo
determina la quantità di memoria allocata alla variabile.
In Python la dichiarazione di variabili non è necessaria in quanto
tutte le variabili sono di tipo riferimento ad oggetti.
Dichiarazione di variabile
Sintassi:
tipo nomeVariabile ;
Semantica:
La dichiarazione di una variabile riserva spazio in memoria per
la variabile e rende la variabile disponibile nella parte del pro-
gramma (blocco) dove appare la dichiarazione. È indispensabile
dichiarare una variabile prima di usarla.
Esempio:
int x ;
int è il tipo usato per i numeri interi. I tipi di dato primitivi sono
illustrati nell’Unità 2.
Dopo questa dichiarazione la variabile x è disponibile per l’utilizzo
nel blocco del programma dove appare la dichiarazione (ad esempio,
all’interno della funzione main, se la dichiarazione appare lì).
tipo nomeVar_1 ;
. . .
tipo nomeVar_n ;
14 Introduzione alla programmazione in C
0020!
1.4.3. Assegnazione
L’istruzione di assegnazione serve per memorizzare un valore in una
variabile.
Assegnazione
Sintassi:
nomeVariabile = espressione ;
Semantica:
Alla variabile nomeVariabile viene assegnato il valore
dell’espressione che si trova a destra del simbolo =. Tale
valore deve essere compatibile con il tipo della variabile (vedi
dopo). Dopo l’assegnazione il valore di una variabile rimane
invariato fino alla successiva assegnazione.
1. Da Python a C 15
int x ;
x = 3 + 2;
int x ;
x = 3.0 + 2.0;
int main () {
int x , y , z ;
// ERRORE le variabili x e y sono indefinite
z = x * y;
...
}
int main () {
int x =1 , y , z ;
y = 2;
// OK le variabili x e y sono inizializzate
z = x * y;
...
}
1.5. Funzioni
Le funzioni sono moduli di programma che svolgono un particolare
compito.
Come in Python le funzioni possono essere predefinite o scritte
dall’utente.
Vediamo innanzitutto come si invoca (chiama) una funzione.
Invocazione di funzione
Sintassi:
nomeFunzione (parametri )
Semantica:
Invoca una funzione fornendole eventuali parametri addizio-
nali. L’invocazione di una funzione comporta l’esecuzione
1. Da Python a C 17
Esempio:
sqrt (169)
Esempio:
√
stampa il valore 13 (cioè il risultato di 52 + 122 ).
Infatti, la funzione pow(5,2) restituisce il valore 25, la funzione
pow(12,2) restituisce 144, l’operatore + calcola la somma dei due valori
e restituisce 169, la funzione sqrt restituisce il valore 13, e infine il valo-
re 13 viene usato dalla funzione printf per stampare tale valore sulla
console di output.
sostituendo alla stringa %d, il valore del secondo argomento (5) della
chiamata ed alla stringa %f il valore del terzo argomento (5.0). La stringa
20 Introduzione alla programmazione in C
{
istruzione
...
istruzione
}
Semantica:
Le istruzioni del blocco vengono eseguite in sequenza. Le varia-
bili dichiarate internamente al blocco non sono visibili all’esterno
1. Da Python a C 21
del blocco.
Esempio:
int a , b ;
...
{
printf ( " dato1 = % d \ n " ,a ) ;
printf ( " dato2 = % d \ n " ,b ) ;
}
// campo d ’ azione
# include < stdio .h > /* funzioni di I / O */
# include < math .h > /* funzioni matematiche */
int main () {
double a = 2.3; // a double
int i = 1; // i int
printf ( " liv . 0 a = % f \ n " , a ) ; // 2.3
printf ( " liv . 0 i = % d \ n " , i ) ; // 1
{
printf ( " liv . 1 a = % f \ n " , a ) ; // 2.3
printf ( " liv . 1 i = % d \ n " , i ) ; // 1
}
// printf (" liv . 1 r = % f \ n " , r ) ; // ERRORE
printf ( " liv . 1 i = % f \ n " ,i ) ; // 1.1
{
int r = 4; // r int
printf ( " liv . 2.2 r = % d \ n " , r ) ; // 4
printf ( " liv . 2.2 a = % f \ n " , a ) ; // 2.3
}
}
i = i + 1; // i int
printf ( " liv . 0 i = % d \ n " , i ) ; // 2
return 0;
}
int main () {
double a ,b ,c , root , root1 , root2 ;
printf ( " Inserisci coefficienti a b c : \ n " ) ;
scanf ( " % lf % lf % lf " , &a , &b , & c ) ;
root = sqrt ( b * b - 4.0 * a * c ) ;
root1 = unmezzo * ( root - b ) / a ;
root2 = - unmezzo * ( root + b ) / a ;
printf ( " radici di % f x ^2 + % f x + % f \ n " , a , b , c ) ;
printf ( " % f e % f \ n " , root1 , root2 ) ;
return 0;
}
equasecondo-if.c
int main () {
double a , b , c ;
printf ( " Inserisci coefficienti a b c : \ n " ) ;
scanf ( " % lf % lf % lf " , &a , &b , & c ) ;
double delta = b * b - 4.0 * a * c ;
if ( delta > 0.0) {
double root = sqrt ( delta ) ;
double root1 = 0.5 * ( root - b ) / a ;
double root2 = - 0.5 * ( root + b ) / a ;
printf ( " radici reali : % lf e % lf \ n " , root1 , root2 ) ;
} else if ( delta < 0.0) {
double root = sqrt ( - delta ) ;
double real_part = - 0.5 * b / a ;
double imag_part = 0.5 * root / a ;
printf ( " radici complesse : % lf + i * % lf e % lf - i
* % lf \ n " ,
real_part , imag_part , real_part , imag_part ) ;
} else {
double root1 = -0.5* b / a ;
printf ( " radici coincidenti : % lf \ n " , root1 ) ;
}
return 0;
}
if (condizione )
istruzione-then
else
istruzione-else
Semantica:
Viene valutata prima la condizione. Se la valutazione fornisce un
valore diverso da 0 (true), viene eseguita istruzione-then, altrimen-
ti viene eseguita istruzione-else. In entrambi i casi l’esecuzione
prosegue con l’istruzione che segue l’istruzione if-else.
Esempio:
int a , b ;
...
if ( a > b )
printf ( " maggiore = % d \ n " , a ) ;
else
printf ( " maggiore = % d \ n " , b ) ;
1.8.3. La variante if
La parte else di un’istruzione if-else è opzionale.
Se manca si parla di istruzione if, la quale consente di eseguire una
parte di codice solo se è verificata una condizione.
Istruzione if
Sintassi:
if (condizione )
istruzione-then
Semantica:
Viene valutata prima la condizione. Se la valutazione fornisce
un valore diverso da 0 (true), viene eseguita istruzione-then e si
procede con l’istruzione che segue l’istruzione if. Altrimenti si
procede direttamente con l’istruzione che segue l’istruzione if.
Esempio:
double a = 1.0;
if (a >0) printf ( " a positivo .\ n " ) ;
int a , b , c ;
...
if ( a > b + c )
...
int a , b ;
...
if ( verifica (a , b ) )
...
Esempio:
int a , b , c , d ;
int trovato ;
...
if (( a >( b + c ) ) || ( a == d ) && ! trovato )
...
int a ;
double b ;
if ( a =0) // assegnazione
...
if ( b ==0) // a p pr os si ma z io ne
...
if ( a == b ) // conversione di tipo
...
int i ;
...
if ( i > 0 && fact ( i ) > 100) {
...
}
Si noti che la funzione fact(i) non viene invocata nel caso in cui i abbia
valore minore o uguale a 0.
Istruzioni if-else che fanno uso di espressioni booleane complesse
potrebbero essere riscritte attraverso l’uso di if-else annidati. In generale
questo comporta però la necessità di duplicare codice.
corrisponde a
1. Da Python a C 29
if ( x < y )
if ( y < z )
printf ( " y compreso tra x e z " ) ;
else
printf ( " y non compreso tra x e z " ) ;
else
printf ( " y non compreso tra x e z " ) ;
if (( x == 1) || ( x == 2) )
printf ( " x uguale a 1 o a 2 " ) ;
else
printf ( " x diverso da 1 e da 2 " ) ;
corrisponde a
if ( x == 1)
printf ( " x uguale a 1 o a 2 " ) ;
else if ( x == 2)
printf ( " x uguale a 1 o a 2 " ) ;
else
printf ( " x diverso da 1 e da 2 " ) ;
if (a > b ) {
printf ( " maggiore = % d \ n " ,a ) ;
printf ( " minore = % d \ n " ,b ) ;
}
Esempio:
30 Introduzione alla programmazione in C
1.8.8. If annidati
Si hanno quando l’istruzione del ramo-then o del ramo-else è un’i-
struzione if-else o if.
Esempio:
temperatura t messaggio
30 < t molto caldo
20 < t ≤ 30 caldo
10 < t ≤ 20 gradevole
t ≤ 10 freddo
int temp ;
...
if (30 < temp )
printf ( " molto caldo " ) ;
else if (20 < temp )
printf ( " caldo " ) ;
else if (10 < temp )
printf ( " gradevole " ) ;
else
printf ( " freddo " ) ;
Osservazioni:
• non serve che la seconda condizione sia composta, ad es. (20 <
temp) && (temp <= 30)
if! if!
if ( a > 0)
if ( b > 0)
printf ( " b positivo " ) ;
else
printf ( " b negativo " ) ;
if ( a > 0) {
if ( b > 0)
printf ( " b positivo " ) ;
}
else
printf ( " a negativo " ) ;
1. Da Python a C 33
Sintassi:
condizione ?espressione-1 :espressione-2
Semantica:
Valuta condizione. Se il risultato è true, allora valuta espressione-
1 e ne restituisce il valore, altrimenti valuta espressione-2 e ne
restituisce il valore.
Esempio:
if ( a > b )
printf ( " maggiore = % d " , a ) ;
else
printf ( " maggiore = % d " , b ) ;
Istruzione switch
Sintassi:
switch (espressione ) {
case etichetta-1 : istruzioni-1
break;
...
case etichetta-n : istruzioni-n
break;
default: istruzioni-default
}
Semantica:
Esempio:
int i ;
...
switch ( i ) {
case 0: printf ( " zero " ) ; break ;
case 1: printf ( " uno " ) ; break ;
case 2: printf ( " due " ) ; break ;
default : printf ( " minore di zero o maggiore di due " ) ;
}
case v1 :
case v2 :
...
case vn :
< istruzioni >
break ;
int a ;
...
switch ( a ) {
case a <0: printf ( " negativo " ) ;
// ERRORE : a <0 non e ’ una costante
case 0: printf ( " nullo " ) ;
case a >0: printf ( " positivo " ) ;
// ERRORE : a >0 non e ’ una costante
}
• quando ci sono più istruzioni nel corpo del ciclo occorre racchiu-
derle in un blocco { ... } (non serve l’indentazione!);
Istruzione while
Sintassi:
while (condizione )
istruzione
SI
Condizione vera? Esegui corpo
while
NO
int i = 0;
while ( i < 100) {
printf ( " * " ) ;
i ++;
}
iniziliazzazione
while (condizione ) {
operazione
passo successivo
}
int i ;
while ( i != 0) {
printf ( " % d " , i * i ) ;
printf ( " prossimo intero " ) ;
}
int i ;
scanf ( " % d " ,& i ) ;
while ( i != 0) {
printf ( " % d " , i * i ) ;
printf ( " prossimo intero " ) ;
}
int i = 0;
while ( i <= 10) { // corretto : ( i < 10)
printf ( " * " ) ;
i ++;
}
int i = 1;
while ( i <= 10) {
printf ( " % d \ n " , i * i ) ;
i ++;
}
int i = 1; // contatore
int s = 0; // accumulatore
while ( i <= 10) {
s += i * i ;
i ++;
}
printf ( " % d \ n " , s ) ;
• ciclo while
• ciclo for
• ciclo do
Semantica: è equivalente a
è equivalente a
int i = 0;
while ( i < 100) {
printf ( " * " ) ;
i ++;
}
Esempio:
La sintassi del for permette che le tre parti siano delle espressioni
qualsiasi, purché inizializzazione; e incremento; siano delle istruzioni (in
particolare, facciano side-effect).
Nell’uso del ciclo for è però buona norma:
• usare le tre parti del for in base al significato sopra descritto, con
riferimento ad una variabile di controllo;
int i , potDi2 ;
for ( i =0 , potDi2 =1; i < 10; i ++ , potDi2 *= 2)
printf ( " 2 alla % d = % d \ n " , i , potDi2 ) ;
1.9.10. Ciclo do
Nel ciclo while la condizione di fine ciclo viene controllata all’inizio
di ogni iterazione. Il ciclo do è simile al ciclo while, con la sola differenza
che la condizione di fine ciclo viene controllata alla fine di ogni iterazione.
Istruzione do
Sintassi:
do
istruzione
while (condizione );
Semantica: è equivalente a
istruzione ;
while (condizione )
istruzione
Quindi:
int i = 0;
do {
printf ( " * " ) ;
i ++;
} while ( i < 100) ;
int i ;
int somma = 0;
do {
printf ( " inserisci intero (0 per terminare ) : " ) ;
scanf ( " % d " , & i ) ;
somma = somma + i ;
} while ( i != 0) ;
printf ( " \ n Totale = % d \ n " , somma ) ;
Si noti che la sintassi del ciclo do richiede che ci sia un ";" dopo while
(condizione). Per aumentare la leggibilità del programma, in particolare
per evitare di confondere la parte while (condizione); di un ciclo do, con
un’istruzione while con corpo vuoto, conviene in ogni caso racchiudere
il corpo del ciclo do in un blocco, ed indentare il codice come mostrato
nell’esempio di sopra.
ripetere la richiesta del dato stesso. Questo può essere fatto utilizzando un
ciclo do.
Esempio: scrivere un frammento di programma che legge da input
un intero, ripetendo la lettura fino a quando non viene immesso un
intero positivo, e restituisce l’intero positivo letto.
...
int i ;
do {
printf ( " inserisci un intero positivo : " ) ;
scanf ( " % d " , & i ) ;
} while ( i <= 0) ;
// all ’ uscita dal ciclo i e ’ un intero positivo
...
do {
printf ( " inserisci un intero positivo : " ) ;
scanf ( " % d " , & i ) ;
} while ( i <= 0) ;
equivale a
Osservazioni:
– o si è trovato un divisore,
– o si decrementa mcd di 1 (al più si arriva ad 1).
x y maggiore − minore
210 63 147
147 63 84
84 63 21
21 63 42
21 42 21
21 21 =⇒ mcd(21,21) = mcd(21,42) = · · · = mcd(210,63)
L’algoritmo può essere realizzato in C al seguente modo:
int main ()
{
int x = 210;
int y = 63;
while ( x != y ) {
if ( x > y )
x = x - y;
else // significa che y > x
y = y - x;
}
printf ( " mcd = % d \ n " , x ) ;
return 0;
}
• Se x = y = 0?
Il risultato è 0.
• Se x = 0 e y > 0 (o viceversa)?
Il risultato dovrebbe essere y, ma l’algoritmo entra in un ciclo
infinito.
Quindi, se si vuole tenere conto del fatto che il programma possa essere
eseguito con valori qualsiasi dei parametri, è necessario inserire una
opportuna verifica.
1. Da Python a C 53
int main ()
{
int x = 210;
int y = 63;
if (( x > 0) && ( y > 0) ) {
while ( x != y )
if ( x > y )
x = x - y;
else // significa che y > x
y = y - x;
printf ( " mcd = % d \ n " , x ) ;
}
else printf ( " dati errati " ) ;
return 0;
}
y, se r = 0 ( x multiplo di y)
mcd( x, y) =
mcd(r, y), se r 6= 0
int main ()
{
int x = ...;
int y = ...;
while (( x != 0) && ( y != 0) ) {
if ( x > y )
x = x % y;
else
y = y % x;
}
if ( x != 0) printf ( " mcd = % d \ n " , x ) ;
else printf ( " mcd = % d \ n " , y ) ;
return 0;
}
int main () {
int N = 10; // dimensione della tavola
int riga , colonna ;
for ( riga = 1; riga <= N ; riga ++) {
for ( colonna = 1; colonna <= N ; colonna ++)
printf ( " %3 d " , riga * colonna ) ;
printf ( " \ n " ) ;
}
return 0;
}
Output prodotto:
1 2 3 4 5 6 7 8 9 10
2 4 6 8 10 12 14 16 18 20
3 6 9 12 15 18 21 24 27 30
4 8 12 16 20 24 28 32 36 40
5 10 15 20 25 30 35 40 45 50
6 12 18 24 30 36 42 48 54 60
7 14 21 28 35 42 49 56 63 70
8 16 24 32 40 48 56 64 72 80
9 18 27 36 45 54 63 72 81 90
10 20 30 40 50 60 70 80 90 100
1. Da Python a C 55
int altezza ;
printf ( " Altezza = " ) ;
scanf ( " % d " , & altezza ) ;
for ( int riga = 1; riga <= altezza ; riga ++) {
// 1 iterazione per ogni riga della piramide
for ( int i = 1; i <= altezza - riga ; i ++)
printf ( " " ) ; // stampa spazi bianchi iniziali
for ( int i = 1; i <= riga * 2 - 1; i ++)
printf ( " * " ) ; // stampa sequenza di asterischi
printf ( " \ n " ) ; // va a capo : fine riga
}
• Fib(1)=1;
• Fib(n)=Fib(n-1)+Fib(n-2)
Scrivere un programma che legge un intero positivo (altrimenti
reitera la richiesta), e calcola il relativo numero di Fibonacci. li
fibo.c
int main ()
{
int Fib ,N , n1 , n2 ;
printf ( " Calcolo il numero di Fibonacci \ n " ) ;
do {
printf ( " Inserisci un numero ( >=0) : " ) ;
scanf ( " % d " ,& N ) ;
} while (N <0) ;
1. Da Python a C 57
if ((0 == N ) || (1 == N ) )
Fib =1;
else {
n1 =1; n2 =1;
for ( int i = 2; i <= N ; i ++) {
Fib = n2 + n1 ;
n2 = n1 ;
n1 = Fib ;
}
}
printf ( " \ nNumero di Fibonacci di % d = % d \ n " , N , Fib )
;
return 0;
}
Note:
double a ;
for ( int i = 0; i < 10; i ++) {
printf ( " Immetti un reale nonnegativo " ) ;
scanf ( " % d " , & a ) ;
if ( a >= 0)
printf ( " % f " , sqrt ( a ) ) ;
else {
printf ( " Errore " ) ;
break ;
}
}
while (condizione ) {
istruzioni-1
if (condizione-break ) break;
istruzioni-2
}
equivale a
1. Da Python a C 59
finito = 0;
while (condizione && !finito) {
istruzioni-1
if (condizione-break ) finito = 1;
else { istruzioni-2 }
}
double a ;
int errore = 0;
Esempio:
60 Introduzione alla programmazione in C
etichetta : istruzione-ciclo ;
goto etichetta ;
int i = 0; float x ;
while ( i < 100) {
printf ( " inserisci numero positivo : " ) ;
scanf ( " % f " , & x ) ;
if (x <=0) goto errore ;
i ++;
}
errore : printf ( " errore : programma interrotto \ n " ) ;
• char
64 Introduzione alla programmazione in C
Tipo int
Dimensione 32 bit (4 byte)
numeri interi in [−231 , +231 − 1]
Dominio
(oltre 4 miliardi di valori)
+ somma
- sottrazione
Operazioni * prodotto
/ divisione intera
% resto della divisione intera
sequenze di cifre che denotano
Letterali
valori del dominio (es. 275930)
1 In alcuni tipi, questa grandezza dipende dalla macchina e/o dal compilatore (vedi
funzione sizeof più avanti). Noi faremo riferimento alle combinazioni più comuni nei
calcolatori moderni.
2. Tipi di dato primitivi 65
Esempio:
I valori limite del tipo int sono definiti nelle costanti INT_MIN e
INT_MAX nel file di sistema limits.h
overflow.c
int x = INT_MAX ;
printf ( " % d \ n " ,x ) ; // Stampa 2147483647
// ( massimo valore ra pp re s en ta bi l e con int )
x ++; // Incrementa x di 1
printf ( " % d \ n " ,x ) ; // Stampa -2147483648
// ( minimo valore r a pp re se nt a bi le con int )
Esempio:
Esempio:
Il qualificatore unsigned indica che il primo bit del vettore non deve
essere considerato come bit di segno.
Tipo float
Dimensione 32 bit (4 byte)
min 1.4012985 · 10−38
232 reali
Dominio max 3.4028235 · 10+38
pos. e neg.
Precisione ∼ 7 cifre decimali
+ somma
- sottrazione
Operazioni * prodotto
/ divisione
sequenze di cifre con punto decimale terminate con
Letterali una f che denotano valori del dominio (es. 3.14f)
rappresentazione in notazione scientifica (es. 314E-2f)
Esempio:
Tipo double
Dimensione 64 bit (8 byte)
min 1.79769313486231570 · 10−308
264 reali
Dominio max 2.250738585072014 · 10+308
pos. e neg.
Precisione ∼ 15 cifre decimali
+ somma
- sottrazione
Operazioni * prodotto
/ divisione
sequenze di cifre con punto decimale, opzionalmente
Letterali terminate con d, che denotano valori del dominio (es. 3.14)
rappresentazione in notazione scientifica (es. 314E-2)
2. Tipi di dato primitivi 69
Esempio:
approssimazione.c
float x = 1222333444.0;
printf ( " x = % f \ n " , x ) ;
x += 1.0;
printf ( " x + 1.0 = % f \ n " , x ) ;
x = 1222333440.000000
x + 1.0 = 1 2 2 2 3 3 3 4 4 0 . 0 0 0 0 0 0
float x = 1222333440.0;
printf ( " x = % f \ n " , x ) ;
x +=65;
printf ( " x + 65 = % f \ n " , x ) ;
x = 1222333440.000000
x + 65 = 1 2 2 2 3 3 3 5 6 8 . 0 0 0 0 0 0
char c = ’A ’;
char d = ’1 ’;
char c = ’A ’;
int i = 65;
printf ( " % c \ n " ,i ) ; // Stampa : A
printf ( " % d \ n " ,c ) ; // Stampa : 65
Operatore Significato
&& AND: congiunzione
|| OR: disgiunzione
! NOT: negazione
# define C O MP EN SO _ OR AR IO 20
...
int compenso = CO M PE NS O_ O RA RI O * orelavoro ;
# define C O MP EN SO _ OR AR IO 20.50 f
Esempio:
char a = ’A ’;
char b = ’B ’;
printf ( " % d \ n " ,a ) ; // Stampa 65
printf ( " % d \ n " ,b ) ; // Stampa 66
printf ( " % d \ n " ,a + b ) ; // Stampa 131
Esempio:
76 Introduzione alla programmazione in C
int x = 3;
int y = x + 4;
int x = 0;
printf ( " % d " ,x =7) ;
printf ( " % d " ,x ) ;
int x = 0 , y = 0;
printf ( " % d " , y = 3 + ( x = 7) ) ; // stampa 10
printf ( " % d " , x ) ; // stampa 7
int x = 0;
if ( x == 0)
printf ( " x vale 0 " ) ; // stampa : x vale 0
if ( x =0)
printf ( " x vale 0 " ) ; // non stampa nulla
x = 5 * ( y = 7) ;
y = 7;
x = 5 * y;
somma += addendo ;
salario *= aumento ;
In generale l’assegnazione:
int x = 1;
x ++; // incrementa x di 1
printf ( " % d " ,x ) ; // stampa 2
--x ; // decrementa x di 1
printf ( " % d " ,x ) ; // stampa 1
Dal punto di vista degli effetti sul valore della variabile a cui sono
applicati, non c’è nessuna differenza tra l’uso prefisso o postfisso dello
stesso operatore: l’esecuzione delle istruzioni x++; o ++x; comporta lo
stesso effetto sulla variabile x, ovvero ne viene incrementato il valore di
1.
Tuttavia, tali operatori possono essere usati all’interno di espressio-
ni. In tal caso, le due forme, pur comportando lo stesso effetto sulla
variabile, danno luogo a valori diversi delle espressioni. In particolare:
int x = 0;
printf ( " % d " ,x ) ; // stampa 0
printf ( " % d " ,x ++) ; // stampa 0
printf ( " % d " ,x ) ; // stampa 1
printf ( " % d " ,x - -) ; // stampa 1
printf ( " % d " ,x ) ; // stampa 0
printf ( " % d " ,++ x ) ; // stampa 1
printf ( " % d " ,--x ) ; // stampa 0
printf ( " % d " ,x ) ; // stampa 0
int i ;
printf ( " Inserisci un valore intero \ n " ) ;
scanf ( " % d " ,& i ) ;
La stringa %d indica alla funzione scanf che il valore letto deve essere
interpretato come un intero. Il parametro &i indica che tale valore deve
essere memorizzato nella variabile i.
La specifica di formato inizia con il carattere % ed è seguita da una
combinazione di caratteri che dipendono dal tipo di dato da memoriz-
zare. La seguente tabella mostra le specifiche per i tipi più comuni.
3 Questa stringa può in realtà essere più complessa, in quanto scanf non si limita a
leggere semplici valori. Tuttavia questi aspetti avanzati non saranno considerati in
questo corso.
82 Introduzione alla programmazione in C
scanf.c (1)
int i ;
float f ;
scanf ( " % d % f " , &i , & f ) ;
scanf.c (2)
int i ;
float f ;
scanf ( " % d " , & i ) ;
scanf ( " % f " , & f ) ;
int i ;
float f ;
scanf ( " % d % f " ,&i ,& f ) ;
printf ( " i =% d \ nf =% f \ n " ,i , f ) ;
i =5
f =12.230000
int i ;
scanf ( " % d " ,& i ) ;
printf ( " % f " ,i ) ;
char c ;
printf ( " Inserisci un carattere \ n " ) ;
scanf ( " % c " ,& c ) ;
printf ( " Il carattere inserito e ’ % c \ n " , c ) ;
84 Introduzione alla programmazione in C
Esempio:
Soluzione:
pitagora.c
int main () {
float c1 , c2 , ip ;
printf ( " Inserisci il valore del primo cateto \ n " ) ;
scanf ( " % f " ,& c1 ) ;
printf ( " Inserisci il valore del secondo cateto \ n " )
;
scanf ( " % f " ,& c2 ) ;
ip = sqrt ( pow ( c1 ,2) + pow ( c2 ,2) ) ;
printf ( " Il valore dell ’ ipotenusa e ’% f \ n " , ip ) ;
}
86 Introduzione alla programmazione in C
Esempio:
cast-implicito.c
double d = 3.23199;
float f = 56.3419;
int i = 100;
char!
short int!
Esempio:
cast-implicito-int.c
int i = 10;
long int j = i ;
float f = 100.23 f ;
double d = f ;
double d = 102.945;
long int i = d ; // i vale 102
conversione-down.c
int i = 32767;
short j = i ; // OK : 32767 r ap p re se nt a bi le come short
printf ( " % d \ n " ,j ) ; // 32767
j = i +1; // NO : 32768 fuori intervallo short
printf ( " % d \ n " ,j ) ; // -32768 !!!
2.9. Casting
Il C permette di convertire dati da un tipo ad un altro in maniera
esplicita. Ad esempio, è possibile convertire il float 3.2 in int, ottenendo
3. Questa operazione è detta cast ed ha la seguente sintassi.
Cast
Sintassi:
(tipo ) espressione ;
Semantica:
Converte il tipo di espressione in tipo.
int main () {
90 Introduzione alla programmazione in C
float x = 7.0 f ;
float y = 2.8 f ;
int a = x / y ; // Divisione reale , risultato = 2.5
printf ( " % d \ n " ,a ) ; // Stampa 2
a = ( int ) x / ( int ) y ; // Divisione intera ,
risultato = 3
printf ( " % d \ n " ,a ) ; // Stampa 3
}
float f = 23.45 f ;
int a = f ; // NO
int a = ( int ) f ; // SI
int i = 10;
int j = sizeof ( i ) ; // j vale 4
j = sizeof ( i +2) ; // j vale 4
j = sizeof ( i +2.0) ; // j vale 8 ( i +2.0 e ’ double )
TipoA a = 0;
TipoB b = 1;
Bool c = a + b ;
I tipi definiti sono sempre compatibili con il tipo base, quindi nell’e-
sempio precedente l’istruzione c=a+b; è valida e ritorna un risultato di
tipo int.
Il vantaggio principale nell’uso di typedef consiste in una migliore
manutenibiltà del programma.
Esempio:
indirizzo contenuto
0xF0000000 00100010
0xF0000001 00010000
0xF0000002 11111110
0xF0000003 11011010
··· ···
1 Gli indirizzi di una macchina a 32 bit vengono rappresentati per brevità con 8 cifre
esadecimali, ciascuna rappresentativa del gruppo di 4 bit corrispondente alla sua
rappresentazione in sistema binario. Il prefisso 0x indica che l’indirizzo riportato è
rappresentato, appunto, nel sistema esadecimale.
94 Introduzione alla programmazione in C
stampa-puntatore.c
int i = 15;
printf ( " L ’ indirizzo di i e ’ % p \ n " , & i ) ;
printf ( " mentre il valore di i e ’ % d \ n " , i ) ;
stampa
L ’ indirizzo di i e ’ 0 xbffff47c
mentre il valore di i e ’ 15
int j = 1;
int i = *& j ;
int * p1 ;
int * p1 ;
* p1 = 10; // ERRORE : l ’ indirizzo contenuto in p1
// non corrisponde a memoria allocata
Esempio:
int * p1 , p2 ;
int i ,j , k ;
int * pt_i , * pt_j , * pt_k ;
pt_i = & i ;
pt_j = & j ;
pt_k = & k ;
* pt_i = 1;
* pt_j = 2;
* pt_k = * pt_i + * pt_j ;
* pt_i = 10;
printf ( " i = % d \ n " , * pt_i ) ;
printf ( " j = % d \ n " , * pt_j ) ;
printf ( " k = % d \ n " , * pt_k ) ;
printf ( " i = % d \ n " , i ) ;
printf ( " j = % d \ n " , j ) ;
printf ( " k = % d \ n " , k ) ;
Il programma stampa:
i = 10
j = 2
k = 3
i = 10
j = 2
k = 3
condivisione.c
char c = ’A ’;
char * pt1 = & c ;
char * pt2 = pt1 ;
printf ( " * pt1 = % c \ n " ,* pt1 ) ;
printf ( " * pt2 = % c \ n " ,* pt2 ) ;
* pt2 = ’B ’; // Modifica tramite pt2
printf ( " % c \ n " ,c ) ; // Ovviamente , c e ’ cambiata !
printf ( " * pt1 = % c \ n " ,* pt1 ) ; // anche pt1 vede la
modifica
pt1!
c!
‘A’!
pt2!
pt1!
c!
‘B’!
pt2!
3.1.6. Assegnazione
Esempio:
assegnazione-puntatore.c
int * p ;
int q = 10;
char c = ’x ’;
p = & q ; // OK
p = & c ; // WARNING ! p punta a int mentre c e ’ char
Esempio:
3. Puntatori 99
int p =1 , q =1 , r =3;
int * p1 =& p , * p2 =& p , * p3 =& q , * p4 =& r ;
if ( p1 == p2 ) // TRUE : p1 e p2 puntano alla stessa
variabile
if (* p1 == * p2 ) // TRUE : variabili puntate da p1 e p2
hanno stesso valore
if ( p2 == p3 ) // FALSE : p2 e p3 puntano a variabili
diverse
if (* p2 == * p3 ) // TRUE : variabili puntate da p2 e p3
hanno stesso valore
if ( p3 == p4 ) // FALSE : p3 e p4 puntano a variabili
diverse
if (* p3 == * p4 ) // FALSE : variabili puntate da p3 e p4
hanno valori diversi
p1! p3! q!
p!
1!
1!
p2! p4! r!
3!
int i = 10;
int * p = & i ;
int * q = p + 2; // indirizzo di p + 2 * sizeof ( int )
indirizzo contenuto
0x00AF0000 00100010
q 0x00AF0009 11011111
··· ···
char c = ’d ’;
char * p = & c ;
char * q = p - 4; // indirizzo di p - 4 * sizeof ( char )
indirizzo contenuto
0x1BA01004 00101110
q 0x1BA01005 00000000
0x1BA01006 10100010
0x1BA01007 00011010
0x1BA01008 00001010
double pi = 3.5;
const double * pt = & pi ;
(* pt ) ++; // ERRORE * pt e ’ costante
pt ++; // OK : pt non e ’ costante
int x ;
int * pt ;
int ** ptpt ;
x = 4;
pt = & x ;
ptpt = & pt ;
printf ( " % d \ n " , ** ptpt ) ;
ptpt! pt! x!
4!
Esempio:
int * pt = NULL ;
...
if ( pt != NULL )
* pt =10;
void * pt ;
int i ;
pt = & i ;
void * pt ;
...
int j = * pt ; // ERRORE !
void * pt ;
...
int * pti = pt ; // Conversione automatica
int * pti2 = ( int *) pt ; // Casting esplicito
Invocazione malloc
Sintassi:
malloc(n );
Semantica:
L’invocazione malloc(n):
indirizzo contenuto
0x014F2100 00100010
n * sizeof(tipo )
Esempio:
3.8.2.1. Deallocazione
Il C delega al programmatore il compito di rilasciare la memoria
inutilizzata. Rilasciare o deallocare un’area di memoria significa renderla
disponibile per usi futuri, in particolare, ad esempio, per l’allocazione
dinamica di nuove variabili.
La funzione preposta a questo scopo ha la seguente intestazione:
void free(void* p)
realloc o calloc –v. seguito) oppure sia il puntatore NULL, caso in cui la
funzione non ha effetto. In tutti gli altri casi, free ha un comportamento
indefinito.
Esempio: Si consideri il seguente frammento di codice:
free.c
* r = 100;
printf ( " % d \ n " ,* p ) ;
Esempio:
fixed-dangling-pointer.c
non sia più accessibile. Questo accade quando non ci sono variabili di
tipo puntatore che fanno riferimento a quell’area di memoria. Quando
ciò si verifica, non vi è alcun modo di recuperare il riferimento ed ac-
cedere nudamente all’area di memoria: essa rimarrà inaccessibile per
tutta la durata del programma ma, poiché ancora allocata, inutilizzabile
per allocare nuova memoria. Tali situazioni sono ovviamente da evitare.
Esempio: Nel seguente frammento di codice, dopo la seconda linea,
viene perso il riferimento alla prima area allocata, che rimane inutilmente
allocata per tutta la durata dell’esecuzione del programma.
riferimento-perso.c
?!
p!
?! ?!
?! ?! ?! ?!
q!
?! ?!
Si osservi che oltre ad essere non più utilizzabile, la prima area di memo-
ria, poichè già allocata, non può essere nemmeno sfruttata per allocare
una nuova area.
int x ;
scanf ( " % d " ,& x ) ; // & x indirizzo della locazione
// contenente il valore inserito
dove
112 Introduzione alla programmazione in C
Semantica:
Definisce una funzione specificandone intestazione e corpo.
Esempio:
int main () {
...
}
Istruzione return
Sintassi:
return espressione ;
dove
Semantica:
L’istruzione return eseguita all’interno di una funzione termina
l’esecuzione della stessa e restituisce il risultato alla parte di
programma dove è stata invocata la funzione.
Esempio:
int main () {
return 0; // terminazione normale del programma
}
return;
void stampaSaluto () {
printf ( " Buon giorno !\ n " ) ;
}
Si noti che in questo caso il ciclo più interno del doppio ciclo è
racchiuso nell’invocazione della funzione moltiplicazione.
• sqrt(double x)
• pow(double b, int e)
int main () {
int i = 1;
double d = 1.0;
printf ( " int somma ( int x , int y ) % d " , somma (i , i ) ) ;
}
• dichiarazioni di tipi;
• dichiarazioni di variabili;
• intestazioni di funzioni;
int main () {
int a , b ;
a =5;
b = raddoppia ( a +1) ;
printf ( " a = % d \ n " ,a ) ;
printf ( " b = % d \ n " ,b ) ;
}
int main () {
int j , i ;
i = 1; j = 2;
printf ( " i = % d \ n " ,i ) ;
printf ( " j = % d \ n " ,j ) ;
swap (& i , & j ) ;
printf ( " dopo swap \ n " ) ;
printf ( " i = % d \ n " ,i ) ;
printf ( " j = % d \ n " ,j ) ;
}
* ym = ( y1 + y2 ) /2;
}
int main () {
double x1 = 1 , y1 = 1 , x2 = 3 , y2 = 5 , xm , ym ;
puntoMedio ( x1 , y1 , x2 , y2 ,& xm ,& ym ) ;
printf ( " punto medio = (% lf ,% lf ) \ n " , xm , ym ) ;
}
int main () {
double d = 195905.94;
float f = ( float ) d ;
int i = ( int ) d ;
short s = ( short ) d ;
unsigned char c = ( unsigned char ) d ;
d 195905.940000
f 195905.937500
i 195905
s -703
c 65 A
int main () {
int i = 1;
int j = 2;
printf ( " i = % d \ n " ,i ) ;
printf ( " j = % d \ n " ,j ) ;
swapRef (i , j ) ;
printf ( " dopo swapRef \ n " ) ;
printf ( " i = % d \ n " ,i ) ;
printf ( " j = % d \ n " ,j ) ;
return 0;
}
int main () {
double * pd = puntatore (5.4) ;
printf ( " pd = % p \ n " , pd ) ;
printf ( " * pd = % f \ n " ,* pd ) ;
return 0;
}
int main () {
double * pd = puntatore (5.4) ;
printf ( " pd = % p \ n " , pd ) ;
printf ( " * pd = % f \ n " ,* pd ) ;
return 0;
}
oppure semplicemente
Programma chiamante:
y = (* f ) ( x ) ;
• campo d’azione (è una nozione statica, che dipende dal testo del
programma)
del blocco stesso. Per ottenere questo effetto occorre usare la parola
riservata static nella definizione della variabile.
Esempio: Il programma seguente
static.c
int ricorda () {
int static c ;
c ++;
return c ;
}
int main () {
printf ( " ricorda () = % d \ n " , ricorda () ) ;
printf ( " ricorda () = % d \ n " , ricorda () ) ;
printf ( " ricorda () = % d \ n " , ricorda () ) ;
}
stampa
ricorda () = 1
ricorda () = 2
ricorda () = 3
int main () {
int a = 5;
a = raddoppia ( a ) ;
a = raddoppia ( a ) ;
stampa ( a ) ;
printf ( " a = % d \ n " ,a ) ;
}
int B ( int pb ) {
/* b0 */ printf ( " In B . Parametro pb = % d \ n " , pb ) ;
/* b1 */ return pb +1;
}
int A ( int pa ) {
/* a0 */ printf ( " In A . Parametro pa = % d \ n " , pa ) ;
/* a1 */ printf ( " Chiamata di B (% d ) .\ n " , pa * 2) ;
/* a2 */ int va = B ( pa * 2) ;
/* a3 */ printf ( " Di nuovo in A . va = % d \ n " , va ) ;
/* a4 */ return va + pa ;
}
int main () {
/* m0 */ printf ( " In main .\ n " ) ;
/* m1 */ int vm = 22;
/* m2 */ printf ( " Chiamata di A (% d ) \ n " , vm ) ;
/* m3 */ vm = A ( vm ) ;
/* m4 */ printf ( " Di nuovo in main . vm = % d \ n " , vm ) ;
/* m5 */ return 0;
}
main A B
... ... ...
100 m0 200 a0 300 b0
101 m1 201 a1 301 return
102 m2 202 a2 ⇒ B(va*2) 302 ...
103 m3 ⇒ A(vm) 203 a3
104 m4 204 return
105 return 205 ...
106 ...
In main .
Chiamata di A (22) .
In A . Parametro pa = 22
Chiamata di B (44) .
In B . Parametro pb = 44
Di nuovo in A . va = 45
Di nuovo in main . vm = 67
pb 44
B VR 45
IR 203
va ? va ? va 45
A pa 22 A pa 22 A pa 22
VR ? VR ? VR 67
IR 104 IR 104 IR 104
m vm 22 m vm 22 m vm 22 m vm 22 m vm 67
1 2 3 4 5
Per comprendere cosa avviene durante l’esecuzione del codice, è ne-
cessario fare riferimento, oltre che alla pila dei RDA, al program counter
(PC), il cui valore è l’indirizzo della prossima istruzione da eseguire.
Analizziamo in dettaglio cosa avviene al momento dell’attivazione
di A(vm) dalla funzione main. Prima dell’attivazione, la pila dei RDA è
come mostrato in 1 nella figura di sopra:
1. vengono valutati i parametri attuali: nel nostro caso il parametro
attuale è l’espressione vm che ha come valore l’intero 22;
134 Introduzione alla programmazione in C
3. viene eliminato dalla pila dei RDA il RDA relativo all’attivazione cor-
rente, e il RDA corrente diviene quello immediatamente precedente nella
pila; contestualmente all’eliminazione del RDA dalla pila dei RDA, un
eventuale valore di ritorno viene copiato in una locazione di memoria del
RDA del chiamante: nel nostro caso, viene eliminato il RDA relati-
vo all’attivazione di A e il RDA corrente diviene quello relativo
all’attivazione di main; inoltre, il valore 67, memorizzato nella
locazione di memoria VR viene assegnato alla variabile vm nel
RDA di main; la pila dei RDA è come mostrato in 5 nella figura di
sopra;
136 Introduzione alla programmazione in C
5.1. Array
Un array è una struttura contenente una collezione di elementi dello
stesso tipo, ciascuno indicizzato da un valore intero. Una variabile di
tipo array è un riferimento alla collezione di elementi che costituisce
l’array.
Per usare un array in C occorre:
1. dichiarare una variabile di tipo array specificandone la dimensione
(numero di elementi contenuti);
2. accedere mediante la variabile agli elementi dell’array per asse-
gnare o leggerne i valori (trattando ciascun elemento come se fosse
una variabile).
Sintassi:
dove:
Semantica:
Alloca un array di n elementi di tipo tipo e crea la variabile
nomeArray di tipo array.
Esempio:
0! 1! 2! 3! 4!
a! ?! ?! ?! ?! ?!
int a [5];
printf ( " % d byte \ n " , sizeof ( a ) ) ; // 20 byte
printf ( " % d elementi \ n " , sizeof ( a ) / sizeof ( int ) ) ; // 5
elementi
Sintassi:
nomeArray [indice ]
dove
Semantica:
Accede all’elemento di indice indice dell’array nomeArray per
leggerlo o modificarlo.
Esempio:
140 Introduzione alla programmazione in C
Sintassi:
dove:
Semantica:
nomeArray viene inizializzato ad un array di n elementi di tipo
tipo, dove l’elemento di indice i ha valore pari al valore restitui-
to dall’espressione espr_i (opportunamente convertita, laddove
necessario).
Esempio:
int v [] = { 4 , 6 , 3 , 1 };
// oppure int v [4] = { 4 , 6 , 3 , 1 };
è equivalente a:
int v [4];
v [0] = 4; v [1] = 6; v [2] = 3; v [3] = 1;
int v [4];
v = { 4 , 6 , 3 , 1 }; // errato
int a [10];
int n_elementi = sizeof ( a ) / sizeof ( int ) ;
for ( int i = 0; i < n_elementi ; i ++) {
printf ( " Inserisci il valore di a [% d ]: " ,i ) ;
scanf ( " % d " ,& a [ i ]) ;
}
int somma = 0;
for ( int i = 0; i < n_elementi ; i ++) {
somma += a [ i ];
}
142 Introduzione alla programmazione in C
char a [3] = { ’a ’ , ’b ’ , ’c ’ };
printf ( " % c \ n " ,* a ) ; // stampa a
g!
0! 1! 2!
f!
1.2f! 3.5f! 7.659f!
*( g +2) =0;
printf ( " f [2]=% f \ n " ,f [2]) ; // stampa f [2]=0.000000
0! 1! 2! 3!
a! 2! 4! 6! 8!
c! b!
int v , i , * p ;
...
v = p [ i ];
v = *( p + i ) ;
char v [3]={ ’a ’ , ’b ’ , ’c ’ };
char * p = v ;
printf ( " % c \ n " ,v [2]) ; // stampa c
printf ( " % c \ n " ,p [2]) ; // stampa c
Esempio d’uso:
int main () {
const int n = 4;
int x [ n ] = {0 ,1 ,2 ,3};
printf ( " somma = % d \ n " , s o mm a V a l o r i A r r a y (x , n ) ) ;
}
0! 1! 2! 3!
x! 0! 1! 2! 3!
v!
int main () {
char t [3] = {1 ,2 ,3};
char * p = t ;
f ( t ) ; // oppure f ( p )
}
Esempio d’uso:
int main () {
const int n =4;
int x [ n ]= {1 ,2 ,3 ,4};
if ( c ercaElem Array (x ,n ,3) ) // cerca 3 nell ’ array x
printf ( " trovato \ n " ) ;
else
printf ( " non trovato \ n " ) ;
}
Esempio d’uso:
int main () {
const int n = 5;
long x [ n ] = { 5 , 3 , 9 , 5 , 12 };
printf ( " Max = % ld " , massimoArray (x , n ) ) ;
}
Esempio d’uso:
5. Tipi di dato indicizzati 149
int main () {
const int n =5;
int x [ n ] = {5 , 3 , 9 , 5 , 12};
for ( int i =0; i < n ; i ++) // stampa 5 3 9 5 12
printf ( " % d " , x [ i ]) ;
printf ( " \ n " ) ;
rovesciaArray (x , n ) ;
for ( int i =0; i < n ; i ++) // stampa 12 5 9 3 5
printf ( " % d " , x [ i ]) ;
printf ( " \ n " ) ;
}
int * creaArray () {
int risultato [5] = {10 ,20 ,30 ,40 ,50};
return risultato ;
}
int main () {
int * a = creaArray () ; // a punta ad una locazione
non allocata !
...
}
150 Introduzione alla programmazione in C
In alternativa alla creazione del nuovo array nel corpo della funzione,
si può allocare l’array (staticamente) nel modulo chiamante e passarlo
come argomento alla funzione, che può effettuare side-effect sull’array.
Tuttavia, per adottare questo approccio è necessario che la dimensione
dell’array sia nota prima dell’esecuzione della funzione.
Esempio:
Esempio d’uso:
int main () {
const int n = 10;
int x [ n ];
i n i z i a l i z z aA r r a y (x , n ) ;
}
int * c r e a A r r a y D i n a m i c o ( int n ) {
int * risultato = ( int *) malloc ( n * sizeof ( int ) ) ;
// Alloca n interi contigui
// Accessibile come se fosse un array
return risultato ;
}
5. Tipi di dato indicizzati 151
int main () {
// allocazione statica
int A [ dimdef ];
int AA [ dimconst ];
// allocazione stack run - time
int n = 0;
printf ( " dimensione dell ’ array : " ) ;
scanf ( " % d " , & n ) ;
int b [ n ];
// allocazione dinamica
int * bb ;
bb = ( int *) malloc ( n * sizeof ( int ) ) ;
// allocazione dinamica tramite funzione
int * c ;
c = crearray ( n ) ;
// allocazione stack run - time tramite funzione -- NO
int * cc ;
cc = crear rayAppes o ( n ) ;
int n ;
printf ( " Inserisci un intero : " ) ;
scanf ( " % d \ n " , & n ) ;
char a [ n ]; // ERRORE : n non e ’ costante
// ( consentito in C99 / C ++)
for ( int i = 0; i < n ; i ++) {
// Popola l ’ array con dei caratteri
printf ( " Inserisci un carattere : " ) ;
scanf ( " % c \ n " , & a [ i ]) ;
}
q!
q! p!
‘a’! ?!
p!
‘a’! q!
p!
‘a’!
‘a’! ?!
int n = 5 , i = 0;
char * p = ( char *) calloc (n , sizeof ( char ) ) ;
char prox_char ;
do {
printf ( " Inserisci un carattere (; per uscire ) : " ) ;
scanf ( " % c " ,& prox_char ) ;
if (i >= n ) {
// array pieno : incrementa la dimensione n
n += n ;
p = ( char *) realloc (p , n * sizeof ( char ) ) ;
}
p [ i ] = prox_char ;
i ++;
} while ( prox_char != ’; ’) ;
// Stampa :
for ( int j = 0; j < i ; j ++) {
printf ( " % c " ,p [ j ]) ;
}
printf ( " \ n " ) ;
int main () {
const int n =5;
int x [ n ] = { 5 , 3 , 9 , 5 , 12 };
int * y = copiaInversa (x , n ) ;
for ( int i =0; i < n ; i ++)
printf ( " % d " , y [ i ]) ;
printf ( " \ n " ) ;
}
int n ;
printf ( " Quanti punti vuoi memorizzare ?\ n " ) ;
scanf ( " % d " ,& n ) ;
float ** a = calloc (n , sizeof ( float *) ) ;
a!
0! 0! 0!
0! 0! 0!
0! 0! 0!
…! …
0! 0! 0!
int n ;
printf ( " Quanti punti vuoi memorizzare ?\ n " ) ;
scanf ( " % d " ,& n ) ;
float ** a = calloc (n , sizeof ( int *) ) ;
// ... elaborazione
// ... elaborazione
Si noti, nel primo ciclo for, l’uso dell’operatore & per ottenere l’indi-
rizzo della componente j-esima dell’array i-esimo, da fornire in input a
scanf.
Si osservi inoltre che il rilascio della memoria deve avvenire in due
fasi: una prima in cui viene rilasciata la memoria occupata dagli array
più “in profondità” nella struttura ed una seconda in cui viene rilasciata
la memoria dell’array superficiale. Se venisse prima deallocata la memo-
ria occupata dall’array a, si perderebbero infatti i riferimenti agli array
più profondi, che non potrebbero quindi essere né usati né deallocati.
5.2. Stringhe
5.2.1. Variabili di tipo stringa in C
Il C non mette a disposizione un tipo speciale per memorizzare
stringhe. Semplicemente, una stringa è memorizzata come un array di
caratteri terminante con il carattere speciale ’\0’ (codice ASCII = 0),
detto terminatore di stringa.
Esempio: Nel seguente frammento di codice la stringa ’Hello’ viene
memorizzata nella variabile s di tipo array di caratteri.
160 Introduzione alla programmazione in C
char s [10];
. . . // i n i z i a l i z z az i o n e di s
char * p = s ; // p punta al primo elemento di s
p!
B! u! o! n! g! i! o! r! n! o! !! \0!
c! c! i! a! o! \0!
char c [5] = { ’c ’ , ’i ’ , ’a ’ , ’o ’ , ’ \0 ’ };
conta-carattere.c
Esempio d’uso:
int main () {
char s [5]= " ciao " ;
char t [5];
printf ( " % s \ n " , c o di fi ca S tr in ga (s ,t ,1) ) ; // stampa djbp
}
sottosequenza.c
5.2.8.2. Lettura
char s [256];
printf ( " Inserisci una stringa \ n " ) ;
scanf ( " % s " ,s ) ;
int strcmp(const char *str1, const char *str2): confronta due stringhe in
base all’ordinamento lessicografico (v. sotto), restituendo: un valo-
re negativo se str1 precede str2; 0 se str1 è uguale a str2; un valore
positivo se str1 segue str2.
char *strcpy(char *dest, const char *src): copia la stringa src in dest (su
cui fa side-effect) e restituisce un puntatore a dest.
char *strcat(char *str1, const char *str2): concatena str2 alla fine di str1
(su cui fa side-effect) e restituisce un puntatore a str1.
2 Per questa ragione, con alcun compilatori, quando si usa la funzione gets, l’esecuzione
del programma produce il messaggio: warning: this program uses gets(), which is
unsafe.
168 Introduzione alla programmazione in C
• s è un prefisso di t, oppure
Esempio:
int main () {
const int N =256;
char nome [ N ];
printf ( " Inserisci il tuo nome : " ) ;
gets ( nome ) ;
printf ( " Il tuo nome contiene % lu caratteri \ n " ,
strlen ( nome ) ) ;
5. Tipi di dato indicizzati 169
char cognome [ N ];
printf ( " Inserisci il tuo cognome : " ) ;
gets ( cognome ) ;
strcat ( nome , " " ) ;
strcat ( nome , cognome ) ;
printf ( " Il tuo nome completo e ’ % s \ n " , nome ) ;
if ( strcmp ( nome , " Mario Rossi " ) !=0)
printf ( " Tu non sei Mario Rossi !\ n " ) ;
char * sottostringa = strstr ( nome , " Ro " ) ;
if ( sottostringa != NULL ) {
printf ( " Il tuo nome completo contiene \" Ro \":
%s\n",
sottostringa ) ;
}
}
Il programma stampa:
5.3. Matrici
Una matrice è una collezione in forma tabellare di elementi dello stes-
so tipo, ciascuno indicizzato da una coppia di interi positivi (0 incluso)
che ne identificano riga e colonna nella tabella.
Esempio: Di seguito è riportata una matrice M di 3 righe e 4 colonne.
Gli indici di riga crescono verso il basso e quelli di colonna verso l’alto.
L’indice della prima riga e della prima colonna è 0. L’elemento generico
con indice di riga i e di colonna j è denotato M[i, j]. Pertanto, ad esempio,
abbiamo: M [0, 0] = 1, M [1, 3] = 9 e M[2, 3] = 19.
1 3 34 24
2 31 39 9
7 6 8 19
5. Tipi di dato indicizzati 171
const int N = 3 , M = 5;
int m [ N ][ M ]; // dichiarazione di una matrice NxM m
int m [][2] = {
{3 ,5} ,
{9 ,12}
};
int m [2][2];
m [0][0] = 3; m [0][1] = 5;
m [1][0] = 9; m [1][1] = 12;
Si noti che per accedere a tutti gli elementi della matrice x vengono
usati due cicli for annidati: quello esterno legge le righe (i), quello interno
legge gli elementi di ogni riga (j). Quando tutti gli elementi di una riga
sono stati stampati, viene stampato il carattere di ritorno a capo.
La stampa per colonne può essere effettuata nel modo seguente (una
colonna per linea di stampa).
Anche in questo caso vengono usati due cicli for annidati: quello
esterno scorre le colonne (i), quello interno scandisce gli elementi di
ogni colonna (j).
Esempio d’uso:
int main () {
const int n_righe =2;
const int n_colonne =3;
int m [][ n_colonne ]= {{1 , 2 , 2} , {7 , 5 , 9}};
printf ( " stampa matrice per righe \ n " ) ;
for ( int i =0; i < n_righe ; i ++) {
for ( int j =0; j < n_colonne ; j ++)
printf ( " % d " ,m [ i ][ j ]) ;
printf ( " \ n " ) ;
}
printf ( " stampa matrice per colonne \ n " ) ;
for ( int i =0; i < n_colonne ; i ++) {
for ( int j =0; j < n_righe ; j ++)
printf ( " % d " ,m [ j ][ i ]) ;
printf ( " \ n " ) ;
}
return 0;
}
Il programma stampa:
L’uso dei cicli annidati permette di accedere a tutti gli elementi delle
matrici A, B (per effettuarne le somme) e C (per inizializzarne i valori).
L’accesso agli elementi di ciascuna matrice segue l’ordine per righe (il
ciclo esterno scorre le righe, quello interno gli elementi di ogni riga) ma
la stessa operazione avrebbe potuto essere realizzata procedendo per
colonne.
In questo caso occorrono tre cicli annidati: uno (esterno) per scorrere
le righe (i) di C, uno (intermedio) per scorrere le colonne (j) di C ed uno
(interno) per calcolare il prodotto scalare, da assegnare a C[i][j], della
riga i di A con la colonna j di B.
Esempio d’uso:
5. Tipi di dato indicizzati 177
Il programma stampa:
5
21
9
int main () {
int righe = 10 , colonne = 5;
int ** m = ( int **) calloc ( righe , sizeof ( int *) ) ;
for ( int i = 0; i < righe ; i ++) {
m [ i ] = ( int *) calloc ( colonne , sizeof ( int ) ) ;
for ( int j = 0; j < colonne ; j ++) {
m [ i ][ j ] = i + j ;
}
}
r a d d o p p i a M at r i c e (m , righe , colonne ) ;
}
int main () {
int righe = 10 , colonne = 5;
int ** m = creaMatrice ( righe , colonne ) ;
int ** trasposta = t r as po ni M at ri ce (m , righe , colonne )
;
}
5.4. File
\!
home!
studente!
Documents!
miofile.txt!
src!
programmi!
• /home/studente/Documents/miofile.txt.
• Documents/miofile.txt
• ../../Documents/miofile.txt
• ./miofile.txt
scrivi-file.c
if ( file == NULL ) {
printf ( " Errore nell ’ apertura del file \ n " ) ;
exit (1) ;
}
if ( file == NULL ) {
printf ( " Errore nell ’ apertura del file \ n " ) ;
exit (1) ;
}
char s [256];
while ( fscanf ( file , " % s " ,s ) != EOF ) {
printf ( " % s " ,s ) ;
}
int close = fclose ( file ) ;
if ( close == EOF ) {
printf ( " Errore nella chiusura del file \ n " ) ;
exit (1) ;
}
if ( file == NULL ) {
printf ( " Errore nell ’ apertura del file \ n " ) ;
exit (1) ;
}
char line [256];
while ( fgets ( line , sizeof ( line ) , file ) != NULL ) {
printf ( " % s " , line ) ;
}
int close = fclose ( file ) ;
if ( close == EOF ) {
printf ( " Errore nella chiusura del file \ n " ) ;
exit (1) ;
}
File f.c
188 Introduzione alla programmazione in C
File main.c
File multipli vengono compilati indicando tutti (e soli) i file .c. Esem-
pio:
File b.c
• preprocessore
• collegamento (linking)
6.2.1. Il preprocessore
Trasforma un programma con direttive di preprocessamento deno-
tate dal simbolo # in un programma C in cui le direttive sono state
eliminate, dopo essere state eseguite.
• direttive di inclusione
• definizioni di macro
• compilazione condizionale
# define pi 3.14 f
# define BEGIN {
# define END }
Sintassi:
Esempio
6. Organizzazione di un programma in file multipli 191
diventa
i = (( j + k ) >(m - n ) ?( j + k ) :( m - n ) ) ;
Sintassi:
#if condizione1
sequenza-istruzioni1
#elif condizione2
sequenza-istruzioni2
...
#else
sequenza-istruzioni-else
192 Introduzione alla programmazione in C
• condizione è un’espressione
Semantica:
Viene valutata prima la condizione1. Se la valutazione for-
nisce un valore diverso da 0, viene inclusa nel programma
sequenza-istruzioni1. Lo stesso avviene per tutte le successive
coppie condizione sequenza-istruzioni, corrispondenti alle sezio-
ni #elif. Se nessuna delle condizioni fornisce un valore maggiore
di 0, viene inclusa nel programma sequenza-istruzioni-else.
Esempio:
...
# define PALMARE 1
...
# if PALMARE
short a , b ;
# else
int a , b ;
# ifndef MYMATH_H
# define MYMATH_H
// calcola le equazioni di secondo grado
int secondoGrado ( double a , double b , double c , double *
x1r ,
double * x1i , double * x2r , double * x2i ) ;
• Ottimizzazione
gcc -c f . c
produce il file f.o che contiene il codice oggetto, cioè il codice tradotto
in linguaggio macchina. Il codice oggetto, per poter essere eseguito, deve
prima essere collegato agli altri moduli che ad esso fanno riferimento o a
cui esso fa riferimento. Questa operazione si chiama collegamento (linking)
ed usualmente viene fatta dal compilatore stesso. Il collegamento ha lo
scopo di risolvere tutti i riferimenti ai simboli del programma. In C il
collegamento viene effettuato con il comando gcc:
gcc -c f . c
gcc -c main . c
gcc -o main f . o main . o
Esempio:
7.1. Record
Supponiamo di dover realizzare un’applicazione per manipolare i
dati anagrafici di persone: nome, cognome e data di nascita. Sarebbe
molto comodo poter trattare ciascuna persona come un’unica variable
contenente due campi stringa per memorizzare nome e cognome, e tre
campi di tipo intero (short) per memorizzare giorno, mese e anno di
nascita. Il tipo di questa variabile sarebbe concettualmente simile ad un
array (di cinque elementi), ma con un’importante differenza: i tipi in
essa contenuti non sono omogenei. Strutture dati di questo tipo vengono
chiamate record.
Sintassi:
struct {
dichiarazione-variabile-1 ;
...
dichiarazione-variabile-n ;
} lista-variabili ;
dove:
198 Introduzione alla programmazione in C
Semantica:
Vengono create le variabili menzionate in lista-variabili e, per
ciascuna di esse, allocata memoria per un un record avente come
membri i campi dichiarati in dichiarazione-variabile-i, i∈ 1, . . . , n.
struct {
char * nome ;
char * cognome ;
short int giorno ;
short int mese ;
short int anno ;
} persona_1 , persona_2 ;
persona_1!
nome! cognome! giorno! mese! anno!
(4 byte) (4 byte)! (2 byte)! (2 byte)! (2 byte)!
7. Tipi di dato strutturati 199
Sintassi:
struct nome-tag {
dichiarazione-variabile-1 ;
...
dichiarazione-variabile-n ;
};
dove:
Una volta definiti, i tag di struttura possono essere usati per definire
variabili.
Esempio: Nel seguente frammento di codice, viene definito il tag di
struttura persona, successivamente utilizzato nelle dichiarazioni delle
variabili persona_1, persona_2 e persona_3.
persona-tag.h
# endif
7. Tipi di dato strutturati 201
persona-tag.c
Sintassi:
typedef struct {
dichiarazione-variabile-1 ;
...
dichiarazione-variabile-n ;
} nome ;
dove:
Semantica:
202 Introduzione alla programmazione in C
persona.h
// File persona . h
# ifndef PERSONA_H
# define PERSONA_H
typedef struct {
char * nome ;
char * cognome ;
short int giorno ;
short int mese ;
short int anno ;
} Persona ;
# endif
Esempio: Nel seguente file, il tag di struttura persona viene usato per
definire il tipo record Persona.
7. Tipi di dato strutturati 203
// Definizione tag :
struct persona {
char * nome ;
char * cognome ;
short int giorno ;
short int mese ;
short int anno ;
};
// Definizione tipo
typedef struct persona Persona ;
Una volta definito un tipo di dato record, esso, come ogni tipo definito
dall’utente, diventa disponibile per l’uso.
Esempio: Nel seguente programma viene dichiarata la variabile
persona_1 di tipo Persona
persona.c
Persona p1 ;
p1 . nome = " Mario " ;
p1 . cognome = " Rossi " ;
p1 . giorno = 7;
p1 . mese = 4;
p1 . anno = 1964;
Persona p1 = {
. cognome = " Rossi " ,
. giorno = 7 ,
. mese = 4 ,
. nome = " Mario " ,
. anno = 1964
};
In questo caso, l’ordine con cui i campi vengono assegnati non conta,
in quanto il campo a cui assegnare il valore è identificato dal designatore.
Entrambe le inizializzazioni degli esempi appena visti hanno esatta-
mente lo stesso effetto della dichiarazione e delle assegnazioni campo a
campo viste in precedenza.
L’inizializzazione di record può aver luogo solo contestualmente alla
sua dichiarazione.
Persona p1 = ... ;
Persona p2 ;
p2 = p1 ;
typedef struct {
float coord [3]; // coordinate nello spazio
char colore [256]; // colore del punto
} PuntoColorato ;
void s t a m p a P u n t o C o l o r a t o ( PuntoColorato p ) ;
# endif
punto-colorato.c
void s t a m p a P u n t o C o l o r a t o ( PuntoColorato p ) {
7. Tipi di dato strutturati 207
punto-colorato-main.c
s t a m p a P u n t o C o l o r a t o ( pc1 ) ;
s t a m p a P u n t o C o l o r a t o ( pc2 ) ;
pc1! pc2!
coord! colore! coord! colore!
0! 0! 0! “bianco”! 0! 0! 0! “bianco”!
pc1! pc2!
coord! colore! coord! colore!
3.0! 0! 0! “bianco”! 0! 0! 0! “bianco”!
Persona * p ;
(* punt_p ) . nome
punt_p - > nome
// Esempio d ’ uso :
int main () {
Persona p = { " Mario " ," Rossi " ,7 ,4 ,1964};
printf ( " eta ’: % d \ n " , calcolaEta (p ,2018) ) ;
}
210 Introduzione alla programmazione in C
// Esempio d ’ uso :
int main () {
Persona p = { " Mario " ," Rossi " ,7 ,4 ,1964};
cambiaEta (p ,2000) ;
printf ( " anno : % d \ n " ,p . anno ) ; // stampa 1964
}
void i n i z i a l i z z a P e r s o n a ( Persona * p ) {
p -> nome = " " ;
p -> cognome = " " ;
p -> giorno = 0;
p -> mese = 0;
p -> anno = 0;
}
// Esempio d ’ uso :
// Esempio d ’ uso :
free ( p1 ) ;
free ( p1 ) ;
}
// Esempio d ’ uso :
typedef struct {
int rows ;
int cols ;
float ** row_ptrs ;
} Mat ;
# ifndef P U N T O C O LO R A T O 2 _ H
# define P U N T O C O LO R A T O 2 _ H
typedef struct {
float X ;
7. Tipi di dato strutturati 215
float Y ;
float Z ;
} Punto ;
typedef struct {
Punto punto ;
char colore [256];
} PuntoCo lorato2 ;
# endif
parente.h
# ifndef PARENTE_H
# define PARENTE_H
// Tag di struttura :
struct parente {
char nome [256];
char cognome [256];
struct parente * padre ;
struct parente * madre ;
};
// Definizione di tipo :
typedef struct parente Parente ;
# endif
// ...
}
7.2. Unioni
Un ulteriore tipo di dato strutturato messo a disposizione del C è
union, o unione. union è un tipo che si comporta in maniera simile ad
un record ma, a differenza di questo, memorizza solo un campo alla volta.
Variabili di tipo union e tipi union vengono rispettivamente dichia-
rate e definiti allo stesso modo dei record, sostituendo la parola chiave
struct con union.
Esempio: Il seguente frammento di codice definisce il tipo di dato
chardouble come una unione contenente i campi c di tipo char e d di
tipo double.
# ifndef CHARDOUBLE_H
# define CHARDOUBLE_H
typedef union {
char c ;
double d ;
} chardouble ;
# endif
218 Introduzione alla programmazione in C
Una volta definito, il tipo di dato può essere usato per dichiarare
variabili.
Esempio:
chardouble cd ;
double d!
(8 byte)
char c!
(1 byte)
La caratteristica distintiva dei tipi union consiste nel fatto che solo
l’ultimo campo a cui è stato assegnato un valore è significativo, ovvero contiene
effettivamente il valore ad esso assegnato, mentre il valore contenuto
negli altri campi è soggetto a modifiche arbitrarie. Questo deriva dal
fatto che diversi campi condividono una stessa regione di memoria (ad
es. il primo byte nella figura precedente) e quindi le modifiche apportate
ad ognuno di essi hanno un impatto anche sugli altri. Per capire come
ciò avvenga, si consideri nuovamente l’organizzazione della memoria
della variabile cd. Come si può vedere i due campi c e d della variabile
condividono il primo byte, pertanto ogni assegnazione al campo c avrà
un impatto sul valore corrente del campo d e viceversa.
L’accesso ai campi di una union si effettua con le stesse modalità del
caso dei record. Ad esempio, l’espressione cd.c denota il campo c della
variabile c.
Esempio: Il seguente frammento di codice mostra l’uso di una varia-
bile di tipo chardouble.
7. Tipi di dato strutturati 219
chardouble cd ;
cd . c = ’a ’;
/* Solo il campo c e ’ significativo
( fino alla prossima assegnazione ) */
// ...
cd . d = 30.0;
/* Solo il campo d e ’ significativo
( fino alla prossima assegnazione ) */
// ...
typedef struct {
char c ;
double d ;
} doublechar ;
Sintassi:
typedef enum {
valore-1 ,
...
valore-n
} nome ;
dove:
// File direzione . h
# ifndef DIREZIONE_H
# define DIREZIONE_H
typedef enum {
ALTO ,
BASSO ,
DESTRA ,
SINISTRA
} Direzione ;
# endif
Il tipo può essere usato per definire una variabile di tipo Direzione:
Direzione d ;
7. Tipi di dato strutturati 221
d = BASSO ;
// ...
if ( d == ALTO ) {...}
if ( d == BASSO ) {...}
// ...
8. Ricorsione
• uno (o più) casi base, per i quali il risultato può essere determinato
direttamente;
x, se y = 0
somma( x, y) =
1 + somma( x, y − 1), se y > 0
0, se y = 0
prodotto( x, y) =
somma( x, prodotto( x, y − 1)), se y > 0
Implementazione:
Implementazione:
int f a t t o r i a l e I t e r a t i v a ( int n ) {
int ris = 1;
while ( n > 0) {
ris = ris * n ;
n - -;
}
return ris ;
}
• terminazione:
Es. n- -; consente di rendere la condizione (n > 0) del ciclo while
falsa
Implementazione ricorsiva, sfruttando la definizione ricorsiva data
precedentemente:
• passo ricorsivo:
Es. return n * fattoriale(n - 1) ;
leggi un elemento
if (elemento valido) {
elabora elemento letto;
8. Ricorsione 227
if (dato vuoto) {
passo base;
}
else {
leggi un elemento
elabora elemento letto;
chiama ricorsivamente lettura sui dati rimanenti;
}
Implementazione ricorsiva:
leggi un elemento;
if (!elemento valido)
return 0;
else
8. Ricorsione 229
if (dato vuoto)
return 0;
else
return 1 + risultato ricorsione sui dati rimanenti;
leggi un elemento;
if (!elemento valido)
return 0;
else if (condizione)
return 1 + risultato-della-chiamata-ricorsiva;
else
return risultato-della-chiamata-ricorsiva;
if (dato vuoto)
return 0;
else if (condizione)
return 1 + risultato ricorsione sui dati rimanenti;
else
return risultato ricorsione sui dati rimanenti;
• se i è vuoto, restituisci 0;
leggi un elemento;
if (!elemento valido)
return elemento-neutro-di-op;
8. Ricorsione 231
else
return valore-elemento op risultato-della-chiamata-ricorsiva;
if (dato vuoto)
return elemento-neutro-di-op;
else
return valore primo elemento <op>
risultato ricorsione sui dati rimanenti;
• leggi un elemento i;
• leggi un elemento i;
oppure
• leggi un elemento i;
• altrimenti,
void f ( char * s ) {
if (* s == ’ \0 ’) { // caso base
...
}
else { // caso ricorsivo
... * s // operazione sul primo carattere
... f ( s +1) // chiamata ricorsiva
}
}
// funzione ricorsiva
void ricorsivo ( int n ) {
if ( n == 0) {
stampa (n , " finito " ) ;
}
else {
stampa (n , " attiva ricorsivo (n -1) " ) ;
ricorsivo (n -1) ;
stampa (n , " finito " ) ;
}
}
236 Introduzione alla programmazione in C
int main () {
int j ;
printf ( " Inserisci livello ricorsione \ n " ) ;
scanf ( " % d " ,& j ) ;
printf ( " Main - attiva ricorsivo (% d ) \ n " ,j ) ;
ricorsivo ( j ) ;
printf ( " Main - Finito \ n " ) ;
}
n 0
IR 532
n 1 n 1 n 1
IR 532 IR 532 IR 532
n 2 n 2 n 2 n 2 n 2
IR 258 IR 258 IR 258 IR 258 IR 258
j 2 j 2 j 2 j 2 j 2 j 2 j 2
1 2 3 4 5 6 7
Facciamo notare che per le diverse attivazioni ricorsive vengono crea-
ti diversi RDA sulla pila, con valori via via decrescenti del parametro
8. Ricorsione 237
0,
se n = 0
F (n) = 1, se n = 1
F ( n − 2) + F ( n − 1) , se n > 1
Lo stato iniziale (a), uno stato intermedio (b), e lo stato finale (c) per un
insieme di 6 dischi sono mostrati nelle seguenti figure:
8. Ricorsione 239
(a)
(b)
(c)
1. sposta n − 1 dischi da 1 a 3
3. sposta n − 1 dischi da 3 a 2
240 Introduzione alla programmazione in C
int main () {
printf ( " Quanti dischi vuoi muovere ?\ n " ) ;
int n ;
scanf ( " % d " ,& n ) ;
printf ( " Per muovere % d dischi da 1 a 2 con 3 come
appoggio :\ n " ,n ) ;
muovi (n , 1 , 2 , 3) ;
}
0 1 2 3 4 5
0 * o o * o o
1 o * o o o o
2 o o * * o o
3 * * o o o o
4 * * * o * *
Palude 1
0 1 2 3 4 5
0 * o o * o o
1 * o o o o o
2 o * o o o *
3 o o * * * o
4 o * o o o o
Palude 2
// dichiara la palude
int palude [ righe ][ colonne ];
void stampaPalude () {
for ( int r = 0; r < righe ; r ++) {
for ( int c = 0; c < colonne ; c ++)
printf ( palude [ r ][ c ]? " * " : " o " ) ;
printf ( " \ n " ) ;
}
}
8. Ricorsione 243
int main () {
initPalude () ;
stampaPalude () ;
int cammino [ colonne ];
for ( int c =0; c < colonne ; c ++) cammino [ c ] = 0;
if ( a t t r a v e r s a P a l u d e ( cammino ) )
for ( int c =0; c < colonne ; c ++) printf ( " % d " ,
cammino [ c ]) ;
else
printf ( " Cammino : cammino inesistente " ) ;
printf ( " \ n " ) ;
}
9. Strutture collegate lineari
struct ElemSCL {
TipoInfoSCL info ;
struct ElemSCL * next ;
};
• modifica di elementi
• creazione
• inserimento ed eliminazione
9. Strutture collegate lineari 249
• deallocazione
Creazione di un nodo:
Collegamento di nodi:
...
delSCL (& scl ) ;
delSCL (& scl ) ;
delSCL (& scl ) ;
9. Strutture collegate lineari 251
• vuota;
if (SCL vuota ) {
passo base
}
else {
elaborazione primo elemento della SCL
chiamata ricorsiva sul resto della SCL
}
leggi dato
if (dato valido ) {
inserimento del dato nella SCL
chiamata ricorsiva sui dati rimanenti
}
TipoSCL scl ;
...
writeSCL ( scl ) ;
Scrittura su file:
9. Strutture collegate lineari 253
9.5.3. Ricerca
9.5.4. Lunghezza
TipoSCL scl ;
...
substElemSCL ( scl , e , n ) ;
TipoSCL scl ;
...
readSCL (& scl ) ;
Lettura da file:
9.7.3. Copia
9.7.4. Eliminazione
Scrittura
9. Strutture collegate lineari 261
Lunghezza
Sostituzione di un elemento
Lettura da file
264 Introduzione alla programmazione in C
Copia
9. Strutture collegate lineari 265
Inserimento in posizione n
Eliminazione
9. Strutture collegate lineari 267
Inversa
invece di
9.10.2. Primo
9.10.3. Resto
// copia la SCL
TipoSCL copySCL ( TipoSCL scl ) {
if ( emptySCL ( scl ) )
return NULL ;
else
return addSCL ( copySCL ( restoSCL ( scl ) ) ,
primoSCL ( scl ) ) ;
}
pile, code, alberi, grafi ecc.; si usa spesso il termine struttura di dati
per riferirsi a questi tipi di dato.
TipoAstratto T
Domini
Costanti
Funzioni
FineTipoAstratto
La scelta dello schema realizzativo costituisce una delle scelte più criti-
che nella realizzazione di un tipo di dato; essa è dettata tanto da consi-
derazioni relative alle prestazioni quanto da considerazioni di natura
modellistica, ovvero dipendenti dall’entità che si vuole modellare con
il tipo di dato in esame. In particolare, se questo viene usato per mo-
dellare valori (si pensi ad esempio a numeri complessi, punti del piano
cartesiano, etc.) è tipicamente indicata une realizzazione che non effettui
side-effect. Quando invece il tipo è introdotto allo scopo di modellare
entità (ad es., persone, oggetti fisici, etc.) è normalmente privilegiata una
realizzazione con side-effect. Nel primo caso, infatti, appare naturale
pensare un valore come immutabile (il valore 3, in quanto valore, non
278 Introduzione alla programmazione in C
p2 = delPos ( p1 ,1) ;
p2 = delPos ( p1 ,1) ;
Osservazioni:
# define FALSE 0
# define TRUE 1
10.3.12. Osservazioni
• Per uno stesso tipo astratto si possono avere più implementazioni
diverse.
R : dominio di reali
Funzioni
creaComplesso(R r, R i) 7→ C
pre: nessuna
post: result è il numero complesso avente r come parte reale e i
come parte immaginaria
Funzioni
reale(C c) 7→ R
pre: nessuna
post: result è il valore della parte reale del numero complesso c
immaginaria(C c) 7→ R
pre: nessuna
post: result è il valore della parte immaginaria del numero com-
plesso c
modulo(C c) 7→ R
pre: nessuna
post: result è il modulo del numero complesso c
fase(C c) 7→ R
pre: nessuna
post: result è la fase del numero complesso c
FineTipoAstratto
typedef struct {
double re , im ;
} Nu me r oC om pl e ss o ;
Funzioni:
10. I tipi di dato astratti 285
double modulo ( Nu me r oC om pl e ss o c ) {
return sqrt ( pow ( c . re ,2) + pow ( c . im ,2) ) ;
}
double fase ( N um e ro Co mp l es so c ) {
return atan2 ( c . im , c . re ) ;
}
typedef ... T1 ;
typedef ... T2 ;
struct Coppia {
T1 primo ;
T2 secondo ;
};
Funzioni:
Coppia formaCoppia ( T1 a , T2 b ) {
Coppia c ;
c . primo = a ; c . secondo = b ;
return c ;
}
T1 pr im a Co mp on e nt e ( Coppia c ) {
return c . primo ;
}
T2 s e c o n d a C o m p o n e n t e ( Coppia c ) {
return c . secondo ;
}
• {e1, e2, ...} denota un insieme contenente gli elementi e1, e2, ecc.
Insieme insiemeVuoto () ;
Insieme inserisci ( Insieme ins , T e ) ;
Insieme elimina ( Insieme ins , T e ) ;
bool estVuoto ( Insieme ins ) ;
bool membro ( Insieme ins , T e ) ;
Insieme * insiemeVuoto () ;
void inserisci ( Insieme * ins , T e ) ;
void elimina ( Insieme * ins , T e ) ;
bool estVuoto ( Insieme * ins ) ;
bool membro ( Insieme * ins , T e ) ;
struct NodoSCL {
T info ;
struct NodoSCL * next ;
};
Insieme * insiemeVuoto () {
Insieme * r = ( Insieme *) malloc ( sizeof ( Insieme ) ) ;
* r = NULL ;
return r ;
}
typedef int T ;
typedef struct {
int size ; // dimensione dell ’ array
int nelem ; // num . elementi validi
T * data ; // array
} Insieme ;
Insieme * insiemeVuoto () {
Insieme * ins = ( Insieme *) malloc ( sizeof ( Insieme ) ) ;
ins - > size = 100;
ins - > nelem = 0;
ins - > data = ( T *) malloc ( ins - > size * sizeof ( T ) ) ;
return ins ;
}
crea(C c) 7→ It
pre: nessuna
post: result è un iteratore per la collezione c inizializzato per pun-
tare ad un primo elemento della collezione
Funzioni
10. I tipi di dato astratti 293
hasNext(It i) 7→ Boolean
pre: nessuna
post: result è true se l’iteratore i punta ad un elemento valido della
collezione, false altrimenti
next(It i) 7→ T
pre: i punta ad un elemento valido
post: result è il valore dell’elemento puntato dall’iteratore i, l’itera-
tore viene incrementato per puntare ad un prossimo elemento della
collezione non ancora visitato
FineTipoAstratto
typedef struct {
NodoSCL * ptr ;
} I te r a t o r e I n s i e m e ;
I t e r a t o r e I ns i e m e * c r e a I t e r a t o r e I n s i e m e ( Insieme * ins ) {
I t e r a t o r e I ns i e m e * r = ( I t er a t or e I n s i e m e *) malloc (
sizeof ( I t e r a t o r e I n s i e m e ) ) ;
r - > ptr = * ins ;
return r ;
}
bool hasNext ( I t e r a t o r e I n si e m e * it ) {
return it - > ptr != NULL ;
}
T next ( I t e ra t o r e I n s i e m e * it ) {
T r = TERRORVALUE ;
if ( it - > ptr != NULL ) {
r = it - > ptr - > info ;
it - > ptr = it - > ptr - > next ;
}
else
printf ( " ERRORE Iteratore non valido .\ n " ) ;
return r ;
}
typedef struct {
Insieme * ins ; // riferimento all ’ insieme da
visitare
int ptr ; // indice del prossimo elemento
} I t er a t o r e I n s i e m e ;
I t e r a t o r e I n s i em e * c r e a I t e r a t o r e I n s i e m e ( Insieme * ins ) {
I t e r a t o r e I n s ie m e * it = ( I t e r a t o r e I ns i e m e *) malloc (
sizeof ( I t e r a t o r e I n s i e m e ) ) ;
it - > ins = ins ;
it - > ptr = 0;
return it ;
}
int hasNext ( I t e r a t o r e I n s ie m e * it ) {
return it - > ptr < it - > ins - > nelem ;
}
T next ( I t er a t o r e I n s i e m e * it ) {
T r = TERRORVALUE ;
if ( hasNext ( it ) ) {
r = it - > ins - > data [ it - > ptr ];
it - > ptr ++;
}
else
printf ( " ERRORE Iteratore non valido .\ n " ) ;
return r ;
}
• (e1 e2 ...) denota una lista il cui primo elemento è e1, il secondo e2,
ecc.
1 I nomi car e cdr in riferimento alle liste erano usati nelle prime implementazioni del
LISP (List Processor), un linguaggio funzionale interamente basato su liste, ideato da
John McCarty nel 1958. Essi fuorono suggeriti dalle abbreviazioni per “contents of the
address part of register number” (car) e “contents of the decrement part of register
number” (cdr), utilizzate in riferimento alla macchina IBM 704, su cui i precursori del
Lisp furono implementati.
10. I tipi di dato astratti 297
Funzioni
cons(T e, Lista l) 7→ Lista
pre: nessuna
post: result è la lista ottenuta da l inserendo e come primo elemento
car(Lista l) 7→ T
pre: l non è la lista vuota
post: result è il primo elemento di l
cdr(Lista l) 7→ Lista
pre: l non è la lista vuota
post: result è la lista ottenuta da l eliminando il primo elemento
FineTipoAstratto
TipoLista listaVuota () ;
int estVuota ( TipoLista l ) ;
TipoLista cons ( T e , TipoLista l ) ;
T car ( TipoLista l ) ;
TipoLista cdr ( TipoLista l ) ;
TipoLista * listaVuota () ;
bool estVuota ( TipoLista * l ) ;
void cons ( T e , TipoLista * l ) ;
T car ( TipoLista * l ) ;
void cdr ( TipoLista * l ) ;
298 Introduzione alla programmazione in C
typedef int T ;
struct NodoSCL {
T info ;
struct NodoSCL * next ;
};
T car ( TipoLista l ) {
if ( l == NULL ) {
printf ( " ERRORE : lista vuota \ n " ) ;
exit (1) ;
}
return l - > info ;
}
10. I tipi di dato astratti 299
typedef struct {
T * data ;
int n ;
} TipoLista ;
TipoLista listaVuota () {
TipoLista l ;
l . n =0;
return l ;
}
T car ( TipoLista l ) {
if ( l . n ==0) {
printf ( " ERRORE : lista vuota \ n " ) ;
exit (1) ;
}
return l . data [0];
}
Funzioni
codaVuota() 7→ Coda
pre: nessuna
post: result è la coda vuota
estVuota(Coda c) 7→ Boolean
pre: nessuna
post: result è true se c è il valore corrispondente alla coda vuota,
false altrimenti corrispondente alla coda vuota, false altrimenti
Funzioni
inCoda(Coda c, T e) 7→ Coda
pre: nessuna
post: result è la coda ottenuta dalla coda c inserendo l’elemento e,
che ne diventa l’ultimo elemento della coda
outCoda(Coda c) 7→ Coda
pre: c non è la coda vuota
post: result è la coda ottenuta dalla coda c eliminando l’elemento
in testa, cioè che tra quelli presenti era stato inserito per primo
primo(Coda c) 7→ T
pre: c non è la coda vuota
post: result è l’elemento in testa alla coda c, cioè l’elemento che tra
quelli presenti in c era stato inserito per primo
FineTipoAstratto
typedef int T ;
struct NodoSCL {
T info ;
struct NodoSCL * next ;
};
Coda * codaVuota () {
return ( Coda *) malloc ( sizeof ( Coda ) ) ;
}
T primo ( Coda * c ) {
if (* c == NULL ) {
printf ( " ERRORE : coda vuota " ) ;
exit (1) ;
}
return (* c ) -> info ;
}
In questo modo si riduce la frequenza delle riallocazioni (si noti che ciò
implica, in generale, spreco di memoria).
Con queste scelte, abbiamo la seguente definizione del tipo Coda:
306 Introduzione alla programmazione in C
typedef struct {
T * data ; // elemento data [0]: testa della coda
int size ; // dimesione array
int nelem ; // dimensione coda
} Coda ;
Coda * codaVuota () {
Coda * r = ( Coda *) malloc ( sizeof ( Coda ) ) ;
r - > data = NULL ;
r - > size = 0;
r - > nelem = 0;
return r ;
}
T primo ( Coda * c ) {
if (c - > nelem == 0) {
printf ( " ERRORE : coda vuota " ) ;
exit (1) ;
}
return c - > data [0];
}
Questa politica di accesso viene detta LIFO (“Last In, First Out”).
Con una pila si risolvono facilmente i problemi in cui i dati vanno
elaborati in ordine inverso dal loro arrivo (dal più recente al più vecchio).
Un tipico esempio d’uso della struttura dati pila è quello in cui essa
viene utilizzata per la gestione delle invocazioni di funzione, ovvero
308 Introduzione alla programmazione in C
la pila dei RDA, dove l’invocazione più recente è la porssima che deve
essere processata.
Il tipo di dato astratto Pila è definito come segue.
TipoAstratto Pila(T)
Domini
Pila : dominio di interesse del tipo
T : dominio degli elementi delle pile
Funzioni
pilaVuota() 7→ Pila
pre: nessuna
post: result è la pila vuota
estVuota(Pila p) 7→ Boolean
pre: nessuna
post: result è true se p è il valore corrispondente alla pila vuota,
false altrimenti
Funzioni
push(Pila p, T e) 7→ Pila
pre: nessuna
post: result è la pila ottenuta dalla pila p inserendo l’elemento e,
che ne diventa l’elemento affiorante
pop(Pila p) 7→ Pila
pre: p non è la pila vuota
post: result è la pila ottenuta dalla pila p eliminando l’elemento
affiorante
top(Pila p) 7→ T
pre: p non è la pila vuota
post: result è l’elemento affiorante della pila p
FineTipoAstratto
• Uno stesso programma che riceve in input 100 nomi (come strin-
ghe) potrebbe impiegare k secondi se ciascuna stringa è formata da
un singolo carattere e 200 k secondi se ciascuna stringa è formata
da 200 caratteri.
• blocco di istruzioni
11. Costo dei programmi, algoritmi di ricerca e di ordinamento 311
{
< istruzione_1 >
....
< instruzione_k >
}
• istruzione condizionale
| f (n)| ≤ cg(n)
Nota: La condizione !r nel controllo del ciclo for non cambia il caso
peggiore e quindi non ha effetto sull’analisi del costo nel caso peggiore.
Implementazione
ordinato, ogni valore viene comunque confrontato con tutti gli altri. Il
numeri di scambi invece dipende dall’array in input e può essere pari
a 0, se l’array è già ordinato, o pari a n se l’array è ordinato in ordine
inverso.
Non esiste un caso peggiore per quanto riguarda il numero di confron-
ti, mentre per il numero di scambi il caso peggiore si presenta quando
l’array è ordinato in ordine inverso.
Il numero di totale di operazioni di confronto è dato da:
n ( n − 1)
(n − 1) + (n − 2) + ...... + 2 + 1 =
2
Operazione base:
se due elementi adiacenti non sono ordinati
scambiali
Metodo:
verifica se la collezione è ordinata e
scambia tutte le coppie non ordinate
Algoritmo:
Passo 0: 1 3 1 4 2 5 9 4 6 5
Passo 1: 1 1 3 2 4 4 5 9 5 6
Passo 2: 1 1 2 3 4 4 5 5 9 6
Passo 3: 1 1 2 3 4 4 5 5 6 9
Passo 4: 1 1 2 3 4 4 5 5 6 9
Collezione ordinata :
1 1 2 3 4 4 5 5 6 9
Implementazione
// funzione principale
void mergeSort ( int * v , int n ) {
mergeSort_r (v ,0 ,n -1) ;
}
11. Costo dei programmi, algoritmi di ricerca e di ordinamento 321
Esempio:
v = 6 12 7 8 5 4 9 3 1
x = 6 -> k = 4
v ’ = 5 4 3 1 6 12 7 8 9
ordinamento del sotto - vettore [5 4 3 1]
ordinamento del sotto - vettore [12 7 8 9]
La scelta del pivot può essere effettuata secondo diversi criteri. Nel-
l’implementazione che segue viene scelto semplicemente il primo ele-
mento del vettore.
Implementazione
11. Costo dei programmi, algoritmi di ricerca e di ordinamento 323
// funzione principale
void quickSort ( int * v , int n ) {
quickSort_r (v ,0 ,n -1) ;
}
1 Nel caso in esame, ovvero quando viene preso come pivot il primo elemento dell’array,
questa situazione si verifica se l’array è già ordinato (crescente o decrescente).
324 Introduzione alla programmazione in C
12.1. Alberi
In questo capitolo viene presentata la struttura dati albero, che rappre-
senta un esempio particolarmente significativo di struttura non lineare.
Infatti, prendendo a modello la definizione induttiva della struttura
dati, la definizione delle funzioni che operano su alberi binari risulta
immediata, mentre le implementazioni con i costrutti di ciclo richiedono
un uso di strutture dati di appoggio.
L’albero è un tipo di dato non lineare utilizzato per memorizzare
informazioni in modo gerarchico. Un esempio di albero è il seguente:
padre di x radice
fratelli di x
figli di x
foglie
x y
radice dell'albero
sottoalbero sottoalbero
sinistro destro
E F
A B C D
v[0]="G" Livello 0
v[1]="E"
Livello 1
v[2]="F"
v[3]="A"
v[4]="B"
Livello 2
v[5]="C"
v[6]="D"
Si noti che i figli del nodo ’F’, che corrisponde all’indice 2, sono ’C’ e
’D’, e questi corrispondono agli indici 5 = 2 · 2 + 1 e 6 = 2 · 2 + 2.
Un modo pratico per costruire una rappresentazione indicizzata di
un albero binario consiste nel disporre nell’array, a partire da v[0], i
valori dei nodi presi da sinistra verso destra su ogni livello a partire
dal livello zero fino al livello massimo: questo appare evidente se si
osservano le due figure precedenti.
Esempio: L’albero binario completo mostrato nella figura seguente:
E F
A B C D
330 Introduzione alla programmazione in C
char [] v = { ’G ’ , ’E ’ , ’F ’ , ’A ’ , ’B ’ , ’C ’ , ’D ’ };
B G
C F H I
D E
char [] v = { ’A ’ , ’B ’ , ’G ’ , ’C ’ , ’F ’ , ’H ’ , ’I ’ , ’D ’ ,
’E ’ , ’0 ’ , ’0 ’ , ’0 ’ , ’0 ’ , ’L ’ , ’0 ’ };
Si dove ’0’ viene usato per indicare che il nodo non esiste.
struct StructAlbero {
TipoI nfoAlber o info ;
bool esiste ;
};
TipoAlbero albero ;
...
// i : nodo di cui stampare i figli
if (i <0 || i >= MaxNodiAlbero )
printf ( " Indice errato ... " ) ;
else {
if (2* i +1 < MaxNodiAlbero && albero [2* i +1]. esiste )
printf ( " sinistro di % d -> % d \ n " , i , albero [2* i +1].
info ) ;
else
printf ( " % d non ha figlio sinistro \ n " , i ) ;
if (2* i +2 < MaxNodiAlbero && albero [2* i +2]. esiste )
printf ( " destro di % d -> % d \ n " , i , albero [2* i +2].
info ) ;
else
printf ( " % d non ha figlio destro \ n " , i ) ;
}
struct StructAlbero {
TipoI nfoAlber o info ;
struct StructAlbero * destro , * sinistro ;
};
Osserviamo che:
E F
A B C D
TipoAlbero A = nodoalb_alloc ( ’A ’) ;
TipoAlbero B = nodoalb_alloc ( ’B ’) ;
TipoAlbero C = nodoalb_alloc ( ’C ’) ;
TipoAlbero D = nodoalb_alloc ( ’D ’) ;
TipoAlbero E = nodoalb_alloc ( ’E ’) ;
TipoAlbero F = nodoalb_alloc ( ’F ’) ;
TipoAlbero G = nodoalb_alloc ( ’G ’) ;
E - > sinistro = A ; E - > destro = B ;
F - > sinistro = C ; F - > destro = D ;
G - > sinistro = E ; G - > destro = F ;
TipoAlbero albero = G ;
albero
G NodoBin
Object info "G"
NodoBin sinistro
NodoBin destro
E NodoBin F NodoBin
Object info "E" Object info "F"
NodoBin sinistro NodoBin sinistro
A NodoBin destro B C NodoBin destro D
E F
A B C D
"( G ( E ( A ( ) ( ) ) ( B ( ) ( ) ) ) " +
"( F ( C ( ) ( ) ) ( D ( ) ( ) ) ) )".
E F
A B C D
12. Alberi binari 337
Si noti che:
338 Introduzione alla programmazione in C
* 7
+ +
4 2 3 9
( -
( *
( +
( 4 ( ) ( ) )
( 2 ( ) ( ) )
)
( +
( 3 ( ) ( ) )
( 9 ( ) ( ) )
)
)
( 7 ( ) ( ) )
)
int valutaEspressione(TipoAlbero a) {
// caso albero vuoto: errore
if (a==NULL) Errore albero vuoto;
// caso nodo foglia: restituisce valore
if (a->sinistro==NULL && a->destro==NULL)
return valore numerico;
else { // caso nodo non foglia: calcola ricorsivamente
// le sottoespressioni destra e sinistra
int sin = valutaEspressione(a->sinistro);
int des = valutaEspressione(a->destro);
// applica operazione
if (nodo-addizione) return sin + des;
else ... analogamente per gli altri casi
else Operazione non valida
}
340 Introduzione alla programmazione in C
if T non è vuoto {
analizza il nodo radice di T
visita il sottoalbero sinistro di T
visita il sottoalbero destro di T
}
E F
A B C D
• visita in preordine: G E A B F C D
• visita simmetrica: A E B G C F D
• visita in postordine: A B E C D F G
342 Introduzione alla programmazione in C
void V is it aP re o rd in e ( TipoAlbero a )
{
/* visita a partire dalla radice */
V is it aI nd i ce Pr e (a , 0) ;
}
/* visita in preordine */
void V is it aI nd i ce Pr e ( TipoAlbero a , int i ) {
if (i >=0 && i < MaxNodiAlbero && a [ i ]. esiste ) {
/* albero non vuoto */
Analizza ( a [ i ]. info ) ; // analizza la radice
V is it aI nd i ce Pr e (a , i * 2 + 1 ) ; // visita sinistro
V is it aI nd i ce Pr e (a , i * 2 + 2 ) ; // visita destro
}
}
void V i s i t a P o s t o r d i n e ( TipoAlbero a ) {
if ( a != NULL ) {
/* albero non vuoto */
V i s i t a P o s t o r d in e (a - > sinistro ) ; // visita sinistro
V i s i t a P o s t o r d in e (a - > destro ) ; // visita destro
Analizza (a - > info ) ; // analizza la radice
}
}
17
10 21
6 13 20 33
2 7
bool p r e s e n t e A l b e r o R i c e r c a ( TipoAlbero a ,
TipoI nfoAlber o elem ) {
if ( a == NULL )
return false ;
else
if (a - > info == elem ) // elemento nella radice
return true ;
else if (a - > info > elem ) // l ’ elemento puo ’ essere
// solo nel sottoalbero sin
return p r e s e n t e A l b e r o R i c e r c a (a - > sinistro , elem ) ;
else // l ’ elemento puo ’ essere solo nel
sottoalbero des
return p r e s e n t e A l b e r o R i c e r c a (a - > destro , elem ) ;
}
void i n s F o g l i a I n P o s i z i o n e ( TipoAlbero * a ,
TipoI nfoAlber o e , char * p ) {
if ( a == NULL )
printf ( " ERRORE : puntatore NULL " )
else if (* a == NULL )
* a = creaAlbBin (e , NULL , NULL ) ;
else if (* p == ’ \0 ’)
return ;
else if (* p == ’s ’)
i n s F o g l i a I n P o s i z i o n e (&((* a ) -> sinistro ) , e , p +1) ;
else if (* p == ’d ’)
i n s F o g l i a I n P o s i z i o n e (&((* a ) -> destro ) , e , p +1) ;
}
i n s e r i s c i F o g l i a I n P o s i z i o n e (a , ’H ’ , " sd " )
G C
B H F
D E
12. Alberi binari 351
• n ha entrambi i figli
B C
D E
12. Alberi binari 353
B F
D E
Nel secondo caso, invece, non esiste una soluzione unica e la scelta
di come gestire i sottoalberi del nodo eliminato dipende dal caso in
esame. A scopo illustrativo, mostriamo una possible soluzione in cui il
sottoalbero destro del nodo da eliminare viene prima reso sottoalbero
sinistro della foglia più a sinistra del sottoalbero con radice il nodo da
eliminare. Questo comporta che il nodo selezionato abbia un solo figlio,
per cui l’eliminazione ricade in uno dei casi precedenti.
Ad esempio, l’eliminazione del nodo B dal primo albero viene ef-
fettuata rendendo dapprima il nodo E figlio sinistro della foglia D,
ottenendo il seguente albero:
B C
D F
D C
E F
void v i s i t a P r o f o n d i t a I t e r a t i v a ( TipoAlbero a ) {
if ( a == NULL ) return ;
Pila * p = pilaVuota () ;
push (p , a ) ;
while (! estVuota ( p ) ) {
TipoAlbero n = top ( p ) ;
analizza ( n ) ;
pop ( p ) ;
if (n - > destro != NULL )
push (p ,n - > destro ) ;
if (n - > sinistro != NULL )
push (p ,n - > sinistro ) ;
}
}
E F
A B C D
A
E B B C
G F F F F D D (Pila vuota)
GEABFGD
E F
A B C D
GEFABCD
12. Alberi binari 357
padre di x radice
fratelli di x
figli di x
foglie
• nient’altro è un albero.
TipoAstratto AlberoN(T)
Domini
Funzioni
albVuoto() 7→ AlberoN
pre: nessuna
post: result è l’albero vuoto (ossia senza nodi)
creaAlb(T r, AlberoN a1, . . . , AlberoN an) 7→ AlberoN
pre: nessuna
post: result è l’albero n-ario che ha r come radice, a1,. . . , an come
sottoalberi
estVuoto(AlberoN a) 7→ Boolean
pre: nessuna
post: result è true se a è vuoto, false altrimenti
radice(AlberoN a) 7→ T
pre: a non è vuoto
post: result è il valore del nodo radice di a
figli(AlberoN a) 7→ Lista[AlberoN]
pre: a non è vuoto
post: result è una lista contenente i figli di a
FineTipoAstratto
13. Alberi n-ari e grafi 363
if T non è vuoto {
analizza il nodo radice di T
while (primofiglio(T) !=NULL) {
visita primofiglio(T)
passa al figlio successivo
}
}
Osserviamo che:
• il campi figli contiene un riferimento ad una lista dei figli del nodo.
Il valore NULL di questo campo indica che si tratta di un nodo
foglia.
TipoAlbero alberoVuoto () {
return NULL ;
}
void s ta mp aP re o rd in e ( TipoAlbero a ) {
if ( a != NULL ) {
printf ( " % d " ,a - > info ) ;
TipoFigli p = a - > figli ;
while ( p != NULL ) {
s ta mp aP r eo rd in e (p - > albero ) ;
p = p - > next ;
}
}
}
• viene inserito "()" per indicare che non vi sono altri figli in un
nodo;
13.1.8. Esercizi
La variante della funzione che calcola la profondità dell’albero bina-
rio è immediata:
13.2. Grafi
Come anticipato, se rimuoviamo il vincolo che ogni nodo abbia uno
ed un solo padre, la struttura dati che otteniamo prende il nome di grafo.
In realtà, in molti testi si introduce prima il grafo, come struttura dati in
grado di rappresentare relazioni binarie e successivamente si considera
come caso particolare, l’albero n-ario.
In questa sezione, dopo una definizione della struttura dati grafo,
faremo cenno ad alcune possibili implementazioni e rivisiteremo le
nozioni di visita in profondità e visita in ampiezza nel caso del grafo.
Un esempio di grafo è il seguente:
13. Alberi n-ari e grafi 373
• ciclo è un cammino che contiene più volte lo stesso nodo del grafo.
• [i, j] = 0, altrimenti
374 Introduzione alla programmazione in C
Matrice delle adiacenze (le occorrenze del valore 0 non sono riportate
per leggibilità):
- 0 1 2 3 4 5 6
0 1
1 1
2 1
3 1 1
4 1 1
5 1 1
6
L’implementazione di questa struttura si ottiene facilmente
struct Grafo {
TipoInfoGrafo valori_nodi [ NumeroNodi ];
int mat_adiacenza [ NumeroNodi ][ NumeroNodi ];
};
• esistenza di cammini tra coppie di nodi del grafo nodi del grafo,
La tecnica per estendere i metodi di visita visti per gli alberi al caso
del grafo consiste nel marcare i nodi durante la visita in modo che la
visita prosegua per un nodo successore soltanto se esso non è già stato
marcato (visitato in precedenza). In sintesi:
anche nel caso della visita del grafo in ampiezza si presenta il pro-
blema di gestire l’eventuale presenza di cicli. Ed, anche in questo caso,
si utilizza la marcatura dei nodi.