You are on page 1of 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);
...

gli operatori logici consentono di concatenare fra loro più espressioni logiche e di negare il risultato di un'espressione logica. allora l'espressione è vera. indicando l'equivalenze: . la scala di priorità è mostrata nella tabella successiva. altrimento l'espressione è falsa. maggiore(>) maggiore-uguale(>=) uguale (==) minore(<) minore-uguale (<=) diverso (!=) Infine. infine l'espressione i è il valore di i dopo il postincremento (17). 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). È 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).Il risultato che si ottiene è: 16. Anche tra questi operatori ci sono delle priorità. riportate nella tabella sottostante. l'espressione ++i incrementa i prima di stamparlo a video (quindi i è 16). Le espressioni logiche possono contenere gli operatori relazionali usare per confrontare tra loro dei valori. 16. 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. 17 Infatti. Un semplice esempio di espressione logica è una variabile: se il suo contenuto è diverso da zero. NOT Logico (!) AND Logico (&&) OR Logico (||) Date due espressioni logiche. la successiva i++ viene valutata al valore corrente di i (16) che verrà poi incrementato (a 17).

Ad esempio. La funzione scanf consente di acquisire dati dallo standard input (la tastiera) e di memorizzarli in una variabile. &a). Mediante l'operatore assegnamento è possibile memorizzare un valore in una variabile: a = 100. passando da una forma a quella negata. Questa operazione viene svolta ogni volta che si esegue il programma.. dimenticandosi di cambiare operatore o di negare le singole espressioni. dunque ha una utilità molto limitata. È quindi necessario far in modo che il programma acquisisca i dati su cui lavorare dall'utente e stampi a video il risultato dell'elaborazione. L'applicazione delle leggi di De Morgan consente di evitare tali errori.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. Quindi. una volta acquisito quanto scritto dall'utente il valore viene memorizzato nella variabile a e poi il programma procede con le istruzioni successive.valori inclusi . Quando il programma nell'esecuzione raggiunge l'istruzione scanf esso si sospende e lascia all'utente la possibilità di digitare qualcosa dalla tastiera. l'istruzione scanf("%d". .. l'espressione per valutare se una variabile num sia compresa tra due valori espremi min e max . Ingresso e uscita Per rendere un programma interessante è necessario poter inserire dei dati durante la sua esecuzione e vedere il risultato che si ottiene. A questo scopo ci sono numerosi "comandi" che consentono di effettuare l'operazione di acquisizione dati e stampa a video. soprattutto considerando che che non è dato vedere alcun risultato. 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>). Ad esempio.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. 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.

ad ogni esecuzione l'utente può inserire valori diversi. Il passo ulteriore è vedere il risultato. fino al prossimo % o alla fine della stringa. è buona norma utilizzare prima una printf che informi l'utente sulla necessità di inserire un dato.. espressione. La funzione printf utilizza tale informazione per convertire e formattare il valore ottenuto dall'espressione che segue la stringa di formato. Dopo la stringa è possibile mettere un numero qualsiasi di altri argomenti. sia nella printf indica che il valore gestito è un intero in base dieci. Se si considera invece l'altro esempio: . Tale elemento (il %) è un segnaposto ad indicare che lì è necessario stampare il valore di un'espressione che è di un tipo ben preciso. espressione. ossia fare stampare sullo standrd output (il video) il risultato dell'elaborazione. Nota: la funzione scanf non fa una richiesta all'utente. Per questo motivo. Il %d sia nella scanf..In questo modo. espressione. 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 .) La stringa deve includere un segnaposto per ciascuna espressione successivamente elencata. producendo risultati diversi. Per questo motivo la stringa prende solitamente il nome di stringa di controllo o stringa di formato. &a). Quando trova un % allora identifica il tipo di dato che deve essere stampato mediante il carattere (o i caratteri) che seguono il %. formattato e scritto a video il valore dell'espressione. 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". la funzione continua nell'analisi della stringa. La forma più generale è: printf(stringa. 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.".. Per esempio: printf("Inserisci un numero intero:"). come ad esempio: . per specificare in quale punto della stringa deve essere posizionato il valore dell'espressione e di che tipo di valore si tratta. printf("Inserisci un numero intero: "). C'è solo la stringa di formato e tutti i caratteri vengono scritti sullo schermo. scanf("%d". Valutato. cosicchè ciò che vede l'utente è appunto la scritta inserita tra le virgolette. semplicemente aspetta che l'utente inserisca il dato. in base alla necessità.

printf("Il valore della variabile a e': %d". che scriverà il valore della variabile a più un'unità. A parte i dettagli tecnici sono due le cose importanti da ricordare: 1. a+1). È anche possibile scrivere invece della variabile a un'espressione. 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. 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. come ad esempio: printf("Il risultato e': %d". il %d specifica che si deve valutare l'espressione che segue la stringa come un numero intero decimale. rappresentato in complemento a due ma notazione virgola mobile (formato standard IEEE 754). che però non corrisponderà al valore esatto. a). Quindi verrà visualizzato solamente il contenuto dei primi due byte. 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 . 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. La specifica %d no nè solamente un identificatore di formato. 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. in quanto non è stato riassegnato. Tutto ciò è molto lontano dal corrispondere anche solo alla parte intera del numero reale. Questi due primi byte verranno interpretati come la rappresentazione in complemento a due di un numero intero dotato di segno. La ragione è che un int utilizza la metà dello spazio occupato da un float. Il valore della variabile resta però invariato.

printf("seconda riga .. La trattazione dettagliata di questi aspetti è oggetto della Lezione 12. per il momento è sufficiente ricordare questa necessità di modifica. per mandare a capo inserendo una nuova riga. variabile. Per indicare questa differenza. 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). Ad esempio: ..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. scanf La funzone scanf è simile alla printf nella forma: scanf(stringa. printf("prima riga ... Se vengono specificati più valori nella stringa di controllo. produrrà a video la stampa di due righe.\n"). senza modificarlo.\n"). con il cursore poi sulla terza riga: prima riga . si antepone al nome della variabile un &. . si presuppone che questi vengano immessi da tastiera in modo separato.... La seconda differenza è che relativa alla stringa di controllo. usando come .) In questo caso la stringa di controllo specifica come i caratteri immessi dall'utente mediante la tastiera debbano essere convertiti e memorizzati nelle variabili... ad esempio il valore di una variabile. seconda riga . variabile.. ci sono però alcune differenze significative: La prima è che mentre la funzione printf valuta il valore di un'espressione. la funzione scanf deve modificare il valore di una variabile per memorizzarci il valore appena acquisito...

h> che deve essere indicata prima del main.&i. Questo significa che per inserire tre interi è possibile digitare i dati in una delle due forme: 345 oppure 3 4 5 Per esempio. celsius. fahr). l'<Invio> o il tabulatore.&j). Librerie standard Per potre utilizzare le funzioni printf e scanf è necessario includere la libreria standard del C che le definisce.h> main() { float celsius. qualsiasi esso sia. È richiesta quindi la direttiva: #include <stdio. la compilazione del programma non va a buon fine in quanto viene segnalato errore di "funzione sconosciuta" in corrispondenza delle printf e scanf. 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.. printf("Inserisci la temperatura in gradi Celsius: "). I due valori possono essere inseriti separati da uno spazio (o un numero qualsivoglia di spazi) oppure andando a capo ogni volta. l'istruzione: scanf("%d %d". In alcuni ambienti di programmazione le librerie vengono incluse automaticamente e la direttiva risulta non necessaria.. acquisisce due numeri interi e li memorizza rispettivamente nelle variabili i e j. Riassumendo . fahr = (celsius * 9)/5 + 32. Unica eccezione è il caso di %c: viene acquisito un carattere.separatore lo spazio. printf("%g gradi Celsius = %g gradi Fahrenheit\n". scanf("%g". } . fahr. &celsius). quindi qualsiasi tasto venga premuto sulla tastiera. Se l'ambiente però non effettua l'inclusione in modo autonomo. quello è l'unico carattere acquisito.. È quindi buona norma utilizzare sempre la direttiva indicata.

che permette di scegliere quale istruzione eseguire tra quelle che la seguono. La clausola else è opzionale. else if (a%2) printf("numero dispari\n").. sono istruzioni seguite da un punto e virgola. forme prefisse o postfisse di ++ e --. se vi è una clausola else.. altrimenti (valore uguale a 0 . else printf("numero pari\n"). x <= y non ha senso. perchè esso viene visto come un'unica istruzione... i < h. if (a==0) printf("numero nullo\n"). chiamate a funzione (che restituiscano un valore o meno). i++) . si passa all'istruzione istruzione2. È possibile costruire una sequenza di test collegando un altro if alla clausola else dell'if precedente. come i++. Non tutte le espressioni possono diventare istruzioni. ad esempio. o la chiamata di una funzione.. seppur composita. Di fatto anche il punto e virgola da solo è un'istruzione che non fa nulla (istruzione vuota). Un blocco di istruzioni può essere utilizzato in tutti quei punti in cui è ammessa la presenza di un'istruzione singola..falso). /* viene riempito l'array ed h è il numero di elementi immessi effettivamente */ if (h > 1) for(i=0.. somma = 0. che indica la fine dell'istruzione. . tra i quali ce n'è uno sprovvisto di clausola else. . float somma. Si consideri il seguente stralcio di codice: . int numeri[N]. h. allora si passa ad eseguire istruzione1. perchè.. Le parentesi graffe { e } racchiudono un insieme di zero o più istruzioni per costituirne un blocco. if-else La più semplice forma di controllo condizionale è costituita dall'istruzione if.. La sintassi è: if (espressione) istruzione1 else istruzione2 Viene valutata l'espressione: se il suo valore è diverso da 0 (spesso uguale a 1 -vero). i. 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=). È necessario fare attenzione quando si concatenano più if. .Le istruzioni e i blocchi Le istruzioni di espressione.0.

i < h.. se esiste. le istruzioni successive vengono eseguite una per volta. else /* ATTENZIONE!!!! */ somma = numeri[0]. sono delle costanti intere. i < h. 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. i++) if (numeri[i] > 0) somma += numeri[i].... else somma = numeri[0]. ... La clausola else risulta essere collegata all'ultimo if che non possiede la clausola. switch Il costrutto switch consente di trasferire il controllo di esecuzione all'interno di un blocco di istruzioni. i++) if (numeri[i] > 0) somma += numeri[i]. altrimenti si salta tutta l'istruzione switch.. if (h > 1){ for(i=0. .. anche se sono associate ad un'altra etichetta case. Il punto scelto è determinato dal risultato della valutazione di una espressione intera. ma è solo un effetto dell'indentazione (aspetto ignorato dal compilatore). default: istruzioni } I valori n... Per poter collegare la clausola else all'ultimo if è necessario utilizzare le parentesi graffe per creare blocchi: ... La clausola else sembra essere collegata al controllo sulla lunghezza dell'array.. allora il controllo viene trasferito alla prima istruzione dopo l'etichetta default.. .. m. in cui sia presente un'etichetta. Una volta trasferito il controllo alla prima istruzione di quelle che seguono l'etichetta case che combacia.if (numeri[i] > 0) somma += numeri[i]. La forma generale del costrutto è: switch (espressione) { case n: istruzioni case m: istruzioni case h: istruzioni . . if (h > 1) for(i=0. Se non vi è alcuna etichetta che combacia. Un'etichetta case o default non spinge ad uscire dallo switch. Se si desidera .. } else somma = numeri[0].. Il codice precedente è in realtà equivalente il seguente: .

l'espressione viene valutata .arrestare l'esecuzione delle istruzioni all'interno del blocco switch è necessario utilizzare l'istruzione break. case '7': val++. Nella stragrande maggioranza dei casi l'istruzione break è necessaria per ottenere il comportamento desiderato. val = 0. case '8': val++. while e do-while Una struttura di iterazione consente di specificare un'azione. case '2': val++.. e ci può essere al più una sola etichetta default. Si parla in questi casi di ciclo. case '3': val++. l'istruzione break trasferisce il controllo all'esterno del blocco. Nello stralcio di codice sottostante è riportato un esempio di utilizzo dell'istruzione switch senza il break. Il ciclo while è generalmente strutturato come segue: while (espressione) istruzione L'espressione viene valutata e. ogni valore associato alle etichette case deve essere unico. printf("carattere %c non appartenente all'alfabeto base16\n". alla prima istruzione che segue lo switch . case '6': val++. All'interno di un blocco switch. case '4': val++. } L'espressione di switch deve essere di tipo char o int. break. ch). case 'b': case 'B': val++. se ha valore diverso da 0 (vero) viene eseguita l'istruzione successiva (che può anche essere un intero blocco di istruzioni. che dovrà essere ripetuta più volte. opportunamente delimitato dalle parentesi graffe). switch(ch){ case 'f': case 'F': val++. case 'e': case 'E': val++. default: val = -1. Tutte le etichette case devono essere espressioni costanti. Una volta che quest'ultima è terminata. case '9': val++. case 'a': case 'A': val++. case '1': val++. . case 'c': case 'C': val++. o un insieme di azioni.. case 'd': case 'D': val++. case '5': val++. In tutte le istruzioni switch singole.

Successivamente ad essa. num--. Questo modo di procedere è praticamente equivalente a: { espressione-iniziale. Il ciclo si ripete finché non si valuta come falsa l'espressione del ciclo .nuovamente e se è nuovamente vera. viene valutata l'espressione del ciclo e. while(num > 0){ printf("*"). prima di qualsiasi altra operazione. poiché l'espressione potrebbe essere falsa già la prima volta. espressione-booleana. . si ripete l'istruzione. Al termine dell'esecuzione del corpo del ciclo. }while(i<0 || i > 15). si valuta nuovamente l'espressione del ciclo e cosi via. e viene eseguita una volta sola. ossia un ciclo a condizione finale. In questo caso. Ciò si ripete fino a quando l'espressione ha valore 0 (falso). num). viene valutata l'espressione-incremento. Fino a quando l'espressione è vera. l'espressione viene valutata al termine dell'esecuzione dell'istruzione (o del blocco di istruzioni). Ecco due esempi di utilizzo dei costrutti di ciclo: printf("Inserire un intero positivo\n"). A volte si desidera eseguire il corpo di un ciclo almeno una volta. Si tratta di un ciclo a condizione iniziale: prima di eseguire il ciclo si valuta la condizione. se questa ha valore diverso da 0 . Quindi. viene eseguita l'istruzione che costituisce il corpo del ciclo. di solito per poter aggiornare i valori delle variabili di ciclo. In tal caso si utilizza un ciclo dowhile. } do{ printf("Inserisci un intero compreso tra 0 e 15.&num). La sintassi è la seguente: for (espressione-iniziale. espressione-incremento) istruzione L'espressione-iniziale permette di inizializzare le variabili di ciclo.è vera -. scanf("%d". nel qual caso il controllo si trasferisce all'istruzione successiva al while.valore 0-. for L'istruzione for è utilizzata per effettuare un ciclo dal principio sino alla fine di un intervallo di valori. La struttura è la seguente: do istruzione while (espressione). Un ciclo while può essere eseguito 0 o più volte. l'istruzione viene ripetuta. inclusi:"). scanf("%d" .

j >= 0. invece. sarebbe appropriato scrivere del codice come: for (i = 0. i++. j = NumElem . o anche da un for. Ad esempio. quella che viene valutata per prima è l'espressione-incremento e solo dopo si valuta l'espressione del ciclo.. continue Un'istruzione continue può essere utilizzata solamente all'interno di un ciclo (for. Ad esempio: while(valore != -1){ if(valore == 0) continue. } } se non fosse che si eseguirebbe sempre l'espressione-incremento se venisse incontrata un'istruzione continue all'intemo del corpo del ciclo. da sinistra a destra. &valore). In un ciclo for. L'istruzione si presenta come: continue.while(espressione-booleana) { istruzione espressione-incremento. . */ } break Un'istruzione break può essere utilizzata per uscire da qualunque blocco di codice appartenente ad un ciclo. while o do-while. per poter far procedere due indici in direzioni opposte. controllato da uno switch. } return Un'istruzione return termina l'esecuzione di un sottoprogramma e restituisce il controllo al chiamante. come molti operatori. 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.. ciò spinge a valutare l'espressione del ciclo immediatamente successivo. j--) { /* . scanf("%d". Il valore dell'espressione nell'istruzione return viene restituito al chiamate come valore restituito dal sottoprogramma (funzione). L'istruzione si presenta come: break. Tali espressioni vengono valutate. while oppure dowhile) e trasfetisce il controllo alla fine del corpo del ciclo. Un'istruzione continue viene spesso utilizzata per saltare una parte del blocco di istruzioni sul quale si sta svolgendo il ciclo. Nel caso di cicli while e do-while.1. /* non considerare il valore 0 */ cont++. La parte di inizializzazione e quella di iterazione di un ciclo for possono essere costituite da elenchi di espressioni separate da virgole.

al fine di caratterizzare il termine del programma. l'istruzione return deve comprendere anche un'espressione di un tipo che possa essere assegnato al tipo da restituire. while(Frase[i] != '\0') i++. e questo ha senso solo per sottoprogrammi di tipo void. /* anche return(i). exit L'istruzione exit termina l'esecuzione dell'intero programma. se si termina l'esecuzione a causa di una qualche condizione di anomalia. arbitrariamente legato alla particolare situazione di anomalia: exit 1. ed in tali casi l'istruzinoe può essere omessa completamente. si restituirà. che però restituisce il controllo al programma chiamante del programma principale. I sottoprogrammi Un sottopramma è un insieme di istruzioni identificate mediante un nome. dunque termina l'esecuzione del programma.subroutine . ai secondi il nome di procedure (o subroutine). ad esempio. in caso contrario. In termini generali ci sono due tipi di sottoprogrammi: quelli che restituiscono un valore al (sotto)programma chiamante. che consente di far comunicare il sottoprogramma con il (sotto)programma chiamante. i=0. Ad esempio: int Lunghezza(char Frase[]) { int i. si restituirà un valore diverso. return i. il valore 0. Ai primi si dà il nome di funzioni.allora si può scrivere semplicemente: return. Se il sottoprogramma ha specificato un tipo per il valore da restituire . Nel caso in cui il programma arrivi al termine perch� è concluso il flusso di elaborazione senza l'insorgere di alcun problema.int main .funzione -.void main. */ } L'espressione dell'istruzione return può essere omessa. Se il programma principale non ha specificato un tipo di dato da restituire . svolge l'elaborazione richiesta ed eventualmente restituisce un . Si tratta di un'istruzione simile all'istruzione return. il programma main restituisce un intero . ed accessibile tramite un'interfaccia.allora si scriverà semplicemente: exit. In alternativa. In tal caso.Se il sottoprogramma non restituisce alcun valore . il controllo viene restituito al chiamante in corrispondenza del termine del sottoprogramma stesso. Quindi possiamo vedere un sottoprogramma come un ambiente che (eventualmente) riceve delle informazioni. e quelli che non lo fanno.

sottrazione\n"). Si supponga di voler scrivere una funzione che scriva a video un insieme di direttive per l'utente.. addizione\n"). 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. Il vantaggio di questa soluzione è che la porzione di codice può essere utilizzata più volte semplicemente scrivendone il nome.. &scelta). moltiplicazione\n"). presupponendo che questa resituisca o meno un valore in base alle specifiche ed al comportamento che si desidera ottenere. In C solitamente questa distinzione viene persa per ciò che concerne la terminologia.. . divisione\n").valore al (sotto)programma che lo ha chiamato. Una funzione è una porzione di codice (sequenza di istruzioni) che vengono raggruppati e a cui viene dato un nome.. Graficamente le due tipologie di sottoprogrammi sono riportate nella figura seguente. e ci si riferisce più semplicemente ai sottoprogrammi con il termine funzione. 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: . scanf("%d". printf("4. con l'esecuzione dell'istruzione return o con il termine del corpo del programma. printf("2. printf("3. printf("Scegli la voce del menù:\n"). come fatto nello stralcio di codice riportato: . printf("1. Quando quest'ultimo termina la propria esecuzione. che costituiscono il menù del programma. il programma chiamante continua l'esecuzione del codice che segue la chiamata al sottoprogramma.

Una funzione è una sottounità di un programma completo: il main stesso è una funzione. divisione\n").. A parte il semplice esempio. num_stampe++. printf("xxxxxxx"). ed il programma chiamante non ne ha alcuna visibilità. Le variabili dichiarate all'interno di una funzione sono significative e visibili esclusivamente all'interno della funzione stessa. Quindi anche una funzione avrà in generale delle variabili che verranno utilizzate al suo interno per effettuare le computazioni desiderate. . } A questo punto è possibile utilizzare la funzione chiamandola: void main() { menu(). addizione\n"). printf("3.. l'istruzione menu(). &scelta). le funzioni hanno lo scopo di rendere un lungo programma come una collezione di porzioni di codice separate su cui lavorare in modo isolato. } void main() { contastampe().. } Nel programma. printf("1. . moltiplicazione\n"). num_stampe = 0. printf("2. di più facile soluzione. printf("4. chiamata dal sistema. è equivalente ad aver scritto direttamente tutte le istruzioni della funzione stessa.. Si tratta di variabili locali alla funzione stessa. suddividendo la soluzione di un problema grosso in tanti piccoli sottoproblemi. 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.menu() { printf("Scegli la voce del menù:\n"). . sottrazione\n"). contastampe(). scanf("%d". 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.

È anche possibile far assumere ai parametri il risultato di un espressione.. int b) { int risultato. come ad esempio: somma(x+2. I parametri vengono elencati nelle parentesi tonde che seguono il nome della funzione. chiamate parametri che vengono utilizzati per passare i valori alla funzione.. 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. y). 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. La differenza fondamentale tra i parametri e le variabili è che i primi hanno un valore iniziale quando la funzione viene eseguita. entrambi di tipo intero.} Ad ogni chiamata della funzione contastampe() la variabile num_stampe viene ricreata e distrutta al termine.si noti che non devono essere definite due variabili locali a e b. Inoltre. In modo duale è anche necessario o possibile trasmettere al programma chiamante il valore calcolato dalla funzione. Per esempio: somma(int a. La lista dei parametri ha come separatore la virgola. risultato = a + b. . mentre le variabili devono essere inizializzate. Quindi. nel corpo della funzione. A questo scopo vengono definite variabili speciali. se la funzione deve svolgere un'elaborazione dei dati del programma chiamante è necessario che i dati le vengano passati. questi a e b non hanno nulla a che fare con altre variabili a e b dichiarate in altre funzioni. in cui a assume il valore di x e b di y. indicando il tipo ed il nome di ogni parametro. In modo analogo. La via più semplice è la restituzione del valore attraverso il nome della . è una chiamata alla funzione somma in cui il parametro a vale 1 e b vale 2. 2). I parametri a e b vengono utilizzati all'interno della funzione come normali variabili . La variabile risultato è dichiarata localmente alla funzione. } Questo codice definisce una funzione chiamata somma con due parametri a e b. z*10). Più semplicemente si può fissare il valore di un parametro al valore di una variabile: somma(x. somma(l.

return (risultato). int b) { . si scriverà dunque: int sum(int a. ma ciò non consente di restituire più di un valore in quanto quando si esegue la prima return la funzione termina.funzione. la situazione tra ingressi ed uscite di una funzione non è uguale: è possibile passare un numero di parametri d'ingresso qualsiasi. 2). Questa istruzione può essere posizionata in qualunque punto della funzione. il tipo restituito è un intero. È necessario aggiungere un'informazione relativa al tipo di dato che la funzione restituisce. void è un tipo standard ANSI C. il tipo indicato è void.. Per riassumere. tuttavia un'istruzione return causa il termine della funzione e restituisce il controllo al programma chiamante. ossia è come se il nome della funzione fosse una variabile dotata di un valore. Si noti che una funzione può avere quante istruzioni return si desidera. istruzione che somma 1 a 2 e memorizza il risultato nella variabile r.. } La chiamata assume quindi la seguente forma: r = somma(1. Ovviamente. Sempre con riferimento alla funzione somma. una funzione ha la seguente forma sintattica: tipo_restituito NomeFunzione(lista tipo-nome) { istruzioni } Nel caso in cui una funzione non restituisca alcun parametro. La funzione void menu() è una funzione senza paramtri d'ingresso e che non restituisce alcun valore. che indica appunto tale eventualità. Il valore viene restituito per mezzo della seguente istruzione: return(value). mentre la funzione può restituire mediante l'istruzione return un singolo valore. } La funzione completa è: int sum(int a. (Per poter restituire più dati è necessario utilizzare un passaggio dell'indirizzo come discusso nella Lezione 11). int b) { int risultato. . risultato = a + b.

è il fatto che nel codice del programma chiamante. allo scopo di focalizzare l'attenzione su alcuni aspetti fondamentali del passaggio dei parametri. resto = ???. r1 = a % b. facendo sempre in modo che una funzione sia sempre definita prima che qualche altra la chiami. non si saprebbe come ricevere i due valori interi restituit. risultato = ris_resto(val1. In alternativa. int b) { int r1. la soluzione poù pulita è dichiarare la funzione prima del main. l'istruzione return consete di restituire una sola informazione. Per esempio: int somma(). Una possibilità è scrivere la definizione della funzione quindi prima del main().. 2. return(b).. Funzioni e prototipi Dove va scritta la definizione di una funzione. r1 = a/b. Queste osservazioni ci permettono di dire che NON potremo scrivere i seguenti stralci di codice: int ris_resto(int a. } Ad evidenziare ulteriormente ciò che comunque dovrebbe essere di immediata comprensione. 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. r2. stampare a video NON significa restituire. Per il momento ci si limita a capire il problema...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. nella fattispecie un solo numero intero. una volta eseguita un'istruzione return il codice successivo non verrà senz'altro eseguito. . resto. int risultato.. Con questa soluzione è però necessario prestare molta attenzione all'ordine con cui si scrivono le funzioni. 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: . . return(a). 3. b) } int ris_resto(int a. r2.. return(a. senza trovare una soluzione. r1 = a/b. r1 = a % b. val2). int b) { int r1. separatamente da dove viene poi definita. . Si considerino i seguenti aspetti: 1.

Per riassumere. Una dichiarazione valida è la seguente: int numeri[6]. ai quali si accede singolarmente mediante un indice che ne individua la posizione all'interno dell'array. Gli array Le variabili semplici. La dimensione è fissata al momento della sua creazione . ossia il numero di elementi che lo compongono. e nell'esempio utilizzato è pari a N..e non può essere mai essere variata. Per quanto riguarda i parametri ricevuti in ingresso è necessario (ANSI C) dichiararne la tipologia ma non il nome. Ogni elemento contiene un unico dato ed è individuato mediante un indice: l'indice del primo elemento dell'array è 0.vettore . è errato scrivere: . Tra parentesi quadre è necessario indicare SEMPRE un'espressione che sia un valore intero costante.void main() { . ovvero una collezione di variabili dello stesso tipo. un array è una struttura di dati composta da un numero determinato di elementi dello stesso tipo. Il numero complessivo degli elementi dell'array viene detto dimensione. che costituisce una variabile strutturata. In base a quanto detto. Array monodimensionali Intuitivamente un array monodimensionale . numeri[0] numeri[1] numeri[2] numeri[3] numeri[4] numeri[5] Viene indicato. l''ultimo elemento di un array di N elementi ha indice N-1. come sempre.in corrispondenza alla dichiarazione . così come per ogni variabile semplice (o non strutturata) è necessario definire il tipo di dati. sono utili ma spesso insufficienti per numerose applicazioni. Per ogni array. int). ciascuno dei quali accessibile in modo indipendente. 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. } 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. come mostrato nel seguente esempio: int restodivisione(int. inoltre è necessario specificarne la dimensione. L'array costituisce una tipologia di dati strutturata e statica . da identificare con nomi diversi: definire un array..può essere utilizzato come un contenitore suddiviso in elementi. 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. capaci di contenere un solo valore.

La specifica di un array bidimensionale prevede l'indicazione del tipo di dati contenuti nell'array. organizzati su 4 righe e 6 colonne. Ad esempio: /* Inizializzazione di un array di numeri interi al valore nullo */ for (i = 0. quindi anche per gli elementi di un array è opportuno prima assegnare un valore e poi leggerlo. 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. quanto un errore semantico (non sempre di facile individuazione). 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. trattata in seguito (Lezione 14). Nel caso in cui non sia possibile stabilire in alcun modo un limite al numero di elementi senza violare i requisiti di generalità. ad esempio. come matrici. a meno che non si sia alla ricerca di valori casuali. la dichiarazione che segue specifica un array bidimensionale di numeri reali. numero di righe e numero di colonne. i++) numeri[i] = 0. Array bidimensionali Gli array bidimensionali sono organizzati per righe e per colonne. il contenuto di una variabile prima che le venga assegnato un valore è ignoto. Ad esempio. livelli[0][0] livelli[0][1] livelli[0][2] livelli[0][3] livelli[0][4] livelli[0][5] . Come per le variabili non strutturate. semplicemente utilizzando un indice che viene modificato ad ogni iterazione. la specifica dell'algoritmo dovrà quindi prevedere o l'esatto numero di dati da gestire oppure un valore massimo di dati. numeri[i]. Accedere all'elemento di indice 6 (o superiore) non causa un errore di sintassi. int i. del nome e delle due dimensioni. Spesso l'array viene utilizzato all'interno di iterazioni per accedere uno dopo l'altro ai suoi elementi. per un totale di 24 elementi: float livelli[4][6].int numeri[]. Per accedere al singolo elemento dell'array è necessario indicare il nome dell'array e l'indice dell'elemento posto tra parentesi quadre. i < 6. L'indice i inizializzato a zero consente di accedere dal principio al primo elemento dell'array e di proseguire fino all'ultimo. sarà necessario utilizzare una struttura dati diversa e più precisamente una struttura dinamica. In generale il singolo elemento di un array può essere utilizzato come una semplice variabile. racchiusa ciascuna tra parentesi quadre. Nel caso in cui sia dunque richiesto l'utilizzo di un array. con indice 5 (si noti che quando l'indice è pari a 6 la condizione è falsa ed il corpo del ciclo non viene eseguito).

. . e lasciando quindi di fatto accessibile al sottoprogramma l'array con la possibilità di modificarne il contenuto. /* Acquisizione da tastiera dei valori della matrice livelli[4][6] */ for (i = 0.){ .. Ogniqualvolta si passa un array pluridimensionale è necessario indicare . Ne consegue che per un array monodimensionale la dichiarazione dell'array come parametro viene fatta così: void funzionex(int numeri[]. quindi è più veloce accedere per righe ai dati memorizzati. j). Per questo motivo è necessario prestare estrema attenzione durante l'accesso agli elementi dell'array. i < 4. . in cui si accede riga per riga ad ogni elemento dell'array. quello più interno le colonne. mediante due cicli annidati. &livelli[i][j]).. ad esempio: livelli[2][4] Come esempio di scansione degli elementi di un array bidimensionale si consideri lo stralcio di codice qui riportato. o più comunemente i bidimensionali trattati in precedenza. Passaggio a sottoprogrammi Il passaggio di array a sottoprogrammi viene sempre fatto per riferimento. specificando gli indici della riga e della colonna dell'elemento di interesse. Il ciclo più esterno scandisce le righe.){ . } Approfondimento Gli elementi vengono memorizzati per righe. j++){ printf("Inserisci l'elemento riga %d colonna %d: "..tra parentesi quadre tutte le dimensioni dell'array ad eccezione della prima... i. passando per valore l'indirizzo dell'array. scanf("%f". la dichiarazione deve essere fatta come segue: void funzioney(int livelli[][6]. i++) for(j = 0.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.. } Nel caso di array pluridimensionali. } .. j < 6.

In tal caso. imin.. imin=0. int num_elem){ . mediante ulteriori parametri. anche tenendo presente che è spesso impossibile distingere in base al contenuto dell'array quali elementi siano "validi" e quali non lo siano. si scriverà il codice seguente: int MinArrayInt(int v[]){ int i. i++) if(v[i] < v[imin]) imin = i. indipendentemente dalla dimensione dell'array utilizzato. 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[]. In primo luogo ad un sottoprogramma potrebbe venir passato solo una porzione ridotta dell'intero array. int num_colonne){ . for(i=1. Nel primo caso. int num_righe. o il numero di righe e di colonne in array bidimensionali. i++) if(v[i] < v[imin]) imin = i. imin. spesso si deve sovradimensionare l'array onde evitare di avere problemi di memoria. i < N. return(imin). Si ricordi infatti che dovendo specificare a priori la dimensione di un array in fase di dichiarazione. il . } Nel caso si decida di optare per la soluzione che non prevede il passaggio anche del numero di elementi dell'array.. 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. return(imin). imin=0. e fanno concludere che la prima soluzione costituisce un codice migliore.. il numero di elementi dell'array monodimensionale. In tal senso. è buona norma. } È possibile fare alcune considerazioni che motivano l'opportunità di passare ad un sottoprogramma anche le dimensioni dell'array. i < l. int l){ int i.. Eccole. } void funzioney(int livelli[][6]. for(i=1. quando si passa l'array come parametro è importante specificare le dimensioni su cui lavorare. passare sempre. } Si consideri a titolo di esempio. come mostrato nei seguenti stralci di codice: void funzionex(int numeri[].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. Non necessariamente tutti gli elementi verrranno quindi utilizzati.

affinchè possa scrivere la funzione deve concordare i parametri che riceve in ingresso. sarà necessario accertarsi che l'array sia effettivamente di dimensione N. Il vantaggio di questo approccio è che è possibile accedere al valore memorizzato mediante il suo nome. Prima di poter utilizzare un puntatore è necessario discutere del significato di due operatori: & e *.ad eccezione della prima! Infine. non al tipo. Si noti che l'asterisco si applica alla singola variabile. q. Questa considerazione purtroppo non è universale. L'introduzione del carattere * davanti al nome della variabile indica che si tratta di un puntatore del tipo dichiarato. . La seguente istruzione salva il valore 10 nell'area di memoria identificata dal nome x: x =10. in quanto anch'esso è una variabile. e comunque. dichiara un puntatore ad un intero (variabile p) ed un intero (variabile q). Un puntatore è una variabile che memorizza l'indirizzo di una locazione di memoria. La dichiarazione precedente riserva un'area di memoria che viene individuata dal nome x. Un puntatore deve essere dichiarato come qualsiasi altra variabile. sarà necessario scrivere funzioni diverse. Quindi: int *p . La prima soluzione costituisce dunque una soluzione piu' flessibile e riutilizzabile.sottoprogramma funziona perfettamente così come è. la dichiarazione specifica una variabile puntatore ad un intero. 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. un'ultima considerazione è legata allo sviluppo di programmi complessi da parte di più persone. Per esempio: int *p. 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 . chiamato indirizzo della locazione di memoria. int x. e nel caso in cui ci siano array di dimensione diverse. La persona che deve scrivere la funzione MinArrayInt. dovrebbe chiedere l'informazione a chi realizzerà la funzione chiamante. se non dovesse ricevere anche l'intero che rappresenta la dimensione dell'array. Nella seconda soluzione invece. 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. Ha quindi bisogno dell'informazione circa la dimensione dell'array: vale quindi la pena pensare. l'indirizzo di una variabile.

a è un puntatore ad intero. Si noti che a è un int e p è un puntatore ad un int quindi a = p. b = 10. p punta a q. c. Dopo questa operazione. ad una variabile. mentre la variabile p punta alla variabile q. mentre b e c sono interi. p = &q. L'unico assegnamento sensato tra un int ed un puntatore ad un int è: a = *p. Si consideri lo stralcio di codice seguente: int *a . a = &b. quindi viene memorizzato in c il valore di b (10). Per ottenere l'indirizzo di una variabile utilizzare & davanti al nome. q.*/ n = q. La prima istruzione salva il valore 10 nella variabile b.. Per ottenere il valore di una variabile utilizzare * di fronte al nome del puntatore. La seconda istruzione copia il valore di q nella variabile n. e *p restituisce il valore memorizzato nella variabile a cui punta p. 3. o punta. In modo analogo: a = &p.L'operatore & restituisce l'indirizzo di una variabile. cerca di memorizzare l'indirizzo di un puntatore in un int ed è altrettanto errato. /* -1. Per dichiarare un puntatore mettere * davanti al nome. . n. Infine. /* -2. . L'operatore * viene chiamato operatore di derefereziazione. 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. Si consideri lo stralcio di codice seguente: int *p . Per riassumere: 1. La seconda istruzione (a = &b) salva in a il valore dell'indirizzo della variabile a. b . l'istruzione c = *a memorizza il valore della variabile puntata da a (ossia b) in c. 2.*/ L'effetto dell'istruzione è memorizzare l'indirizzo della variabile q nella variabile p. c = *a. Delle tre variabili dichiarate. Dopo questa istruzione a punta a b.. è un errore perchè cerca di memorizzare un indirizzo in un int.

{ int temp. int *b). a = b. y). b = temp.. int b) { int temp. y). b) che scambia il contenuto di a e b. *b = temp. /* 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. x = 18. la funzione corretta è dunque: int scambia(int *a .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. otterremmo un risultato diverso da quello desiderato: . scambia(x. printf("funz2: var1 = %d var2 = %d\n". . *b). b). y = 22. printf("prima: var1 = %d var2 = %d\n". x. printf("funz: var1 = %d var2 = %d\n". void scambia(int a . *a = *b. x.. . a. printf("dopo: var1 = %d var2 = %d\n". 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.. y). Il passaggio dei parametri in C viene fatto sempre per valore. In linea di massima l'operazione dovrebbe essere semplice: scrivere una funzione scambia(a.. Alla funzione vengono quindi passati gli indirizzi delle variabili. Quindi se si considera lo stralcio di codice seguente che effettua la chiamata alla funzione scritta. ossia il loro indirizzo cosicchè la funzione possa accedere direttamente alla memoria (tramite appunto l'indirizzo) e modificarne il valore. y. Infatti *a è il contenuto della variabile a cui punta a. temp = a. *a. temp = *a. int x. /* solo per debug*/ } Questo non va. facendo una copia del valore della variabile che viene passata e su questa copia agisce la funzione.

x. .. 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. &a).. 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). 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. Più precisamente. In modo analogo *(a + 1) è uguale a a[1] e così via. x = 18. y). int x. il codice è il seguente: . y = 22. x. &y). quindi viene chiamata con scanf("%d". La regola è che ogniqualvolta si passa ad una funzione una variabile il cui contenuto deve essere modificato è necessario passare l'indirizzo della variabile. printf("prima: var1 = %d var2 = %d\n". y. La funzione printf non modifica il valore dei suoi parametri.Naturalmente anche la chiamata della funzione deve essere adattata: è necessario passare due indirizzi e non più due interi. A video si ottiene: prima: var1 = 18 var2 = 22 funz: var1 = 22 var2 = 18 dopo: var1 = 22 var2 = 18 che è ciò che si desidera.. 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. printf("dopo: var1 = %d var2 = %d\n". Quando si scrive un'espresisone come a[i] questa viene convertita in un'espressione a puntatori che restituisce il valore dell'elemento appropriato. scambia(&x.. . per memorizzarci quello appena acquisito. 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]. y). quindi viene chiamata con printf("%d". Quindi. a[i] è equivalente a *(a + i) ossia il valore a cui punta a + i. Infatti a equivale a &a[0]. a) ma la funzione scanf modifica il valore della variabile.

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]. Quindi se si dichiare un array di int e si somma uno al puntatore all'array. un float quattro. i< n . Un ulteriore punto legato a array e funzioni è il passaggio di un array ad una funzione: di default si passa il puntatore all'array. sommare uno ad un puntatore ad array significa farlo passare al successivo elemento.. /* 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. È possibile utilizzare gli operatori di incremento e decremento (++ e --) con i puntatori. riempirandom(numeri. L'accesso agli array mediante indici è equivalente all'aritmetica dei puntatori. } void riempirandom(int a[] . il numero degli elementi presenti). che è . ++pa) *pa = rand()%n + 1. ciò che si desidera. i.dopotutto. ossia a == &a[0] e *a == a[0]. NMAX). in linea di massima..La possibilità di sommare 1 ad un puntatore può essere interessante ma è necessario capire esattamente cosa succede. D'altra parte. 2. In altre parole. Si consideri il seguente esempio. int n) . for (i = 0. ossia a+i = &a[i] e *(a+i) == a[i]. ma non con il nome dell'array perchè quello è un puntatore costante e non può essere modificato. . Il nome di un array è un puntatore costante al primo elemento dell'array. for (i = 0. i++) a[i] = rand()%n + 1. cosa che provoca un errore durante l'esecuzione quando si tenta di scriverci un valore! Si può scrivere anche: void riempirandom(int *pa . Ciò che è sufficiente ricordare è che l'aritmetica dei puntatori si basa su unità del tipo di dati trattato. /* rand()%n + 1 genera il valore casuale */ } oppure.. in generale un int occupa due byte. . Ad esempio. questo si muove di quattro byte. int n) { int i. int n) { int i.. 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. se si dichiara un array di float e si somma uno al puntatore all'array. i< n . il puntatore si muoverà di due byte. Per riassumere: 1. usando l'aritmetica dei puntatori: void riempirandom(int *pa . ++i.

È possibile accedere ad ogni elemento dell'array singolarmente. che realizzano operazioni di frequente utilizzo. La caratteristica rilevante è che ad indicare il termine della sequenza di caratteri c'è un carattere terminatore: '\0'.h . Si consideri la seguente dichiarazione di un array di caratteri di 20 elementi: char Vocabolo[20]. come si fa per ogni altro tipo di array. char... Inoltre. Quando si parla di caratteri. in quanto costituiscono nell'insieme un vocabolo o un'intera frase (con i debiti spazi). ++pa. In genere una collezione di numeri interi è comoda per tenere uniti tutti i dati.'z' 'A'.. ancora void riempirandom(int *pa . che però hanno un significato proprio. è possibile avere ulteriori vantaggi dovuti all'esistenza del terminatore ed alla sua interpretazione.h sono diponibili un certo insieme di funzioni per la manipolazione delle stringhe. . ++i) *(pa+i)=rand()%n+1. che ci sia stato messo dalle funzioni di manipolazione della stringa. int n) { int i... for(i = 0.con funzioni di utilità di frequente utilizzo. invece. Il C consente quindi di interpretare una sequenza di caratteri come una singola unità. #define DIM 20 char Vocabolo[DIM+1]. se un algoritmo prevede che si debba gestire vocaboli "di al più 20 caratteri" è necessario dichiarare un array di 21 elementi. i< n .'9'). Ad esempio. /* rand()%n + 1 genera il valore casuale */ } Le stringhe È possibile definire array per gestire qualsiasi tipo di dato semplice. Tra queste c'è ad esempio la funzione int strlen(char[]) . Una stringa può includere caratteri alfanumerici ('a'. i< n . 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. int. /* rand()%n + 1 genera il valore casuale */ } oppure.. -. for( .'Z' '0'.... È importante ricordarsi di dimensionare opportunamente l'array includendo un elemento anche per contenere il terminatore. oppure direttamente da programma durante l'esecuzione. $ ed altri. È inoltre possibile manipolare l'intero array come un'unica entità purchè esista un carattere terminatore '\0' in uno degli elementi dell'array. float. ++i) *(pa)=rand()%n+1. caratteri speciali come +.{ int i. In C una stringa è un array di caratteri e come tale ne eredita le proprietà ed il comportamento di base. può essere interessante poter manipolare l'intero insieme di caratteri appartenenti ad un array. per una più efficiente manipolazione e vi è una apposita libreria standard string.

char[]) confronta due stringhe e restituisce 0 se il loro contenuto è identico. Si può però scrivere: strcopy(a. direttamente. L'array costituisce la soluzione in numerosi casi ma non tutti. Utilizza dei tipi di dati semplici. mediante un'espressione del tipo: char a[l0]."prova") Le strutture L'array è un esempio di struttura dati. char o double e li organizza in un array lineare di elementi. La parte di codice non copia ordinatamente i caratteri presenti nell'array a nei caratteri dell'array b. Ci sono numerose altre funzioni. in quanto c'è la restrizione che tutti i suoi elementi siano dello stesso tipo. Non è possibile scrivere: a = "prova". Ciò che viene effettivamente fatto è far in modo che b punti allo stesso insieme di caratteri di a senza farne una copia. Si potrebbe pensare che fosse possibile assegnare una stringa ad un'altra. a). Questo viene sempicemente realizzato contando il numero di caratteri dal primo fino al carattere terminatore. b[10]. char a[l0]. b = a. Quindi. come int. 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[]. Si . La funzione int strcmp(char[]. printf("inizio a: %s <> b: %s\n" a). perchè a indica l'inizio dell'array (è un puntatore) mentre "prova" è una stringa costante.In alcuni casi è però necessario poter gestire all'interno della propria struttura un mix di dati di tipo diverso.char[]) che effettivamente effettua la copia elemento ad elemento dell'array a nell'array b> fino al carattere terminatore. scanf("%s". Il codice seguente esemplifica quanto detto. Questa realtà ha effetto anche sulla inizializzazione degli array. Infatti il confronto a == b darebbe esito positivo solamente se i due array puntassero allo stesso insieme di caratteri. tra cui citiamo solo l'importate funzione di confronto tra stringhe. i=i+2) b[i] = '-'.che restituisce il numero di caratteri presenti nella stringa ricevuta in ingresso come parametro. e non se il loro contenuto fosse identico. printf("fine a: %s <> b: %s\n" a). i < 10. for(i = 0. /* l'utente inserisce la stringa "abcdefghij" */ b = a. b[10]. modificando poi i valori di b si modificano quelli di a.

separati da un punto . nel seguente modo: struct s_dipendente { char nominativo[40].. Definizione di una struct La dichiarazione di una struct è un processo a due fasi.nominativo)... Il nominativo richiede una stringa. int anni.consideri a titolo d'esempio l'informazione relativa al nominativo. Ad esempio. Accesso ai campi della struttura Per accedere ai campi della struttura. ossia un array di caratteri terminati dal carattere '\0'.anni = 30.. utilizzata poi per definire tutte le variabili necessarie con la suddetta struttura. dipendente. int salario. per scrivere un valore o per leggerlo. La sintassi per la definizione di una struct è la seguente: struct nome_struttura { lista dei campi (tipo-nome) }. anni e salario. è necessario indicare il nome della variabile seguito da quello del campo di interesse. . In seguito è possibile definire variabili come segue: struct nome_struttura nome_variabile. . . }. A questo punto è possibile definire una variabile con la struttura appena introdotta: struct s_dipendente dipendente.. 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. dipendente. printf("%s\n". Per prima cosa si definisce la struct che consente di memorizzare questo tipo di informazioni. all'età e al loro salario... La variabile si chiama dipendente ed è del tipo struct s_dipendente definito precedentemente. per la struttura precedentemente dichiarata: struct s_dipendente dipendente . La prima è la definizione di una struttura con i campi dei tipi desiderati. l'età ed il salario richiedono interi. Si consideri il seguente esempio: si desidera gestire i dati di un insieme di persone relativamente al loro nominativo.

reale + b.. b. Si noti che non deve utilizzare il nome della struttura s_dipendente. A questo punto potrebbe quindi essere conveniente scriversi un insieme di funzioni che effettuino le operazioni elementari sui numeri complessi da richiamare ogni volta.immaginaria. 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.anni ad una variabile di tipo intero. Per riassumere. float immaginaria..immaginaria + b. . ma il nome della variabile. }.nominativo. Per accedere ai dati del contabile si segue il percorso: azienda.immaginaria = a..reale = b.contabile. È possibile effettuare operazioni di assegnamento tra i vari campi della struct come ad esempio: a. c.reale.Una volta individuato il campo d'interesse mediante la sequenza nome_variabile. A questo punto è possibile definire due variabili: struct s_complesso a. e nel caso di dipendente. per la quale è necessario invece scrivere: c. l'aspetto significativo delle struct è determinato dalla possibilità di memorizzare informazioni di natura diversa al'interno di un'unica variabile. .reale = a. anche perchè ci possono essere più variabili con la stessa struttura. Una struct può essere utilizzata per integrare un gruppo di variabili che formano un'unità coerente di informazione.reale. Si consideri a tale scopo il seguente esempio: struct s_complesso { float reale.. D'altra parte non si può scrivere un'espressione del tipo c = a + b. 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. c. }.nome_campo si ha a che fare con una variabile normale. . Si consideri il seguente esempio: struct s_azienda { char nome[30]. struct s_dipendente contabile.

} Definita la funzione somma è possibile chiamarla. c.immaginaria + b. ->) alla struttura e . .' ha una priorità superiore all'asterisco '*'.e.' è particolarmente prona ad errori. struct s_complesso *b . x = somma(y. y.immaginaria = a.reale = a. Di fatto l'utilizzo di puntatori a struct è estremamente comune e la combinazione della notazione '*' e '. Se si desidera che una funzione possa cambiare il valore di un parametro è necessario passarne il puntatore.anni è il campo anni della struttura s_dipendente a cui punta ptr. esiste quindi una forma alternativa più diffusa che equivale a (*ptr). struct s_complesso *c) { c->reale = a->reale + b->reale.. struct s_dipendente * ptr definisce un puntatore ad una struttura s_dipendente. struct s_complesso b) { struct s_complesso c. ed è la seguente: prt->anni Questa notazione dà un'idea più chiara di ciò che succede: prt punta (i. void s_complesso somma(struct s_complesso *a . .anni. Il funzionamento è pressoch è inalterato: (*ptr). È necessario utilizzare le parentesi in quanto il punto '.reale. c. ed è un numero intero.Strutture e funzioni La maggior parte dei compilatori C consente di passare a funzioni e farsi restituire come parametri intere strutture. struct s_complesso somma(struct s_complesso a . Si tenga presente che il passaggio di una struct per valore può richiedere un elevato quantitativo di memoria. return (c). Puntatori a strutture Come per tutti i tipi fondamentali è possibile definire un puntatore ad una struct. z. z).reale + b. 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. c->immaginaria = a->immaginaria + b->immaginaria.immaginaria. come nel seguente esempio: struct s_complesso x.anni "preleva" il campo di interesse..

Si ipotizzi che ci siano al più cento diverse combinazioni di marche e modelli da dover gestire. concessionario[i]... }.} In questo caso c è un puntatore e la chiamata deve essere fatta così: somma(&x. typedef struct s_automobile auto.marca). a cui si accede come segue: .. char modello[70]. gets(concessionario[i]. I campi delle struct dei singoli elementi dell'array vengono poi trattati normalmente. printf("inserisci il nome della marca: \n"). . in grado di modellare entità dati del mondo reale. Ogni elemento dell'array ha i suoi campi marca... .venduto=0. come segue: void main() { auto concessionario[100]. Si consideri il seguente esempio: struct s_automobile { char marca[50]. } In questo modo si dichiara un array di struct s_automobile di cento elementi. &y. Per poter gestire le informazioni di un concessionario è a questo punto necessario poter dichiarare delle variabili che memorizzino i dati relativi alle automobili vendute. int venduto.. &z). modello e venduto. Nell'ambito del programma sarà quindi necessario disporre di 100 elementi di tipo auto e a tal scopo verrà dichiarato un array. int i. 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 questo caso si risparmia spazio nella chiamata alla funzione. in base al loro tipo (nell'esempio rispettivamente come un array di caratteri ed un intero) .

float immaginaria. Riportata all'esempio precedente. b. La dichiarazione è più compatta. . int a. come mostrato di seguito: struct s_complesso { float reale. discusse nella Lezione 15. definito appunto dall'utente. I File .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.. che si va ad affiancare ai tipi fondamentali del C. float immaginaria. Si consideri il caso della struct per la gestione dei numeri complessi. ... void main() { . } Frequentemente la dichiarazione del tipo mediante la typedef viene fatta concorrentemente alla dichiarazione della struct. il codice che si ottiene è il seguente: typedef struct s_complesso { float reale. y. secondo la seguente sintassi: typedef struct nome_struttura { lista dei campi (tipo-nome) } nome_tipo_struttura. ce si chiama complesso ed è possibile utilizzarlo nella dichiarazione di variabili. 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.. Strutture e liste concatenate Le strutture vengono utilizzate per la realizzazione del tipo elementare che costituisce l'elemento di base delle liste dinamiche concatenate. char. typedef struct s_complesso complesso. } complesso. }.. complesso x..

il carattere di tabulazione ('\t') e il carattere a-capo ('\n').h .Il flusso . . Per aprire un file ed associarlo ad uno stream l'istruzione da utilizzarsi è la fopen(). Il tipo FILE viene definito nella libreria stdio. comune a tutti i dispositivi periferici del calcolatore. sebbene ci possa essere una discrepanza tra il contenuto del file e lo stream) e quelli binari (utilizzati per qualiasi tipo di dato). la libreria standard contiene numerose funzioni per un approccio efficiente. flessibile epotente. e via discendo. Uno stream di testo è composto di linee.h. 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. alla tastiera. allo schermo. il cui prototipo è mostrato di seguito: FILE *fopen(char *. La funzione fopen(). nel caso più comune è uno stream è l'interfaccia logica ad un file. decrementato. Ci sono due tipi di stream: quelli di testo (costituiti da una sequenza di caratteri ASCII. rispettivamente il nome del file da aprire e il modo in cui aprirlo (il tipo di accesso che si desidera fare). tra cui ad esempio la dimensione. Si tratta di una struttura con diverse informazioni relative al file. Per quest'ultimo aspetto. In linea di massima ci si riferirà sempre a stream di testo. vengono visti tutti in modo analogo. Anche se questi file differiscono nella forma e nelle loro capacità. e dualmente lo stream viene disassociato mediante una operazione di chiusura. ad una porta. Il puntatore al file verrà utilizzato da tutte le funzioni che lavorano con i file e non va esplicitamente manipolato (incrementato. In base a come il C definisce il termine "file". questo può far riferimento ad un file su disco. I caratteri sono esclusivamente caratteri stampabili. ossia un'interfaccia logica. 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. Un concetto importante in C è il flusso (stream). come tutte le funzioni di sistema si trova nella libreria standard di sistema stdio. Uno stream viene collegato ad un file mediante un'operazione di apertura. La posizione corrente è il punto in cui si farà il prossimo accesso nel file. se si tratta di lettura. Essa riceve in ingresso due parametri.char *). c'è spazio a sufficienza e si hanno i permessi nel caso di scrittura) l'istruzione restituisce un puntatore a file (tipo FILE*) valido.stream Sebbene il linguaggio C non abbia dei metodi nativi per gestire l'ingresso/uscita su file.

in base al modo in cui è stato aperto. fine del file) nel caso si verifichi un errore. La funzione fclose() restituisce 0 se viene eseguita con successo. 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 riceve in ingresso come parametro il puntatore al file da chiudere: il puntatore deve essere valido. FILE *)... altrimenti restituisce EOF (end of file. . Lo stralcio di codice mostra l'utilizzo della funzione fopen() e il controllo del risultato prima di procedere: .. il cui prototipo è il seguente: int fclose(FILE *). 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. FILE*). if ((fp = fopen(NomeFile.. FILE*). NomeFile). Il valore restituito dalla funzione fget() può essere assegnato ad una variabile di tipo carattere. else{ ..). se si verifica un errore o termina il file la funzione restituisce EOF (situazione indicata dal carattere stesso EOF).. restituisce un puntatore a NULL. } Per chiudere un file è disponibile la funzione di libreria fclose(). 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). . "r")) == NULL) printf("Errore nell'apertura del file %s\n". Una volta aperto un file. altrimenti restituisce EOF. FILE *fp. 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. La funzione fput() restituisce il carattere scritto nel caso non vi siano problemi.. è possibile leggere e/o scrivere utilizzando le seguenti funzioni: int fgetc(FILE *). char *fgets(char* . Se la funzione fopen() non va a buon fine.. int . ottenuto mediante una precedente fopen(). char NomeFile[30]. Le prime due funzioni hanno i seguenti prototipi: int fputs(char* . condizione che va verificata prima di procedere nell'accesso al contenuto del file. int fputc(int ..

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. Il carattere terminatore della stringa non viene scritto e non viene neppure aggiunto automaticamente un ritorno a capo. Il compilatore vede dal tipo della variabile. eventualmente sovradimensionandolo).). altrimenti restituisce 0. La funzione legge al più num-1 caratteri e li memorizza nella stringa ricevuta come primo parametro.. char *.). La funzione fputs() scrive la stringa ricevuta come primo parametro. La funzione remove() riceve in ingresso il nome del file da cancellare mentre la funzione rewind() il puntatore al file da riposizionare. La stringa letta viene terminata con il terminatore '\0'. int fscanf(FILE *. La memoria dinamica Tutte le variabili dichiarate ed utilizzate nelle precedenti lezioni venivano allocate in modo statico. sul file indicato dal puntatore a file (il secondo parametro).. Le altre due funzioni sono fprintf() e fscanf(). solamente che accedono a file invece che allo standard input (tastiera) e output (video). 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. I prototipi sono i seguenti: int remove(char *). La funzione fget() legge una sequenza di caratteri dal file puntato dall'apposito puntatore. altrimenti restituisce il puntatore nullo. Invece di ridirigere le operazioni di ingresso/uscita verso la console. Per staticità si intende che i dati non cambieranno di dimensione nella durata del programma (si ricordi il vincolo di dimensionare opportunamento un array. La funzioe restituisce la stringa se non si verifcano problemi.. void rewind(FILE *). I loro prototipi sono: int fprintf(FILE *. Il prototipo è: int feof(FILE *fp). al momento della dichiarazione. che costiuisce il terzo parametro.. quanti byte devono essere allocati. riservando loro spazio nella porzione di memoria denominata Stack che è destinata alla memoria statica. . queste funzioni operano sul file specificato dal puntatore al file ricevuto come primo parametro. . Il vantaggio di queste due funzioni è che rendono estremamente semplice scrivere una grande quantità e varietà di dati su un file di testo.Le funzioni fputs() e fgets() scrivono e leggono una stringa da un file. Queste funzioni operano esattamente come la funzione printf() e scanf() rispettivamente. Nel caso i caratteri siano meno oppure si incontri il carattere acapo o EOF. altrimenti un valore non negativo. Per il resto queste operazioni sono analoghe alle printf() e scanf(). char * . La funzione restituisce EOF nel caso in cui si verifica un errore.

e-3. (int *). onde evitare di riservare una quantità di memoria sbagliata.. Nel caso lo spazio sia esaurito. int *numx. 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.) e restituisce il numero di byte necessari per memorizzare un dato di quel tipo..Esiste una porzione di memoria denominata Heap ('mucchio' in italiano) dove è possibile allocare porzioni di memoria in modo dinamico durante l'esecuzione del programma. La funzione. viene fatto un cast esplicito al tipo intero. restituisce il puntatore nullo (NULL). La funzione C per allocare dinamicamente uno spazio di memoria per una variabile è la seguente: void * malloc(size_t). Per cui.. Ad esempio. appartenente alla libreria standardstdlib. /*-2-*/ .h riserva uno blocco di memoria didim byte dalla memoria heap e restituisce il puntatore a tale blocco. 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 cui verrà assegnato il valore dell'indirizzo del blocco richiesto quando si allocherà della memoria.. numx = (int *) malloc(sizeof(int)).è mostrata nella figura seguente. -2.. *numx = 34. /*-3-*/ Poichè la malloc restituisce un puntatore genericovoid *. .. corrispondenti ai punti -1-. La situazione della memoria. ma potrebbe essere differente su architetture diverse. si utilizza l'operatore sizeof che prende come parametro il tipo di dato (int. Per identificare la dimensione della memoria da allocare dinamicamente. A questo scopo esistono specifiche funzioni della libreria standard (malloc e free) per l'allocazione e il rilascio della memoria. per poter sfruttare la possibilità di allocare dinamicamente della memoria è necessario dichiarare delle variabili che siano dei puntatori. è opportuno far uso di tale funzione. a fronte di richieste di spazio per variabili. . float. In generale questo valore è pari a 4 byte. Quindi. Si ricordi che il numero di byte necessari per memorizzare un numero intero dipende dall'architettura del calcolatore e dal compilatore stesso. /*-1-*/ . come mostrato nel seguito: .

. In questo modo è possibile scrivere programmi in cui non sia noto a priori il numero di dati da trattare. Si consideri il seguente stralcio di codice. il prototipo è il seguente: void free(void *). int *Numeri. È anche possibile allocare dinamicamente un numero di byte sufficienti a contenere più dati dello stesso tipo.. Per poter gestire situazioni in cui il numero dei dati non è mai conosciuto. i < n. in modo tale che possa essere riutilizzata. La funzione free effettua questa operazione. /* vengono allocati n * numero_byte_per_un_intero */ for(i = 0.. ossia un array allocato dinamicamente. .-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.. . } . trattate nella Lezione 15. printf("Quanti dati si desidera inserire?").omesso controllo di validità*/ Numeri = (int *)malloc(n * sizeof(int)). scanf("%d". anche se rimane il vincolo che tale informazione debba essere prima o poi fornita al programma. che dopo aver chiesto all'utente quanti dati intende inserire. che siano in grado di allocare di volta in volta la memoria necessaria: si tratta delle liste concatenate. i++){ printf("Inserisci il dato %d: " i+1). e può variare durante l'esecuzione in base all'elaborazione è necessario utilizzare delle strutture dati opportune. 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. &Numeri[i]).. n.. &n). scanf("%d". Rilascio della memoria dinamicamente allocata Quando la memoria allocata dinamicamente non serve più è opportuno (!) liberarla. /* numero di dati . riducendo i rischi di esaurire la memoria. alloca dinamicamente la memoria per poi procedere nell'elaborazione .

/* alloca memoria per un elemento di tipo nodo_t */ if (n1) { /* verifica che non ci siano stati problemi di memoria */ n1->carattere = val_c. c'è l'opzione di includere una etichetta. 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. i cui campi sono i dati veri e propri ed un campo di tipo puntatore. Di seguito quindi struct nodo_s costituisce un nome alternativo per il tipo nodo_t. Per convenzione. da lì in poi ogni elemento punterà a quello successivo. ad esempio nodo_s dopo la parola chiave struct. Infatti i nodi vengono creati solo quando c'è un nuovo dato da memorizzare.. l'ultimo nodo punterà a NULL ad indicare il termine della lista. nodo_t *n1. È necessario utilizzare struct nodo_s * invece di nodo_t * in quanto il compilatore non ha ancora visto il nome nodo_t. 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.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. Nella definizione di un tipo di struttura in C. se non per verifare che punti a NULL causa un errore durante l'esecuzione. } nodo_t. ordinarli in base ad un qualche criterio. eliminarli. int frequenza. Si accede alla lista concatenata mediante un puntatore al primo elemento della lista. 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. È definito nella libreria stddef. . struct nodo_s * prox. Ogni nodo della lista. typedef struct nodo_s { char carattere. Nella parte di dichiarazione del tipo di utilizza poi struct nodo_s * per il puntatore ad un altro elemento dello stesso tipo. Nota size_t è un tipo utilizzato per le dimensioni dei tipi in memoria. n1->frequenza = 1. .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. n1 = (nodo_t *)malloc(sizeof(nodo_t)).

testa = NULL. *temp. while(temp){ /*ci si ferma quando temp = NULL*/ printf("%c: %d\n". nodo_t *testa. e poi seguire la catena di puntatori all'elemento successivo. testa = NULL. Lo stralcio di codice proposto nel seguito effettua la stampa di tutti i nodi della lista: . ma solo un puntatore ad un nodo. . void main() { nodo_t * testa. temp = temp->prox... per cui all'inizio del programma si inizializza la testa a NULL. } 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 .. /* riempimento della lista */ temp = testa. Si tratta di un puntatore ad un elemento di tipo nodo_t. /* inizializzato a puntare a NULL */ /* verrà modificato quando inserito nella lista */ . la lista è vuota.. in altri casi (come nell'esempio successivo dell'inserimento in coda) è desiderabile fermarsi sull'ultimo elemento della lista. } Attraversamento/scorrimento della lista In numerosi situazioni è necessario processare tutti i nodi della lista o comunque si deve esaminarli alla ricerca di uno specifico... } Una volta inserito il nodo nella lista il puntatore n1 potrà essere utilizzato per creare un nuovo nodo. temp->carattere... . La testa della lista La testa della lista ha come obiettivo indicare sempre il primo nodo della lista.n1->prox = NULL.. non serve un intero nodo in quanto non memorizza un dato. . Inizialmente.. mediante la testa. Per attraversare la lista è necessario posizionarsi sul primo nodo. Se si desidera processare tutti i nodi ci si fermerà nell'attraversamento quando la lista termina.. inserimento nella lista .. temp->frequenza).

come nuovo primo nodo. 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). in cui si suppone che nuovo punti ad un nuovo nodo. 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. che si basano su quelle fondamentali citate.Ci sono altre operazioni che è possibile svolgere. . Inserimento in testa Viene trattato in primo luogo il caso generale in cui la lista ha almeno un elemento. infatti. Dall'analisi si deduce che nell'inserimento di un nuovo elemento in testa alla lista non è necessario trattare esplicitamente il caso di lista vuota. testa = nuovo. come per esempio l'inserimento di un nodo in un punto ben preciso della struttura per mantenere o realizzare un ordinamento dei nodi. con i campi già impostati al valore corretto.. L'effetto della seconda istruzione non cambia. Si ricordi. che accedere ad un puntatore nullo crea un errore durante l'esecuzione.testa punterà a NULL ad indicare questa situazione.. La figura seguente mostra quali sono i passi da svolgere per inserire un nodo all'inizio della lista. Ci si ricordi che aggiornando un puntatore per indirizzarlo ad un nodo diverso sa quello attualmente puntato contemporaneamente elimina il precedente collegamento. il che indica che è l'ultimo elemento (essendo l'unico è così). Lo stralcio di codice che effettua l'inserimento in testa è illustrato qui di seguito. e testa che punta correttamente al primo nodo della lista: . Per cui. nuovo->prox = testa. Automaticamente vengono eliminati i vecchi collegamenti. Il codice prima indicato mantiene la propria validità: infatti dopo la prima istruzione nuovo->prox punterà a NULL. . 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. in figura. Nel caso in cui la lista sia vuota...

Lo stralcio di codice che gestisce anche il caso lista vuota è il seguente: . 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. /* temp punta all'ultimo */ temp->prox = nuovo. 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). } /* quando si arriva qua temp punta all'ultimo elemento */ temp->prox = nuovo. if (temp){ while(temp->prox) /* si arriva solo se temp non è NULL..Inserimento in coda Per effettuare l'inserimento in coda è necessario scorrere tutta la lista e portarsi sull'ultimo elemento. non è stata eseguita in quanto viene fatta nel momento in cui si crea un nuovo nodo. non c'è possibilità di generare errore */ temp=temp->prox. *temp. facendo in modo che quando si sta per effettuare l'operazione di aggiornamento. 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. /* inserimento */ L'istruzione nuovo->prox = NULL. *nuovo. Per eliminare un nodo. il puntatore indirizzi l'ultimo nodo. Le figure seguenti mostrano la sequenza di operazioni.. Verrà mostrato il caso più generale in cui si desidera eliminare un . temp = testa. temp = testa. } else /*caso lista vuota*/ testa = nuovo. . while(temp->prox){ /* scansione della lista */ temp = temp->prox. 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... nodo_t *testa.

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. char valore_cercato. altrimenti sarebbe possibile interrompere la scansione della lista non appena si è terminato di eliminare un elemento dalla lista ed il successivo non è da eliminare. *canc. che sono le situazioni che provocherebbero un errore nell'esecuzione del precedente codice privo di controlli. /* spostamento dei puntatori */ free(canc). Il seguente sottoprogramma gestisce tutte le casistiche citate. } /* eliminazione di un elemento qualsiasi */ . nodo_t *canc.. *temp. /* quando si e' qua. 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 definirà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. 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. Le figure seguenti mostrano la sequenza delle operazioni da svolgere. nodo_t * EliminaN(nodo_t * head.. int val) { nodo_t * tmp.. tmp = head. . /* i casi speciali non sono trattati */ while(temp->prox && temp->prox->carattere != valore_cercato) temp = temp->prox. da quello in cui l'elemento è il primo della lista e il caso in cui nella lista c'è un solo elemento. free(tmp). ipotizzando che la lista non sia ordinata. /* punta all'elemento da cancellare */ temp->prox = canc->prox. quindi si provvederà a spostare i puntatori e a chiamare la free.nodo che si trova in mezzo alla lista facendo poi alcune considerazioni sugli altri casi.. . Il codice è riportato qua di seguito. temp = testa. Il puntatore canc punta al nodo da eliminare. in cui non si considera il caso generale.

ognuna delle quali raccolga 100. e di delegare a loro la raccolta di una certa cifra.000 euro. 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 . Se cerchiamo di codificare questa strategia in pseudo-codice. » Un esempio Per avere un'idea di cosa sia la ricorsione. La soluzione è nel cercare di trovare altre persone che si dedichino alla raccolta dei soldi. 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. Lo stesso ragionamento si può fare fino ad arrivare ad avere dieci persone che raccolgano per un delegato i 100 euro.000./* 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.000 persone e chiedere a ciascuna di queste 100 euro. questi recluteranno 10 persone ciascuna delle quali deve raccogliere 10.000 di euro.000 persone potrebbe però essere un po' difficile. è necessario trovare 10. } 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. Se ad esempio si sa che ogni persona interpellata di solito è disposta a metterci 100 euro. return head. si pensi ad una delle prospettive di guadagno che talvolta vengono pubblicizzate: il vostro compito è di raccogliere 1. free(canc).000 euro. Per esempio si puo' pensare di individuare 10 persone. Trovare 10. } 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. Se anche queste dieci persone adottano la stessa strategia. È importante capire che il problema verrà scomposto in problemi della stessa natura. tmp->prox = tmp->prox->prox.

non è altro che il problema iniziale. siccome il problema è lo stesso. Applicando il processo di redifinizione ogni volta che viene chiamato il sottoprogramma ricorsivo il problema viene ridotto al caso semplice. il sottoprogramma RaccogliDenaro finisce per chiamare se stesso se il contributo da raccogliere è inferiore a 100 euro. facile da risolvere. Nel contesto della programmazione. nello pseudo-codice.solo con un n più piccolo. . avere un sottoprogramma che chiama se stesso prende il nome di ricorsione. Il caso generico può essere ridefinito in base a problemi più vicini ai casi semplici. la condizione indicata nell'if per la gestione del caso semplice si chiama condizione di terminazione. 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.raccogliere n euro . 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. 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. Inoltre. Il compito è lo stesso . lo si può risolvere chiamando il sottoprogramma originale. Quindi. 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. La figura seguente illustra tale approccio: la soluzione del caso semplice è rappresentata dalla soluzione del problema di dimensione 1. ma su una scala più piccola. non ricorsiva.

/* passo ricorsivo */ Il codice completo è quindi: int moltiplica(int a. il prodotto a x (b-1) P2. int b) { int ris. if(b == 1) ris = a. somma a al risultato della soluzione del problema P1.2.1 Il caso semplice si ha quando n == 1 è vera. Somma a al risultato del problema P1.1.1. Si procede così fino a quando: P1... b-1).1. /* passo ricorsivo */ return(ris).» 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.1 P2. Prodotto a x (b-1) P1.. La forma della ricorsione prima indicata diventa quindi: if(b == 1) ris = a. Somma a al risultato del problema P1. Prodotto a x 1 P1. ottenendo così la seguente cosa: P1... Prodotto a x (b-2) P1.2. Il problema P1. b-1). 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.1.. else ris = a + moltiplica(a. } Una classe di problemi per cui la ricorsione risulta una soluzione interessante è quella che coinvolge delle sequenze (o liste) di elementi di lunghezza variabile. /* caso semplice */ else ris = a + moltiplica(a. Somma a al risultato del problema P1. .

la traccia dell'esecuzione è mostrata nella figura seguente. L'esecuzione dell'istruzione return in uscita dalla funzione svuota lo stack restituendo il valore che c'era in cima allo stack. Per maggior chiarezza sono stati rappresentati degli spazi di memoria distinti. Si consideri la chiamata seguente: moltiplica(6. In questo modo. Questo indirizzo serve per sapere a che punto rientrare dalla chiamata a sottoprogramma. Ogni volta che una funzione viene chiamata. ogni chiamata a funzione. insieme all'indirizzo di memoria dell'istruzione che effettua la chiamata. Il record di attivazione mostra il valore dei parametri per ogni chiamata e l'esecuzione della funzione. anche se in realtà il compilatore mantiene un unico pila di sistema (o stack). riserva alla funzione spazio necessario per i parametri e per le variabili locali. .» come seguire una funzione ricorsiva che restituisce un valore Per poter seguire cosa verrà mostrato il record di attivazione per ogni chiamata a sottoprogramma. anche quelle ricorsive. cosicché tutto possa funzionare correttamente nel rispetto delle regole di visibilità.3). i suoi parametri e le sue variabili locali vengono messi sullo stack.