Le funzioni nella programmazione imperativa

Da aptiva.

Indice

Classe destinataria e scuola

Questa Unità Didattica è rivolta a studenti di una classe terza dell’Istituto Tecnico indirizzo Informatica e Telecomunicazioni, articolazione Informatica, per i quali sono previste 6 ore settimanali di Informatica.

Collocazione del tema all’interno dei programmi ministeriali

L’argomento trattato in tale Unità Didattica compare in diversi punti delle Linee Guida per gli Istituti Tecnici, sia nel Primo Biennio che nel Secondo Biennio.

Per quanto riguarda il Primo Biennio, nella disciplina “Tecnologie Informatiche”, il tema concorre al raggiungimento della seguente competenza generale:

“individuare le strategie appropriate per la soluzione di problemi”.

In termini di conoscenze e abilità nelle Linee Guida è riportato

Conoscenze Abilità
Fondamenti di programmazione Impostare e risolvere problemi utilizzando un linguaggio di programmazione

Per quanto riguarda il Secondo Biennio, nella disciplina “Informatica”, il tema concorre al raggiungimento della seguente competenza:

“Utilizzare le strategie del pensiero razionale negli aspetti dialettici ed algoritmici per affrontare situazioni problematiche elaborando opportune soluzioni”

In termini di conoscenze e abilità nelle Linee Guida è riportato

Conoscenze Abilità
Paradigmi di programmazione
Logica iterativa e ricorsiva
Analizzare e confrontare algoritmi diversi per la soluzione dello stesso problema

Tempi dell’intervento

Per questa Unità Didattica si ipotizza che saranno necessarie circa 20 ore di lezione, corrispondenti a circa 3 settimane, così suddivise:

Lezioni teoriche frontali e dialogiche 6 ore
Esercizi in classe 6 ore
Esercizi in laboratorio 5 ore
Verifica sommativa 2 ore
Consegna e correzione della verifica sommativa 1 ora

Prerequisiti

  • Conoscenza del concetto di algoritmo;
  • conoscenza della simbologia dei diagrammi di flusso;
  • conoscenza di uno pseudocodice.
  • Conoscenza del linguaggio di programmazione scelto:
    • tipi di dato;
    • variabili e costanti;
    • operatori aritmetici e logici;
    • struttura di un programma;
    • istruzioni condizionali;
    • istruzioni di iterazione.

Competenze - Obiettivi di apprendimento

Lo scopo di questa Unità Didattica è quello di far acquisire agli studenti alcune competenze esprimibili in termini di conoscenze e abilità.

Conoscenze

  • Procedimento algoritmico;
  • definizione di problema e sottoproblema;
  • sintassi di dichiarazione, definizione e chiamata di funzioni in pseudocodice;
  • concetti di scomposizione dei programmi top-down e bottom-up;
  • concetto di visibilità delle variabili;
  • modalità di passaggio delle variabili alle funzioni.

Abilità

  • Pervenire alla soluzione di problemi utilizzando logiche elaborative;
  • esprimere procedimenti risolutivi attraverso algoritmi;
  • formalizzare gli algoritmi utilizzando formalismi diversi;
  • applicare la metodologia top-down nella scomposizione dei problemi;
  • comprendere la visibilità di una variabile;
  • far buon uso dei parametri per creare un buon processo d'interfacciamento tra i sottoprogrammi.

Metodologie didattiche

Questa Unità Didattica è svolta in parallelo al Percorso Didattico su un linguaggio di programmazione, in modo da affiancare le esercitazioni in aula all’applicazione pratica svolta in laboratorio.

In generale si alterneranno momenti di lezione frontale e di lezione dialogata, lavoro in gruppo e lavoro individuale, esercitazioni in classe e in laboratorio di informatica. L'insegnante presenterà i contenuti introducendo gli argomenti attraverso la proposta di situazioni problematiche, alla quale seguirà la spiegazione e le esercitazioni sia in classe che in laboratorio, al fine di formalizzare e fissare i concetti.

Si cercherà di coinvolgere gli studenti con domande mirate, con lo svolgimento di esercizi alla lavagna, con attribuzione di esercizi e problemi da risolvere in gruppo o individualmente come compito per casa e anche con eventuali esempi di teatralizzazione.

Materiali e strumenti utilizzati

Libro di testo adottato, appunti del docente, schede di esercitazioni e per il lavoro di gruppo, lavagna, gessi, laboratorio di informatica, compilatore e IDE per il linguaggio di programmazione, videoproiettore.

Sviluppo dei contenuti

Introduzione: concetto e utilizzo di sottoprogrammi

Tempi: 2 ore di lezione teorica

Si introduce il concetto di funzione attraverso la metodologia top-down, cercando di spiegare come si scompone un problema in tanti sottoproblemi semplici riducendo la complessità. Questa tecnica non riguarda solo l'analisi del problema, ma si ripercuote nella creazione degli algoritmi, implementando i sottoproblemi come sottoprogrammi. Se all'inizio dell’anno scolastico sono state dedicate alcune ore di lezione alla storia dell'Informatica, si può far presente agli studenti che si suppone che la prima ad ipotizzare il concetto di sottoprogramma fu Ada Lovelace.

Si inizierà proponendo agli alunni di risolvere un problema complesso relativo ad una situazione di vita quotidiana, che può essere scelta dagli stessi studenti, ad esempio "organizzare una cena di compleanno in pizzeria con i propri amici", "preparare la colazione" o "preparare la merenda".

Consideriamo ad esempio "preparare la merenda", si farà osservare che tale problema si può decomporre in altri sottoproblemi più semplici e si scriverà alla lavagna una possibile scomposizione:

  1. preparare un panino con la crema di nocciole
  2. preparare una spremuta di arance
  3. servire in tavola la merenda

Anche i tre sottoproblemi sono a loro volta scomponibili, ad esempio "preparare una spremuta di arance" si può decomporre in

2a) prendere 2 arance dal portafrutta

2b) prendere lo spremiagrumi dalla dispensa

2c) spremere le arance

2d) versare la spremuta in un bicchiere

Prima di procedere con la formalizzazione dell'esempio, è necessario trattare il discorso della scomposizione di un problema in generale, dando la seguente definizione:

dato un problema complesso X, un secondo problema Y che con altri concorre a costituire X, è detto sottoproblema del problema X. 
Anche X può essere a sua volta sottoproblema di un altro problema.

Nel nostro esempio: i problemi 2a, 2b, 2c, 2d sono sottoproblemi di 2 che è a sua volta sottoproblema del problema di partenza.

La scomposizione in sottoproblemi può essere descritta con il seguente diagramma, che si chiama grafo ad albero, i cui nodi rappresentano problemi e sottoproblemi.

Sottoprogramma.jpg

La metodologia che si sta utilizzando è detta top-down (“dall’alto verso il basso”), essa è una tecnica di sviluppo degli algoritmi nella quale si affronta il problema prima dal punto di vista generale e in seguito si occupa di rifinire le sue parti. Tale metodologia è basata sul concetto di sottoprogramma e consiste nell’associare al problema generale (top) un programma, detto programma principale, che viene suddiviso in tanti sottoprogrammi più semplici, scomponibili a loro volta fino ad arrivare a sottoprogrammi elementari (down).

Si ritiene necessario ricordare agli studenti che esiste una tecnica alternativa a questa, la metodologia bottom-up (“dal basso verso l’alto”), nella quale si affronta il problema partendo dallo sviluppo dai dettagli che lo compongono fino a giungere alla risoluzione del caso più complesso.

Possiamo ora proporre in pseudocodice il problema di “preparare la merenda”:

ALGORITMO Preparare la merenda
PROGRAMMA Principale
INIZIO
  Preparare un panino con la crema di nocciole
  Preparare una spremuta di arance
  Servire in tavola la merenda
FINE

Come esempio di sottoprogramma si può svolgere in pseudocodice “preparare una spremuta di arance”, invitando poi gli studenti a fare gli altri come compito a casa.

SOTTOPROGRAMMA Preparare una spremuta di arance
INIZIO
 Prendere 2 arance dal portafrutta
 Prendere lo spremiagrumi dalla dispensa
 Spremere le arance
    Versare la spremuta in un bicchiere 
FINE

Successivamente, si potrebbe chiamare alla lavagna un alunno, proporre l’analisi di un ulteriore problema e il riconoscimento dei sottoproblemi in cui può essere scomposto.

A questo punto si formalizzeranno i concetti dando la definizione di sottoprogramma presa dal libro di testo (Gallo & Salerno, 2013).

Un sottoprogramma è una parte del programma che risolve un particolare sottoproblema. Tecnicamente è una parte dell’algoritmo 
risolutivo che non può essere eseguita autonomamente, ma soltanto su richiesta (invocazione) e sotto stretto controllo del
programma o del sottoprogramma che lo invoca, il quale per tale motivo è denominato “chiamante”.

Infine, chiediamo agli studenti quando convenga descrivere un’attività mediante un sottoprogramma e, guidandoli, con opportuni suggerimenti li si farà giungere a queste conclusioni:

  1. quando la sequenza d'istruzioni risolve un problema che può essere d'interesse anche al di fuori del nostro specifico, quindi si favorisce il riutilizzo del codice;
  2. quando le istruzioni si presentano più volte nel programma: i sottoprogrammi permettono di scrivere meno codice e dunque di occupare meno memoria;
  3. per rendere il programma più leggibile;
  4. per favorire la scrittura corretta di un programma: si evitano errori, infatti riscrivere o copiare il codice può generare o moltiplicare gli errori;
  5. per controllare più facilmente la struttura complessiva del programma: la suddivisione in parti permette di comprendere il programma (astrazione dai dettagli), di implementare ed eventualmente modificare più facilmente il codice.

Le funzioni

Tempi: 1 ora di lezione teorica + 2 ore di esercizi in classe + 2 ore di esercizi in laboratorio

Possiamo iniziare la lezione ponendo agli studenti la seguente domanda: “cosa permette ai programmatori di suddividere un programma in sottoprogrammi?” La risposta è “le funzioni”.


Nota didattica. Possiamo far presente agli alunni che in realtà in tutti i programmi svolti fino ad ora abbiamo già utilizzato una funzione: la funzione principale main.

Inoltre, abbiamo utilizzato altre funzioni come printf, scanf e rand incluse nella libreria standard del linguaggio C e le funzioni della libreria matematica, come sqrt, pow. Il nostro scopo è ora quello di spiegare come creare funzioni personalizzate da aggiungere a quelle fornite direttamente dalle librerie del linguaggio e quindi scrivere i nostri programmi come combinazione di questi due tipi di funzioni.

Procediamo con definire le funzioni e la loro struttura, cercando di proporre sempre esempi per fissare e chiarire i concetti.


Definizione

Una funzione è un sottoprogramma che in base a dei parametri ricevuti in input produce un solo ed unico risultato in output.


Questo concetto è molto simile a quello di funzione matematica: infatti, dati N valori in ingresso viene restituito un solo valore in uscita, però differisce dal suo corrispettivo matematico perché permette di eseguire altre azioni oltre che calcolare valori (come ad esempio la stampa di un valore).

Fino ad ora abbiamo visto che i blocchi d’istruzioni venivano racchiusi all’interno della funzione main, richiamata quando si esegue un programma, analogamente una funzione viene definita con il seguente blocco ad essa dedicato:

tipo_risultato nome_funzione(lista_parametri)
{
  blocco di istruzioni
  istruzione_ritorno;
}

La prima riga

tipo_risultato nome_funzione(lista_parametri)

rappresenta l’intestazione della funzione, in cui vengono definiti il nome, i parametri con i relativi tipi e il tipo del valore restituito.

Il nome_funzione è un qualsiasi nome, che generalmente per comodità e leggibilità del codice rispecchia lo scopo della funzione. Il tipo_risultato è il tipo del risultato che viene restituito (int, float, char, etc.), se non specificato per il compilatore è sempre un int. Nel caso in cui non venga restituito nessun valore avremo come tipo void, un valore vuoto, che si può dire equivalga alle vecchie procedure stile Pascal. La lista_parametri è un insieme di parametri, detti parametri formali, indicati con tipo e nome separati da una virgola

tipo1 param1,tipo2 param2,…,tipoN paramN

e sono valori che la funzione utilizzerà per produrre un risultato. Ricordiamo che è anche possibile non avere nessun parametro in ingresso, come vedremo in seguito negli esempi.L’istruzione_ritorno è l’istruzione return che indica il valore che verrà restituito dalla funzione nel caso in cui il tipo_risultato non sia void.

Il blocco delle istruzioni racchiuso tra parentesi graffe rappresenta il corpo della funzione.

Vediamo ora qualche esempio per rendere più chiaro ciò che abbiamo detto.

Come primo esempio vediamo una funzione che esegue e restituisce il prodotto fra due variabili intere.

int prodotto(int a, int b)
{
  return a*b;
}

Osserviamo che la funzione prodotto attende in ingresso due parametri interi a e b e restituirà alla funzione chiamante un numero intero, che è il risultato del calcolo a*b.

Vediamo ora l’esempio di una funzione che non restituisce nessun valore. Essa attende in ingresso un numero intero e restituirà alla funzione chiamante non un valore, ma la stampa dell’intero in ingresso.

void stampa(int intValore)
{
  printf(“%d”, intValore);
}

Vediamo una funzione senza parametri, evidenziando che è necessario indicare l’assenza di parametri mettendo tra le parentesi void. Essa non attende in ingresso alcun parametro, però restituirà alla funzione chiamante un numero intero (valore inserito quando la funzione verrà invocata dalla funzione chiamante).

int inserimento_valore(void)
{
  int valore = 0;
  printf(“Inserisci un valore: ”);
  scanf(“%d”, &valore);
  return valore;
}

Infine vediamo come ultimo esempio una funzione senza parametri e che non restituisce nessun valore. Tale funzione può essere usata ad esempio per stampare più volte una serie di messaggi molto lunghi.

void saluto(void)
{
  printf(“Ciao a tutti”);
}

Invocazione

Ora vediamo come eseguire le istruzioni presenti in una funzione, ossia come invocare o richiamare una funzione. Una funzione può essere richiamata all’interno di un’altra funzione oppure dal main, specificando gli opportuni parametri.

L’invocazione è costituita dal nome della funzione e dalla lista ordinata dei parametri, detti parametri attuali, tra parentesi tonde

nome_funzione(parametri attuali)

Abbiamo visto che nell’intestazione delle funzioni sono presenti i parametri formali, che rappresentano i valori che la funzione utilizzerà per produrre un risultato, mentre nell’invocazione i parametri che passiamo alla funzione sono detti parametri attuali, che dunque rappresentano i valori assunti dai parametri formali all’interno della funzione nel momento in cui essa viene invocata. Più precisamente, i parametri formali sono nomi, mentre quelli attuali sono valori e i parametri formali assumono il valore contenuto nei parametri attuali nel momento in cui la funzione viene invocata. Per questo motivo possiamo chiamare con lo stesso nome o anche con nomi diversi i parametri attuali e quelli formali: infatti, la corrispondenza tra i due tipi di parametri non dipende dal nome, ma dalla posizione all’interno dell’invocazione, durante la quale i parametri attuali devono essere tanti quanti sono quelli attuali, dello stesso tipo e ordinati allo stesso modo.

Per maggior chiarezza, consideriamo per esempio la funzione che calcola e restituisce il valore assoluto di un numero reale.

Parametri.jpg

Vediamo ora gli esempi di invocazione nel main di alcune delle funzioni definite precedentemente.

Esempio di programma che utilizza la funzione prodotto

int prodotto(int a, int b)        /*dichiarazione della funzione*/ 
{
  return a*b;
}
int main(void)
{
  int prod = 0, x = 4, y = 5;
  prod = prodotto(x, y);          /*invocazione della funzione*/       
  printf(“Il prodotto è %d”, prod);
  return 0;
}

Nell’invocazione i parametri attuali x e y sono i valori che assumono i parametri formali a e b della funzione e sono associati ad essi per posizione: il valore di x viene assegnato ad a e il valore di y a b.

Osserviamo inoltre che, se lo scopo del programma era solamente quello di calcolare e stampare il prodotto di due numeri interi, invece di salvare il valore di ritorno di prodotto in una variabile intera prod,nel main si poteva procedere così:

int main(void)
{
  int x = 4, y = 5;
  printf(“Il prodotto è %d”, prodotto(x, y));
  return 0;
}

Vediamo un esempio più complesso, nel cui main vengono invocate tutte le funzioni definite in precedenza:

scrivere un programma che dati in input due numeri interi calcoli e stampi il loro prodotto.

int main(void)
{
  int prod = 0, x = 0, y = 0;
  x = inserimento_valore();
  y = inserimento_valore();
  prod = prodotto(x, y);
  stampa(prod);
  return 0;
}

Prototipo

Il prototipo di una funzione è la sua semplice dichiarazione, dove viene specificata la funzione omettendo il blocco di istruzioni. Esso viene scritto in generale prima del main, permette di verificare l’esatta corrispondenza dei parametri e di rendere più facile la lettura del codice. Questo è necessario nel caso in cui si voglia invocare una funzione prima ancora di averla definita oppure se è presente in un altro file.

Questa caratteristica è stata implementata nell’ANSI C, nelle versioni precedenti non era presente, prendendo l’idea dal linguaggio di programmazione C++.

Il prototipo è costituito dal tipo del valore restituito, dal nome della funzione e dalla lista ordinata dei tipi di parametri formali tra parentesi tonde

tipo-risultato nome_funzione(tipo1, tipo2,…,tipoN);

essendo un’istruzione termina con punto e virgola.

Vediamo ora un esempio di prototipi riutilizzando le funzioni viste in precedenza.

int prodotto(int, int);
void stampa(int);
int inserimento_valore(void);

Concludiamo con un esempio completo:

scrivere un programma che, preso in input il lato di un quadrato, calcoli e stampi la sua area.

#include<stdio.h>
float areaQuadrato(float);       /*prototipo della funzione*/
int main(void)
{
  float lato = 0;
  printf(“Inserire la lunghezza del lato: ”);
  scanf(“%f”, &lato);
  printf(“Il valore dell’area è: %f”, areaQuadrato(lato));
  return 0;
}
float areaQuadrato(float lato)    /*dichiarazione della funzione*/
{
  return lato*lato;
}

Esercizi

Riportiamo ora alcuni esempi di esercizi da svolgere prima in classe o per compito a casa, che successivamente saranno riproposti in laboratorio.

Quando si assegnano esercizi agli studenti in aula, è importante lasciare un po’ di tempo affinché ognuno di essi imposti la propria strategia risolutiva. Nel frattempo l’insegnante può dedicarsi agli alunni che presentano maggiori difficoltà per guidarli nella risoluzione degli esercizi. Successivamente, si può chiamare alla lavagna un alunno per presentare il proprio metodo risolutivo ed eventualmente commentarlo insieme alla classe.

  1. Creare una funzione che stampa un triangolo di lato N disegnato utilizzando il carattere “#”.
  2. Scrivere un programma che calcoli e restituisca la differenza tra i quadrati di due numeri.
  3. Date in input le misura della base e dell’altezza di un rettangolo, calcolare e stampare il perimetro e l’area del rettangolo.
  4. Dato in input il prezzo di un articolo e la percentuale di sconto, calcolare e stampare lo sconto sul prezzo e il valore del prezzo scontato.
  5. Scrivere un programma, utilizzando una funzione, che dato in input un numero intero maggiore di 1 dica se è primo oppure no.
  6. Scrivere un programma che calcoli il discriminante di un’equazione di secondo grado ax2+bx+c=0. La funzione da utilizzare deve avere come parametri formali i coefficienti a, b, c dell’equazione e restituisce il valore -1 se l’equazione è impossibile, altrimenti restituisce il valore del discriminante.
  7. Sia data una botte d’acqua vuota di N litri. La botte viene riempita utilizzando esclusivamente due caraffe, una da 10 litri e una da 3 litri. Creare una funzione che restituisca il numero di caraffe per ogni tipo necessarie per riempire il più possibile la botte.
  8. Versione in linguaggio C dell’esercizio riportato nel libro di testo (Formichi & Meini, 2012). “Un famoso “gioco” matematico consiste nel costruire una sequenza di numeri interi in modo che ogni numero sia la metà del precedente se esso è pari, altrimenti il successore del suo triplo: partendo da un numero qualsiasi si procede fino a raggiungere 1. Scrivere una funzione in linguaggio C che abbia come argomento il numero iniziale e restituisca il numero di iterazioni necessario per concludere la sequenza. Scrivere un programma in C che, utilizzando la funzione precedente, visualizzi la lunghezza delle sequenze per numeri iniziali compresi tra due valori forniti in input”.

Nota didattica per i docenti: errori commessi più frequentemente dagli studenti

Nel trattare in classe gli argomenti presentati fin'ora, ho cercato di fare un elenco degli errori commessi più frequentemente dagli studenti, che riporto qui in seguito in modo da segnalare gli aspetti sui quali riporre più attenzione.

Il primo problema che ho riscontrato è quello che gli alunni cercano di sviluppare i programmi mediante una funzione unica con diversi compiti. Consideriamo l’esercizio 3 della lista precedente “Date in input le misura della base e dell’altezza di un rettangolo, calcolare e stampare il perimetro e l’area del rettangolo” e vediamo una possibile versione proposta da un mio studente:

void rettangolo(float base, float altezza)
{
  float perimetro = 0, area = 0;
  perimetro = 2 * (base + altezza);
  area = base * altezza;
  printf(“Il perimetro del rettangolo vale %f”, perimetro);
  printf(“L’area del rettangolo vale %f”, area);          
}
int main(void)
{
  float x, y, p = 0, a = 0;
  printf(“Inserisci la misura della base del rettangolo: ”);
  scanf(“%f”, &x);
  printf(“Inserisci la misura dell’altezza del rettangolo: ”);
  scanf(“%f”, &y);
  rettangolo(x,y);
  return 0;
}

Tale risoluzione dell’algoritmo è corretta, però, per rendere il codice più semplice da scrivere o modificare e per facilitarne il suo riutilizzo, sarebbe meglio utilizzare più funzioni che eseguano un singolo e ben definito compito. Ad esempio, in questo caso si potrebbero definire le funzioni perimetroRettangolo e areaRettangolo per calcolare e restituire come valore di ritorno rispettivamente il perimetro e l’area del rettangolo. La stampa dei due valori può essere svolta nel main dopo aver invocato le due funzioni così definite.

Consideriamo ora l’esercizio 2 della lista precedente: “Scrivere un programma che calcoli e restituisca la differenza tra i quadrati di due numeri”. Ecco esempi di codice errato svolto da alcuni miei alunni.

int differenzaQuadrati(int a, int b)
{
  int a, b;
  return a*a – b*b;
}           

L’errore commesso è quello di dichiarare i parametri formali anche come variabili locali della funzione.

int differenzaQuadrati(int a, int b)
{
  int diff = 0;
  diff = a*a – b*b;
}           

Il codice è errato, perché manca l’istruzione return diff;

In questo caso, hanno dimenticato di restituire un valore in una funzione che dovrebbe farlo.

int differenzaQuadrati(int a, int b)
{
  int diff = 0;
  diff = a*a – b*b;           
  printf(“%d”, diff);    
}

Il codice è errato perché l’istruzione return diff; oppure visto che la funzione stampa direttamente la differenza di quadrati senza restituire alcun valore, allora nell’intestazione si doveva dichiarare void il tipo di valore di ritorno.

Un altro errore comune è quello di cercare di restituire un valore ad una funzione dove il tipo da ritornare è dichiarato void.

Visibilità e ciclo di vita delle variabili

Tempi: 1 ora di lezione teorica + 1 ora di esercizi in classe

Attraverso le funzioni viene introdotto un nuovo importante concetto che è quello della visibilità delle variabili e il loro ciclo di vita. Questo concetto viene visto attraverso la definizione di variabile globale e locale.

Una variabile globale è dichiarata all’esterno di ogni blocco di istruzione ed è visibile in tutto il programma a partire dal punto in cui è stata dichiarata in poi, inoltre esiste finché il programma è in esecuzione.

Una variabile locale è definita e visibile solo all’interno di un blocco di istruzioni, il suo ciclo di vita è limitato a quel singolo blocco.

Una cattiva abitudine è definire variabili locali di un blocco con lo stesso nome di variabili più esterne, commettendo l’errore di pensare di modificare il valore di quella più esterna e invece quella a cui ci si riferisce è sempre quella relativa al blocco che si sta eseguendo.

Vediamo ora un esempio di variabili globale e locale.

#include<stdio.h>
int i = 0;
int inc(void)
{
  int j = 0;
  i++;
  return j + 1;
}
int main(void)
{
  int k = 0;
  k= inc();
  i++;
  printf(“%d %d”, i, k);
  return 0;
}

Nell’esempio precedente vediamo che i è una variabile globale, il cui valore viene modificato sia nella funzione inc che nel main e questo è possibile perché la variabile è visibile in tutto il programma. Mentre j e k sono variabili locali utilizzabili solo all’interno dei blocchi di istruzioni dove sono state definite.

Vediamo ora l’esempio con lo stesso nome ai due tipi di variabili.

#include<stdio.h>
int i = 0;
int inc(void)
{
  int i = 0;
  i++;
  return i + 1;
}

Nell’esempio precedente la variabile globale non viene mai modificata, come già spiegato sopra.

Per verificare se gli studenti hanno compreso il concetto di visibilità delle variabili, possiamo scrivere alla lavagna il codice di un algoritmo, in cui sono presenti sia variabili globali che locali (aventi eventualmente lo stesso nome) e chiedere alla classe quali valori verranno stampati nell’esecuzione del programma. Vediamo un possibile esempio.

#include<stdio.h>
int w = 3;                     /*variabile globale*/
void primaFunzione(void)
{
  w = 5;
  int x = 3;                   /*variabile locale*/
}
void secondaFunzione(void)
{
  int w = 6;                   /*variabile locale*/
  printf(“%d”, w);
}
int terzaFunzione(void)
{
  return w + 1;
}
int main(void)
{
  printf(“%d”, w);
  primaFunzione();
  printf(“%d”, w);
  if(w != 3)
  {
     int w = 4;                /*variabile locale*/
     secondaFunzione();
     printf(“%d”, w);
  }
  w = terzaFunzione();
  printf(“%d”, w);
  return 0;
}

La prima istruzione printf(“%d”, w); stampa il valore 3, ossia il valore iniziale della variabile globale. Quando viene invocata la prima funzione, alla variabile globale w viene assegnato il valore 5, quindi la seconda istruzione printf(“%d”, w); stamperà 5. Nel sottoblocco viene invocata la seconda funzione, la quale restituisce la stampa della variabile locale w in essa dichiarata, ossia 6; inoltre, viene dichiarata la variabile locale w inizializzata a 4, quindi con l’ultima istruzione printf(“%d”, w); verrà stampato il valore 4. Infine, alla variabile globale viene assegnato il valore restituito dall’invocazione della terza funzione, ossia 6, che successivamente viene stampato. Infatti, la terza funzione incrementa il numero 5, l’ultimo valore assegnato alla variabile globale w.

Il passaggio dei parametri per valore e per indirizzo

Tempi: 2 ore di lezione teorica + 3 ore di esercizi in classe + 3 ore di esercizi in laboratorio

Esistono due modi per passare i parametri ad una funzione: per valore e per indirizzo.

Nel passaggio di parametri per valore viene fatta una copia dei valori degli argomenti e passata alla funzione invocata. Più precisamente, quando viene invocata la funzione, il valore dei parametri attuali viene copiato nelle variabili dei corrispondenti parametri formali. In questo modo tutte le modifiche eseguite sulla copia non hanno nessun effetto sulla variabile utilizzata dal chiamante.

Per chiarire tale concetto, possiamo svolgere in laboratorio il “classico” esempio di una funzione che effettua lo scambio del contenuto di due variabili:

void scambio(int x, int y)
{
  int box = 0;
  box = x;
  x = y;
  y = box;
}
int main(void)
{
  int a, b;
  printf(“Inserisci due numeri interi: ”);
  scanf(“%d”, &a);
  scanf(“%d”, &b);
  scambio(a, b);
  printf(“Il valore di a è %d”, a);
  printf(“Il valore di b è %d”, b);
  return 0;
}

Chiediamo ai nostri studenti di far eseguire il programma e di riferirci le loro osservazioni. Subito si accorgeranno che il programma non fa quello che abbiamo richiesto: infatti, passando i parametri per valore lo scambio avviene solo localmente nel codice della funzione, senza poi riflettersi all’esterno, quindi inserendo i numeri interi 2 e 7 rispettivamente come parametri attuali per a, b, dopo l’esecuzione di scambio il loro contenuto non cambierà.

Dunque per soddisfare la nostra richiesta abbiamo bisogno di utilizzare una tecnica in cui le modifiche del valore dei parametri effettuate nel corpo della funzione invocata si riflettano nel valore degli argomenti della funzione chiamante. Tale tecnica è il passaggio di parametri per indirizzo.

Nel linguaggio C i parametri passati alle funzioni sono per valore, per poter parlare del passaggio di parametri per indirizzo dobbiamo prima fare una piccola introduzione al concetto di puntatore. Infatti, nel linguaggio C, la tecnica del passaggio di parametri per indirizzo è un caso particolare dell’utilizzo dei puntatori.

Il puntatore è una variabile che contiene un indirizzo di memoria dov’è immagazzinato il valore. Il riferimento ad un valore viene chiamato deriferimento. Vediamo ora l’esempio di dichiarazione di un puntatore ad un valore intero.

int *intPtr;

I puntatori possono essere inizializzati con un indirizzo, 0 oppure NULL. Quest’ultimo valore è una costante che indica il riferimento a nessun dato.

L’operatore unario di indirizzo indicato con ampersannd (&) restituisce l’indirizzo del suo operando. Vediamo ora un esempio.

int x = 7;
int *xPtr;
xPtr = &x;

In questo esempio al puntatore xPtr viene assegnato l’indirizzo della variabile x. Ora per avere il valore si utilizzata l’operatore di deriferimento (*). Vediamo un esempio di stampa.

printf(“%d”, *xPtr);

Ora possiamo passare a parlare del passaggio di parametri per indirizzo. Abbiamo già detto che quando si ha la necessità di modificare direttamente il valore della variabile che viene passata come parametro ad una funzione, utilizziamo il passaggio per indirizzo.

Questo metodo è reso possibile passando alla funzione l’indirizzo della variabile attraverso l’operatore di indirizzo e poi nella funzione utilizzando il suo valore con l’operatore di deriferimento. Vediamo ad esempio la funzione che esegue il quadrato di un valore intero.

void funzione_quadrato(int *value){
  return *value * *value;
}
int main(void)
{
  int numero = 3;
  funzione_quadrato(&numero);
  printf(“%d”, numero);
  return 0;
}

Il valore stampa dall’esempio precedente sarà effettivamente il quadrato di 3.

Ritorniamo ora al nostro esempio dello scambio di due valori e riscriviamo il codice utilizzando il passaggio per indirizzo:

void scambio(int *x, int *y)
{
  int box = 0;
  box = *x;
  *x = *y;
  *y = box;
}
int main(void)
{
  int a, b;
  printf(“Inserisci due numeri interi: ”);
  scanf(“%d”, &a);
  scanf(“%d”, &b);
  scambio(&a, &b);
  printf(“Il valore di a è %d”, a);
  printf(“Il valore di b è %d”, b);
  return 0;
}

In questo caso, inserendo 2 e 7 rispettivamente come valori delle variabili a, b, dopo l’invocazione della funzione scambio, tali valori risulteranno scambiati anche nel programma principale.

Il passaggio di parametri per indirizzo permette inoltre di scrivere funzioni che producono diversi risultati senza utilizzare variabili globali: i risultati sono trasmessi alla funzione chiamante mediante i parametri e non tramite l’istruzione return.

Esempio: scrivere un programma che permetta di stabilire se un numero intero positivo è multiplo di 5 e, in caso contrario, stampi il resto del numero nella divisione per 5.

#include<stdio.h>
int multiploCinque(int num, int *resto)
{
  if(num % 5 == 0)
     return 0;
  else
  {
     *resto = num % 5;
     return -1;
  }
} 
int main(void)
{
  int esito, n, r = 0;
  printf(“Inserisci un numero intero positivo: ”);
  scanf(“%d”, &n);
  esito = multiploCinque(n,&r);
  if(esito == 0)
    printf(“Il numero è multiplo di 5”);
  else
     printf(“Il numero non è multiplo di 5 e il resto vale %d”, r);
    return 0;
}

Esempio di teatralizzazione: passaggio di parametri per valore

Per far comprendere meglio il passaggio per valore, possiamo proporre la teatralizzazione del seguente semplice esercizio: scrivere un programma che presi in input due numeri reali calcoli e stampi il risultato dell’operazione scelta dall’utente.

Prima di tutto possiamo assegnare l’esercizio, eventualmente come compito per casa e poi chiamare uno studente alla lavagna per la correzione:

#include<stdio.h>
float somma(float a, float b)
{
  return a + b;
}
float differenza(float a, float b)
{
  return a - b;
}
float prodotto(float a, float b)
{
  return a * b;
}
float quoziente(float a, float b)
{
  if(b != 0)
    return a/b;
  else
    return -1;
}
int main(void)
{
  int operazione;
  float x, y;
  printf(“Inserisci due numeri reali: ”);
  scanf(“%f”, &x);
  scanf(“%f”, &y);
  printf(“Puoi eseguire le seguenti operazioni: \n”);
  printf(“1. Addizione \n”);
  printf(“2. Sottrazione \n”);
  printf(“3. Prodotto \n”);
  printf(“4. Divisione \n”);
  printf(“Inserisci il numero associato all’operazione scelta:”);
  scanf(“%d”, &operazione);
  switch (operazione)
  {
     case 1:
       printf(“%f”, somma(x,y));
       break;
     case 2:
       printf(“%f”, differenza(x,y));
       break;
     case 3:
       printf(“%f”, prodotto(x,y));
       break;
     case 4:
       if(quoziente(x,y) != -1)
          printf(“%f”, quoziente(x,y));
       else
          if(quoziente(x,y) == -1 && y != 0)   
              printf(“%f”, quoziente(x,y));
          else
              printf(“Operazione priva di significato!”);
       break;
     default:
       printf(“Scelta non valida!”);
       break;
  } 
  return 0;      
}

Teatralizzazione. Scegliamo 6 alunni volontari e assegniamo loro i ruoli: uno interpreterà la funzione main (M), 4 interpreteranno le operazioni (S, D, P, Q) e l’ultimo sarà l’utente (U). Ci procuriamo delle matite e dei foglietti in cui scrivere i valori delle variabili e li distribuiamo ai nostri interpreti. Posizioniamo M alla cattedra e U di fronte a lui, mentre S, D, P e Q si posizioneranno in 4 banchi dell’aula (ad esempio due a destra e due a sinistra della cattedra).

La prima cosa che dobbiamo rappresentare è l’esecuzione dell’intero programma, quindi stabiliamo la seguente regola: in ogni istante rimarrà in piedi solamente l’alunno che rappresenta il programma o il sottoprogramma che è in esecuzione, mentre tutti gli altri rimarranno seduti. È importante sottolineare che, nel programma principale, quando viene invocata una funzione l’esecuzione del main viene temporaneamente interrotta per eseguire il sottoprogramma.

La teatralizzazione avviene nei seguenti passi:

  1. Inizialmente M è in piedi, S, D, P e Q sono seduti. L’allievo che rappresenta l’utente U può rimanere sempre in piedi.
  2. M chiede a U di fornirgli due numeri reali;
  3. U prende due foglietti e scrive su di essi due numeri reali e li consegna uno alla volta a M. M assegna i valori nell’ordine di ricezione alle variabili a e b rispettivamente.
  4. M illustra ad U le possibili operazioni che può eseguire e gli dice di sceglierne una;
  5. U scrive su un foglietto il numero corrispondente all’operazione scelta e lo consegna a M. Supponiamo che U abbia scelto la moltiplicazione e quindi scrive sul foglietto il numero 3. M posizionerà il foglietto ricevuto nel rettangolo con etichetta “operazione”.
  6. M copia i valori di a e b in altri due foglietti, consegna le copie a P una alla volta e poi si siede;
  7. P si alza in piedi, calcola il prodotto tra il primo e il secondo numero ricevuto, scrive il risultato in un foglietto che restituisce a M e poi si siede;
  8. M si alza in piedi, legge il risultato a U e poi si siede. L’esecuzione dell’intero programma è così terminata.

Bisogna ricordare agli studenti che abbiamo rappresentato il passaggio di parametri per valore, per questo motivo M consegna a P una copia dei foglietti ricevuti da U, tenendo per sé gli originali. A questo punto si può porre alla classe la seguente domanda: “come dobbiamo procedere per rappresentare il passaggio di parametri per indirizzo?”. In tal caso, M avrebbe consegnato direttamente a P i foglietti ricevuti da U.

Funzioni aventi come parametri degli array

Anche gli array possono essere parametri per le funzioni, ma il passaggio avviene solo per indirizzo, in realtà quello che accade è che viene passato l’indirizzo del primo elemento.

L’unico inconveniente è che la funzione non ha nessuna informazione sulla lunghezza dell’array, quindi bisogna passare anch’essa.

Vediamo ora un esempio di prototipo di funzione.

void ordina(int vettore[], int lunghezza);

L’invocazione avviene nel seguente modo.

ordina(vettore, lunghezza);

Nel caso si vogliano passare come parametri array bidimensionali, è obbligatorio esplicitare almeno il numero delle colonne, ossia la lunghezza di ogni riga, per permettere al compilatore di determinare le posizioni degli elementi in memoria. Infatti, gli elementi di una matrice sono memorizzati in maniera sequenziale per righe.

Vediamo un esempio di una funzione che esegue la trasposta di una matrice quadrata.

void trasposta (float matrice[][5], int dimensione);

Esercizi

Riportiamo ora altri esercizi da svolgere prima in classe o per compito a casa, che successivamente saranno riproposti in laboratorio. L’implementazione dei seguenti algoritmi deve essere svolta utilizzando le funzioni.

  1. Realizzare un programma che indichi se un’equazione di primo grado ax+b=0 è determinata, indeterminata o impossibile. Nel caso in cui l’equazione sia determinata, stampare la sua soluzione.
  2. Scrivere un programma che, dopo aver generato casualmente due vettori di 30 numeri interi positivi, calcoli e stampi la somma di tutti gli elementi pari del primo vettore e il prodotto di tutti gli elementi dispari del secondo.
  3. Si prendano in input due vettori di 20 elementi: il primo contenente i voti della verifica di Informatica degli alunni della classe e il secondo contenente i loro numeri di matricola corrispondenti (l’elemento j del secondo vettore contiene il numero di matricola dell’alunno il cui voto è l’elemento j del primo vettore). Scrivere un programma che calcoli e stampi la media aritmetica dei voti della verifica e successivamente stampi i due vettori ordinati in ordine decrescente in base al voto mantenendo la corrispondenza voto-matricola.
  4. Scrivere un programma che prenda in input una matrice 8 × 8, calcoli e stampi la sua traccia (somma dei valori appartenenti alla diagonale principale).
  5. Realizzare un programma che, dopo aver generato casualmente una matrice 4 × 4 di elementi interi, calcoli e stampi la matrice trasposta e determini se la matrice di partenza è simmetrica oppure no.
  6. Realizzare un programma che, presa in input una stringa, conti e stampi il numero delle vocali e delle consonanti inserite mediante le due funzioni vocali e consonanti.

Riportiamo la versione in linguaggio C di alcuni esercizi assegnati agli studenti tratti dal libro di testo (Formichi & Meini, 2012).

  1. Codice a barre. I codici a barre dei prodotti sono composti da 13 cifre di cui l’ultima è la cifra di controllo che si determina a partire dalle prime 12 con le seguenti regole: moltiplicare per 3 tutte le cifre in posizione “dispari” (la prima, la terza,… fino all’undicesima); sommare i 12 valori così ottenuti; prendere il resto della divisione per 10 della somma ottenuta. Scrivere una funzione C che, a partire da un vettore di 12 elementi corrispondenti alle singole cifre di un codice a barre, restituisca la cifra di controllo calcolata con le regole illustrate. Scrivere un programma in C che richieda all’utente l’inserimento delle cifre di un codice a barre e visualizza la corrispondente cifra di controllo calcolata con la funzione precedente.
  2. Scrivere una funzione che, a partire da una matrice numerica di R × C elementi, costruisca due vettori rispettivamente di dimensione R e C: il primo vettore dovrà contenere le somme degli elementi di ogni riga della matrice, il secondo vettore le somme degli elementi di ogni colonna. Scrivere un programma che, dopo aver chiesto all’utente l’inserimento dei singoli elementi della matrice, visualizzi gli elementi dei vettori ottenuti come risultato dell’applicazione della precedente funzione.
  3. Cifrario di Cesare. Giulio Cesare utilizzava un codice segreto per comunicare in forma scritta con i propri generali: ogni singola lettera veniva sostituita con la lettera che si trova 3 posizioni dopo nell’ordinamento alfabetico (se consideriamo l’alfabeto inglese A→ D, B→ E,…..,X→A, Y→ B, Z→ C), i simboli delle cifre numeriche e i segni di punteggiatura erano lasciati invariati. Scrivere due programmi in C: il primo deve codificare secondo la regola di Cesare una stringa di testo inserita dall’utente e visualizzare il risultato, il secondo deve accettare una stringa segreta e decodificarla visualizzando il testo originale.

Controllo dell’apprendimento: verifiche e valutazione con relative griglie

La valutazione formativa si esegue tramite brevi colloqui, esercitazioni in classe o in laboratorio e correzione degli esercizi assegnati per casa, verificando l'acquisizione progressiva delle conoscenze e delle abilità previste come obiettivi specifici.

Al termine dell'unità didattica verrà svolta una verifica scritta, nella quale verranno proposti esercizi simili a quelli esaminati in classe, ma non solo, per permettere di verificare il livello di assimilazione degli argomenti trattati e l'autonomia nella risoluzione degli esercizi.

Verifica scritta. Creare un programma che, dopo aver ricevuto in input un vettore V di 11 numeri interi e altri due numeri interi X e Y, implementi le seguenti funzioni:

  1. calcola e restituisce la somma dei cubi dei numeri X e Y; [1 punto]
  2. calcola e stampa il prodotto di tutti i numeri non negativi del vettore V; [2 punti]
  3. genera un nuovo vettore U, sempre avente 11 elementi interi come V, contenente numeri casuali compresi tra 8 e 25. Successivamente calcola il vettore differenza W, sottraendo ad ogni elemento di V l’elemento di U nella posizione corrispondente, e restituisce il minimo valore di W. [3 punti]
  4. Facoltativo. Scrivere una funzione che determini il Massimo Comun Divisore (M.C.D.) tra X e il valore restituito dalla funzione precedente. Se uno o entrambi tali numeri sono negativi, la funzione li trasforma in positivi prima di effettuare il calcolo dell’M.C.D., invece, se entrambi valgono zero, le funzione restituisce zero. [1 punto]

Tutte le operazioni di input e output, se non diversamente indicato, devono essere gestite nel main.

Griglia di valutazione. Per determinare il voto della verifica sommativa è stato attribuito ad ogni esercizio un punteggio. La diversità di punteggio rappresenta un diverso livello di difficoltà in termini di conoscenze e abilità. Per ogni esercizio verranno valutate la correttezza sintattica, la logica dell’algoritmo e l’efficacia del programma. Naturalmente, nel caso di errore nello svolgimento dell'algoritmo, verrà attribuito solo parte del punteggio completo. Per fare questo, si stabilirà di volta in volta, a seconda della gravità dell'errore commesso, quanto farlo pesare e di quanto abbassare il punteggio. Tale diminuzione di punteggio verrà applicata a ciascun studente che avrà commesso lo stesso errore. Nella griglia di valutazione utilizzata si parte da un punteggio di base di 3 punti; ad ogni esercizio verrà assegnato un punteggio in modo tale che la somma dei punti arrivi ad un massimo di 7. La sufficienza si raggiunge totalizzando 3 punti, il voto minimo sarà 3/10 e quello massimo 10/10.

Punteggio 0 0,5 1 1,5 2 2,5 3 3,5 4 4,5 5 5,5 6 6,5 7
Voto 3 3,5 4 4,5 5 5,5 6 6,5 7 7,5 8 8,5 9 9,5 10

Approfondimento

Come approfondimento proponiamo le funzioni ricorsive. Fino ad ora abbiamo visto funzioni che si richiamano tra di loro in modo gerarchico. Una funzione è detta ricorsiva quando richiama se stessa direttamente oppure attraverso un’altra funzione.

Una funzione che è in grado di risolvere solo un problema semplice, se viene invocata per risolvere un caso base restituirà subito il risultato, altrimenti suddividerà il problema in due parti una che concettualmente sa risolvere e un’altra no. Per rendere possibile la ricorsione questo secondo problema dovrà essere simile a quello originale, ma in versione più semplificata o più piccola. A questo punto la funzione invocherà una copia di se stessa che andrà a lavorare sul problema più piccolo, questa viene definita chiamata ricorsiva.

La chiamata ricorsiva potrà generare altre chiamate, continuerà a suddividere finché il secondo problema non sarà ricondotto ad un caso base.

Per capire questo nuovo concetto facciamo un esempio molto famoso che è il calcolo del fattoriale di un numero intero non negativo.

n! = n * (n-1) * (n-2) * … * 1

Ricordiamo che 0! è per definizione uguale a 1. Vediamo prima di tutto come potremmo implementarlo in modo iterativo, ad esempio il fattoriale del numero 4.

fattoriale = 1;
valore = 4;
for ( i = valore; i >= 1; i-- )
{
  fattoriale = fattoriale * i;
}

Ora vediamo come sia possibile scrivere questo esempio in modo ricorsivo.

long fattoriale (long valore){
  if(valore <= 1)
  {
    return 1;
  } 
  else 
  {
    return valore * fattoriale(valore - 1);
  }
}
int main(void)
{
 printf(“%ld”, fattoriale(4));
}
Ricorsione.png

Esercizi. Proponiamo alla classe lo svolgimento dei seguenti esercizi.

  1. Scrivere una funzione ricorsiva per calcolare la somma dei primi N numeri naturali, dove N è dato in input dall’utente.
  2. Scrivere una funzione ricorsiva che calcoli il Massimo Comun Divisore tra due numeri interi non negativi a e b, con a>b: se b è uguale a 0, allora MCD(a,b) = a, altrimenti MCD(a,b) = MCD(b,a%b).

Bibliografia

  • Deitel, H.M., Deitel, P.J.: Corso completo di programmazione. Apogeo, Milano (2000).
  • Formichi, F., Meini, G.: Corso di Informatica. Per Informatica, volume 1. Zanichelli Scuola, Bologna (2012).
  • Gallo, P., Salerno, F.: Cloud. Informatica - Secondo Biennio. Istituti tecnici – settore tecnologico indirizzo Informatica e Telecomunicazioni. Articolazione Informatica. Minerva Scuola, Milano (2013).

Sitografia

  • www.indire.it
  • www.csfieldguide.org.nz/
Strumenti personali
Namespace

Varianti
Azioni
Navigazione
Strumenti