Blue Flower

Lo sviluppo top-down e le funzioni


I programmi visti negli esempi precedenti riguardavano problemi relativamente semplici. Quando il livello di complessità aumenta, conviene suddividere il programma in sottoprogrammi secondo
la metodologia di sviluppo top-down.
Si tratta in sostanza di definire quali siano le parti fondamentali di cui si compone il programma (top), e procedere poi al dettaglio di ogni singola parte (down). Ogni parte, che compone il programma, corrisponde ad un modulo che svolge una specifica funzionalità per la risoluzione del problema, cioè il programma viene scomposto in moduli funzionalmente indipendenti.
Viene facilitato il lavoro di manutenzione del software, perché si può intervenire con modifiche o correzioni su un solo modulo, avendo nel contempo cognizione di quello che fa l’intero programma.
Inoltre alcuni moduli, essendo funzionalmente indipendenti, possono essere riutilizzati, senza bisogno di grandi modifiche, all’interno di altri programmi.
Nel linguaggio C i sottoprogrammi sono rappresentati attraverso le funzioni.


Nei programmi presentati precedentemente sono già state utilizzate le funzioni, anche se sono state identificate come istruzioni del linguaggio C. L’istruzione scanf è in realtà una funzione che serve a ricevere i dati che l’utente introduce da tastiera; l’istruzione printf è una funzione che si occupa di mandare sul video i caratteri corrispondenti ai dati o ai messaggi di output. Inoltre abbiamo utilizzato funzioni di calcolo, quali la funzione sqrt per la radice quadrata di un numero.
Sono tutti esempi di funzioni predefinite nel linguaggio C o, come si dice nel linguaggio informatico, funzioni built-in, cioè moduli software che il programmatore può usare senza implementarli.
Questi moduli vengono utilizzati indicandone semplicemente il nome.
La dichiarazione delle funzioni che si intendono utilizzare nel programma va posta in testa al programma, immediatamente dopo la sezione dedicata alle direttive e alla dichiarazioni delle variabili del programma.
La sintassi generale per la dichiarazione di una funzione è:

tipo di dato restituito nome della funzione (elenco dei parametri)
{

………………………..;
………………………..;
………………………..;
return valore restituito;
}

Poiché la funzione restituisce un valore, occorre specificare, prima del nome che identifica la funzione, il tipo del valore restituito. Dopo il nome della funzione, le parentesi tonde servono a contenere l’elenco degli argomenti passati alla funzione, detti parametri. Le istruzioni che formano la funzione sono delimitate dalle parentesi graffe e rappresentano il codice che viene eseguito alla chiamata della funzione.
Il corpo della funzione contiene come ultima istruzione, la parola return seguita dal valore o dalla variabile contenente il risultato restituito dalla funzione. L’istruzione return provoca il ritorno del controllo alla funzione principale o, in generale, alla funzione chiamante.
Se il tipo restituito dalla funzione non è specificato, si assume, in mancanza (per default), che il tipo sia int.
Se si vuole invece che la funzione non restituisca alcun valore, bisogna specificare, come tipo di dato restituito, il tipo void (in italiano vuoto o privo):

void nome della funzione (elenco dei parametri)

L’elenco dei parametri segue la sintassi della dichiarazione delle variabili, vista negli esempi precedenti; i parametri sono separati dalla virgola e per ciascun parametro deve essere indicato il tipo (diversamente dalla dichiarazione delle variabili):

tipo nome della funzione (tipo1 nome1, tipo2 nome2, …)

Il tipo void può essere usato anche nella dichiarazione dei parametri: il tipo void scritto tra parentesi tonde indica che nessun parametro viene passato alla funzione. Questo è il valore di default; infatti:

void f (void)

si può anche scrivere:

void f ( )

L’esempio seguente mostra la codifica di una funzione che esegue la somma di due numeri interi, ricevuti come parametri, e restituisce il valore calcolato.

int Somma (int a, int b)
{
int x;
x = a + b;
return x;
}

Quando la funzione ha, come tipo di ritorno, il tipo void, l’istruzione return non è seguita da alcun valore o espressione, in quanto tale tipo indica proprio il fatto che la funzione non restituisce nulla alla funzione chiamante; in questo caso l’istruzione return, in fondo all’implementazione, può essere omessa.
L’esempio seguente mostra la codifica di una funzione che esegue la somma di due numeri e visualizza il risultato, senza restituire nulla alla funzione chiamante.

void StampaSomma (int a, int b)
{
int x;
x = a + b;
printf("Il totale e': %d \n", x);
}

La seguente funzione di stampa è un esempio di funzione che non restituisce alcun valore e che non riceve alcun parametro come argomento: il tipo restituito è void e il tipo void scritto tra parentesi tonde indica che nessun parametro viene passato alla funzione.

void Stampa (void)
{

printf("stampa di prova \n");
printf("fine della funzione \n");
}

Vediamo ora come si possa effettuare la chiamata di una funzione da un qualunque punto del programma, dopo averla dichiarata e definita: occorre specificare il nome della funzione seguito dall’elenco, tra parentesi tonde, dei valori da assegnare ai parametri della funzione:

nome della funzione (elenco dei valori da passare ai parametri);

Al momento della chiamata, il compilatore esegue il controllo di corrispondenza tra i parametri specificati nella definizione della funzione e i valori passati alla funzione.
Un esempio di chiamata della funzione Somma definita in precedenza è il seguente:

int totale;
int subtot1;
int subtot2;
...
totale = Somma(subtot1, subtot2);
...

Il main è a tutti gli effetti una funzione: questo è il motivo della presenza della coppia di parentesi tonde dopo la parola main, come visto negli esempi precedenti. Tuttavia la funzione main ha una caratteristica specifica: è la funzione che viene eseguita per prima all’avvio del programma.
L’istruzione return può essere presente anche nella funzione main, in tal caso il valore viene restituito direttamente al sistema operativo, che è il programma chiamante della funzione.
Per completezza quindi, negli esempi successivi, sarà indicata la parola int prima di main, per indicare il tipo del valore restituito, e alla fine del main verrà scritta l’istruzione return seguita dal valore 0.
L’uso di return con il valore 0 è il modo più comune per indicare la terminazione di un programma che non ha trovato errori durante l’esecuzione.

ESEMPIO: Calcolo delle soluzioni di un’equazione di secondo grado

Un’equazione di secondo grado scritta nella forma ax2 + bx + c = 0 è caratterizzata dai coefficienti a,b,c. Dopo aver calcolato il discriminante Δ (delta) con la formula b * b – 4 * a * c, si possono riconoscere tre situazioni:

Δ < 0 non esistono soluzioni reali,
Δ = 0 le due soluzioni reali sono coincidenti,
Δ > 0 ci sono due soluzioni reali, distinte.

Se esistono soluzioni reali, queste si ottengono dalla formula (–b ± √Δ)/(2 * a).
Perché il programma comprenda tutte le situazioni possibili, occorre prevedere anche che sia uguale a zero. Infatti, se a = 0 si deve procedere alla soluzione dell’equazione di primo grado:

bx + c = 0.

In questa eventualità bisogna poi riconoscere i sottocasi:

b = 0 e c = 0    equazione indeterminata,
b = 0                equazione impossibile.

Da questa analisi sono state individuate tre sottofunzioni richiamate dal main:

  • calcolo di delta
  • risoluzione dell’equazione di primo grado nel caso a = 0
  • visualizzazione delle soluzioni quando delta è maggiore o uguale a 0.

Programma C

/* Equazioni.c : soluzioni di un'equazione di secondo grado */
#include <stdio.h>
#include <math.h>
/* input */
float a, b, c; /* coefficienti equazione */
/* output */
float x1, x2;
/* lavoro */
float delta; /* discriminante */

void RisolviPrimoGrado()
{
if ((b==0) && (c==0))
{
printf("Equazione indeterminata \n");
}
else
{
if (b==0) printf("Equazione impossibile \n");
else printf("x = %8.2f \n", -c/b);
}
return;
} /* RisolviPrimoGrado */

float CalcolaDelta()
{
delta = b*b – 4*a*c;
return delta;
} /* CalcolaDelta */
void ScriviSoluzioni()
{
if (delta < 0) {
printf("Non esistono soluzioni reali \n");
}
else
{
x1 = (-b-sqrt(delta)) / (2*a);
x2 = (-b+sqrt(delta)) / (2*a);
printf("x1 = %8.2f \n", x1);
printf("x2 = %8.2f \n", x2);
}
return;
} /* ScriviSoluzioni */
/* funzione principale */

int main(void)
{
printf("Tre coefficienti: ");
scanf("%f %f %f", &a, &b, &c);
if (a != 0)
{
CalcolaDelta();
ScriviSoluzioni();
}
else
{
RisolviPrimoGrado();
}
return 0;
}

In questo esempio tutte le funzioni sono senza parametri. La funzione CalcolaDelta restituisce un valore di tipo float, le altre non restituiscono alcun valore e quindi sono di tipo void.
Nel programma inoltre le variabili sono dichiarate all’inizio, dopo le direttive e al di fuori del mainperché esse sono utilizzate anche dalle funzioni: le variabili dichiarate all’esterno del main e delle funzioni si chiamano variabili globali, in contrapposizione a quelle dichiarate all’interno delle funzioni o all’interno del main, che prendono il nome di variabili locali.
Si noti infine l’uso della direttiva #include <math.h>, perché il problema richiede il calcolo della radice quadrata, realizzato dalla funzione predefinita sqrt.
Per rendere più efficace la lettura e l’interpretazione del codice, può essere utile ripetere, dopo la parentesi graffa che chiude il corpo della funzione, il nome della funzione stessa come commento (racchiuso tra i delimitatori /* … */): in questo modo appare più evidente dove inizia e dove termina la funzione.