Lezione 1 – 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.
Questa metodologia facilita 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.
Funzioni built-in
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.
Dichiarazione delle funzioni
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.
tipo_di_dato_restituito nome_funzione (elenco_parametri) {
/* istruzioni */
return valore_restituito; /* se non void */
}
Esempi di funzioni
int Somma(int a, int b){
int x = a + b;
return x;
}
void StampaSomma(int a, int b){
int x = a + b;
printf("Il totale è: %d\n", x);
}
void Stampa(void){
printf("Stampa di prova\n");
printf("Fine della funzione\n");
}
La chiamata di una funzione avviene specificandone il nome seguito dall’elenco dei parametri (se previsti). Esempio:
int totale;
int subtot1, subtot2;
/* ... */
totale = Somma(subtot1, subtot2);
Esercizi
1) Identifica tre vantaggi concreti dell’approccio top-down su un problema reale
2) Trasforma un programma monolitico in 3 moduli e descrivi le interfacce
3) Elenca 5 funzioni “built-in” che hai già usato e il relativo header
printf/scanf → <stdio.h>, sqrt/pow → <math.h>, strlen/strcmp → <string.h>.
Lezione 2 – Il main() e l’equazione di secondo grado (funzioni senza parametri)
Il main è a tutti gli effetti una funzione: questo è il motivo della presenza della coppia di parentesi
tonde dopo la parola main. 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, negli esempi indicheremo int prima di main e
alla fine scriveremo return 0;.
return 0 rappresenta la terminazione “senza errori” di un programma.Esempio completo: soluzioni di un’equazione di secondo grado (versione con variabili globali)
Un’equazione di secondo grado scritta nella forma ax² + 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 ± sqrt(Δ)) / (2 * a).
Occorre inoltre prevedere il caso a = 0: l’equazione si riduce a primo grado bx + c = 0, con i sottocasi:
b = 0ec = 0→ equazione indeterminata,b = 0ec ≠ 0→ equazione impossibile,b ≠ 0→ soluzione unicax = -c / b.
/* Equazioni.c : soluzioni di un'equazione di secondo grado */
#include <stdio.h>
#include <math.h>
/* input */
double a, b, c; /* coefficienti equazione */
/* output */
double x1, x2;
/* lavoro */
double delta; /* discriminante */
void RisolviPrimoGrado() {
if ((b == 0.0) && (c == 0.0)) {
printf("Equazione indeterminata \n");
} else {
if (b == 0.0) printf("Equazione impossibile \n");
else printf("x = %8.3f \n", -c / b);
}
return;
} /* RisolviPrimoGrado */
double CalcolaDelta() {
delta = b * b - 4.0 * a * c;
return delta;
} /* CalcolaDelta */
void ScriviSoluzioni() {
if (delta < 0.0) {
printf("Non esistono soluzioni reali \n");
} else if (delta == 0.0) {
x1 = -b / (2.0 * a);
printf("x1 = x2 = %8.3f \n", x1);
} else {
x1 = (-b - sqrt(delta)) / (2.0 * a);
x2 = (-b + sqrt(delta)) / (2.0 * a);
printf("x1 = %8.3f \n", x1);
printf("x2 = %8.3f \n", x2);
}
return;
} /* ScriviSoluzioni */
/* funzione principale */
int main(void) {
printf("Tre coefficienti: ");
if (scanf("%lf %lf %lf", &a, &b, &c) != 3) { puts("Input non valido"); return 1; }
if (a != 0.0) {
CalcolaDelta();
ScriviSoluzioni();
} else {
RisolviPrimoGrado();
}
return 0;
}
- In questo esempio tutte le funzioni sono senza parametri.
CalcolaDeltarestituisce undouble;RisolviPrimoGradoeScriviSoluzionisonovoid.- Le variabili
a, b, c, x1, x2, deltasono globali (dichiarate fuori damain). - Serve
<math.h>per usaresqrt; con GCC linka con-lm.
Focus su main() e return
Nel listato, main legge i tre coefficienti, decide se trattare l’equazione di secondo grado
(a != 0) oppure richiamare la funzione per il primo grado. Il return 0; finale restituisce al sistema
operativo lo stato “nessun errore”.
Esercizi suggeriti
1) Stampare anche un messaggio specifico quando Δ = 0 (radici coincidenti)
delta == 0.0.2) Formattare le radici con tre cifre decimali (%8.3f)
3) Validare l’input
scanf; eventualmente ripeti la lettura in un ciclo.Lezione 3 – Funzioni con parametri (riuso, locali/globali, parametri formali e attuali)
L’uso delle funzioni risponde all’esigenza di costruire programmi ben organizzati secondo la metodologia top-down.
Un’altra esigenza è il riuso dello stesso gruppo di istruzioni in contesti diversi: per questo si rendono parametrici i valori
utilizzati dalle funzioni, passando loro gli argomenti dalla funzione chiamante (main o un’altra funzione).
riceve i dati in ingresso come parametri e restituisce (eventualmente) un risultato di ritorno.
Esempio: equazione di secondo grado – versione con parametri
Riprendiamo l’esempio dell’equazione di secondo grado ax² + bx + c = 0, introducendo parametri nelle funzioni.
Questo elimina la dipendenza da variabili globali e rende i moduli più riusabili.
/* Equazioni2.c : soluzioni di un'equazione di secondo grado */
#include <stdio.h>
#include <math.h>
void RisolviPrimoGrado(double c2, double c3) {
if ((c2 == 0.0) && (c3 == 0.0)) {
printf("Equazione indeterminata \n");
} else {
if (c2 == 0.0) printf("Equazione impossibile \n");
else printf("x = %8.3f \n", -c3 / c2);
}
}
double CalcolaDelta(double c1, double c2, double c3) {
return c2 * c2 - 4.0 * c1 * c3;
}
void ScriviSoluzioni(double c1, double c2, double c3, double d) {
double x1, x2;
if (d < 0.0) {
printf("Non esistono soluzioni reali \n");
} else if (d == 0.0) {
x1 = -c2 / (2.0 * c1);
printf("x1 = x2 = %8.3f \n", x1);
} else {
x1 = (-c2 - sqrt(d)) / (2.0 * c1);
x2 = (-c2 + sqrt(d)) / (2.0 * c1);
printf("x1 = %8.3f \n", x1);
printf("x2 = %8.3f \n", x2);
}
}
/* funzione principale */
int main(void) {
double a, b, c; /* coefficienti equazione */
double delta; /* discriminante */
printf("Tre coefficienti: ");
if (scanf("%lf %lf %lf", &a, &b, &c) != 3) return 1;
if (a != 0.0) {
delta = CalcolaDelta(a, b, c);
ScriviSoluzioni(a, b, c, delta);
} else {
RisolviPrimoGrado(b, c);
}
return 0;
}
- Le variabili
a,b,c,deltasono locali amain. Le altre funzioni operano sui parametri. CalcolaDeltarestituisce undouble, mentreRisolviPrimoGradoeScriviSoluzionisonovoid.- Le variabili
x1ex2sono locali aScriviSoluzioni.
Variabili locali e globali (richiamo)
Le variabili dichiarate dentro una funzione sono locali e visibili solo in quell’ambito.
Le variabili dichiarate fuori da tutte le funzioni sono globali e visibili ovunque.
Nei progetti reali è buona pratica minimizzare le globali, per migliorare modularità, testabilità e manutenzione.
Parametri formali e parametri attuali
Si chiama passaggio di parametri l’operazione con cui il chiamante invia i valori alla funzione.
I nomi scritti nella firma della funzione (ad esempio double c1, double c2, double c3) sono i parametri formali.
Le variabili usate nella chiamata (ad esempio a, b, c in CalcolaDelta(a, b, c)) sono i parametri attuali.
/* formali */ double CalcolaDelta(double c1, double c2, double c3);
/* attuali */ delta = CalcolaDelta(a, b, c);
I valori di a, b, c vengono copiati nei parametri formali c1, c2, c3.
- Riuso del codice (stesse funzioni, dati diversi).
- Incapsulazione (i dettagli restano dentro la funzione).
- Manutenzione semplificata (modifica locale, effetto controllato).
- Testabilità (si testano i moduli isolatamente).
Mini-esercizi
1) Restituire un codice di esito invece delle stampe
2) Restituire le radici via parametri di output
double*; in C++ referenze double&.3) Separare I/O ed elaborazione
Lezione 4 – Passaggio di parametri: per valore vs per indirizzo
Nel linguaggio C il passaggio dei parametri può avvenire in due modi:
- Per valore: la funzione riceve una copia dei valori; le modifiche sui parametri formali non influenzano le variabili del chiamante.
- Per referenza (per indirizzo): la funzione riceve l’indirizzo delle variabili del chiamante; le modifiche ai parametri formali si riflettono sulle variabili originali.
&variabile e si usano puntatori nella firma della funzione.Dentro la funzione si dereferenzia con
*p per leggere/scrivere il valore puntato.Esempio – Ordinamento crescente di tre numeri (versione base)
/* TreNumeri.c : ordinamento crescente di tre numeri */
#include <stdio.h>
int main(void) {
int a, b, c, temp;
printf("Tre numeri: ");
scanf("%d %d %d", &a, &b, &c);
if (a > b) { temp = a; a = b; b = temp; }
if (a > c) { temp = a; a = c; c = temp; }
if (b > c) { temp = b; b = c; c = temp; }
printf("Numeri ordinati:\n%d\n%d\n%d\n", a, b, c);
return 0;
}
Ordina e riutilizzarla più volte.Passaggio per indirizzo: funzione Ordina con puntatori
/* TreNumeri2.c : ordinamento crescente di tre numeri */
#include <stdio.h>
void Ordina (int *x, int *y) {
int temp;
if (*x > *y) {
temp = *x;
*x = *y;
*y = temp;
}
return;
}
/* funzione principale */
int main(void) {
int a, b, c;
printf("Tre numeri: ");
scanf("%d %d %d", &a, &b, &c);
Ordina(&a, &b);
Ordina(&a, &c);
Ordina(&b, &c);
printf("Numeri ordinati:\n%d\n%d\n%d\n", a, b, c);
return 0;
}
Ordina riceve gli indirizzi di a, b, c. Usando *x e *y (dereferenziazione) modifica i valori nelle celle di memoria originali.Per valore vs per indirizzo: esempi minimi
/* Parametri1.c : per valore */
#include <stdio.h>
void Modifica (int y) {
y += 2;
}
int main(void) {
int x = 1;
printf("%d \n", x);
Modifica(x);
printf("%d \n", x);
return 0;
}
Output: 1 e 1. La funzione ha modificato la copia, non x.
/* Parametri2.c : per indirizzo */
#include <stdio.h>
void Modifica (int *y) {
*y += 2;
}
int main(void) {
int x = 1;
printf("%d \n", x);
Modifica(&x);
printf("%d \n", x);
return 0;
}
Output: 1 e 3. La funzione ha modificato la cella di memoria di x.
Esempio combinato: uno per valore, uno per indirizzo
/* Incrementa.c : per valore e per indirizzo */
#include <stdio.h>
void Aggiungi (int x, int *y) {
x++;
(*y)++;
printf("Nella funzione chiamata: %d %d \n", x, *y);
return;
}
int main(void) {
int a = 0, b = 0;
Aggiungi (a, &b);
printf("Nella funzione principale: %d %d \n", a, b);
return 0;
}
Output (ordine): 1 1 poi 0 1. a per valore (non cambia), b per indirizzo (cambia).
- Per valore: la funzione non deve modificare i dati del chiamante (stampe, formattazioni, calcoli che ritornano un valore).
- Per indirizzo: la funzione deve aggiornare le variabili del chiamante o restituire più risultati (scambi, accumuli, output multipli).
Mini-esercizi
1) Implementa swap(int *p, int *q) e usala per riordinare tre interi
swap in tre chiamate come in TreNumeri2.c.2) Funzione che imposti max e min di tre numeri (parametri per indirizzo)
*max e *min con confronti successivi.3) Estendi Aggiungi per ritornare anche la somma via puntatore
void Aggiungi(int x, int *y, int *somma).Lezione 5 – Prototipi di funzione (dichiarazione vs definizione)
Le funzioni di un programma C possono comparire in qualsiasi ordine nel file sorgente, ma per poterle
usare in main() o in altre funzioni è necessario che il compilatore ne conosca la firma
(tipo di ritorno e lista dei parametri). Per questo si introducono i prototipi: dichiarazioni sintetiche poste in testa al programma
(subito dopo le #include e le eventuali variabili globali) che informano il compilatore su come verrà chiamata la funzione.
La definizione (il corpo con le istruzioni) può essere scritta più avanti, tipicamente dopo il main().
- Sostengono l’approccio top-down: in alto l’“indice” (prototipi), sotto i dettagli (definizioni).
- Permettono di chiamare funzioni definite più avanti o in altri file.
- Abilitano il controllo dei tipi su argomenti e valore di ritorno alla chiamata.
Sintassi del prototipo
/* dichiarazione/prototipo */
tipo_ritorno nomeFunzione(tipo1 p1, tipo2 p2, ...);
/* definizione (implementazione) */
tipo_ritorno nomeFunzione(tipo1 p1, tipo2 p2, ...) {
/* ... istruzioni ... */
return valore; /* se non void */
}
Nei prototipi è possibile indicare solo i tipi senza i nomi dei parametri, ma specificare anche i nomi aiuta la leggibilità.
Esempio completo: area e perimetro di un quadrato (prototipi + definizioni)
Scomponiamo il problema in tre parti (input lato, calcolo area, calcolo perimetro) e dichiariamo i prototipi in testa al file.
Le variabili locali restano incapsulate nelle rispettive funzioni.
/* Quadrato.c : perimetro e area del quadrato */
#include <stdio.h>
/* Prototipi (dichiarazioni) */
int Tastiera(void);
void CalcoloArea (int l);
void CalcoloPerimetro (int l);
/* funzione principale */
int main(void) {
int lato;
lato = Tastiera();
CalcoloArea(lato);
CalcoloPerimetro(lato);
return 0;
}
/* Definizioni (implementazioni) */
int Tastiera(void) {
int misura; /* variabile locale */
printf("Introduci il lato: ");
scanf("%d", &misura);
return misura;
}
void CalcoloArea (int l) {
double area; /* variabile locale */
area = (double)l * (double)l;
printf("Area = %10.2f \n", area);
}
void CalcoloPerimetro (int l) {
double perim; /* variabile locale */
perim = l * 4.0;
printf("Perimetro = %10.2f \n", perim);
}
- I prototipi compaiono subito dopo le
#includee prima delmain(). - Le funzioni non restitutive sono dichiarate
void; quelle che restituiscono un valore specificano il tipo di ritorno. - Le variabili
misura,area,perimsono locali alle rispettive funzioni.
Esempio: numero pari o dispari (uso del prototipo)
/* PariDispari.c : numero pari o dispari */
#include <stdio.h>
/* Prototipo */
int Pari(int x);
/* funzione principale */
int main(void) {
int y;
printf("Un numero: ");
scanf("%d", &y);
if (Pari(y)) printf("%d numero pari \n", y);
else printf("%d numero dispari \n", y);
return 0;
}
/* Definizione */
int Pari(int x) {
return (x % 2 == 0) ? 1 : 0;
}
if (Pari(y)) ... equivale a if (Pari(y) == 1) ..., ovvero “se la funzione restituisce vero (1)”.Programmi multi-file e header
Nei progetti più grandi, funzioni e variabili possono essere distribuite su più file .c.
I prototipi delle funzioni (e le dichiarazioni condivise) si collocano in file di intestazione .h, inclusi con #include "miofile.h".
In compilazione si elencano tutti i .c necessari; il linker unirà gli oggetti in un eseguibile unico.
- Raggruppa tutti i prototipi dopo le
#includeper avere una “mappa” del programma. - Evita prototipi non coerenti con le definizioni (tipi e numero di parametri devono combaciare).
- Separa interfaccia (prototipi) e implementazione (corpi) per migliorare la leggibilità.
Mini-esercizi
1) Crea geometria.h con i prototipi e implementa in geometria.c
double areaQuadrato(int), double perimetroQuadrato(int).2) Separa PariDispari.c in main.c, pari.c e pari.h
"pari.h" nel main.c e compila con tutti i sorgenti.3) Aggiungi inputInteroPositivo() con prototipo, definizione dopo main
Lezione 6 – Funzioni predefinite (librerie standard C)
Il linguaggio C possiede alcune funzioni predefinite (built-in), che il programmatore può usare
nei programmi senza doverle implementare. Queste funzioni sono organizzate in librerie; per
richiamarle si inserisce all’inizio del file la direttiva #include con il relativo file di intestazione
(header) che contiene i prototipi delle funzioni.
<stdio.h>: funzioni standard di input/output (es.printf,scanf);<math.h>: funzioni matematiche (es.sqrt,pow,log);<string.h>: funzioni per la manipolazione delle stringhe (es.strlen,strcmp);<stdlib.h>: utilità varie (es.absper interi, conversioni, allocazione dinamica…).
Esempio – Implementare (per esercizio) il valore assoluto di un double
Se la funzione fabs non fosse già disponibile, potremmo definirla così (solo a scopo didattico).
Per evitare conflitti di nome con la fabs di libreria, usiamo my_fabs.
/* ValoreAssoluto.c : valore assoluto (versione didattica) */
#include <stdio.h>
double my_fabs(double x) {
return (x < 0.0) ? -x : x;
}
int main(void) {
double y;
printf("Introduci un numero: ");
if (scanf("%lf", &y) != 1) return 1;
printf("%10.2f \n", my_fabs(y));
return 0;
}
Per gli interi esiste la funzione abs(int) in <stdlib.h>.
Esempio – Calcolo della potenza (implementazione manuale)
Per esercizio, possiamo implementare l’elevamento a potenza moltiplicando la base per se stessa
un numero di volte pari all’esponente (intero e non negativo).
/* PotInt.c : potenza con esponente intero non negativo */
#include <stdio.h>
double pot_int(double base, int exp) {
double p = 1.0;
for (int i = 0; i < exp; ++i) p *= base;
return p;
}
int main(void) {
double base; int e;
printf("Base: "); if (scanf("%lf", &base) != 1) return 1;
printf("Esponente (intero >= 0): "); if (scanf("%d", &e) != 1 || e < 0) return 1;
printf("Potenza = %lf \n", pot_int(base, e));
return 0;
}
Esempio – Uso di pow da <math.h>
La funzione pow(base, esponente) accetta double e restituisce un double.
/* PotInt2.c : potenza di un numero (pow) */
#include <stdio.h>
#include <math.h>
/* funzione principale */
int main(void) {
double base, esponente;
double risultato;
printf("Base: ");
if (scanf("%lf", &base) != 1) return 1;
printf("Esponente: ");
if (scanf("%lf", &esponente) != 1) return 1;
risultato = pow(base, esponente);
printf("Potenza = %lf \n", risultato);
return 0;
}
pow è più completa e gestisce casi generali (esponenti reali, negativi, ecc.).Compilazione con GCC:
gcc PotInt2.c -o PotInt2 -lm.Funzioni matematiche comuni (<math.h>)
pow(base, esponente)– elevamento a potenza (argomentidouble).exp(x)– calcola ex (numero di Nepero).sqrt(x)– radice quadrata.log(x)– logaritmo naturale (base e).log10(x)– logaritmo in base 10.
Funzioni per stringhe (<string.h>)
strlen(s)– lunghezza della stringas.strcpy(dest, src)– copiasrcindest.strncpy(dest, src, n)– copia i primincaratteri disrcindest.strcmp(s1, s2)– confronto tra due stringhe (0 se uguali, <0 ses1<s2, >0 ses1>s2).strcat(s1, s2)– concatenazione: appendes2in coda as1. Nota:s1deve poter contenere il risultato completo.
Includere gli header necessari prima delle funzioni che li richiedono. Gli header hanno “include guard” quindi le inclusioni ripetute non creano conflitti.
Mini-esercizi
1) Dati due double, stampa: pow(a,b), sqrt(|a|), log10(|b|+1)
fabs di <math.h> per i valori assoluti; ricorda il link con -lm.2) Funzione che rimuove gli spazi doppi da una stringa
3) Reimplementa il valore assoluto usando fabs e, per interi, abs
double vs int) e i relativi header.