Le funzioni
Ver. 2.4
© 2010 - Claudio Fornaro - Corso di programmazione in C
Struttura modulare
Per semplificare la struttura di un programma complesso è possibile suddividerlo in moduli
Un modulo è un blocco di codice che assolve ad un compito preciso (ad es. calcola la radice quadrata) e a cui è stato dato un nome
Un programma consta di un modulo principale (il main) ed eventuali altri moduli di supporto
Quando un modulo richiama un altro modulo, il chiamante viene sospeso finché il chiamato non ha terminato la sua esecuzione
In C i moduli sono chiamati funzioni
Ogni funzione può richiamare (far eseguire) qualsiasi altra funzione (anche se stessa)
3
Struttura modulare
Le due chiamate del modulo StampaCiao fanno eseguire ogni volta le istruzioni che lo costituiscono (sospendendo il chiamante)
...
scanf...
StampaCiao() ...
for ...
switch ...
printf...
...
StampaCiao() if (x==2) then ...
...
scanf...
StampaCiao() ...
for ...
switch ...
printf...
...
StampaCiao() if (x==2) then ...
printf("### # # ###\n");
printf("# # # # #\n");
printf("# # # # # #\n");
printf("# # ### # #\n");
printf("### # # # ###\n");
printf("### # # ###\n");
printf("# # # # #\n");
printf("# # # # # #\n");
printf("# # ### # #\n");
printf("### # # # ###\n");
Modulo chiamato (StampaCiao) Modulo chiamante
4
Struttura modulare
Vantaggi della programmazione modulare:
poiché i moduli “nascondono” al loro interno i dettagli di come una certa funzionalità venga realizzata, il programma complessivo ha un livello di astrazione maggiore: il modulo viene visto come un insieme di macro-istruzioni (“black-box”)
il codice per ottenere una certa funzionalità viene scritto una volta sola e viene richiamato ogni volta che è necessario (ma la chiamata richiede tempo)
il codice complessivo è più corto
essendo più piccoli, i moduli sono più semplici da realizzare e da verificare
il codice di un modulo correttamente funzionante
può essere riutilizzato in altri programmi
Variabili locali
Ogni funzione è un piccolo programma a sé stante, isolato dalle altre funzioni
All’interno di una funzione possono essere definite delle variabili locali (cioè hanno scope locale): le altre funzioni non le “vedono”
Variabili con lo stesso nome in funzioni diverse sono quindi completamente scorrelate
(possono dunque anche essere di tipo diverso)
Vengono create ogni volta che si entra nella funzione e distrutte (perdendo il valore) quando si esce
Le inizializzazioni avvengono ad ogni chiamata, senza inizializzazione il contenuto è indefinito
Parametri e valore restituito
Essendo le variabili interne locali, private, per passare ad una funzione i dati da elaborare è necessario utilizzare variabili speciali dette parametri
La funzione comunica al modulo chiamante il risultato della sua elaborazione producendo un unico valore detto valore restituito o valore di ritorno
main
funzione
parametri risultato
7
Definizione di funzioni
tipo nomeFunzione(parametri) {
definizione_variabili_locali istruzioni
eventuale return }
tipo indica il tipo del valore restituito (es. sqrt restituisce un double)
Se la funzione non restituisce valori (ad esempio StampaCiao visualizza soltanto) bisogna indicare il tipo void:
void StampaCiao(...)
Se non si mette nulla viene supposto int corpo della funzione
8
Definizione di funzioni
Se la funzione non ha parametri (ad esempio StampaCiao), si indica void tra le
parentesi:
void StampaCiao(void)
Se non si indica nulla, il compilatore C non
attua alcun controllo sui parametri (invece per
un compilatore C++ è come se l’argomento
fosse esplicitamente void)
Chiamata di funzione
Si chiama una funzione indicandone il nome seguito da una coppia di parentesi contenenti i valori da elaborare (separati da virgole) eleva(y,2);
Se la funzione non richiede parametri, le parentesi sono vuote, ma devono esserci:
StampaCiao();
Il valore restituito può essere assegnato ad una variabile o utilizzato in un’espressione, altrimenti viene semplicemente scartato:
x = eleva(y,2);
y = 3*eleva(2,k)-4*k;
eleva(3,5);
Ritorno da una funzione
La funzione termina (cioè l’esecuzione torna al modulo chiamante) quando viene eseguita l’istruzione
return risultato;
Una funzione può avere più istruzioni return
risultato è il valore restituito dalla funzione al chiamante, è un’espressione qualsiasi
return x*2;
Se il tipo restituito dalla funzione è void, non si deve indicare risultato, inoltre la return che precede la graffa di chiusura (solo questa) può essere omessa
I valori delle variabili locali vengono persi
11
Ritorno da una funzione
Nel main la return termina il programma
Per terminare un programma dall’interno di una funzione (e passare lo status al Sistema Operativo) si utilizza la funzione
exit(status)
dichiarata in <stdlib.h>:
exit(EXIT_SUCCESS);
12
Esempio di funzione
#include <stdio.h>
int eleva(int b, int e) {
int k=1;
while (e-- > 0) k *= b;
return k;
}
Continua (stesso file)...
Esempio di funzione
Continuazione (stesso file) main()
{
int x, y;
printf("Introduci numero: ");
scanf("%d", &x);
y = eleva(x, 2);
printf("%d^%d = %d\n", x,2,y);
return 0;
}
Alla chiamata, la x del main viene copiata nella b di eleva, mentre il 2 del main viene copiato nella e di eleva
Tipo di una funzione
Il tipo di una funzione è determinato dal tipo del valore restituito e dal tipo, numero e ordine dei suoi parametri
int eleva(int b, int e)
eleva è una funzione che ha un primo
paramero int, un secondo parametro int e restituisce un int
15
Scope di una funzione
Lo scope di una funzione (nome e tipo) si estende dal punto in cui viene definita fino a fine file: la funzione può essere utilizzata solo dalle funzioni che nello stesso file seguono la sua definizione (vale anche per il main)
f1() {...}
f2() {...}
main() {...}
f1 non “vede” e non può quindi usare f2, f2 vede f1, il main vede f1 e f2
16
Scope di una funzione
Il compilatore verifica che le chiamate a funzione siano coerenti con le corrispondenti definizioni (cioè abbiano lo stesso tipo)
E’ necessario che la chiamata a funzione sia nello scope della funzione stessa
Se il compilatore trova una funzione di cui non conosce il tipo (non è in scope, ad esempio f1 che chiama f2), allora presuppone che essa sia definita altrove e quindi:
non fa controlli sugli argomenti
presuppone che restituisca un int
segnala il possibile problema con un Warning
Prototipo di una funzione
Il prototipo di una funzione è una dichiarazione che estende lo scope della funzione (nome e tipo)
Il corpo della funzione (la sua definizione ) può quindi essere collocato:
in un punto successivo a dove viene chiamata (nell’esempio seguente, eleva è definita dopo il main dove viene utilizzata)
in un altro file di codice sorgente C
in una libreria (compilata)
Lo scopo primario degli header file è quello di fornire al compilatore i prototipi delle funzioni delle librerie del C (ad es. stdio.h contiene i prototipi di scanf, printf, getchar, etc.)
Esempio di funzione
#include <stdio.h>
int eleva(int b, int e); prototipo main()
{
int x, y;
printf("Introduci numero: ");
scanf("%d", &x);
y = eleva(x, 2);
printf("%d^%d = %d\n", x,2,y);
return 0;
}
Continua (stesso file)...
19
Esempio di funzione
Continuazione (stesso file)
int eleva(int b, int e) {
int k=1;
while (e-- > 0) k *= b;
return k;
}
20
Prototipo di una funzione
I prototipi possono essere collocati:
prima del main (come nell’esempio eleva)
tra una funzione e l’altra
insieme alle definizioni delle variabili locali di una funzione
Il prototipo estende lo scope della funzione (nome e tipo) dal punto dove è indicato:
fino a fine file se esso è collocato esternamente alle funzioni (prima del main o tra due funzioni)
fino a fine funzione se è interno ad una funzione
(collocato con le variabili locali di una funzione)
Prototipo di una funzione
Il prototipo di una funzione è simile alla definizione della funzione, salvo che:
manca il corpo
i nomi dei parametri possono essere omessi (ma i tipi devono essere presenti!)
ha un punto e virgola alla fine
int eleva(int, int);
I nomi dei parametri dei prototipi:
se non sono omessi, possono essere diversi da quelli usati nella definizione della funzione
sono scorrelati dagli altri identificatori (nomi uguali si riferiscono comunque a identificatori diversi
sono utili per descrivere il significato dei parametri:
int eleva(int base, int esponente);
Parametri attuali e formali
Parametri attuali (o argomenti):
sono i valori (variabili, costanti o espressioni) indicati tra le parentesi alla chiamata di una funzione
eleva(x,2)
Parametri formali:
sono le variabili indicate tra le parentesi nella definizione della funzione
int eleva(int b, int e)
23
Parametri attuali e formali
Nella chiamata ad una funzione bisogna indicare un argomento per ciascuno dei parametri formali
I parametri attuali e quelli formali
corrispondono in base alla posizione (il primo attuale al primo formale, etc.)
I nomi dei parametri formali sono scorrelati (e quindi tipicamente diversi) dai nomi di eventuali variabili usate come argomenti (inoltre gli argomenti possono essere valori costanti o il risultato di espressioni)
I parametri formali hanno lo stesso scope delle variabili locali della funzione
24
Passaggio dei parametri
I dati possono essere passati ad una funzione esclusivamente per valore (by value):
alla chiamata della funzione vengono create nuove variabili con i nomi di ciascuno dei parametri formali e in esse viene copiato il valore del corrispondente parametro attuale
main() main()
eleva() eleva()
x
2
b
e
Passaggio dei parametri
Come per le assegnazioni, se il parametro attuale e il corrispondente formale sono di tipo diverso c’è una conversione automatica al tipo del parametro formale (se è di tipo meno capiente può essere generato un Warning)
Poiché in memoria i parametri formali e quelli attuali sono completamente distinti e
indipendenti, cambiare il valore di un
parametro formale non modifica il parametro attuale corrispondente , neppure se questo è una semplice variabile (è ovviamente
impossibile modificare una costante o il
risultato di un’espressione): nell’esempio visto la modifica di b non si ripercuote su x
Variabili locali static
Le variabili locali hanno classe di allocazione automatica: vengono create ogni volta che si esegue la funzione ed eliminate ogni volta che questa termina (perdendone il valore)
Le variabili locali di classe di allocazione statica invece non vengono mai rimosse dalla memoria per cui non perdono il loro valore quando la funzione termina (resta quello che aveva al termine della chiamata precedente)
Le variabili statiche non richiedono la
ri-allocazione della memoria ad ogni chiamata della funzione, quindi il programma può
essere più veloce
27
Variabili locali static
Si richiede una classe di allocazione statica e non automatica mediante lo specificatore di classe di allocazione static:
static int cont = 0;
Se non inizializzate esplicitamente, vengono inizializzate automaticamente a 0 (che nel caso dei puntatori viene automaticamente convertito in NULL)
28
Variabili locali static
L’inizializzazione delle variabili locali static avviene idealmente solo la prima volta che si esegue la funzione (in realtà i valori vengono inizializzati dal compilatore)
Possono essere inizializzate dal compilatore solo con espressioni costanti:
numeri e #define
valori enum
indirizzi di memoria di variabili statiche
Non possono essere inizializzate con:
valori const
variabili e risultati di funzioni
indirizzi di memoria di variabili automatiche
Variabili locali static
int conta(void) {
static int cont = 0;
return ++cont;
}
Ogni volta che conta viene chiamata, essa incrementa il contatore cont e ne restituisce il valore, se non fosse statica ma automatica restituirebbe sempre il valore 1 perché cont verrebbe ri-inizializzata ogni volta a 0
Variabili locali static
char *nomeMese(int n) {
static char *nome[] = {
"inesistente", "gennaio",
"febbraio", ecc... };
if (n<1 || n>12) return nome[0];
else
return nome[n];
}
La stringa di cui viene restituito il puntatore può essere utilizzata dal chiamante in quanto essendo statica non viene rimossa dalla memoria
31
Passaggio dei parametri
Il passaggio di parametri nella modalità
per riferimento (by reference) prevede che la modifica del parametro formale si
ripercuota identica sul corrispondente
parametro attuale (deve essere una variabile)
In C non esiste il passaggio per riferimento, ma questo può essere simulato passando per valore alla funzione il puntatore al dato da passare (che deve essere una variabile, non può essere il risultato di un calcolo)
Nella scanf le variabili scalari sono precedute da & perché devono essere modificate dalla funzione e quindi se ne passa l’indirizzo
32
Passaggio dei parametri
#include <stdio.h>
void fz(int *); prototipo main()
{
int x=2;
fz(&x);
printf("%d\n", x);
return EXIT_SUCCESS;
}
void fz(int *p) {
*p = 12;
}
Passaggio dei parametri
Nell’esempio:
il main alloca x, gli assegna il valore 2 e chiama fz passandole l’indirizzo di x (&x) presente in una variabile temporanea (“senza nome”)
alla chiamata di fz, l’indirizzo di x (che è BF32F0) viene copiato by-value in p
fz accede a x come *p, modificandola in 12
quando fz termina, x vale 12
main()
fz(&x);
x 12
main()
fz(&x);
x 12
fz(int*p)
*p=12;
fz(int*p)
*p=12;
BF32F0
&x
2 BF32F0
p
x
Passaggio dei parametri
void swap(int *, int *); prototipo main()
{
int x=12, y=24;
swap(&x, &y);
}
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
35
Passaggio di vettori
Per passare un vettore come argomento, si indica il suo nome senza parentesi
x = media(vettore);
Il parametro formale corrispondente definirà un vettore dello stesso tipo (in genere senza dimensione in quanto ininfluente)
float media(int v[]);
La funzione deve conoscere in qualche modo la dimensione del vettore (indicarlo tra le parentesi quadre non serve a nulla):
viene passato come argomento
float media(int v[], int lung);
è noto a priori (es. una #define)
si usano variabili esterne (vedere più avanti)
36
Passaggio di vettori
Possono essere passati vettori con dimensioni diverse, ma dello stesso tipo T
Quando si passa un vettore-di-T , poiché si indica il nome del vettore, in realtà si passa l’indirizzo di memoria del suo primo elemento (e questa non dà alcuna informazione né restrizione sulla lunghezza del vettore)
Il parametro formale è quindi in realtà un puntatore-a-T , la forma v[] viene convertita automaticamente in *v, si possono usare le due definzioni indifferentemente
float media(int *v);
Passaggio di matrici
Per passare una matrice come argomento, si indica il suo nome senza parentesi
x = media(matrice);
Il parametro formale corrispondente dichiara una matrice dello stesso tipo (in genere senza la prima dimensione in quanto ininfluente) void funz(int matrice[][10])
La funzione deve conoscere in qualche modo le dimensioni della matrice (indicarle tra le parentesi quadre non serve a questo scopo)
Passaggio di matrici
Possono essere passate matrici con diverso numero di righe, ma devono avere lo stesso numero di colonne e gli elementi devono essere dello stesso tipo T
Poiché una matrice è un vettore-di-vettori, quando essa viene passata ad una funzione, viene in realtà passato l’indirizzo del primo elemento del vettore-di-vettori
Il tipo del parametro formale corrispondente è quindi un puntatore-a-vettore e NON un
puntatore-a-puntatore (il decadimento da vettore a puntatore avviene una volta sola)
39
Passaggio di matrici
Nel caso dell’esempio, il parametro formale è un puntatore-a-vettore-di-10- int:
int (*matrice)[10])
La forma matrice[][10] viene convertita automaticamente in (*matrice)[10], si possono usare le due definzioni indifferentem.
void funz(int (*matrice)[10])
E’ quindi un errore scrivere:
void funz(int **matrice)
oltre all’errore di tipo, si perde la dimensione delle colonne e quindi non si può determinare la posizione degli elementi della matrice
40
Passaggio di matrici
In memoria l’elemento Mx[i][j] viene determinato con il calcolo
indirizzo_di_Mx+NC*i+j dove NC è il numero delle colonne,
come si vede il numero delle righe non serve
Per passare ad una funzione una matrice con qualsiasi numero di righe e qualsiasi numero di colonne vi sono diverse soluzioni, si
rimanda alle slide relative all’allocazione
dinamica
Passaggio di vettori multidim.
Quanto visto per le matrici può essere esteso ai vettori multidimensionali
In particolare:
nel parametro formale si può tralasciare la dimensione del solo primo parametro
il parametro formale è un puntatore ad un vettore di X vettori di Y vettori di Z vettori di .... di tipo T
la funzione deve conoscere i valori di tutte le dimensioni
possono essere passate matrici con la sola prima dimensione diversa
Parametri const
Il modificatore const applicato ai parametri formali impedisce che all’interno della
funzione si possa modificarne il valore int funzione(const int v)
Permette di proteggere i parametri da una successiva incauta modifica (per prevenire errori di programmazione)
Ad esempio, questo richiede al compilatore di segnalare se un puntatore-a-dato-costante viene assegnato ad un puntatore-a-dato- variabile (ad esempio passandolo come parametro), cosa che by-passerebbe la restrizione
43
Parametri puntatori const
1.
Puntatore variabile a dati variabili int f(int *p)
{
*p = 12; OK, dato variabile p++; } OK, puntatore variabile
Si può passare un int*
Non si può passare un const int*
(non c’è conversione automatica di tipo perché dentro la funzione nulla vieterebbe di poter cambiare il valore alla variabile puntata):
int x=12;
const int *y=&x;
f(y); ERRORE
44
Parametri puntatori const
2.
Puntatore variabile a dati costanti
int f(const int *p) /* int const */
{*p = 12; NO, dato costante p++;} OK, puntatore variabile
Si può passare un int* (c’è conversione di tipo automatica in quanto si passa ad un tipo più restrittivo)
Si può passare un const int*
Note:
Tipicamente utilizzato per passare un puntatore ad una struct o ad un vettore impedendo che possano essere modificati
Non si può passare senza cast un Tipo** dove
è richiesto un const Tipo**
Parametri puntatori const
3.
Puntatore costante a dati variabili int f(int * const p)
{*p = 12; OK, dato variabile p++;} NO, puntatore costante
Si può passare un int*
4.
Puntatore costante a dati costanti int f(const int * const p) {*p = 12; NO, dato costante
p++;} NO, puntatore costante
Si può passare un int*
Variabili esterne
Vengono definite (riservando memoria) esternamente al corpo delle funzioni:
in testa al file, tipicamente dopo le direttive
#include e #define
oppure tra una funzione e un’altra
Sono visibili e condivisibili da tutte le funzioni che nello stesso file seguono la definizione
Possono essere utilizzate come metodo alternativo per comunicare dati ad una funzione e per riceverne
A questo scopo si usino con parsimonia:
rendono poco evidente il flusso dei dati all’interno del programma
47
Variabili esterne
Hanno classe di allocazione statica: non vengono mai rimosse dalla memoria e, salvo inizializzazione esplicita, vengono inizializzate automaticamente a 0 (i puntatori a NULL)
Una variabile locale (interna ad una funzione) con lo stesso nome di una esterna copre la visibilità di quella esterna alla quale quindi non può accedere con quel nome (non è buona pratica di programmazione)
48
Variabili esterne
#include<...>
int uno;
main() {
uno = 12;
}
long due;
void fun1() {
uno = 21; due=55;
}
int tre;
int fun2() {
return uno + due + tre;
}
tre
due
uno
extern
Lo specificatore di classe di allocazione extern permette di estendere la visibilità delle variabili esterne
La clausola extern viene premessa ad una definizione di variabile per trasformarla in dichiarazione (non riserva memoria)
extern int x;
Indica al compilatore che la variabile è definita (con allocazione di memoria, senza extern) altrove (più avanti nello stesso file o in un altro file)
In seguito il linker ricondurrà tutte le dichiarazioni all’unica definizione
extern
La dichiarazione di una variabile extern può essere sia interna ad una funzione, sia esterna
#include<...>
extern int due; dichiarazione
funz4() esterna a funz
{
extern long uno; dichiarazione
int tre; interna a funz
tre = uno + due;
}
long uno = 0, due = 0; definizioni di var esterne
51
extern
All’interno di una funzione, la clausola extern associata ad una variabile la
identifica come variabile esterna definita dopo la funzione stessa o in altro file
Nelle funzioni che, nello stesso file, seguono la definizione di una variabile esterna, tutta la dichiarazione (con extern) è opzionale
fun3()
{ extern long abc; dichiarazione abc = 12;} utilizzo
long abc; definizione fun4()
{ extern long abc; dich. opzionale abc = 21;} utilizzo
52
Documentazione delle funzioni
E’ utile scrivere sempre la documentazione relativa allo scopo e all’uso di una funzione come commento iniziale contenente:
Scopo: a cosa serve la funzione
Parametri: tipo e descrizione di ciascuno
Valore restituito: tipo e descrizione
Possibilmente anche:
Pre-condizioni: requisiti particolari sui parametri che devono essere soddisfatti da chi invoca la funzione (es. param > 29)
Post-condizioni: garanzie date dalla funzione sul
valore restituito o sullo stato del programma,
purché le precondizioni siano state soddisfatte
(es. risultato >= 23 && risultato <= 32)
Chiamata di funzione - dettagli
Il programma compilato è costituito da due parti distinte:
code segment: codice eseguibile
data segment: costanti e variabili note alla compilazione (statiche ed esterne)
Quando il programma viene eseguito, il Sistema Operativo alloca spazio di memoria per:
il code segment (CS)
il data segment (DS)
lo stack e lo heap (condiviso)
Stack e heap
DATA CODE STACK
HEAP
Chiamata di funzione - dettagli
Lo stack contiene inizialmente le variabili locali della funzione main()
Lo heap inizialmente è vuoto e serve per contenere i blocchi di memoria
allocati dinamicamente con funzioni malloc() (trattate in altre slide)
Stack e heap crescono nell’area condivisa nel senso indicato dalle frecce
STACK
Stack e heap
HEAP DATI CODICE
55
Chiamata di funzione - dettagli
Quando viene chiamata una funzione, sullo stack vengono prima copiati i valori dei suoi argomenti e poi vi
viene allocato un Activation Record (o stack frame ) per contenere tutte le variabili locali (e altro)
Quando la funzione termina, gli argomenti e l’AR vengono rimossi dallo stack che quindi ritorna nello stato
precedente la chiamata
Stack e heap
HEAP DATI CODICE
STACK
56
Chiamata di funzione - dettagli
Nell’Activation Record viene anche
memorizzato l’ indirizzo di ritorno dalla
funzione: la locazione di memoria che contiene l’istruzione del chiamante da cui continuare dopo che la funzione è terminata
Queste operazioni di allocazione e deallocazione di spazio sullo stack e in
generale il meccanismo di chiamata e ritorno da una funzione richiedono tempo
In casi estremi di necessità di elevate performance si può cercare di limitare il numero delle chiamate a funzione, a costo di ricopiare lo stesso codice in più punti
(eventualm. utilizzando macro con argomenti )
Esercizi
1.
Si scriva un programma che per 10 volte chieda all’utente un valore e ne calcoli il logaritmo in base 2.
Per il calcolo del logaritmo si scriva una funzione con prototipo:
double log2(double a);
che calcoli il valore utilizzando la formula:
2 log log
2log
e
e
x
x
Esercizi
2.
Si scriva un programma che chieda all’utente 10 valori e di questi calcoli la radice quadrata.
Per il calcolo della radice quadrata si scriva una funzione con prototipo:
double radice(double a, double prec);
che calcoli il valore approssimato della radice quadrata di a con il metodo di Newton:
x
isono approssimazioni successive della radice quadrata di a. Si assuma x
0= a e si iteri
fintanto che x
i– x
i+1> prec .
i i
i
x
x a
x 2
1
1
59
Esercizi
3.
Si scriva una funzione con prototipo:
double media(double v[], int len);
che calcoli e restituisca la media di un vettore di double passato come argomento.
Si scriva un programma che riempia due vettori di lunghezza differente (es. a[8] e b[10], li passi a media e visualizzi il risultato per ciascuno di essi. La funzione non esegua operazioni di input/output.
60
Esercizi
4.
Si scriva una funzione con prototipo:
void rovescia(char s[]);
che rovesci la stringa passata come
argomento (modifica del parametro). Si scriva un programma che chieda una stringa, la passi a rovescia e la visualizzi.
5.
Si scriva una funzione con prototipo:
int contastr(char a[], char x[]);
che conti quante volte la stringa x sia contenuta in a. N.B. “bb” in “bbb”: 2 volte
6.
Si scriva una funzione undup che modifichi una stringa eliminandone i caratteri duplicati:
esempio: “ciao come va?” “ciao mev?”
Esercizi
7.
Si scriva una funzione con prototipo:
void ordina(int v[], int len, int ord);
che ordini in senso crescente e decrescente il vettore di int passato come argomento.
Il senso dell’ordinamento venga indicato dal parametro ord (decrescente=0, crescente=1).
Si scriva un main di test.
Esercizi
8.
Si scriva la funzione sommaVett che calcoli la somma di due vettori. Si scriva un main che chieda la dimensione dei vettori (max 100), ne chieda i valori, li passi alla funzione e visualizzi il vettore dei risultati. Non si alteri il contenuto dei due vettori da sommare. La funzione non faccia input/output.
Per non usare variabili esterne o variabili locali static, alla funzione bisogna passare anche il vettore dei risultati (il contenuto iniziale non è rilevante).
63
Esercizi
9.
Un file di testo denominato Parole.txt contiene una lista di parole, una per riga.
Non è noto a priori di quante righe sia composto. Si scriva un programma che chieda all’utente di introdurre una parola e visualizzi tutte le parole presenti nel file che anagrammate danno la parola introdotta. Si scrivano due funzioni: una che riordina alfabeticamente le lettere di una stringa (“telefono” “eeflnoot”) e un’altra che modifichi una stringa trasformandone tutti i caratteri in minuscolo.
64
Esercizi
10.
Si scriva una funzione con prototipo:
double media(double M[][10],
int righe, int colonne);
che calcoli e restituisca la media di una
generica matrice di 10 colonne passata come argomento. Si scriva un programma che riempia due matrici di dimensioni richieste all’utente (massime 8x10 e 12x10, il massimo di 10 colonne ciascuna è per coerenza con il prototipo), le passi a media e visualizzi il risultato per ciascuna di esse.
La funzione non faccia input/output.
Esercizi
11.
Scrivere una funzione con prototipo
int parseToIntVect(char *stringa, int vett[], int n);
che analizzi la stringa data estraendo da essa i primi n valori numerici interi e li
inserisca nel vettore vett[]. Se vi sono più di n valori, i successivi vengano ignorati. La funzione restituisca il numero di elementi letti (che possono quindi essere meno di n). Si supponga che il file non contenga caratteri diversi da cifre e whitespace. Si scriva un
main di test.
tvett[cont]Esercizi
Soluzione a stati
ws=white space
t=stringa per contenere i caratteri da trasf. in numero cont=contatore valori
c=carattere i-esimo della stringa
tvett[cont]
cont++
ct[0]
no-op
ct[i]
FUORI DENTRO
!ws !ws
ws ws
67
Esercizi
12.
Si scriva un programma che permetta di calcolare il prodotto di 2 numeri interi senza segno introdotti dall’utente e composti ciascuno da un massimo di 1000 cifre.
Conviene modularizzare il codice utilizzando opportune funzioni di supporto (ad esempio per moltiplicare un numero per una cifra, per fare lo shift di n posizioni a sinistra di un numero e per sommare due numeri).
68
Progetti multi-file
E’ possibile suddividere le funzioni che costituiscono un eseguibile su più file (detti translation unit )
Ciascun file viene compilato separatamente e il linker li assembla per costituire un unico file eseguibile
Uno solo dei file deve contenere la definizione della funzione principale main
L’insieme dei file sorgenti viene spesso chiamato progetto
In ciascun file si collocano funzioni che
insieme forniscono una certa funzionalità (ad
es. la gestione di uno stack)
Progetti multi-file
Ciascuno dei file si comporta come una
libreria di funzioni, salvo che queste vengono compilate (e non solo linkate) con il
programmma principale
Un file con funzioni specifiche per fornire una determinata funzionalità può essere
facilmente riutilizzato in altri programmi:
basta includerlo nel progetto
Progetti multi-file
Ciascun file ha bisogno delle sole direttive
#include e #define necessarie al codice di quel file
Per usare in un file una funzione dichiarata in un altro file (non può essere static), si deve indicarne il prototipo (extern opzionale)
Spesso si raggruppano tutte le #define, le variabili esterne e i prototipi di tutte le funzioni (non può essere static) di un progetto in un unico file .h e i file .c del progetto che ne abbisognano lo includono (con virgolette):
#include "mioheader.h"
71
Progetti multi-file
Le variabili esterne usate in tutti i file devono essere definite solo in uno dei file, mentre gli altri devono avere la dichiarazione extern corrispondente (vedere le slide sulla
compilazione condizionale)
Non si può usare extern con variabili esterne con specificatore di classe di allocazione static in un altro file (sono utilizzabili solo dalle funzioni di quel file)
72
Progetti multi-file – extern
La clausola extern permette di usare una variabile esterna anche in altri file, purché faccia parte dello stesso progetto
È compito del linker:
controllare che le varie dichiarazioni siano conformi alla definizione
controllare che la definizione sia unica
associare definizione e dichiarazioni alla stessa
zona di memoria
Progetti multi-file – static
Le variabili esterne con specificatore di classe di memorizzazione static hanno scope limitato alle funzioni definite in quel file (solo a quelle successive alla definizione): sono
“private” ad uso esclusivo delle funzioni di quel file
static int funzione(int x)
La keyword static si riferisce alla funzione, non al valore restituito
Linkage di variabili e funzioni
Il linkage esprime la corrispondenza di identificatori (variabili o costanti) omonimi presenti in più blocchi e/o in più translation unit diverse (linkate insieme) e/o in librerie
Un identificatore con linkage esterno è visibile in più translation unit (es. variabili e funzioni esterne non static)
Un identificatore con linkage interno è visibile solo nella translation unit dove è definito (es. variabili e funzioni static)
Un identificatore non ha linkage se è locale al blocco dove è definito (es. variabili locali, parametri di funzioni, tag, membri, etc.)
75
Linkage di variabili e funzioni
Un identificatore con linkage esterno è significativo almeno nei primi 6 caratteri, maiuscole e minuscole rappresentano caratteri uguali
Un identificatore con linkage interno è significativo almeno nei primi 31 caratteri, maiuscole e minuscole rappresentano caratteri diversi
Il linkage di un identificatore preceduto dalla clausola extern è quello stabilito dalla sua precedente dichiarazione nella stessa
translation unit (ad es. una dichiarazione con extern relativa ad una variabile definita static più sopra nel file ha linkage interno)
76
Lo stack
Uno stack (o pila) è una struttura dati di tipo LIFO (Last In First Out) in cui i valori vengono prelevati nell’ordine inverso a quello di
introduzione
Introduzione: push
Prelievo: pop
A
B A
C B A
B
A A
Esercizi
13.
Scrivere in un file separato la realizzazione di uno stack basato su un vettore di interi . Si realizzino le funzioni push e pop (con opportuni parametri ) che restituiscano 1 in caso di errore (stack pieno o vuoto) e 0 altrimenti (non devono fare I/O). Si scriva un main a menu in grado di verificarne il
funzionamento. I prototipi siano in un file .h.
Il vettore deve essere static perché solo push e pop ne abbiano accesso. Si utilizzi un puntatore p che punti alla prima locazione libera utilizzando le seguenti espressioni:
per push: *p++ = val;
per pop: val = *--p;
La coda
Una coda è una struttura dati di tipo FIFO (First In First Out) in cui i valori vengono prelevati nello stesso ordine di introduzione
Introduzione: enqueue
Prelievo: dequeue
A B A C B A
C B C
coda testa
79
Esercizi
14.
Scrivere in un file separato la realizzazione di una coda basata su un vettore di interi . Si scrivano le funzioni enqueue e dequeue (con opportuni parametri ) che restituiscano 1 in caso di errore (coda piena o vuota) e 0
altrimenti (non devono fare I/O). Si scriva un main a menu in grado di verificarne il
funzionamento. I prototipi siano in un file .h.
Si utilizzino due puntatori: testa punta alla cella con il prossimo valore da prelevare, coda punta alla prossima cella da riempire.
Il vettore sia static.
Si può notare che la enqueue è identica alla push.
80
Il buffer circolare (coda)
Testa e coda non sono più fisse, ma si rincorrono
E’ necessario un contatore delle posizioni libere per discriminare la condizione “pieno”
da “vuoto” in cui testa e coda coincidono
5 4 3 2 1 0 testa coda
C B A
1 2 3 4
5
0
A B C
testa
coda
Il buffer circolare (coda)
Aggiunta (in coda)
if ( non pieno ) aggiungi in coda fa avanzare coda posti liberi – 1
Prelievo (dalla testa)
if ( non vuoto ) preleva valore fa avanzare la testa posti liberi +1
Esercizio
15.