Sei sulla pagina 1di 52

LINGUAGGIO C

Le variabili
Una variabile un'area di memoria cui viene dato un nome, in grado di memorizzare un singolo
valore (numerico o un carattere). Per poter utilizzare una variabile per salvare un valore
necessario dichiararla specificandone il nome ed il tipo di valore per i quali di desidera utilizzarla;
dunque, prima di utilizzare una variabile necessario dichiarla. Nel C ANSI la dichiarazione delle
variabili viene messa all'inizio della funzione che ne far uso.
Ci sono cinque tipi "primitivi":

int - numero intero.


float - numero in virgola mobile, ossia un numero con una parte frazionaria.
double - numero in virgola mobile in doppia precisione.
char - un carattere.
void - un tipo per usi speciali

Uno degli aspetti meno chiari del linguaggio la quantit di memoria che ciascuno di questi tipi
richiede non predefinita, in quanto dipende dall'architettura del calcolatore. Per individuare
quanta memoria viene messa a disposizione dall'architettura del proprio calcolatore possibile
compilare ed eseguire il programma di seguito riportato, il quale visualizza il numero di byte
utilizzati dai vari tipi, mediante l'uso dell'operatore sizeof.
#include <stdio.h>
#include <stdlib.h>
int main()
{
char c;
int a;
float f;
double d;
printf("\n*** occupazione di memoria per ogni tipo di variabile ***\n");
printf("carattere (char): %d byte\n", sizeof(c));
printf("intero (int): %d byte\n", sizeof(a));
printf("reale singola precisione (float): %d byte\n", sizeof(f));
printf("reale doppia precisione (double): %d byte\n", sizeof(d));
}

Variabile intera
Una variabile di tipo int pu memorizzare un valore compreso tra i -32768 e +32767 nel caso di un
impiego di 2 byte per le variabili di questo tipo (i valori derivano da 2^15, numero con segno
rappresentabile in complemento a 2 su 16 bit). Per dichiarare una variabile di tipo intero si utilizza
l'istruzione seguente:

int variable name;

Ad esempio:
int a;

dichiara una variabile chiamata a di tipo intero. Per assegnare un valore alla variabile intera si usa
l'espressione:
a=10;

Il linguaggio C utilizza il carattere = per l'operatore di assegnamento. Un'espressione della forma


a=10; dovrebbe essere interpretata come prendi il valore numerico 10 e salvalo nella locazione di
memoria associata alla variabile intera a.
In generale, una variabile di tipo int occupa 2 byte di memoria.

Variabile reale
Il C ha due tipi per dichiarare variabili con una parte frazionaria: float e double, che differiscono per
la quantit di memoria che occupano e per la precisione che consentono di garantire
Una variabile di tipo float (da floating point, virgola mobile) consente di rappresentare numeri con
una precisione di 7 cifre, per valori che vanno (pi o meno) da 1.E-36 a 1.E+36.
In generale, una variabile di tipo float occupa 4 byte di memoria.
Una variabile di tipo double (da double precision, doppia precisione) consente di rappresentare un
numero con una precisione di 13 cifre, per valori che vanno da 1.E-303 a 1.E+303.
In generale, una variabile di tipo float occupa 8 byte di memoria.

Variabile carattere
Alla base del C ci sono numeri e caratteri, anche se questi ultimi vengono in realt visti anch'essi
come numeri, e pi precisamente come il valore intero del codice ASCII corrispondente al
carattere trattato. Per dichiarare una variabile di tipo carattere si utilizza la parola chiave char.
Un carattere viene memorizzato in un byte.
Per esempio, la seguente una dichiarazione di una variabile di tipo carattere:
char c;

Per assegnare, o memorizzare, un carattere nella variabile c necessario indicare il carattere tra
apici singoli, come segue:
c = 'A';

Si noti che in una variabile di tipo char si pu memorizzare un solo carattere.

NOTA
Si faccia attenzione al fatto che i singoli caratteri sono indicati tra apici singoli e non tra doppi
apici:
char alfa, beta;
alfa = 'x'; /* -1- */
beta = "y"; /* -2- */

La prima istruzione corretta, la seconda semanticamente scorretta.


La parte di dichiarazione del tipo specifica che tipi di valori, e quali comportamenti, sono
supportati dall'entit che si sta dichiarando. Non vi differenza tra variabili dichiarate attraverso
un'unica dichiarazione o mediante dichiarazioni multiple dello stesso tipo.
Ad esempio:
float x, y;

equivalente a
float x;
float y;

Ogni inizializzazione viene effettuata con l'assegnamento (utilizzando l'operatore =) di


un'espressione del tipo appropriato. Ad esempio:
x = 3.14;
y = 2.81;

Cambiamento automatico di tipo


Nelle espressioni aritmetiche possibile mischiare variabili numeriche di tipo diverso. In quasi tutti
i casi i valori con una precisione inferiore vengono convertiti ad una precisione superiore ed
utilizzati dagli operatori. Si consideri lo stralcio ci codice seguente:
int a;
float b;
...
... = a*b;

L'espressione a*b viene valutata dopo aver convertito il valore intero in un float (avr parte
frazionaria nulla) e dopo aver svolto la moltiplicazione. Il risultato della moltiplicazione un valore
di tipo float. Nel caso in cui questo venga assegnato ad una variabile float tutto funziona
normalmente. Nel caso in cui venga assegnato ad una variabile di tipo int il valore verr
automaticamente troncato (non arrotondato).
Questa conversione automatica di tipo vale anche per i caratteri. Un carattere viene rappresentato
come un carattere ASCII esteso o un altro codice con un valore tra 0 e 255. Un altro modo di
vedere la cosa che una variabile di tipo char una variabile intera di un solo byte in grado di
memorizzare un valore compreso tra 0 e 255, che pu anche essere interpretato come un
carattere.

Tipi definiti dall'utente


La parola chiave typedef viene utilizzata per assegnare un alias ad un qualsiasi tipo fondamentale
oppure derivato (introdotto dall'utente e derivato dai tipi fondamentali). Con typedef non si
definisce un nuovo tipo, ma si introduce un nome che corrisponde a un tipo definito. La sintassi
la seguente:
typedef nome_tipo nuovo_nome_tipo;

A titolo di esempio si consideri la seguente dichiarazione:


typedef int boolean;

In tal modo possibile dichiarare variabili di tipo boolean che possono assumere tutti i valori interi:
int a, b;
boolean finito;

Rinominare un tipo pu essere utile per rendere pi leggibile il programma e per evitare
espressioni altrimenti complesse. Quando si ha a che fare con struct l'utilizzo di typedef risulta
particolarmente comodo (Lezione 12)

Espressione di assegnamento
Una volta dichiarata una variabile possibile utilizzarla (se si omette la dichiarazione il compilatore
lo segnala con un errore) per memorizzare un valore ed in seguito manipolarlo.
possibile memorizzare un valore con la seguente sintassi:
nome_variabile = valore;

Si faccia attenzione a non scambiare la variabile a cui assegnare il valore con quella che contiene il
valore da assegnare: ci che sta a sinistra dell'operatore = la destinazione dell'assegnamento e
pu essere solo una variabile. Ci che sta a destra la sorgente e pu essere qualsiasi espressione
che dia un valore. L'esempio seguente:
a = 10;

memorizza il valore 10 nella variabile int chiamata a. Sarebbe sbagliato sintatticamente scrivere
l'espressione al contrario:
10 = a;

e come errore sintattico il compilatore lo segnalerebbe. Non cos nel caso in cui si desideri
assegnare alla variabile a il valore contenuto nella variabile b:
a = b;

In questo caso girare l'espressione produrrebbe un effetto diverso e non sarebbe sintatticamente
scorretto!

b = a;

Espressioni e operatori aritmetici


Si definisce espressione aritmetica un insieme di variabili, costanti e richiami di funzione connessi
da operatori aritmetici. Il risultato di un'espressione aritmetica sempre un valore numerico. Nella
tabella sottostante sono presentati gli operatori aritmetici e le loro priorit in ordine dalla pi alta
alla pi bassa ...

moltiplicazione (*)
somma (+)

negazione (- unario)
divisione (/)

modulo (%)
sottrazione (-)

assegnamento (=)
L'unico operatore unario (e cio che si applica ad un solo operando) la negazione: Se x ha valore
5, l'espressione
-x;

ha valore -5.
L'operatore modulo, %, calcola il resto della divisione intera tra i due operandi. L'assegnamento =
anch'esso un operatore e ha una posizione all'interno della scala di priorit, ed la pi bassa.
Data la priorit degli operatori necessario utilizzare le parentesi tonde () per ottenere la corretta
sequenza di valutazione delle espressioni quando sono articolate e composte da pi operatori,
come per esempio la seguente:
a=10.0 + 2.0 * 5.0 - 6.0 / 2.0

che equivale a:
a=10.0 + (2.0 * 5.0) - (6.0 / 2.0)

e non a:
a=(10.0 + 2.0) * (5.0 - 6.0) / 2.0

Gli operatori di incremento e decremento


Gli operatori ++ e -- sono di incremento e decremento rispettivamente, e possono essere applicati
a variabili numeriche. L'espressione i++ equivale a i = i + 1, a parte il fatto che la variabile i viene
valutata solo una volta nella forma compatta.
Gli operatori di incremento e decremento possono essere sia prefissi che postfissi: essi possono
apparire sia prima sia dopo l'operando a cui si riferiscono. Se l'operatore viene posto prima
(prefisso) l'operazione viene eseguita prima di restituire il valore dell'espressione; se viene posto
dopo (postfisso) l'operazione viene eseguita dopo aver utilizzato il valore originale. Ad esempio:
...
int i;
i = 15;
printf("%d, %d, %d", ++i, i++, i);
...

Il risultato che si ottiene :


16, 16, 17

Infatti, l'espressione ++i incrementa i prima di stamparlo a video (quindi i 16); la successiva i++
viene valutata al valore corrente di i (16) che verr poi incrementato (a 17); infine l'espressione i
il valore di i dopo il postincremento (17).
opportuno evitare di modificare pi volte il valore di una variabile per evitare di rendere il codice
illeggibile
Applicando l'operatore di incremento (o decremento) ad una variabile di tipo char si passa al
carattere successivo (o precedente).

Espressioni e operatori logici


Un'espressione logica un'espressione che genera come risultato un valore vero o falso e viene
utilizzata dalle istruzioni di controllo del flusso di esecuzione. La valutazione delle espressioni
logiche pu avere o un valore diverso da zero (interpretato come vero) oppure un valore pari a
zero (interpretato come falso). Un semplice esempio di espressione logica una variabile: se il suo
contenuto diverso da zero, allora l'espressione vera, altrimento l'espressione falsa.
Le espressioni logiche possono contenere gli operatori relazionali usare per confrontare tra loro
dei valori. Anche tra questi operatori ci sono delle priorit, riportate nella tabella sottostante.
maggiore(>)

maggiore-uguale(>=)
uguale (==)

minore(<)

minore-uguale (<=)
diverso (!=)

Infine, gli operatori logici consentono di concatenare fra loro pi espressioni logiche e di negare il
risultato di un'espressione logica; la scala di priorit mostrata nella tabella successiva.
NOT Logico (!)
AND Logico (&&)
OR Logico (||)
Date due espressioni logiche, l'applicazione degli operatori logici conduce al risultato mostrato
nella seguente tabella della verit:
esp1 esp2 !(esp1) esp1 && esp2 esp1 || esp2
falso falso vero

falso

falso

falso vero vero

falso

vero

vero falso falso

falso

vero

vero vero falso

vero

vero

possibile verificare che valgono anche le seguenti due leggi (leggi di De Morgan) che mettono in
relazione gli operatori logici AND e OR, indicando l'equivalenze:

esp1 && esp2 = !(!(esp1) || !(esp2))


esp1 || esp2 = !(!(esp1) && !(esp2))

Mediante queste due leggi possibile girare le espressioni logiche in base alla propria capacit di
esprimerle e al tipo di condizione che richiesta. Ad esempio, l'espressione per valutare se una
variabile num sia compresa tra due valori espremi min e max - valori inclusi - pu essere scritta
come:
(num >= min) && (num <= max)

L'espressione che esprime la condizione opposta :


!((num >= min) && (num <= max))

che equivale a:
(!(num >= min) || !(num <= max))

che semplificata diventa:


(num < min) || (num > max)

Frequentemente si commettono errori nel girare le espressioni logiche, passando da una forma a
quella negata, dimenticandosi di cambiare operatore o di negare le singole espressioni.
L'applicazione delle leggi di De Morgan consente di evitare tali errori.

Ingresso e uscita
Per rendere un programma interessante necessario poter inserire dei dati durante la sua
esecuzione e vedere il risultato che si ottiene. Mediante l'operatore assegnamento possibile
memorizzare un valore in una variabile: a = 100;. Questa operazione viene svolta ogni volta che si
esegue il programma, dunque ha una utilit molto limitata, soprattutto considerando che che non
dato vedere alcun risultato.
quindi necessario far in modo che il programma acquisisca i dati su cui lavorare dall'utente e
stampi a video il risultato dell'elaborazione. A questo scopo ci sono numerosi "comandi" che
consentono di effettuare l'operazione di acquisizione dati e stampa a video.
La funzione scanf consente di acquisire dati dallo standard input (la tastiera) e di memorizzarli in
una variabile. Ad esempio, l'istruzione
scanf("%d", &a);

legge un valore intero e lo memorizza nella variabile a. Traslasciamo per un istante il significato di
%d e dell'& e vediamo la differenza rispetto a a = 100;.
Quando il programma nell'esecuzione raggiunge l'istruzione scanf esso si sospende e lascia
all'utente la possibilit di digitare qualcosa dalla tastiera. Il programma per la verit non prosegue
fino a che l'utente non ha inserito qualcosa e ha premuto poi il tasto <Invio> (o <Enter>). Quindi, una
volta acquisito quanto scritto dall'utente il valore viene memorizzato nella variabile a e poi il
programma procede con le istruzioni successive.

In questo modo, ad ogni esecuzione l'utente pu inserire valori diversi, producendo risultati
diversi.
Il passo ulteriore vedere il risultato, ossia fare stampare sullo standrd output (il video) il risultato
dell'elaborazione. Questo viene fatto mediante la funzione printf: per visualizzare il valore
memorizzato in una variabile necessario scrivere:
printf("Il valore della variabile a e': %d", a);

Il %d sia nella scanf, sia nella printf indica che il valore gestito un intero in base dieci.
Nota:
la funzione scanf non fa una richiesta all'utente, semplicemente aspetta che l'utente inserisca il
dato. Per questo motivo, buona norma utilizzare prima una printf che informi l'utente sulla
necessit di inserire un dato, come ad esempio:
...
printf("Inserisci un numero intero: ");
scanf("%d", &a);

Le funzioni di ingresso e uscita nel dettaglio


printf

La funzione printf ha sempre come primo argomento tra le parentesi tonde una stringa (ossia una
sequenza di caratteri delimitata dalle virgolette - ". Dopo la stringa possibile mettere un numero
qualsiasi di altri argomenti, in base alla necessit. La forma pi generale :
printf(stringa, espressione, espressione, espressione...)

La stringa deve includere un segnaposto per ciascuna espressione successivamente elencata, per
specificare in quale punto della stringa deve essere posizionato il valore dell'espressione e di che
tipo di valore si tratta.
Per questo motivo la stringa prende solitamente il nome di stringa di controllo o stringa di
formato.
Il funzionamento il seguente: la printf scorre la stringa da sinistra verso destra e scrive a video (il
dispositivo d'uscita standard) tutti i caratteri che incontra. Quando trova un % allora identifica il
tipo di dato che deve essere stampato mediante il carattere (o i caratteri) che seguono il %. Tale
elemento (il %) un segnaposto ad indicare che l necessario stampare il valore di un'espressione
che di un tipo ben preciso. La funzione printf utilizza tale informazione per convertire e
formattare il valore ottenuto dall'espressione che segue la stringa di formato. Valutato, formattato
e scritto a video il valore dell'espressione, la funzione continua nell'analisi della stringa, fino al
prossimo % o alla fine della stringa. Per esempio:
printf("Inserisci un numero intero:");

C' solo la stringa di formato e tutti i caratteri vengono scritti sullo schermo, cosicch ci che vede
l'utente appunto la scritta inserita tra le virgolette.
Se si considera invece l'altro esempio:

printf("Il valore della variabile a e': %d", a);

il %d specifica che si deve valutare l'espressione che segue la stringa come un numero intero
decimale. Il risultato sar vedere a video la scritta:
Il valore della variabile a e': xx

dove xx il valore che in quel momento memorizzato nella variabile a. anche possibile scrivere
invece della variabile a un'espressione, come ad esempio:
printf("Il risultato e': %d", a+1);

che scriver il valore della variabile a pi un'unit. Il valore della variabile resta per invariato, in
quanto non stato riassegnato.
La specifica %d no n solamente un identificatore di formato, ma uno specificatore di conversione:
indica il tipo di valore risultante dall'espressione e come tale tipo di dato deve essere convertito in
caratteri da visualizzare sullo schermo.
Se per un qualsiasi motivo l'espressione indicata dopo la stringa di formato ha un valore reale
(derivante da un float) verr comunque stampato qualcosa, che per non corrisponder al valore
esatto.
La ragione che un int utilizza la met dello spazio occupato da un float. Quindi verr visualizzato
solamente il contenuto dei primi due byte. Questi due primi byte verranno interpretati come la
rappresentazione in complemento a due di un numero intero dotato di segno. Tutto ci molto
lontano dal corrispondere anche solo alla parte intera del numero reale, rappresentato in
complemento a due ma notazione virgola mobile (formato standard IEEE 754).
A parte i dettagli tecnici sono due le cose importanti da ricordare:
1. L'identificatore che segue il % specifica il tipo di variabile che deve essere visualizzato e il formato
dell'espressione che segue
2. Nel caso ci sia una differenza tra l'identificatore indicato e il valore calcolato dell'espressione il dato
visualizzato non necessariamente corretto e pu causare errori anche su gli altri elementi della
printf.

Gli identificatori di formato %


Gli identificatori previsti dall'ANSI C sono:
Tipo

Espressione

A video

%c
%d (%i)
%e (%E)
%f
%g (%G)
%o
%p
%s
%u
%x (%X)

char
int
float or double
float or double
float or double
int
pointer
array of char
int
int

singolo carattere
intero con segno
formato esponenziale
reale con segno
utilizza %f o %e in base alle esigenze
valore base 8 senza segno
valore di una variabile puntatore
stringa (sequenza) di caratteri
intero senza segno
valore base 16 senza segno

Caratteri di controllo
Ci sono alcuni codici di controllo che non stampano caratteri visibili ma contibuiscono a formattare
ci che viene stampato:
\b
\f
\n
\r
\t
\'
\0

cancella
avanzamento carta
nuova linea
a capo (senza una nuova linea)
tabulatore
apice
null

In generale solamente il codice \n viene utilizzato di frequente, per mandare a capo inserendo una
nuova riga. Ad esempio:
...
printf("prima riga ...\n");
printf("seconda riga ...\n");

produrr a video la stampa di due righe, con il cursore poi sulla terza riga:
prima riga ...
seconda riga ...
scanf

La funzone scanf simile alla printf nella forma:


scanf(stringa, variabile, variabile, ...)

In questo caso la stringa di controllo specifica come i caratteri immessi dall'utente mediante la
tastiera debbano essere convertiti e memorizzati nelle variabili. ci sono per alcune differenze
significative:
La prima che mentre la funzione printf valuta il valore di un'espressione, ad esempio il valore di
una variabile, senza modificarlo, la funzione scanf deve modificare il valore di una variabile per
memorizzarci il valore appena acquisito. La trattazione dettagliata di questi aspetti oggetto della
Lezione 12, per il momento sufficiente ricordare questa necessit di modifica. Per indicare
questa differenza, si antepone al nome della variabile un &.
La seconda differenza che relativa alla stringa di controllo.
La regola che la funzione scanf processa la stringa di controllo da sinistra a destra ed ad ogni
segnaposto cerca di interpretare i caratteri ricevuti in ingresso in relazione all'identificatore (gli
identificatori sono gli stessi della funzione printf). Se vengono specificati pi valori nella stringa di
controllo, si presuppone che questi vengano immessi da tastiera in modo separato, usando come

separatore lo spazio, l'<Invio> o il tabulatore.


Questo significa che per inserire tre interi possibile digitare i dati in una delle due forme:
345

oppure
3
4
5

Per esempio, l'istruzione:


scanf("%d %d",&i,&j);

acquisisce due numeri interi e li memorizza rispettivamente nelle variabili i e j. I due valori possono
essere inseriti separati da uno spazio (o un numero qualsivoglia di spazi) oppure andando a capo
ogni volta.
Unica eccezione il caso di %c: viene acquisito un carattere, qualsiasi esso sia, quindi qualsiasi
tasto venga premuto sulla tastiera, quello l'unico carattere acquisito.

Librerie standard
Per potre utilizzare le funzioni printf e scanf necessario includere la libreria standard del C che le
definisce. richiesta quindi la direttiva:
#include <stdio.h>

che deve essere indicata prima del main. In alcuni ambienti di programmazione le librerie vengono
incluse automaticamente e la direttiva risulta non necessaria. Se l'ambiente per non effettua
l'inclusione in modo autonomo, la compilazione del programma non va a buon fine in quanto
viene segnalato errore di "funzione sconosciuta" in corrispondenza delle printf e scanf. quindi buona
norma utilizzare sempre la direttiva indicata.

Riassumendo ....
Si scriva un programma che chiede all'utente un numero che indica la temperatura in gradi
centigradi e stampa a video l'equivalente in Fahrenheit
#include <stdio.h>
main()
{
float celsius, fahr;
printf("Inserisci la temperatura in gradi Celsius: ");
scanf("%g", &celsius);
fahr = (celsius * 9)/5 + 32;
printf("%g gradi Celsius = %g gradi Fahrenheit\n", celsius, fahr);
}

Le istruzioni e i blocchi
Le istruzioni di espressione, come i++, o la chiamata di una funzione, sono istruzioni seguite da un
punto e virgola, che indica la fine dell'istruzione. Di fatto anche il punto e virgola da solo
un'istruzione che non fa nulla (istruzione vuota). Non tutte le espressioni possono diventare
istruzioni, perch, ad esempio, x <= y non ha senso. Solo i seguenti tipi di espressioni possono
essere considerate istruzioni aggiungendo un punto e virgola alla fine:

espressioni di assegnamento (contengono un = oppure un op=),


forme prefisse o postfisse di ++ e --,
chiamate a funzione (che restituiscano un valore o meno).

Le parentesi graffe { e } racchiudono un insieme di zero o pi istruzioni per costituirne un blocco.


Un blocco di istruzioni pu essere utilizzato in tutti quei punti in cui ammessa la presenza di
un'istruzione singola, perch esso viene visto come un'unica istruzione, seppur composita.
if-else

La pi semplice forma di controllo condizionale costituita dall'istruzione if, che permette di


scegliere quale istruzione eseguire tra quelle che la seguono. La sintassi :
if (espressione)
istruzione1
else
istruzione2

Viene valutata l'espressione: se il suo valore diverso da 0 (spesso uguale a 1 -vero), allora si passa
ad eseguire istruzione1; altrimenti (valore uguale a 0 - falso), se vi una clausola else, si passa
all'istruzione istruzione2. La clausola else opzionale.
possibile costruire una sequenza di test collegando un altro if alla clausola else dell'if precedente.
...
if (a==0)
printf("numero nullo\n");
else if (a%2)
printf("numero dispari\n");
else
printf("numero pari\n");
....

necessario fare attenzione quando si concatenano pi if, tra i quali ce n' uno sprovvisto di
clausola else. Si consideri il seguente stralcio di codice:
...
int numeri[N], h, i;
float somma;
somma = 0.0;
... /* viene riempito l'array ed h il numero di elementi immessi effettivamente */
if (h > 1)
for(i=0; i < h; i++)

if (numeri[i] > 0)
somma += numeri[i];
else /* ATTENZIONE!!!! */
somma = numeri[0];
....

La clausola else sembra essere collegata al controllo sulla lunghezza dell'array, ma solo un effetto
dell'indentazione (aspetto ignorato dal compilatore). La clausola else risulta essere collegata
all'ultimo if che non possiede la clausola. Il codice precedente in realt equivalente il seguente:
...
if (h > 1)
for(i=0; i < h; i++)
if (numeri[i] > 0)
somma += numeri[i];
else
somma = numeri[0];
....

Per poter collegare la clausola else all'ultimo if necessario utilizzare le parentesi graffe per creare
blocchi:
...
if (h > 1){
for(i=0; i < h; i++)
if (numeri[i] > 0)
somma += numeri[i];
} else
somma = numeri[0];
....
switch

Il costrutto switch consente di trasferire il controllo di esecuzione all'interno di un blocco di


istruzioni, in cui sia presente un'etichetta. Il punto scelto determinato dal risultato della
valutazione di una espressione intera. La forma generale del costrutto :
switch (espressione) {
case n: istruzioni
case m: istruzioni
case h: istruzioni
...
default: istruzioni
}

I valori n, m, ... sono delle costanti intere. Se il valore dell'espressione combacia con il valore di
una delle etichette case il controllo viene trasferito alla prima istruzione che segue tale etichetta.
Se non vi alcuna etichetta che combacia, allora il controllo viene trasferito alla prima istruzione
dopo l'etichetta default, se esiste, altrimenti si salta tutta l'istruzione switch.
Una volta trasferito il controllo alla prima istruzione di quelle che seguono l'etichetta case che
combacia, le istruzioni successive vengono eseguite una per volta, anche se sono associate ad
un'altra etichetta case. Un'etichetta case o default non spinge ad uscire dallo switch. Se si desidera

arrestare l'esecuzione delle istruzioni all'interno del blocco switch necessario utilizzare l'istruzione
break. All'interno di un blocco switch, l'istruzione break trasferisce il controllo all'esterno del blocco,
alla prima istruzione che segue lo switch .
Nello stralcio di codice sottostante riportato un esempio di utilizzo dell'istruzione switch senza il
break. Nella stragrande maggioranza dei casi l'istruzione break necessaria per ottenere il
comportamento desiderato.
...
val = 0;
switch(ch){
case 'f':
case 'F': val++;
case 'e':
case 'E': val++;
case 'd':
case 'D': val++;
case 'c':
case 'C': val++;
case 'b':
case 'B': val++;
case 'a':
case 'A': val++;
case '9': val++;
case '8': val++;
case '7': val++;
case '6': val++;
case '5': val++;
case '4': val++;
case '3': val++;
case '2': val++;
case '1': val++;
break;
default: val = -1;
printf("carattere %c non appartenente all'alfabeto base16\n", ch);
}

L'espressione di switch deve essere di tipo char o int. Tutte le etichette case devono essere
espressioni costanti. In tutte le istruzioni switch singole, ogni valore associato alle etichette case
deve essere unico, e ci pu essere al pi una sola etichetta default.

while e do-while
Una struttura di iterazione consente di specificare un'azione, o un insieme di azioni, che dovr
essere ripetuta pi volte. Si parla in questi casi di ciclo. Il ciclo while generalmente strutturato
come segue:
while (espressione)
istruzione

L'espressione viene valutata e, se ha valore diverso da 0 (vero) viene eseguita l'istruzione


successiva (che pu anche essere un intero blocco di istruzioni, opportunamente delimitato dalle
parentesi graffe). Una volta che quest'ultima terminata, l'espressione viene valutata

nuovamente e se nuovamente vera, si ripete l'istruzione. Ci si ripete fino a quando


l'espressione ha valore 0 (falso), nel qual caso il controllo si trasferisce all'istruzione successiva al
while.
Un ciclo while pu essere eseguito 0 o pi volte, poich l'espressione potrebbe essere falsa gi la
prima volta. Si tratta di un ciclo a condizione iniziale: prima di eseguire il ciclo si valuta la
condizione.
A volte si desidera eseguire il corpo di un ciclo almeno una volta. In tal caso si utilizza un ciclo dowhile, ossia un ciclo a condizione finale. La struttura la seguente:
do
istruzione
while (espressione);

In questo caso, l'espressione viene valutata al termine dell'esecuzione dell'istruzione (o del blocco
di istruzioni). Fino a quando l'espressione vera, l'istruzione viene ripetuta.
Ecco due esempi di utilizzo dei costrutti di ciclo:
printf("Inserire un intero positivo\n");
scanf("%d" ,&num);
while(num > 0){
printf("*");
num--;
}
do{
printf("Inserisci un intero compreso tra 0 e 15, inclusi:");
scanf("%d", num);
}while(i<0 || i > 15);
for

L'istruzione for utilizzata per effettuare un ciclo dal principio sino alla fine di un intervallo di
valori. La sintassi la seguente:
for (espressione-iniziale; espressione-booleana;
espressione-incremento)
istruzione

L'espressione-iniziale permette di inizializzare le variabili di ciclo, e viene eseguita una volta sola,
prima di qualsiasi altra operazione. Successivamente ad essa, viene valutata l'espressione del ciclo
e, se questa ha valore diverso da 0 - vera -, viene eseguita l'istruzione che costituisce il corpo del
ciclo. Al termine dell'esecuzione del corpo del ciclo, viene valutata l'espressione-incremento, di
solito per poter aggiornare i valori delle variabili di ciclo. Quindi, si valuta nuovamente
l'espressione del ciclo e cosi via. Il ciclo si ripete finch non si valuta come falsa l'espressione del
ciclo - valore 0-. Questo modo di procedere praticamente equivalente a:
{
espressione-iniziale;

while(espressione-booleana) {
istruzione
espressione-incremento;
}
}

se non fosse che si eseguirebbe sempre l'espressione-incremento se venisse incontrata


un'istruzione continue all'intemo del corpo del ciclo. La parte di inizializzazione e quella di iterazione
di un ciclo for possono essere costituite da elenchi di espressioni separate da virgole. Tali
espressioni vengono valutate, come molti operatori, da sinistra a destra. Ad esempio, per poter far
procedere due indici in direzioni opposte, sarebbe appropriato scrivere del codice come:
for (i = 0, j = NumElem - 1; j >= 0; i++, j--) {
/* ... */
}
break

Un'istruzione break pu essere utilizzata per uscire da qualunque blocco di codice appartenente ad
un ciclo, controllato da uno switch, o anche da un for, while o do-while. In generale verr utilizzato
solamente all'interno dello switch per indicare il termine della sequenza di istruzioni da eseguire
una volta trovata l'etichetta che combacia con l'espressione dello switch. L'istruzione si presenta
come:
break;
continue

Un'istruzione continue pu essere utilizzata solamente all'interno di un ciclo (for, while oppure dowhile) e trasfetisce il controllo alla fine del corpo del ciclo. Nel caso di cicli while e do-while, ci spinge
a valutare l'espressione del ciclo immediatamente successivo. In un ciclo for, invece, quella che
viene valutata per prima l'espressione-incremento e solo dopo si valuta l'espressione del ciclo.
L'istruzione si presenta come:
continue;

Un'istruzione continue viene spesso utilizzata per saltare una parte del blocco di istruzioni sul quale
si sta svolgendo il ciclo. Ad esempio:
while(valore != -1){
if(valore == 0)
continue; /* non considerare il valore 0 */
cont++;
scanf("%d", &valore);
}
return

Un'istruzione return termina l'esecuzione di un sottoprogramma e restituisce il controllo al


chiamante. Il valore dell'espressione nell'istruzione return viene restituito al chiamate come valore
restituito dal sottoprogramma (funzione).

Se il sottoprogramma non restituisce alcun valore - subroutine - allora si pu scrivere


semplicemente:
return;

Se il sottoprogramma ha specificato un tipo per il valore da restituire - funzione -, l'istruzione return


deve comprendere anche un'espressione di un tipo che possa essere assegnato al tipo da
restituire. Ad esempio:
int Lunghezza(char Frase[])
{
int i;
i=0;
while(Frase[i] != '\0')
i++;
return i; /* anche return(i); */
}

L'espressione dell'istruzione return pu essere omessa, e questo ha senso solo per sottoprogrammi
di tipo void, ed in tali casi l'istruzinoe pu essere omessa completamente. In tal caso, il controllo
viene restituito al chiamante in corrispondenza del termine del sottoprogramma stesso.
exit

L'istruzione exit termina l'esecuzione dell'intero programma. Si tratta di un'istruzione simile


all'istruzione return, che per restituisce il controllo al programma chiamante del programma
principale, dunque termina l'esecuzione del programma. Se il programma principale non ha
specificato un tipo di dato da restituire - void main- allora si scriver semplicemente:
exit;

In alternativa, il programma main restituisce un intero - int main - al fine di caratterizzare il termine
del programma. Nel caso in cui il programma arrivi al termine perch concluso il flusso di
elaborazione senza l'insorgere di alcun problema, si restituir, ad esempio, il valore 0, in caso
contrario, se si termina l'esecuzione a causa di una qualche condizione di anomalia, si restituir un
valore diverso, arbitrariamente legato alla particolare situazione di anomalia:
exit 1;

I sottoprogrammi
Un sottopramma un insieme di istruzioni identificate mediante un nome, ed accessibile tramite
un'interfaccia, che consente di far comunicare il sottoprogramma con il (sotto)programma
chiamante.
In termini generali ci sono due tipi di sottoprogrammi: quelli che restituiscono un valore al
(sotto)programma chiamante, e quelli che non lo fanno. Ai primi si d il nome di funzioni, ai secondi il nome
di procedure (o subroutine). Quindi possiamo vedere un sottoprogramma come un ambiente che
(eventualmente) riceve delle informazioni, svolge l'elaborazione richiesta ed eventualmente restituisce un

valore al (sotto)programma che lo ha chiamato.


Graficamente le due tipologie di sottoprogrammi sono riportate nella figura seguente.

In C solitamente questa distinzione viene persa per ci che concerne la terminologia, e ci si riferisce pi
semplicemente ai sottoprogrammi con il termine funzione, presupponendo che questa resituisca o meno
un valore in base alle specifiche ed al comportamento che si desidera ottenere.

Quando viene chiamato un sottoprogramma il flusso di esecuzione abbandona il programma in cui


viene fatta la chiamata ed inizia ad eseguire il corpo del sottoprogramma. Quando quest'ultimo
termina la propria esecuzione, con l'esecuzione dell'istruzione return o con il termine del corpo del
programma, il programma chiamante continua l'esecuzione del codice che segue la chiamata al
sottoprogramma.
Una funzione una porzione di codice (sequenza di istruzioni) che vengono raggruppati e a cui
viene dato un nome. Il vantaggio di questa soluzione che la porzione di codice pu essere
utilizzata pi volte semplicemente scrivendone il nome.
Si supponga di voler scrivere una funzione che scriva a video un insieme di direttive per l'utente,
che costituiscono il men del programma, come fatto nello stralcio di codice riportato:
...
printf("Scegli la voce del men:\n");
printf("1. addizione\n");
printf("2. sottrazione\n");
printf("3. moltiplicazione\n");
printf("4. divisione\n");
scanf("%d", &scelta);
...

Per rendere la parte di codice una funzione necessario racchiudere il codice tra un paio di
parentesi graffe per renderle un blocco di codice e dare un nome alla funzione:

menu()
{
printf("Scegli la voce del men:\n");
printf("1. addizione\n");
printf("2. sottrazione\n");
printf("3. moltiplicazione\n");
printf("4. divisione\n");
}

A questo punto possibile utilizzare la funzione chiamandola:


void main()
{
menu();
scanf("%d", &scelta);
...
}

Nel programma, l'istruzione menu(); equivalente ad aver scritto direttamente tutte le istruzioni
della funzione stessa.
A parte il semplice esempio, le funzioni hanno lo scopo di rendere un lungo programma come una
collezione di porzioni di codice separate su cui lavorare in modo isolato, suddividendo la soluzione
di un problema grosso in tanti piccoli sottoproblemi, di pi facile soluzione.

Funzioni e variabili locali


Nella funzione utilizzata per chiarire la filosofia di fondo delle funzioni non ci sono variabili e
questo un caso particolarmente semplice. Una funzione una sottounit di un programma
completo: il main stesso una funzione, chiamata dal sistema. Quindi anche una funzione avr in
generale delle variabili che verranno utilizzate al suo interno per effettuare le computazioni
desiderate.
Le variabili dichiarate all'interno di una funzione sono significative e visibili esclusivamente
all'interno della funzione stessa, ed il programma chiamante non ne ha alcuna visibilit. Si tratta di
variabili locali alla funzione stessa.
Le variabili che una funzione dichiara sono create quando la funzione viene chiamata e vengono
distrutte quando la funzione termina.
Si consideri il seguente esempio:
void contastampe()
{
int num_stampe;
num_stampe = 0;
printf("xxxxxxx");
num_stampe++;
}
void main()
{
contastampe();
...
contastampe();

Ad ogni chiamata della funzione contastampe() la variabile num_stampe viene ricreata e distrutta al
termine.

...
Se si desidera quindi che un valore computato da una funzione resti disponibile anche dopo il
termine dell'esecuzione della funzione necessario che questo venga trasmesso al programma
chiamante. In modo analogo, se la funzione deve svolgere un'elaborazione dei dati del programma
chiamante necessario che i dati le vengano passati.
A questo scopo vengono definite variabili speciali, chiamate parametri che vengono utilizzati per
passare i valori alla funzione. I parametri vengono elencati nelle parentesi tonde che seguono il
nome della funzione, indicando il tipo ed il nome di ogni parametro. La lista dei parametri ha come
separatore la virgola. Per esempio:
somma(int a, int b)
{
int risultato;
risultato = a + b;
}

Questo codice definisce una funzione chiamata somma con due parametri a e b, entrambi di tipo
intero. La variabile risultato dichiarata localmente alla funzione, nel corpo della funzione. I
parametri a e b vengono utilizzati all'interno della funzione come normali variabili - si noti che non
devono essere definite due variabili locali a e b. Inoltre, questi a e b non hanno nulla a che fare con
altre variabili a e b dichiarate in altre funzioni.
La differenza fondamentale tra i parametri e le variabili che i primi hanno un valore iniziale
quando la funzione viene eseguita, mentre le variabili devono essere inizializzate. Quindi,
somma(l, 2);

una chiamata alla funzione somma in cui il parametro a vale 1 e b vale 2. anche possibile far
assumere ai parametri il risultato di un espressione, come ad esempio:
somma(x+2, z*10);

che far assumere ad a il valore pari a x+2 (in base a quanto varr x al momento della chiamata e b
pari a z*10.
Pi semplicemente si pu fissare il valore di un parametro al valore di una variabile:
somma(x, y);

in cui a assume il valore di x e b di y.


In modo duale anche necessario o possibile trasmettere al programma chiamante il valore
calcolato dalla funzione. La via pi semplice la restituzione del valore attraverso il nome della

funzione, ossia come se il nome della funzione fosse una variabile dotata di un valore. Il valore
viene restituito per mezzo della seguente istruzione:
return(value);

Questa istruzione pu essere posizionata in qualunque punto della funzione, tuttavia un'istruzione
return causa il termine della funzione e restituisce il controllo al programma chiamante.
necessario aggiungere un'informazione relativa al tipo di dato che la funzione restituisce. Sempre
con riferimento alla funzione somma, il tipo restituito un intero; si scriver dunque:
int sum(int a, int b)
{
...
}

La funzione completa :
int sum(int a, int b)
{
int risultato;
risultato = a + b;
return (risultato);
}

La chiamata assume quindi la seguente forma:


r = somma(1, 2);

istruzione che somma 1 a 2 e memorizza il risultato nella variabile r.


Ovviamente, la situazione tra ingressi ed uscite di una funzione non uguale: possibile passare
un numero di parametri d'ingresso qualsiasi, mentre la funzione pu restituire mediante
l'istruzione return un singolo valore.
Si noti che una funzione pu avere quante istruzioni return si desidera, ma ci non consente di
restituire pi di un valore in quanto quando si esegue la prima return la funzione termina. (Per
poter restituire pi dati necessario utilizzare un passaggio dell'indirizzo come discusso nella
Lezione 11).
Per riassumere, una funzione ha la seguente forma sintattica:
tipo_restituito NomeFunzione(lista tipo-nome)
{
istruzioni
}

Nel caso in cui una funzione non restituisca alcun parametro, il tipo indicato void, che indica
appunto tale eventualit. La funzione void menu() una funzione senza paramtri d'ingresso e che
non restituisce alcun valore.
void un tipo standard ANSI C.

Cosa succede quando un afunzione deve restituire al chiamante pi di un valore?


Per semplicit esaminiamo il caso in cui una funzione riceva in ingresso due numeri interi a e b e
debba restituire al chiamante sia il valore della divisione tra a e b sia il resto. Per il momento ci si
limita a capire il problema, senza trovare una soluzione, allo scopo di focalizzare l'attenzione su
alcuni aspetti fondamentali del passaggio dei parametri. Si considerino i seguenti aspetti:
1. l'istruzione return consete di restituire una sola informazione, nella fattispecie un solo numero
intero;
2. una volta eseguita un'istruzione return il codice successivo non verr senz'altro eseguito;
3. stampare a video NON significa restituire.

Queste osservazioni ci permettono di dire che NON potremo scrivere i seguenti stralci di codice:
int ris_resto(int a, int b)
{
int r1, r2;
r1 = a/b;
r1 = a % b;
return(a, b)
}
int ris_resto(int a, int b)
{
int r1, r2;
r1 = a/b;
r1 = a % b;
return(a);
return(b);
}

Ad evidenziare ulteriormente ci che comunque dovrebbe essere di immediata comprensione, il


fatto che nel codice del programma chiamante, non si saprebbe come ricevere i due valori interi
restituit, visto che l'assegnamento prevede che ci sia una variabile a sinistra come destinazione
dell'assegnamento e a destra dell'uguale un'espressione che ne determina il valore:
...
int risultato, resto;
...
risultato = ris_resto(val1, val2);
resto = ???;
...

Funzioni e prototipi
Dove va scritta la definizione di una funzione, prima o dopo il main()? L'unico requisito che la
tipologia della funzione (tipo di dato restituito e tipo dei parametri) sia nota prima che la funzione
venga usata. Una possibilit scrivere la definizione della funzione quindi prima del main(). Con
questa soluzione per necessario prestare molta attenzione all'ordine con cui si scrivono le
funzioni, facendo sempre in modo che una funzione sia sempre definita prima che qualche altra la
chiami. In alternativa, la soluzione po pulita dichiarare la funzione prima del main,
separatamente da dove viene poi definita. Per esempio:
int somma();

void main()
{
...
}

Qui si dichiara il nome della funzione somma e si indica che restituisce un int. A questo punto la
definizione della funzione pu essere messa ovunque.
Per quanto riguarda i parametri ricevuti in ingresso necessario (ANSI C) dichiararne la tipologia
ma non il nome, come mostrato nel seguente esempio:
int restodivisione(int, int);

Gli array
Le variabili semplici, capaci di contenere un solo valore, sono utili ma spesso insufficienti per
numerose applicazioni. Quando si ha la necessit di trattare un insieme omogeneo di dati esiste
un'alternativa efficiente e chiara all'utilizzo di numerose variabili dello stesso tipo, da identificare
con nomi diversi: definire un array, ovvero una collezione di variabili dello stesso tipo, che
costituisce una variabile strutturata. L'array costituisce una tipologia di dati strutturata e statica .
La dimensione fissata al momento della sua creazione - in corrispondenza alla dichiarazione - e
non pu essere mai essere variata.

Array monodimensionali
Intuitivamente un array monodimensionale - vettore - pu essere utilizzato come un contenitore
suddiviso in elementi, ciascuno dei quali accessibile in modo indipendente. Ogni elemento
contiene un unico dato ed individuato mediante un indice: l'indice del primo elemento dell'array
0, l''ultimo elemento di un array di N elementi ha indice N-1. Il numero complessivo degli
elementi dell'array viene detto dimensione, e nell'esempio utilizzato pari a N.
Per riassumere, un array una struttura di dati composta da un numero determinato di elementi
dello stesso tipo, ai quali si accede singolarmente mediante un indice che ne individua la posizione
all'interno dell'array.
Per ogni array, cos come per ogni variabile semplice (o non strutturata) necessario definire il
tipo di dati; inoltre necessario specificarne la dimensione, ossia il numero di elementi che lo
compongono. Una dichiarazione valida la seguente:
int numeri[6];

numeri[0]

numeri[1]

numeri[2]

numeri[3]

numeri[4]

numeri[5]

Viene indicato, come sempre, prima il tipo della variabile (int) poi il nome (numeri) ed infine tra
parentesi quadre la dimensione (6): l'array consente di memorizzare 6 numeri interi. Tra parentesi
quadre necessario indicare SEMPRE un'espressione che sia un valore intero costante. In base a
quanto detto, errato scrivere:

int numeri[];
int i, numeri[i];

Il vincolo di dover specificare in fase di dichiarazione la dimensione dell'array porta ad un


sovradimensionamento dell'array al fine di evitare di non disporre dello spazio necessario durante
l'escuzione del programma. Nel caso in cui sia dunque richiesto l'utilizzo di un array, la specifica
dell'algoritmo dovr quindi prevedere o l'esatto numero di dati da gestire oppure un valore
massimo di dati. Nel caso in cui non sia possibile stabilire in alcun modo un limite al numero di
elementi senza violare i requisiti di generalit, sar necessario utilizzare una struttura dati diversa
e pi precisamente una struttura dinamica, trattata in seguito (Lezione 14).
Per accedere al singolo elemento dell'array necessario indicare il nome dell'array e l'indice
dell'elemento posto tra parentesi quadre, ad esempio, per accedere al primo elemento si dovr
scrivere:
numeri[0]

Si noti che gli elementi dell'array vanno dall'elemento di indice 0 a quello di indice 5. Accedere
all'elemento di indice 6 (o superiore) non causa un errore di sintassi, quanto un errore semantico
(non sempre di facile individuazione). In generale il singolo elemento di un array pu essere
utilizzato come una semplice variabile.
Spesso l'array viene utilizzato all'interno di iterazioni per accedere uno dopo l'altro ai suoi
elementi, semplicemente utilizzando un indice che viene modificato ad ogni iterazione. Ad
esempio:
/* Inizializzazione di un array di numeri interi al valore nullo */
for (i = 0; i < 6; i++)
numeri[i] = 0;

L'indice i inizializzato a zero consente di accedere dal principio al primo elemento dell'array e di
proseguire fino all'ultimo, con indice 5 (si noti che quando l'indice pari a 6 la condizione falsa
ed il corpo del ciclo non viene eseguito).
Come per le variabili non strutturate, il contenuto di una variabile prima che le venga assegnato un
valore ignoto, quindi anche per gli elementi di un array opportuno prima assegnare un valore e
poi leggerlo, a meno che non si sia alla ricerca di valori casuali.

Array bidimensionali
Gli array bidimensionali sono organizzati per righe e per colonne, come matrici. La specifica di un
array bidimensionale prevede l'indicazione del tipo di dati contenuti nell'array, del nome e delle
due dimensioni, numero di righe e numero di colonne, racchiusa ciascuna tra parentesi quadre. Ad
esempio, la dichiarazione che segue specifica un array bidimensionale di numeri reali, organizzati
su 4 righe e 6 colonne, per un totale di 24 elementi:
float livelli[4][6];

livelli[0][0]

livelli[0][1]

livelli[0][2]

livelli[0][3]

livelli[0][4]

livelli[0][5]

livelli[1][0]

livelli[1][1]

livelli[1][2]

livelli[1][3]

livelli[1][4]

livelli[1][5]

livelli[2][0]

livelli[2][1]

livelli[2][2]

livelli[2][3]

livelli[2][4]

livelli[2][5]

livelli[3][0]

livelli[3][1]

livelli[3][2]

livelli[3][3]

livelli[3][4]

livelli[3][5]

L'accesso ai singoli elementi dell'array bidimensionale avviene in modo analogo a quanto avviene
per gli array monodimensionali, specificando gli indici della riga e della colonna dell'elemento di
interesse, ad esempio:
livelli[2][4]

Come esempio di scansione degli elementi di un array bidimensionale si consideri lo stralcio di


codice qui riportato, in cui si accede riga per riga ad ogni elemento dell'array, mediante due cicli
annidati. Il ciclo pi esterno scandisce le righe, quello pi interno le colonne.
/* Acquisizione da tastiera dei valori della matrice livelli[4][6] */
for (i = 0; i < 4; i++)
for(j = 0; j < 6; j++){
printf("Inserisci l'elemento riga %d colonna %d: ", i, j);
scanf("%f", &livelli[i][j]);
}

Approfondimento
Gli elementi vengono memorizzati per righe, quindi pi veloce accedere per righe ai dati
memorizzati.

Passaggio a sottoprogrammi
Il passaggio di array a sottoprogrammi viene sempre fatto per riferimento, passando per valore
l'indirizzo dell'array, e lasciando quindi di fatto accessibile al sottoprogramma l'array con la
possibilit di modificarne il contenuto. Per questo motivo necessario prestare estrema
attenzione durante l'accesso agli elementi dell'array.
Ogniqualvolta si passa un array pluridimensionale necessario indicare - tra parentesi quadre tutte le dimensioni dell'array ad eccezione della prima. Ne consegue che per un array
monodimensionale la dichiarazione dell'array come parametro viene fatta cos:
void funzionex(int numeri[], ...){
...
}

Nel caso di array pluridimensionali, o pi comunemente i bidimensionali trattati in precedenza, la


dichiarazione deve essere fatta come segue:
void funzioney(int livelli[][6], ...){
...
}

La specifica del numero di colonne un'informazione di servizio e non pu essere utilizzata dal
programmatore come specifica del numero di colonne dell'array. In tal senso, buona norma,
passare sempre, mediante ulteriori parametri, il numero di elementi dell'array monodimensionale,
o il numero di righe e di colonne in array bidimensionali, come mostrato nei seguenti stralci di
codice:
void funzionex(int numeri[], int num_elem){
...
}
void funzioney(int livelli[][6], int num_righe, int num_colonne){
...
}

Si consideri a titolo di esempio, il seguente problema:


"scrivere una funzione che restituisca l'indice dell'elemento di valor minimo di un array di numeri interi"
Decidendo di scrivere una funzione che riceva in ingresso l'array ed il numero di elementi si arriva una
soluzione di questo tipo:
int MinArrayInt(int v[], int l){
int i, imin;
for(i=1, imin=0; i < l; i++)
if(v[i] < v[imin])
imin = i;
return(imin);
}

Nel caso si decida di optare per la soluzione che non prevede il passaggio anche del numero di
elementi dell'array, si scriver il codice seguente:
int MinArrayInt(int v[]){
int i, imin;
for(i=1, imin=0; i < N; i++)
if(v[i] < v[imin])
imin = i;
return(imin);
}

possibile fare alcune considerazioni che motivano l'opportunit di passare ad un


sottoprogramma anche le dimensioni dell'array, e fanno concludere che la prima soluzione
costituisce un codice migliore. Eccole.
In primo luogo ad un sottoprogramma potrebbe venir passato solo una porzione ridotta dell'intero
array. Si ricordi infatti che dovendo specificare a priori la dimensione di un array in fase di
dichiarazione, spesso si deve sovradimensionare l'array onde evitare di avere problemi di
memoria. Non necessariamente tutti gli elementi verrranno quindi utilizzati. In tal caso, quando si
passa l'array come parametro importante specificare le dimensioni su cui lavorare, anche
tenendo presente che spesso impossibile distingere in base al contenuto dell'array quali
elementi siano "validi" e quali non lo siano.
Una seconda considerazione a favore del passaggio esplicito delle dimensioni dell'array mediante
opportuni paramentri, la possibilit di riutilizzare i sottoprogrammi senza doverli "aggiustare" di
volta in volta. Nel primo caso, indipendentemente dalla dimensione dell'array utilizzato, il

sottoprogramma funziona perfettamente cos come . Nella seconda soluzione invece, sar
necessario accertarsi che l'array sia effettivamente di dimensione N, e nel caso in cui ci siano array
di dimensione diverse, sar necessario scrivere funzioni diverse. La prima soluzione costituisce
dunque una soluzione piu' flessibile e riutilizzabile.
Questa considerazione purtroppo non universale. Il fatto che nel passaggio di array pluridimensionali sia
necessario specificare le dimensioni ad eccezione della prima fa s che una funzione possa essere riutilizzata
solo per array che hanno tutti le stesse dimensioni - ad eccezione della prima!

Infine, un'ultima considerazione legata allo sviluppo di programmi complessi da parte di pi


persone. La persona che deve scrivere la funzione MinArrayInt, affinch possa scrivere la funzione
deve concordare i parametri che riceve in ingresso, e comunque, se non dovesse ricevere anche
l'intero che rappresenta la dimensione dell'array, dovrebbe chiedere l'informazione a chi realizzer
la funzione chiamante. Ha quindi bisogno dell'informazione circa la dimensione dell'array: vale
quindi la pena pensare, che ogni informazione necessaria debba essere trasmessa tramite il
meccanismo dei parametri!

I puntatori
Una variabile un'area di memoria a cui viene dato un nome.
int x;

La dichiarazione precedente riserva un'area di memoria che viene individuata dal nome x. Il
vantaggio di questo approccio che possibile accedere al valore memorizzato mediante il suo
nome. La seguente istruzione salva il valore 10 nell'area di memoria identificata dal nome x:
x =10;

Il calcolatore fa accesso alla propria memoria non utilizzando i nomi delle variabili ma utilizzando
una mappa della memoria in cui ogni locazione viene individuata univocamente da un numero,
chiamato indirizzo della locazione di memoria.
Un puntatore una variabile che memorizza l'indirizzo di una locazione di memoria, l'indirizzo di
una variabile.
Un puntatore deve essere dichiarato come qualsiasi altra variabile, in quanto anch'esso una
variabile. Per esempio:
int *p;

la dichiarazione specifica una variabile puntatore ad un intero. L'introduzione del carattere *


davanti al nome della variabile indica che si tratta di un puntatore del tipo dichiarato. Si noti che
l'asterisco si applica alla singola variabile, non al tipo. Quindi:
int *p , q;

dichiara un puntatore ad un intero (variabile p) ed un intero (variabile q).


Prima di poter utilizzare un puntatore necessario discutere del significato di due operatori: & e *.

L'operatore & restituisce l'indirizzo di una variabile. Si consideri lo stralcio di codice seguente:
int *p , q, n;
...
p = &q; /* -1- */
n = q; /* -2- */

L'effetto dell'istruzione memorizzare l'indirizzo della variabile q nella variabile p. Dopo questa
operazione, p punta a q. La seconda istruzione copia il valore di q nella variabile n, mentre la
variabile p punta alla variabile q.
L'operatore * ha la seguente capacit: se applicato ad una variabile puntatore restituisce il valore
memorizzato nella variabile a cui punta: p memorizza l'indirizzo, o punta, ad una variabile, e *p
restituisce il valore memorizzato nella variabile a cui punta p. L'operatore * viene chiamato
operatore di derefereziazione.
Per riassumere:
1. Per dichiarare un puntatore mettere * davanti al nome.
2. Per ottenere l'indirizzo di una variabile utilizzare & davanti al nome.
3. Per ottenere il valore di una variabile utilizzare * di fronte al nome del puntatore.

Si consideri lo stralcio di codice seguente:


int *a , b , c;
b = 10;
a = &b;
c = *a;

Delle tre variabili dichiarate, a un puntatore ad intero, mentre b e c sono interi. La prima
istruzione salva il valore 10 nella variabile b. La seconda istruzione (a = &b) salva in a il valore
dell'indirizzo della variabile a. Dopo questa istruzione a punta a b. Infine, l'istruzione c = *a
memorizza il valore della variabile puntata da a (ossia b) in c, quindi viene memorizzato in c il
valore di b (10).
Si noti che a un int e p un puntatore ad un int quindi
a = p;

un errore perch cerca di memorizzare un indirizzo in un int.


In modo analogo:
a = &p;

cerca di memorizzare l'indirizzo di un puntatore in un int ed altrettanto errato.


L'unico assegnamento sensato tra un int ed un puntatore ad un int :
a = *p;

Passaggio di una variabile o del puntatore alla variabile


Si consideri a titolo d'esempio il seguente problema: si scriva una funzione che scambia il
contenuto di due variabili. In linea di massima l'operazione dovrebbe essere semplice: scrivere una
funzione scambia(a, b) che scambia il contenuto di a e b.
void scambia(int a , int b)
{
int temp;
temp = a;
a = b;
b = temp;
printf("funz: var1 = %d var2 = %d\n", a, b); /* solo per debug*/
}

Questo non va. Il passaggio dei parametri in C viene fatto sempre per valore, facendo una copia
del valore della variabile che viene passata e su questa copia agisce la funzione. Quindi se si
considera lo stralcio di codice seguente che effettua la chiamata alla funzione scritta, otterremmo
un risultato diverso da quello desiderato:
...
int x, y;
x = 18;
y = 22;
printf("prima: var1 = %d var2 = %d\n", x, y);
scambia(x, y);
printf("dopo: var1 = %d var2 = %d\n", x, y);
...

A video si ottiene:
prima: var1 = 18 var2 = 22
funz: var1 = 22 var2 = 18
dopo: var1 = 18 var2 = 22

La soluzione al problema passare (sempre per valore) il riferimento alla variabile, ossia il loro
indirizzo cosicch la funzione possa accedere direttamente alla memoria (tramite appunto
l'indirizzo) e modificarne il valore. Alla funzione vengono quindi passati gli indirizzi delle variabili; la
funzione corretta dunque:
int scambia(int *a , int *b);
{
int temp;
temp = *a;
*a = *b;
*b = temp;
printf("funz2: var1 = %d var2 = %d\n", *a, *b); /* solo per debug*/
}

Si noti che i due parametri a e b sono puntatori e quindi per scambiare il valore necessario
utilizzare gli operatori di dereferenziazione per far s che i valori delle variabili a cui puntano
vengano scambiati. Infatti *a il contenuto della variabile a cui punta a.

Naturalmente anche la chiamata della funzione deve essere adattata: necessario passare due
indirizzi e non pi due interi. Quindi, il codice il seguente:
...
int x, y;
x = 18;
y = 22;
printf("prima: var1 = %d var2 = %d\n", x, y);
scambia(&x, &y);
printf("dopo: var1 = %d var2 = %d\n", x, y);
...

A video si ottiene:
prima: var1 = 18 var2 = 22
funz: var1 = 22 var2 = 18
dopo: var1 = 22 var2 = 18

che ci che si desidera.


La regola che ogniqualvolta si passa ad una funzione una variabile il cui contenuto deve essere
modificato necessario passare l'indirizzo della variabile.
Si faccia attenzione che chiamare la funzione passando il valore della variabile invece del suo
indirizzo quando il parametro dichiarato l'indirizzo causa lo scambio di contenuto di due aree
casuali della memoria, provocando danni possibilmente all'intero sistema e non solo al
programma in esecuzione! Non si tratta infatti di un errore sintattico ma piuttosto di un errore
semantico.

Approfondimento
La necessit di passare l'indirizzo ad una funzine spiega anche il perch le due funzioni di I/O printf e scanf
sono diverse. La funzione printf non modifica il valore dei suoi parametri, quindi viene chiamata con
printf("%d", a) ma la funzione scanf modifica il valore della variabile, per memorizzarci quello appena
acquisito, quindi viene chiamata con scanf("%d", &a).

Puntatori e array
In C c' uno stretto legame tra puntatori e array: nella dichiarazione di un array si sta di fatto dichiarando
un puntatore a al primo elemento dell'array:

int a[10];
Infatti a equivale a &a[0]. L'unica differenza tra a e una variabile puntatore che il nome dell'array
un puntatore costante: non si modifica la posizione a cui punta (altrimenti si perde una parte
dell'array). Quando si scrive un'espresisone come a[i] questa viene convertita in un'espressione a
puntatori che restituisce il valore dell'elemento appropriato. Pi precisamente, a[i] equivalente a
*(a + i) ossia il valore a cui punta a + i. In modo analogo *(a + 1) uguale a a[1] e cos via.

La possibilit di sommare 1 ad un puntatore pu essere interessante ma necessario capire


esattamente cosa succede. Ad esempio, in generale un int occupa due byte, un float quattro.
Quindi se si dichiare un array di int e si somma uno al puntatore all'array, il puntatore si muover
di due byte. D'altra parte, se si dichiara un array di float e si somma uno al puntatore all'array,
questo si muove di quattro byte.
In altre parole, sommare uno ad un puntatore ad array significa farlo passare al successivo
elemento, che ,dopotutto, ci che si desidera. Ci che sufficiente ricordare che l'aritmetica
dei puntatori si basa su unit del tipo di dati trattato. possibile utilizzare gli operatori di
incremento e decremento (++ e --) con i puntatori, ma non con il nome dell'array perch quello
un puntatore costante e non pu essere modificato.
Per riassumere:
1. Il nome di un array un puntatore costante al primo elemento dell'array, ossia a == &a[0] e *a ==
a[0].
2. L'accesso agli array mediante indici equivalente all'aritmetica dei puntatori, ossia a+i = &a[i] e
*(a+i) == a[i].

Un ulteriore punto legato a array e funzioni il passaggio di un array ad una funzione: di default si
passa il puntatore all'array. Questo consente di scrivere funzioni che possono accedere all'intero
array senza dover passare ogni singolo valore contenuto nell'array: si passa il puntatore al primo
elemento (e, in linea di massima, il numero degli elementi presenti).
Si consideri il seguente esempio, con le possibili implementazioni: scrivere una funzione che
riempie un array di interi con numeri casuali (mediante la funzione di libreria rand())
void main() /* stralcio di programma chiamante */
{
int numeri[NMAX], i;
...
riempirandom(numeri, NMAX);
...
}
void riempirandom(int a[] , int n)
{
int i;
for (i = 0; i< n ; i++)
a[i] = rand()%n + 1; /* rand()%n + 1 genera il valore casuale */
}

oppure, usando l'aritmetica dei puntatori:


void riempirandom(int *pa , int n)
{
int i;
for (i = 0; i< n ; ++i, ++pa)
*pa = rand()%n + 1; /* rand()%n + 1 genera il valore casuale */
}
Si faccia attenzione a non inserire l'espressione pa = 0 nella prima parte del for altrimenti si punta alla cella

di memoria di indirizzo 0, cosa che provoca un errore durante l'esecuzione quando si tenta di scriverci un
valore! Si pu scrivere anche:
void riempirandom(int *pa , int n)

{
int i;
for(i = 0; i< n ; ++i)
*(pa+i)=rand()%n+1; /* rand()%n + 1 genera il valore casuale */
}

oppure, ancora
void riempirandom(int *pa , int n)
{
int i;
for( ; i< n ; ++pa, ++i)
*(pa)=rand()%n+1; /* rand()%n + 1 genera il valore casuale */
}

Le stringhe
possibile definire array per gestire qualsiasi tipo di dato semplice, int, float, char, .... In genere una
collezione di numeri interi comoda per tenere uniti tutti i dati, che per hanno un significato
proprio. Quando si parla di caratteri, invece, pu essere interessante poter manipolare l'intero
insieme di caratteri appartenenti ad un array, in quanto costituiscono nell'insieme un vocabolo o
un'intera frase (con i debiti spazi). Il C consente quindi di interpretare una sequenza di caratteri
come una singola unit, per una pi efficiente manipolazione e vi una apposita libreria standard string.h - con funzioni di utilit di frequente utilizzo.
Una stringa pu includere caratteri alfanumerici ('a'...'z' 'A'...'Z' '0'...'9'), caratteri speciali come +, -,
$ ed altri. La caratteristica rilevante che ad indicare il termine della sequenza di caratteri c' un
carattere terminatore: '\0'. In C una stringa un array di caratteri e come tale ne eredita le
propriet ed il comportamento di base. Inoltre, possibile avere ulteriori vantaggi dovuti
all'esistenza del terminatore ed alla sua interpretazione.
Si consideri la seguente dichiarazione di un array di caratteri di 20 elementi:
char Vocabolo[20];

possibile accedere ad ogni elemento dell'array singolarmente, come si fa per ogni altro tipo di
array. inoltre possibile manipolare l'intero array come un'unica entit purch esista un carattere
terminatore '\0' in uno degli elementi dell'array, che ci sia stato messo dalle funzioni di
manipolazione della stringa, oppure direttamente da programma durante l'esecuzione.
importante ricordarsi di dimensionare opportunamente l'array includendo un elemento anche per
contenere il terminatore. Ad esempio, se un algoritmo prevede che si debba gestire vocaboli "di al
pi 20 caratteri" necessario dichiarare un array di 21 elementi.
#define DIM 20
char Vocabolo[DIM+1];

Funzioni di manipolazione delle stringhe


Una stringa un array di carattere e la fine dei dati validi demarcata dal carattere nullo '\0'. Nella
libreria string.h sono diponibili un certo insieme di funzioni per la manipolazione delle stringhe, che
realizzano operazioni di frequente utilizzo. Tra queste c' ad esempio la funzione int strlen(char[])

che restituisce il numero di caratteri presenti nella stringa ricevuta in ingresso come parametro.
Questo viene sempicemente realizzato contando il numero di caratteri dal primo fino al carattere
terminatore.
Si potrebbe pensare che fosse possibile assegnare una stringa ad un'altra, direttamente, mediante
un'espressione del tipo:
char a[l0], b[10];
b = a;

La parte di codice non copia ordinatamente i caratteri presenti nell'array a nei caratteri dell'array
b. Ci che viene effettivamente fatto far in modo che b punti allo stesso insieme di caratteri di a
senza farne una copia. Quindi, modificando poi i valori di b si modificano quelli di a. Il codice
seguente esemplifica quanto detto.
char a[l0], b[10];
scanf("%s", a); /* l'utente inserisce la stringa "abcdefghij" */
b = a;
printf("inizio a: %s <> b: %s\n" a);
for(i = 0; i < 10; i=i+2)
b[i] = '-';
printf("fine a: %s <> b: %s\n" a);

Ci che si ottiene a video :


inizio a: abcdefghij <> b: abcdefghij
fine a: -b-d-f-h-j <> b: -b-d-f-h-j

Per copiare il contenuto di una stringa in un'altra necessaria la funzione char * strcopy(char[],char[])
che effettivamente effettua la copia elemento ad elemento dell'array a nell'array b> fino al
carattere terminatore.
Ci sono numerose altre funzioni, tra cui citiamo solo l'importate funzione di confronto tra stringhe.
Infatti il confronto a == b darebbe esito positivo solamente se i due array puntassero allo stesso
insieme di caratteri, e non se il loro contenuto fosse identico.
La funzione int strcmp(char[],char[]) confronta due stringhe e restituisce 0 se il loro contenuto
identico.
Questa realt ha effetto anche sulla inizializzazione degli array. Non possibile scrivere:
a = "prova";

perch a indica l'inizio dell'array ( un puntatore) mentre "prova" una stringa costante. Si pu
per scrivere:
strcopy(a,"prova")

Le strutture
L'array un esempio di struttura dati. Utilizza dei tipi di dati semplici, come int, char o double e li
organizza in un array lineare di elementi. L'array costituisce la soluzione in numerosi casi ma non
tutti, in quanto c' la restrizione che tutti i suoi elementi siano dello stesso tipo.In alcuni casi
per necessario poter gestire all'interno della propria struttura un mix di dati di tipo diverso. Si

consideri a titolo d'esempio l'informazione relativa al nominativo, anni e salario. Il nominativo


richiede una stringa, ossia un array di caratteri terminati dal carattere '\0', l'et ed il salario
richiedono interi.
Con le conoscenze acquisite fino ad ora possibile solamente dichiarare delle variabili separate,
soluzione non altrettanto efficiente dell'utilizzo di una unica struttura dati individuata da un unico
nome: il Linguaggio C a tale scopo dispone della struct.

Definizione di una struct


La dichiarazione di una struct un processo a due fasi. La prima la definizione di una struttura con
i campi dei tipi desiderati, utilizzata poi per definire tutte le variabili necessarie con la suddetta
struttura. Si consideri il seguente esempio: si desidera gestire i dati di un insieme di persone
relativamente al loro nominativo, all'et e al loro salario. Per prima cosa si definisce la struct che
consente di memorizzare questo tipo di informazioni, nel seguente modo:
struct s_dipendente
{
char nominativo[40];
int anni;
int salario;
};

A questo punto possibile definire una variabile con la struttura appena introdotta:
struct s_dipendente dipendente;

La variabile si chiama dipendente ed del tipo struct s_dipendente definito precedentemente.


La sintassi per la definizione di una struct la seguente:
struct nome_struttura
{
lista dei campi (tipo-nome)
};

In seguito possibile definire variabili come segue:


struct nome_struttura nome_variabile;

Accesso ai campi della struttura


Per accedere ai campi della struttura, per scrivere un valore o per leggerlo, necessario indicare il
nome della variabile seguito da quello del campo di interesse, separati da un punto .. Ad esempio,
per la struttura precedentemente dichiarata:
struct s_dipendente dipendente
...
dipendente.anni = 30;
...
printf("%s\n", dipendente.nominativo);
...

Una volta individuato il campo d'interesse mediante la sequenza nome_variabile.nome_campo si ha a


che fare con una variabile normale, e nel caso di dipendente.anni ad una variabile di tipo intero.
Si noti che non deve utilizzare il nome della struttura s_dipendente, ma il nome della variabile, anche perch
ci possono essere pi variabili con la stessa struttura. Nel caso in cui una struct contenga campi costituiti da
altre struct si utilizzano i nomi di ogni struttura separati da punti fino a quando non si arriva al campo finale
della struttura.
Si consideri il seguente esempio:
struct s_azienda
{
char nome[30];
...
struct s_dipendente contabile;
...
};

Per accedere ai dati del contabile si segue il percorso: azienda.contabile.nominativo.


Per riassumere, l'aspetto significativo delle struct determinato dalla possibilit di memorizzare
informazioni di natura diversa al'interno di un'unica variabile. Una struct pu essere utilizzata per
integrare un gruppo di variabili che formano un'unit coerente di informazione.
Ad esempio il Linguaggio C non possiede un tipo fondamentale per rappresentare i numeri
complessi: una soluzione semplice consiste nell'utilizzare una struct e nel definire un insieme di
funzioni per la manipolazione di variabili. Si consideri a tale scopo il seguente esempio:
struct s_complesso
{
float reale;
float immaginaria;
};

A questo punto possibile definire due variabili:


struct s_complesso a, b, c;

possibile effettuare operazioni di assegnamento tra i vari campi della struct come ad esempio:
a.reale = b.reale;

D'altra parte non si pu scrivere un'espressione del tipo c = a + b, per la quale necessario invece
scrivere:
c.reale = a.reale + b.reale;
c.immaginaria = a.immaginaria + b.immaginaria;

A questo punto potrebbe quindi essere conveniente scriversi un insieme di funzioni che effettuino
le operazioni elementari sui numeri complessi da richiamare ogni volta.

Strutture e funzioni
La maggior parte dei compilatori C consente di passare a funzioni e farsi restituire come parametri
intere strutture. Se si desidera che una funzione possa cambiare il valore di un parametro
necessario passarne il puntatore.
struct s_complesso somma(struct s_complesso a , struct s_complesso b)
{
struct s_complesso c;
c.reale = a.reale + b.reale;
c.immaginaria = a.immaginaria + b.immaginaria;
return (c);
}

Definita la funzione somma possibile chiamarla, come nel seguente esempio:


struct s_complesso x, y, z;
...
x = somma(y, z);

Si tenga presente che il passaggio di una struct per valore pu richiedere un elevato quantitativo di
memoria.

Puntatori a strutture
Come per tutti i tipi fondamentali possibile definire un puntatore ad una struct.
struct s_dipendente * ptr

definisce un puntatore ad una struttura s_dipendente. Il funzionamento pressoch inalterato:


(*ptr).anni

il campo anni della struttura s_dipendente a cui punta ptr, ed un numero intero. necessario
utilizzare le parentesi in quanto il punto '.' ha una priorit superiore all'asterisco '*'.
Di fatto l'utilizzo di puntatori a struct estremamente comune e la combinazione della notazione
'*' e '.' particolarmente prona ad errori; esiste quindi una forma alternativa pi diffusa che
equivale a (*ptr).anni, ed la seguente:
prt->anni

Questa notazione d un'idea pi chiara di ci che succede: prt punta (i.e. ->) alla struttura e .anni
"preleva" il campo di interesse.
L'utilizzo di puntatori consente di riscrivere la funzione di somma di numeri complessi passando
come parametri non le struct quanto i puntatori a queste.
void s_complesso somma(struct s_complesso *a , struct s_complesso *b , struct s_complesso *c)
{
c->reale = a->reale + b->reale;
c->immaginaria = a->immaginaria + b->immaginaria;

In questo caso c un puntatore e la chiamata deve essere fatta cos:


somma(&x, &y, &z);

In questo caso si risparmia spazio nella chiamata alla funzione, in quanto si passano i tre indirizzi
invece delle strutture intere

Array di strutture
Uno dei punti id forza del C la capacit di combinare insieme tipi fondamentali e tipi derivati per
ottenere strutture dati complesse a piacere, in grado di modellare entit dati del mondo reale. Si
consideri il seguente esempio:
struct s_automobile
{
char marca[50];
char modello[70];
int venduto;
};
typedef struct s_automobile auto;

Per poter gestire le informazioni di un concessionario a questo punto necessario poter dichiarare
delle variabili che memorizzino i dati relativi alle automobili vendute. Si ipotizzi che ci siano al pi
cento diverse combinazioni di marche e modelli da dover gestire. Nell'ambito del programma sar
quindi necessario disporre di 100 elementi di tipo auto e a tal scopo verr dichiarato un array,
come segue:
void main()
{
auto concessionario[100];
int i;
...
}

In questo modo si dichiara un array di struct s_automobile di cento elementi. Ogni elemento
dell'array ha i suoi campi marca, modello e venduto, a cui si accede come segue:
...
printf("inserisci il nome della marca: \n");
gets(concessionario[i].marca);
concessionario[i].venduto=0;
...

I campi delle struct dei singoli elementi dell'array vengono poi trattati normalmente, in base al loro
tipo (nell'esempio rispettivamente come un array di caratteri ed un intero)

Definizione di un nuovo tipo per le strutture


La dichiarazione della struct per poter gestire insiemi di dati non omogenei viene spesso
completata introducendo un nuovo tipo, definito appunto dall'utente, che si va ad affiancare ai tipi
fondamentali del C. Si consideri il caso della struct per la gestione dei numeri complessi. Al fine di
evitare di dover scrivere ogni volta che si dichiara una variabile struct s_complesso possibile definire
un nuovo tipo apposito:
typedef struct s_complesso complesso;

In questo modo abbiamo introdotto un nuovo tipo che si affianca ad int, char, ... ce si chiama
complesso ed possibile utilizzarlo nella dichiarazione di variabili, come mostrato di seguito:
struct s_complesso
{
float reale;
float immaginaria;
};
typedef struct s_complesso complesso;
...
void main()
{
...
int a, b;
complesso x, y;
}

Frequentemente la dichiarazione del tipo mediante la typedef viene fatta concorrentemente alla
dichiarazione della struct, secondo la seguente sintassi:
typedef struct nome_struttura
{
lista dei campi (tipo-nome)
} nome_tipo_struttura;

La dichiarazione pi compatta. Riportata all'esempio precedente, il codice che si ottiene il


seguente:
typedef struct s_complesso
{
float reale;
float immaginaria;
} complesso;

Strutture e liste concatenate


Le strutture vengono utilizzate per la realizzazione del tipo elementare che costituisce l'elemento
di base delle liste dinamiche concatenate, discusse nella Lezione 15.

I File

Il flusso - stream
Sebbene il linguaggio C non abbia dei metodi nativi per gestire l'ingresso/uscita su file, la libreria
standard contiene numerose funzioni per un approccio efficiente, flessibile epotente.
Un concetto importante in C il flusso (stream), ossia un'interfaccia logica, comune a tutti i
dispositivi periferici del calcolatore; nel caso pi comune uno stream l'interfaccia logica ad un
file. In base a come il C definisce il termine "file", questo pu far riferimento ad un file su disco, allo
schermo, alla tastiera, ad una porta, e via discendo. Anche se questi file differiscono nella forma e
nelle loro capacit, vengono visti tutti in modo analogo.
Uno stream viene collegato ad un file mediante un'operazione di apertura, e dualmente lo stream
viene disassociato mediante una operazione di chiusura. La posizione corrente il punto in cui si
far il prossimo accesso nel file.
Ci sono due tipi di stream: quelli di testo (costituiti da una sequenza di caratteri ASCII, sebbene ci
possa essere una discrepanza tra il contenuto del file e lo stream) e quelli binari (utilizzati per
qualiasi tipo di dato). In linea di massima ci si riferir sempre a stream di testo.
Uno stream di testo composto di linee. Ogni linea ha zero o pi caratteri ed terminata da un
carattere di a-capo (carattere con codice ASCII 10) che l'ultimo carattere della linea. I caratteri
sono esclusivamente caratteri stampabili, il carattere di tabulazione ('\t') e il carattere a-capo
('\n').
Per aprire un file ed associarlo ad uno stream l'istruzione da utilizzarsi la fopen(), il cui prototipo
mostrato di seguito:
FILE *fopen(char *,char *);

La funzione fopen(), come tutte le funzioni di sistema si trova nella libreria standard di sistema
stdio.h . Essa riceve in ingresso due parametri, rispettivamente il nome del file da aprire e il modo in
cui aprirlo (il tipo di accesso che si desidera fare). Per quest'ultimo aspetto, le modalit consentite
sono:
Modo
r
w
a
rb
wb
ab
r+
w+
a+
r+b
w+b
a+b

Significato
apre un file di testo in lettura
crea un file di testo per scriverci
apre un file in scrittura e si posiziona alla fine (appende il testo)
apre un file binario in lettura
crea un file binario in scrittura
apre un file binario e si posiziona alla fine
apre un file di testo in lettura/scrittura
crea un file di testo in lettura/scrittura
apre o crea un file di testo in lettura/scrittura
apre un file binario in lettura/scrittura
crea un file binario in lettura/scrittura
apre un file binario e si posiziona alla fine per lettura/scrittura

Se l'operazione di apertura ha successo (il file c' e si hanno i permessi corretti, se si tratta di
lettura, c' spazio a sufficienza e si hanno i permessi nel caso di scrittura) l'istruzione restituisce un
puntatore a file (tipo FILE*) valido.
Il tipo FILE viene definito nella libreria stdio.h. Si tratta di una struttura con diverse informazioni
relative al file, tra cui ad esempio la dimensione. Il puntatore al file verr utilizzato da tutte le
funzioni che lavorano con i file e non va esplicitamente manipolato (incrementato, decrementato,

...).
Se la funzione fopen() non va a buon fine, restituisce un puntatore a NULL, condizione che va
verificata prima di procedere nell'accesso al contenuto del file. Lo stralcio di codice mostra
l'utilizzo della funzione fopen() e il controllo del risultato prima di procedere:
...
FILE *fp;
char NomeFile[30];
...
if ((fp = fopen(NomeFile, "r")) == NULL)
printf("Errore nell'apertura del file %s\n", NomeFile);
else{
...
}

Per chiudere un file disponibile la funzione di libreria fclose(), il cui prototipo il seguente:
int fclose(FILE *);

La funzione riceve in ingresso come parametro il puntatore al file da chiudere: il puntatore deve
essere valido, ottenuto mediante una precedente fopen(). La funzione fclose() restituisce 0 se viene
eseguita con successo, altrimenti restituisce EOF (end of file, fine del file) nel caso si verifichi un
errore.
Una volta aperto un file, in base al modo in cui stato aperto, possibile leggere e/o scrivere
utilizzando le seguenti funzioni:
int fgetc(FILE *);
int fputc(int , FILE *);

La prima funzione (getc()) legge il byte successivo dallo stream indicato dal puntatore al file e lo restituisce
come intero (il valore ASCII corrispondente al carattere); se si verifica un errore o termina il file la funzione
restituisce EOF (situazione indicata dal carattere stesso EOF). Il valore restituito dalla funzione fget() pu
essere assegnato ad una variabile di tipo carattere.

La funzione duale fput() scrive un byte corrispondente al carattere ricevuto in ingresso come primo
parametro nel file associato al puntatore a file indicato come secondo parametro della funzione.
Sebbene il tipo del primo parametro un intero la funzione pu ricevere in ingresso un carattere
(si ricordi sempre il legame tra un carattere e il suo codice ASCII). La funzione fput() restituisce il
carattere scritto nel caso non vi siano problemi, altrimenti restituisce EOF.

Funzioni per file di testo


Le librerie standard del C mettono a disposizione quattro funzioni per semplificare le operazioni di
accesso ai file in grado di gestire una maggior quantit di dati rispetto al singolo byte. Le prime due
funzioni hanno i seguenti prototipi:
int fputs(char* , FILE*);
char *fgets(char* , int , FILE*);

Le funzioni fputs() e fgets() scrivono e leggono una stringa da un file. La funzione fputs() scrive la
stringa ricevuta come primo parametro, sul file indicato dal puntatore a file (il secondo
parametro). La funzione restituisce EOF nel caso in cui si verifica un errore, altrimenti un valore non
negativo. Il carattere terminatore della stringa non viene scritto e non viene neppure aggiunto
automaticamente un ritorno a capo.
La funzione fget() legge una sequenza di caratteri dal file puntato dall'apposito puntatore, che
costiuisce il terzo parametro. La funzione legge al pi num-1 caratteri e li memorizza nella stringa
ricevuta come primo parametro. Nel caso i caratteri siano meno oppure si incontri il carattere acapo o EOF. La stringa letta viene terminata con il terminatore '\0'.
La funzioe restituisce la stringa se non si verifcano problemi, altrimenti restituisce il puntatore nullo.
Le altre due funzioni sono fprintf() e fscanf(). Queste funzioni operano esattamente come la funzione
printf() e scanf() rispettivamente, solamente che accedono a file invece che allo standard input
(tastiera) e output (video). I loro prototipi sono:
int fprintf(FILE *, char *, ...);
int fscanf(FILE *, char * ...);

Invece di ridirigere le operazioni di ingresso/uscita verso la console, queste funzioni operano sul
file specificato dal puntatore al file ricevuto come primo parametro. Per il resto queste operazioni
sono analoghe alle printf() e scanf(). Il vantaggio di queste due funzioni che rendono
estremamente semplice scrivere una grande quantit e variet di dati su un file di testo.
La funzione feof() restituisce un valore diverso da zero se il puntatore al file che riceve come unico
parametro ha raggiunto la fine del file, altrimenti restituisce 0.
Il prototipo :
int feof(FILE *fp);

Funzioni del file system


possibile accedere a due funzioni del file system per eliminare un file e per riportarsi all'inizio di
un file aperto senza doverlo chiudere e riaprire. I prototipi sono i seguenti:
int remove(char *);
void rewind(FILE *);

La funzione remove() riceve in ingresso il nome del file da cancellare mentre la funzione rewind() il
puntatore al file da riposizionare.

La memoria dinamica
Tutte le variabili dichiarate ed utilizzate nelle precedenti lezioni venivano allocate in modo statico,
riservando loro spazio nella porzione di memoria denominata Stack che destinata alla memoria
statica. Il compilatore vede dal tipo della variabile, al momento della dichiarazione, quanti byte
devono essere allocati. Per staticit si intende che i dati non cambieranno di dimensione nella
durata del programma (si ricordi il vincolo di dimensionare opportunamento un array,
eventualmente sovradimensionandolo).

Esiste una porzione di memoria denominata Heap ('mucchio' in italiano) dove possibile allocare
porzioni di memoria in modo dinamico durante l'esecuzione del programma, a fronte di richieste
di spazio per variabili.

Allocazione dinamica
Con questo metodo di allocazione possibile allocaren byte di memoria per un tipo di dato (n sta per la
grandezza di byte che devono essere riservati per quel tipo di dato). A questo scopo esistono specifiche
funzioni della libreria standard (malloc e free) per l'allocazione e il rilascio della memoria. Per identificare la
dimensione della memoria da allocare dinamicamente, si utilizza l'operatore sizeof che prende come
parametro il tipo di dato (int, float, ...) e restituisce il numero di byte necessari per memorizzare un dato di
quel tipo.
Si ricordi che il numero di byte necessari per memorizzare un numero intero dipende dall'architettura del
calcolatore e dal compilatore stesso. In generale questo valore pari a 4 byte, ma potrebbe essere
differente su architetture diverse. Per cui, onde evitare di riservare una quantit di memoria sbagliata,
opportuno far uso di tale funzione.

La funzione C per allocare dinamicamente uno spazio di memoria per una variabile la seguente:
void * malloc(size_t);

La funzione, appartenente alla libreria standardstdlib.h riserva uno blocco di memoria didim byte
dalla memoria heap e restituisce il puntatore a tale blocco. Nel caso lo spazio sia esaurito,
restituisce il puntatore nullo (NULL). Quindi, per poter sfruttare la possibilit di allocare
dinamicamente della memoria necessario dichiarare delle variabili che siano dei puntatori, a cui
verr assegnato il valore dell'indirizzo del blocco richiesto quando si allocher della memoria.
Ad esempio, per poter allocare dello spazio per una variabile intera necessario aver dichiarato
una variabile puntatore ad intero e poi nel corpo del programma aver chiesto lo spazio in memoria
mediante la funzionemalloc, come mostrato nel seguito:
...
int *numx;
/*-1-*/
...
numx = (int *) malloc(sizeof(int)); /*-2-*/
...
*numx = 34;
/*-3-*/

Poich la malloc restituisce un puntatore genericovoid *, viene fatto un cast esplicito al tipo intero,
(int *).
La situazione della memoria, corrispondenti ai punti -1-, -2- e-3- mostrata nella figura seguente.

-1-

-2-

-3-

abbastanza intuitivo che possibile allocare dinamicamente tutta la memoria che si desidera
(pur di non esaurire la memoria heap) per poter gestire un numero di dati qualsivoglia, non noto a
priori e pur di aver dichiarato dei puntatori per poter accedere alla memoria allocata
dinamicamente. Nell'esempio fatto abbiamo bisogno di una variabile puntatore ad intero per poi
poter gestire la memoria allocata dinamicamente.
anche possibile allocare dinamicamente un numero di byte sufficienti a contenere pi dati dello
stesso tipo, ossia un array allocato dinamicamente. Si consideri il seguente stralcio di codice, che
dopo aver chiesto all'utente quanti dati intende inserire, alloca dinamicamente la memoria per poi
procedere nell'elaborazione
...
int *Numeri, n;
...
printf("Quanti dati si desidera inserire?");
scanf("%d", &n); /* numero di dati - omesso controllo di validit*/
Numeri = (int *)malloc(n * sizeof(int)); /* vengono allocati n * numero_byte_per_un_intero */
for(i = 0; i < n; i++){
printf("Inserisci il dato %d: " i+1);
scanf("%d", &Numeri[i]);
}
...

In questo modo possibile scrivere programmi in cui non sia noto a priori il numero di dati da
trattare, anche se rimane il vincolo che tale informazione debba essere prima o poi fornita al
programma. Per poter gestire situazioni in cui il numero dei dati non mai conosciuto, e pu
variare durante l'esecuzione in base all'elaborazione necessario utilizzare delle strutture dati
opportune, che siano in grado di allocare di volta in volta la memoria necessaria: si tratta delle
liste concatenate, trattate nella Lezione 15.

Rilascio della memoria dinamicamente allocata


Quando la memoria allocata dinamicamente non serve pi opportuno (!) liberarla, in modo tale
che possa essere riutilizzata, riducendo i rischi di esaurire la memoria. La funzione free effettua
questa operazione; il prototipo il seguente:
void free(void *);

La memoria riceve in ingresso un parametro: il puntatore alla memoria che deve essere liberata.
Una volta eseguita l'istruzione fare accesso al puntatore senza prima riassegnarlo, se non per
verifare che punti a NULL causa un errore durante l'esecuzione.
Nota
size_t un tipo utilizzato per le dimensioni dei tipi in memoria. definito nella libreria stddef.h

Le liste concantenate
Una lista concatenata una sequenza di nodi in cui ogni nodo collegato al nodo successivo:
possibile aggiungere collegare nella lista un numero qualsivoglia di nodi, eliminarli, ordinarli in
base ad un qualche criterio. Si accede alla lista concatenata mediante un puntatore al primo
elemento della lista, da l in poi ogni elemento punter a quello successivo. Per convenzione,
l'ultimo nodo punter a NULL ad indicare il termine della lista.
Ogni nodo della lista, oltre a mantenere il collegamento all'elemento successivo memorizza anche
i dati veri e propri che devono essere gestiti: l'infrastruttura della lista un accessorio per poter
richiedere la memoria di volta in volta in base alle esigenze. Infatti i nodi vengono creati solo
quando c' un nuovo dato da memorizzare.

Strutture per la realizzazione dei nodi delle liste


Per costruire una lista concatenata solitamente si dichiara un tipo di dato per i nodi della lista, i cui
campi sono i dati veri e propri ed un campo di tipo puntatore.
typedef struct nodo_s {
char carattere;
int frequenza;
struct nodo_s * prox;
} nodo_t;

Nella definizione di un tipo di struttura in C, c' l'opzione di includere una etichetta, ad esempio
nodo_s dopo la parola chiave struct. Di seguito quindi struct nodo_s costituisce un nome alternativo
per il tipo nodo_t.
Nella parte di dichiarazione del tipo di utilizza poi struct nodo_s * per il puntatore ad un altro
elemento dello stesso tipo. necessario utilizzare struct nodo_s * invece di nodo_t * in quanto il
compilatore non ha ancora visto il nome nodo_t.

Creare nuovi nodi


Per creare un nuovo nodo necessario allocare la memoria, verificare che non sia stata esaurita e
quindi memorizzare il valore dei dati negli appositi campi della struttura del nodo.
...
nodo_t *n1;
n1 = (nodo_t *)malloc(sizeof(nodo_t)); /* alloca memoria per un elemento di tipo nodo_t */
if (n1) { /* verifica che non ci siano stati problemi di memoria */
n1->carattere = val_c;
n1->frequenza = 1;

n1->prox = NULL; /* inizializzato a puntare a NULL */


/* verr modificato quando inserito nella lista */
... inserimento nella lista ...
}

Una volta inserito il nodo nella lista il puntatore n1 potr essere utilizzato per creare un nuovo
nodo.

La testa della lista


La testa della lista ha come obiettivo indicare sempre il primo nodo della lista. Si tratta di un
puntatore ad un elemento di tipo nodo_t, non serve un intero nodo in quanto non memorizza un
dato, ma solo un puntatore ad un nodo.
Inizialmente, la lista vuota, per cui all'inizio del programma si inizializza la testa a NULL.
void main()
{
nodo_t * testa;
...
testa = NULL;
...
}

Attraversamento/scorrimento della lista


In numerosi situazioni necessario processare tutti i nodi della lista o comunque si deve
esaminarli alla ricerca di uno specifico. Per attraversare la lista necessario posizionarsi sul primo
nodo, mediante la testa, e poi seguire la catena di puntatori all'elemento successivo.
Se si desidera processare tutti i nodi ci si fermer nell'attraversamento quando la lista termina, in
altri casi (come nell'esempio successivo dell'inserimento in coda) desiderabile fermarsi
sull'ultimo elemento della lista. Lo stralcio di codice proposto nel seguito effettua la stampa di tutti
i nodi della lista:
...
nodo_t *testa, *temp;
testa = NULL;
... /* riempimento della lista */
temp = testa;
while(temp){ /*ci si ferma quando temp = NULL*/
printf("%c: %d\n", temp->carattere, temp->frequenza);
temp = temp->prox;
}

Collegare i nodi
Le operazioni fondamentali per la gestione della lista concatenata sono le seguenti:

inserire un nodo all'inizio della lista (in testa)


inserire un nodo alla fine della lista (in coda)
eliminare un nodo dalla lista

Ci sono altre operazioni che possibile svolgere, come per esempio l'inserimento di un nodo in un punto
ben preciso della struttura per mantenere o realizzare un ordinamento dei nodi, che si basano su quelle
fondamentali citate.

Le operazioni di inserimento ed eliminazione consistono nell'aggiornare opportunamente i


puntatori di alcuni nodi in modo tale che il nuovo nodo venga incluso nella catena oppure in modo
che ne venga escluso.
Nell'affrontare ognuna di queste operazioni necessario considerare i casi particolari in cui la lista
vuota e in cui c' un unico elemento (in alcune situazioni anche il caso con solo due nodi pu
risultare speciale). Si ricordi, infatti, che accedere ad un puntatore nullo crea un errore durante
l'esecuzione.

Inserimento in testa
Viene trattato in primo luogo il caso generale in cui la lista ha almeno un elemento. La figura
seguente mostra quali sono i passi da svolgere per inserire un nodo all'inizio della lista, come
nuovo primo nodo.

Ci si ricordi che aggiornando un puntatore per indirizzarlo ad un nodo diverso sa quello


attualmente puntato contemporaneamente elimina il precedente collegamento. Per cui, in figura,
sono due le operazioni da effettuare: primo fare puntare il nuovo nodo (accessibile tramite il
puntatore nuovo) all'attuale primo nodo (quello puntato dalla testa) e secondo fare puntare la
testa al nuovo (primo) nodo. Automaticamente vengono eliminati i vecchi collegamenti. Lo stralcio
di codice che effettua l'inserimento in testa illustrato qui di seguito, in cui si suppone che nuovo
punti ad un nuovo nodo, con i campi gi impostati al valore corretto, e testa che punta
correttamente al primo nodo della lista:
...
nuovo->prox = testa;
testa = nuovo;
...

Nel caso in cui la lista sia vuota,testa punter a NULL ad indicare questa situazione. Il codice prima
indicato mantiene la propria validit: infatti dopo la prima istruzione nuovo->prox punter a NULL, il
che indica che l'ultimo elemento (essendo l'unico cos). L'effetto della seconda istruzione non
cambia.
Dall'analisi si deduce che nell'inserimento di un nuovo elemento in testa alla lista non necessario trattare
esplicitamente il caso di lista vuota.

Inserimento in coda
Per effettuare l'inserimento in coda necessario scorrere tutta la lista e portarsi sull'ultimo
elemento, facendo in modo che quando si sta per effettuare l'operazione di aggiornamento, il
puntatore indirizzi l'ultimo nodo. Le figure seguenti mostrano la sequenza di operazioni.

Lo stralcio di codice che effettua lo scorrimento della lista fino all'ultimo elemento e l'operazione
di inserimento in coda riportato qui di seguito.
...
nodo_t *testa, *temp, *nuovo;
temp = testa;
while(temp->prox){ /* scansione della lista */
temp = temp->prox;
}
/* quando si arriva qua temp punta all'ultimo elemento */
temp->prox = nuovo; /* inserimento */

L'istruzione nuovo->prox = NULL; non stata eseguita in quanto viene fatta nel momento in cui si crea
un nuovo nodo.
In questo caso necessario gestire il caso specifico della lista vuota in quanto se la lista vuota
(temp = NULL) l'accesso successivo al puntatore temp con l'istruzione temp->prox causerebbe un errore
durante l'esecuzione. Lo stralcio di codice che gestisce anche il caso lista vuota il seguente:
...
temp = testa;
if (temp){
while(temp->prox) /* si arriva solo se temp non NULL, non c' possibilit di generare errore */
temp=temp->prox;
/* temp punta all'ultimo */
temp->prox = nuovo;
} else /*caso lista vuota*/
testa = nuovo;

Eliminazione di un elemento della lista


Questa l'operazione che richiede maggior attenzione in quanto sono necessari due puntatori
d'appoggio per poter svolgere l'eliminazione senza perdere una parte della lista concatenata. Per
eliminare un nodo, oltre a sistemare opportunamente i puntatori dei nodi che rimangono nella
lista anche necessario liberare la memoria del nodo mediante l'istruzione free (per questo motivo
serve un secondo puntatore). Verr mostrato il caso pi generale in cui si desidera eliminare un

nodo che si trova in mezzo alla lista facendo poi alcune considerazioni sugli altri casi. Le figure
seguenti mostrano la sequenza delle operazioni da svolgere.

Il puntatore canc punta al nodo da eliminare, il puntatore di supporto temp punta al nodo
precedente a quello da cancellare: si scorrer quindi la lista fino a trovare il punto in cui fermare
temp e di conseguenza si definircanc, quindi si provveder a spostare i puntatori e a chiamare la
free. Il codice riportato qua di seguito, in cui non si considera il caso generale.
...
nodo_t *canc, *temp;
char valore_cercato;
...
temp = testa; /* i casi speciali non sono trattati */
while(temp->prox && temp->prox->carattere != valore_cercato)
temp = temp->prox;
/* quando si e' qua, o non si trovato l'elemento o si su quello che precede quello da eliminare */
if (temp->prox->carattere == valore_cercato){ /* si pu procedere all'eliminazione */
canc = temp->prox; /* punta all'elemento da cancellare */
temp->prox = canc->prox; /* spostamento dei puntatori */
free(canc); /* libera la memoria */
} /*non c' un else perch se l'elemento non c' non si fa nulla */

I casi particolari sono costituiti dal caso lista vuota, da quello in cui l'elemento il primo della lista
e il caso in cui nella lista c' un solo elemento, che sono le situazioni che provocherebbero un
errore nell'esecuzione del precedente codice privo di controlli.
Il seguente sottoprogramma gestisce tutte le casistiche citate, ipotizzando che la lista non sia
ordinata, altrimenti sarebbe possibile interrompere la scansione della lista non appena si
terminato di eliminare un elemento dalla lista ed il successivo non da eliminare.
nodo_t * EliminaN(nodo_t * head, int val)
{
nodo_t * tmp, *canc;
tmp = head;
/* se la lista e' vuota non c'e' nulla da eliminare */
/* il caso viene gestito dal codice seguente */
/* eliminazione del primo elemento della lista */
while(tmp && tmp->frequenza == val){
head = head->prox;
free(tmp);
tmp = head;
}
/* eliminazione di un elemento qualsiasi */

/* tmp punta al primo elemento */


/* che non e' da eliminare (altrimenti saremmo ancora nel ciclo) */
while(tmp)
if(tmp->prox && tmp->prox->frequenza == val){
canc = tmp->prox;
tmp->prox = tmp->prox->prox;
free(canc);
} else
/* si va avanti solo se non si cancella un nodo_tento */
/* altrimenti con nodo_tenti contigui da cancellare si */
/* dimentica di cancellare il secondo */
tmp = tmp->prox;
return head;
}

La ricorsione
La ricorsione una tecnica di programmazione in cui la risoluzione di problemi di grandi
dimensione viene fatta mediante la soluzione di problemi pi piccoli della stessa forma.
importante capire che il problema verr scomposto in problemi della stessa natura.

Un esempio
Per avere un'idea di cosa sia la ricorsione, si pensi ad una delle prospettive di guadagno che
talvolta vengono pubblicizzate: il vostro compito di raccogliere 1.000.000 di euro.
Visto che praticamente impossibile pensare di trovare una persona che versi l'intera cifra si deve
pensare di raccogliere l'intera cifra mediante la somma di contributi piu' piccoli. Se ad esempio si
sa che ogni persona interpellata di solito disposta a metterci 100 euro, necessario trovare
10.000 persone e chiedere a ciascuna di queste 100 euro. Trovare 10.000 persone potrebbe per
essere un po' difficile. La soluzione nel cercare di trovare altre persone che si dedichino alla
raccolta dei soldi, e di delegare a loro la raccolta di una certa cifra. Per esempio si puo' pensare di
individuare 10 persone, ognuna delle quali raccolga 100.000 euro. Se anche queste dieci persone
adottano la stessa strategia, questi recluteranno 10 persone ciascuna delle quali deve raccogliere
10.000 euro. Lo stesso ragionamento si pu fare fino ad arrivare ad avere dieci persone che
raccolgano per un delegato i 100 euro.
Se cerchiamo di codificare questa strategia in pseudo-codice, l'algoritmo risultante il seguente:
void RaccogliDenaro(int n)
{
if(n <= 100)
Chiedi i soldi ad una sola persona
else {
trova dieci volontari
Ad ogni volontario chiedi di raccogliere n/10 euro
Somma i contributi di tutti i dieci volontari
}
}

La cosa che importante notare che la macro istruzione


Ad ogni volontario chiedi di raccogliere n/10 euro

non altro che il problema iniziale, ma su una scala pi piccola. Il compito lo stesso - raccogliere
n euro - solo con un n pi piccolo.
Inoltre, siccome il problema lo stesso, lo si pu risolvere chiamando il sottoprogramma originale.
Quindi, nello pseudo-codice, si pu pensare di scrivere:
void RaccogliDenaro(int n)
{
if(n <= 100)
Chiedi i soldi ad una sola persona
else {
trova dieci volontari
Ad ogni volontario RaccogliDenaro(n/10)
Somma i contributi di tutti i dieci volontari
}
}

Alla fine, il sottoprogramma RaccogliDenaro finisce per chiamare se stesso se il contributo da


raccogliere inferiore a 100 euro. Nel contesto della programmazione, avere un sottoprogramma
che chiama se stesso prende il nome di ricorsione.
Lo scopo dell'esempio dunque quello di illustrare l'idea di affrontare la soluzione di un problema
facendo riferimento allo stesso problema su scala ridotta.
I problemi che possono essere risolti tramite una forma ricorsiva hanno le seguenti caratteristiche:

Uno o pi casi semplici del problema hanno una soluzione immediata, non ricorsiva;
Il caso generico pu essere ridefinito in base a problemi pi vicini ai casi semplici;
Applicando il processo di redifinizione ogni volta che viene chiamato il sottoprogramma ricorsivo il
problema viene ridotto al caso semplice, facile da risolvere.

L'algoritmo ricorsivo avr sovente la seguente forma:


if( il caso semplice)
risolvilo
else
ridefinisci il problema utilizzando la ricorsione

Quando si individua il caso semplice, la condizione indicata nell'if per la gestione del caso semplice
si chiama condizione di terminazione.
La figura seguente illustra tale approccio: la soluzione del caso semplice rappresentata dalla
soluzione del problema di dimensione 1.

la moltiplicazione
Come altro esempio si prenda in considerazione il seguente problema: effettuare il prodotto tra
due numeri a e b conoscendo l'operatore somma ma non l'operatore prodotto e sapendo solo che:

un numero moltiplicato per uno uguale a se stesso


il problema del prodotto a x b pu essere spezzato in due parti:
P1. il prodotto a x (b-1)
P2. somma a al risultato della soluzione del problema P1.

Il problema P1, per quanto non risolubile (perch continuiamo a non conoscere l'operatore
prodotto) pi vicino al caso semplice ed possibile pensare di spezzare anche tale problema in
due parti, ottenendo cos la seguente cosa:
P1. Prodotto a x (b-1)
P1.1. Prodotto a x (b-2)
P1.2. Somma a al risultato del problema P1.1
P2. Somma a al risultato del problema P1.

Si procede cos fino a quando:


P1.1...1. Prodotto a x 1
P1.1...2. Somma a al risultato del problema P1...1

Il caso semplice si ha quando n == 1 vera. La forma della ricorsione prima indicata diventa quindi:
if(b == 1)
ris = a;
/* caso semplice */
else
ris = a + moltiplica(a, b-1); /* passo ricorsivo */

Il codice completo quindi:


int moltiplica(int a, int b)
{
int ris;
if(b == 1)
ris = a;
else
ris = a + moltiplica(a, b-1); /* passo ricorsivo */
return(ris);
}

Una classe di problemi per cui la ricorsione risulta una soluzione interessante quella che
coinvolge delle sequenze (o liste) di elementi di lunghezza variabile.

come seguire una funzione ricorsiva che restituisce un valore


Per poter seguire cosa verr mostrato il record di attivazione per ogni chiamata a
sottoprogramma. Il record di attivazione mostra il valore dei parametri per ogni chiamata e
l'esecuzione della funzione.
Si consideri la chiamata seguente: moltiplica(6,3);
la traccia dell'esecuzione mostrata nella figura seguente.

Per maggior chiarezza sono stati rappresentati degli spazi di memoria distinti, anche se in realt il
compilatore mantiene un unico pila di sistema (o stack). Ogni volta che una funzione viene
chiamata, i suoi parametri e le sue variabili locali vengono messi sullo stack, insieme all'indirizzo di
memoria dell'istruzione che effettua la chiamata. Questo indirizzo serve per sapere a che punto
rientrare dalla chiamata a sottoprogramma. L'esecuzione dell'istruzione return in uscita dalla
funzione svuota lo stack restituendo il valore che c'era in cima allo stack.
In questo modo, ogni chiamata a funzione, anche quelle ricorsive, riserva alla funzione spazio
necessario per i parametri e per le variabili locali, cosicch tutto possa funzionare correttamente
nel rispetto delle regole di visibilit.

Potrebbero piacerti anche