Benvenuti alle dispense di Informatica - Funzioni
In queste dispense vedremo come si usano le funzioni e le struct in C, con particolare attenzione ad alcuni casi tipici di utilizzo.
Il libro di testo adottato è CORSO DI INFORMATICA 3ED. - VOLUME 1 PER INFORMATICA di Fiorenzo Formichi, Giorgio Meini e altri, editore Zanichelli.
Ricordatevi che il lavoro dell'informatico non è aggiustare computer, passare dei cavi o scrivere un'applicazione. Il nostro mestiere è aiutare le persone a risolvere problemi. Dobbiamo quindi sempre avere bene in mente quale problema stiamo risolvendo e per chi, per fare in modo che il nostro lavoro sia veramente utile agli altri.
Funzioni
La funzione è un blocco di codice che può essere usato più volte con parametri diversi.
int somma(int a, int b) { // line 1
int c = a+b; // line 2
return c; // line 3
} // line 4
Analizziamo la struttura di una funzione:
- il primo
int
nella riga 1 è il tipo del valore di ritorno - la parola
somma
è il nome della funzione - le variabili
a
eb
sono i parametri della funzione - il blocco di codice nelle parentesi graffe è il codice che verrà eseguito dalla funzione
- nella riga 3, la parola chiave
return
fa restituire alla funzione il valorec
e termina il blocco
Nota: se la funzione non ritorna nessun valore, il tipo del valore di ritorno è
void
Abbiamo anche le seguenti definizioni:
- la riga 1 è chiamata dichiarazione della funzione, dove dichiaro appunto il nome della funzione, il valore di ritorno ed i parametri
- il blocco di codice viene chiamato definizione della funzione
La funzione può essere usata (si dice anche chiamata o invocata) nei seguenti modi:
int main() {
int s1 = somma(1,2);
int s2 = somma(5,9);
int s3 = somma(s1,10);
int s3 = somma(-4,s2);
int s4 = somma(s1,s3);
// etc...
return 0;
}
Quindi facciamo attenzione:
- il numero dei parametri con cui chiamo la funzione deve essere la stesso della definizione della funzione
- il tipo dei parametri deve essere lo stesso, e nello stesso ordine
Fate inoltre attenzione che il nome dei parametri all'interno della funzione dipende solo dalla dichiarazione della funzione, non da come viene chiamata! Nel nostro esempio, nel main chiamo la funzione in molti modi diversi, ma dentro la funzione il primo parametro si chiama sempre a
ed il secondo sempre b
.
In linea generale, posso scrivere una funzione che faccia qualsiasi cosa, ma la maggior parte delle volte le operazioni che voglio fare rientrano in alcuni casi tipici che è bene saper individuare e risolvere in maniera corretta. Nelle prossime pagine vedremo alcuni dei casi più comuni che riguardano funzioni che hanno in ingresso un array.
Riduzione
Quando ho una funzione che prende come parametro un singolo array e ritorna un unico valore scalare (cioè un valore unico, non un array), la funzione prende il nome di funzione di riduzione, perché riduce una serie di valori ad un singolo valore.
Ad esempio la somma applicata ad un array è una funzione di riduzione:
int somma(int array[], int len) { // line 1
int ret = 0; // line 2
for (int i = 0; i < len; i++) { // line 3
ret += array[i]; // line 4
} // line 5
return ret; // line 6
} // line 7
Le funzioni di riduzione hanno tutte la stessa struttura:
- nella dichiarazione, la funzione prende esattamente due parametri, un array e la lunghezza dell'array
- ritorna sempre un tipo (non può essere
void
) - il tipo di ritorno può essere diverso dal tipo degli elementi dell'array
- nella riga 2, dichiaro una variabile con lo stesso tipo del valore di ritorno
- all'interno ha un for che itera su tutti gli elementi dell'array
- all'interno del for eseguo un'operazione specifica per la funzione di riduzione che sto scrivendo
- l'ultima riga è un return che torna la variabile dichiarata nella riga 2
Questa funzione viene chiamata nel seguente modo:
int main() {
int arr[] = {10,15,18,20};
int s = somma(arr,4);
printf("La somma dell'array è: %d\n");
return 0;
}
Altri esempi
int prodotto(int array[], int len) {
int ret = 0;
for (int i = 0; i < len; i++) {
ret *= array[i];
}
return ret;
}
float media(int array[], int len) {
float ret = 0;
int somma = 0;
for (int i = 0; i < len; i++) {
counter += array[i];
}
ret = somma / len;
return ret;
}
int max(int array[], int len) {
// inizializzo il valore di ritorno con il primo valore dell'array
int ret = array[0];
for (int i = 0; i < len; i++) {
// controllo se l'elemento corrente è maggiore, in caso aggiorno ret
if (array[i] > ret) {
ret = array[i];
}
}
return ret;
}
Mappatura
La funzione di mappatura associa ad ogni elemento di un array un elemento di un altro array.
La struttura di una funzione di mappatura è la seguente:
void calcola_quadrato(int arr_in[], int arr_out[], int len){
for (int i = 0; i < len; i++) {
arr_out[i] = arr_in[i]*arr_in[i];
}
return; // opzionale quando la funzione ritorna void
}
Viene invocata nel seguente modo:
int main() {
int a1[] = {1,2,3,4};
int a2[4];
calcola_quadrato(a1,a2,4);
}
Cose a cui prestare attenzione:
- la lunghezza dell'array di input deve essere la stessa dell'array di output
- il tipo degli elementi dell'array di input può essere diverso del tipo degli elementi dell'array di output
Altri esempi
void trasforma_maiuscolo(char str_in[], char str_out[], int len){
for (int i = 0; i < len; i++) {
str_out[i] = str_in[i] - 32;
}
return;
}
void dimezza(char arr_in[], char arr_out[], int len){
for (int i = 0; i < len; i++) {
arr_out[i] = arr_in[i] / 2.0F; // importante .0F per forzare una divisione tra float
}
return;
}
Filtro
La funzione di filtro prende in ingresso un array e ritorna un array con alcuni elementi dell'array di ingresso.
Questa tipologia di funzione presenta un problema nuovo: prima di invocare la funzione, non so quanto sarà lungo l'array di output. Per risolvere il problema, posso usare l'allocazione di memoria dinamica, cioè la heap.
Gestione della heap
La gestione della heap è responsabilità del programmatore, che deve quindi assicurarsi che tutto il ciclo di vita della variabile sia corretto.
Nota: nella stack non dobbiamo preoccuparci del ciclo di vita della variabile perché è gestita automaticamente dal compilatore.
Il ciclo di vita è formato da:
- creazione: per creare una variabile nella heap, uso la funzione di sistema
malloc()
che prende in ingresso il numero di byte che voglio riservare in memoria; malloc ritorna un puntatore alla variabile appena creata - manipolazione: posso manipolare l'area di memoria in diversi modi, in particolare a noi interessa poter cambiare il numero di byte della memoria riservata, che si ottiene con la funzione di sistema
realloc()
- distruzione: la memoria nella heap si libera con la funzione di sistema
free()
Funzione di filtro
Detto questo, torniamo ora alla nostra funzione di filtro.
La struttura di una funzione di filtro è la seguente:
// voglio ritornare un array con solo i valori maggiori o uguali a 18
int* maggiorenne(int arr_in[], int n, int* counter) {
// all'inizio l'array di output ha stessa dimensione di arr_in
int* arr_out = malloc(sizeof(int)*n);
for (int i = 0; i < n; i++){
if (arr_in[i]>18) {
arr_out[*counter] = arr_in[i];
(*counter)++;
}
}
// realloco la memoria in modo da occupare solo lo spazio necessario
arr_out = realloc(arr_out,sizeof(int)* (*counter) );
return arr_out;
}
Posso chiamare questa funzione nel seguente modo:
int main() {
int x[] = {10, 20, 30};
int z_counter = 0;
int* z = maggiorenne(x,3,&z_counter);
printf("Numero di elementi filtrati: %d\n",z_counter);
for (int i = 0; i < z_counter; i++){
printf("Elemento %d filtrato: %d\n",i,z[i]);
}
free(z); // devo distruggere z quando non mi serve più!
return 0;
}
Si può visualizzare la gestione della memoria di questa funzione con PythonTutor:
Cose a cui prestare attenzione:
- il tipo degli elementi dell'array di input deve essere lo stesso del tipo degli elementi dell'array di output
- la lunghezza dell'array di input varia da un minimo di zero (array vuoto) ad un massimo della lunghezza dell'array di input
- ricordarsi sempre di distruggere la variabile (liberare la memoria) quando non uso più la variabile creata nella malloc, altrimenti genero un bug chiamato memory leak.
Strutture
Le strutture sono tipi di dato che permettono di raggruppare insieme più variabili per creare qualcosa di semanticamente più elaborato.
Vedi capitolo del libro A7 a pag.A187
Immaginiamo ad esempio di voler memorizzare le informazioni riguardanti gli studenti di una classe. Posso creare una struct che contenga tutte le informazioni insieme:
struct Nominativo {
char nome[20];
char indirizzo[20];
unsigned int anno; // anno di nascita
unsigned int mese; // mese di nascita
unsigned int giorno; // giorno di nascita
};
Nota: a differenza del libro, qui useremo solo la prima lettera maiuscola per il nome delle struct, che è una convenzione più usata. Anche tutto maiuscolo va bene comunque. NON va bene invece tutto minuscolo, in quanto il nome della struttura è un tipo, non una variabile, e quindi non può iniziare con una lettera minuscola.
Per utilizzare questa struttura:
int main(void) {
struct Nominativo prof; // dichiaro una variabile di tipo struct Nominativo
strncpy(prof.nome, "Claudio Capobianco", 20);
strncpy(prof.indirizzo, "Via Tal dei Tali", 20);
prof.anno = 2000;
prof.mese = 1;
prof.giorno = 1;
return 0;
}
Le variabili all'interno di una struct si definiscono anche campi
o attributi
.
Cose a cui prestare attenzione:
- deve essere sempre chiaro cosa deve contenere un campo di una struct, mettendo un nome autoesplicativo (es.
int anno_nascita
) oppure un commento (es.int anno; //anno di nascita
); senza questi accorgimenti non sarebbe chiaro cosa deve contenere la variabile (anno di nascita? iscrizione? diploma?)
Stampa dei campi di una struct
Ogni volta che creiamo una struct, è utile scrivere una funzione che ne stampi i valori in modo opportuno. Ad esempio, per la nostra struct nominativo:
void stampa_nominativo(struct NOMINATIVO n) {
printf("Nome: %s\n", n.nome);
printf("Indirizzo: %s\n", n.indirizzo);
printf("Data di nascita: %d/%d/%d\n", n.giorno, n.mese, n.anno);
}
Possiamo implementare la nostra funzione stampa in modo da rendere più leggibili i valori, ad esempio nel nostro caso abbiamo scritto la data di nascita in una sola riga, come si fa convenzionalmente.
Prestate sempre quindi attenzione a stampare i valori in modo corretto:
- tutte le grandezze devono avere l'unità di misura (metri, secondi, anni, litri, etc.)
- i valori devono essere stampati nel modo in cui l'utente se li aspetta (ad esempio, non 1.5 ore ma 1h 30m)
Uso delle funzioni con strutture: riduzione
Le funzioni che abbiamo visto con gli array di elementi semplici possono essere facilmente estesi con array di strutture.
Scenario
Immaginiamo di avere un negozio di animali. Creiamo la struttura per rappresentare un animale in vendita.
struct Animale {
char specie[20];
char razza[20];
int eta; // giorni
float peso; // chilogrammi
char mangime[50];
float prezzo_vendita; // euro
};
Scriviamo anche la funzione di stampa:
void stampa_animale(struct Animale animale) {
printf("Specie %s, razza %s, ha %d giorni, pesa %.1f kg, "
"mangia %s.\n", animale.specie, animale.razza, animale.eta, animale.peso,
animale.mangime);
printf("Il prezzo di vendita è %.2f euro.\n", animale.prezzo_vendita);
}
Ed infine testiamo la funzione nel main:
int main() {
struct Animale gatto = {"gatto", "siamese", 100, 1.2F, "cibo per gatti", 600.00F};
stampa_animale(gatto);
return 0;
}
Stampa migliorata
Quando stampiamo l'età, è utile stamparla in modo più chiaro per l'utente.
Vogliamo che:
- se l'età è inferiore ad un mese, stampiamo solo i giorni
- se l'età è maggiore di un mese ma minore di un anno, stampiamo mesi e giorni
- se l'età è maggiore di un anno, stampiamo anni, mesi e giorni
- è accettabile un'età approssimativa con mesi di 30 giorni e anni di 365 giorni
La nuova funzione di stampa diventa:
void stampa_animale(struct Animale animale) {
int anni = animale.eta / 365;
int mesi = (animale.eta % 365) / 30;
int giorni = (animale.eta % 365) % 30;
printf("Specie %s, razza %s, pesa %.1f kg, "
"mangia %s.\n",
animale.specie, animale.razza, animale.peso, animale.mangime);
if (animale.eta <= 30) {
printf("L'animale ha %d giorni\n", giorni);
} else if (animale.eta < 365) {
printf("L'animale ha %d mesi e %d giorni\n", mesi, giorni);
} else {
printf("L'animale ha %d anni, %d mesi e %d giorni\n", anni, mesi, giorni);
}
printf("Il prezzo di vendita è %.2f euro.\n", animale.prezzo_vendita);
}
Array di struct
Creiamo un array con tutti gli animali del negozio.
Ci sono diversi metodi per creare un array di struct, ne facciamo vedere alcuni. Tutti i seguenti blocchi portano allo stesso risultato.
// Dichiaro gli oggetti e poi li assegno
struct Animale gatto = {"gatto", "siamese", 100, 1.2F, "cibo per gatti", 600.00F};
struct Animale cane = {"cane", "labrador", 80, 5.5F, "croccantini", 800.00F};
struct Animale animali[2];
animali[0] = gatto;
animali[1] = cane;
// Dichiaro gli oggetti e poi li assegno nell'inzializzazione dell'array
struct Animale gatto = {"gatto", "siamese", 100, 1.2F, "cibo per gatti", 600.00F};
struct Animale cane = {"cane", "labrador", 80, 5.5F, "croccantini", 800.00F};
struct Animale animali[] = {gatto, cane}; // non serve specificare la dimensione dell'array
// Dichiaro l'array e poi lo assegno
struct Animale animali[2];
strncpy(animali[0].specie,"gatto",20);
strncpy(animali[0].razza,"siamese",20);
animali[0].eta = 100;
animali[0].peso = 1.2F;
strncpy(animali[0].mangime,"cibo per gatti",50);
animali[0].prezzo_vendita = 600.00F;
strncpy(animali[1].specie,"cane",20);
strncpy(animali[1].razza,"labrador",20);
animali[1].eta = 80;
animali[1].peso = 5.5F;
strncpy(animali[0].mangime,"croccantini",50);
animali[1].prezzo_vendita = 800.00F;
// Dichiaro l'array e lo inizializzo nella stessa riga
struct Animale animali[] = {
{"gatto", "siamese", 100, 1.2F, "cibo per gatti", 600.00F},
{"cane", "labrador", 80, 5.5F, "croccantini", 800.00F}
};
Quale uso? Ovviamente dipende, da quanto è grande la struct, se ho già gli oggetti da inserire, preferenze personali, etc. In generale preferire sempre la chiarezza e rendere difficili gli errori, quindi se la struttura ha tanti campi, meglio non rischiare di sbagliare facendo inizializzazioni manuali troppo lunghe.
Riduzione
Calcoliamo il prezzo medio degli animali del negozio. L'impostazione della funzione è la stessa che abbiamo già visto.
float calcola_media(struct Animale animali[], int len) {
float ret = 0.0F;
float somma = 0.0F;
for (int i = 0; i < len; i++) {
somma += animali[i].prezzo_vendita;
}
return somma/len;
}
Uso delle funzioni con strutture: mappatura
Immaginiamo di voler inserire gli animali in un catalogo, nel quale però ci interessa salvare solo specie e razza; vogliamo inoltre aggiungere un identificativo sotto forma di numero intero.
Creiamo una nuova struct AnimaleCatalogo
e la relativa funzione di stampa.
struct AnimaleCatalogo {
char specie[20];
char razza[20];
int id; // identificativo
};
// Creo la funzione di stampa della nuova struct
void stampa_animale_catalogo(struct AnimaleCatalogo animale) {
printf("animaliCatalogo: id %d, specie %s, razza %s\n", animale.id,
animale.razza, animale.specie);
return;
}
Ora scriviamo la funzione che mappa l'array degli animali con quelli di animali catalogo. Copiamo solo i campi che ci interessano e per gli altri mettiamo dei valori di default come richiesto dallo scenario.
void mappa_catalogo(struct Animale arr_in[],
struct AnimaleCatalogo arr_out[], int len) {
for (int i = 0; i < len; i++) {
strncpy(arr_out[i].specie, arr_in[i].specie, 20);
strncpy(arr_out[i].razza, arr_in[i].razza, 20);
arr_out[i].id = i; // come identificativo metto la posizione nell'array
}
return;
}
int main() {
struct Animale animali[] = {
{"gatto", "siamese", 100, 1.2F, "cibo per gatti", 600.00F},
{"cane", "labrador", 80, 5.5F, "croccantini", 800.00F}};
struct AnimaleCatalogo animaliCatalogo[2];
mappa_catalogo(animali, animaliCatalogo, 2);
for (int i = 0; i < 2; i++) {
stampa_animale_catalogo(animaliCatalogo[i]);
}
return 0;
}
Uso delle funzioni con strutture: filtro
Immaginiamo di volere un array con solo gli animali con un prezzo inferiore ai 700 euro.
Creiamo la funzione filtra_prezzo
per questo scopo.
struct Animale *filtra_prezzo(struct Animale animali[], int n, int *counter) {
struct Animale *arr_out = malloc(sizeof(struct Animale) * n);
for (int i = 0; i < n; i++) {
if (animali[i].prezzo_vendita <= 700.0F) {
arr_out[*counter] = animali[i];
(*counter)++;
}
}
arr_out = realloc(arr_out, sizeof(struct Animale) * (*counter));
return arr_out;
}
Ora testiamo la funzione nel main, per controllare che il nuovo array contenga effettivamente solo gli animali con il prezzo richiesto.
int main() {
struct Animale animali[] = {
{"gatto", "siamese", 100, 1.2F, "cibo per gatti", 600.00F},
{"cane", "labrador", 80, 5.5F, "croccantini", 800.00F}};
int counter = 0;
struct Animale *animaliEconomici =
filtra_prezzo(animali, 2, &counter);
for (int i = 0; i < counter; i++) {
stampa_animale(animaliEconomici[i]);
}
free(animaliEconomici);
}