La migliore motivazione per una buona organizzazione modulare dei programmi mi è stata fornita involontariamente da un progettista elettronico: "mai disegnare uno schema che occupa più di un foglio protocollo".
Abbiamo accennato già qualche volta al fatto che in C non c'è distinzione
fra funzioni e subroutine. Occorre sempre specificare
un tipo per il valore di ritorno di una funzione. Esiste però anche il tipo
void, introdotto nel linguaggio proprio per indicare quando
il valore restituito da una funzione, oppure il suo argomento,
non hanno importanza.
Come nel caso di tutti gli altri linguaggi di alto livello, Fortran incluso,
il passaggio del controllo fra una funzione (ad esempio main())
e un'altra, avviene inserendo gli argomenti della funzione nello stack,
cioè in una struttura dati di tipo LIFO (last-in first-out), che
è la più adatta a rappresentare in modo simmetrico una sequenza di
chiamate successive.
Il C permette di utilizzare come argomenti sia il valore di una
variabile, che l'indirizzo in memoria della variabile stessa.
Nel caso venga passato ad una funzione il valore della variabile,
ovviamente la funzione non è in grado di modificare la variabile.
Nel caso degli array viene passato sempre l'indirizzo di
partenza dell'array, ma non la sua dimensione.
Le variabili locali di una funzione vengono create ex-novo ogni
volta che la funzione viene chiamata, e non vengono implicitamente
inizializzate.
Solo le variabili dichiarate
con l'attributo static rimangono le stesse fra la chiamata
di una funzione e la successiva.
Analizziamo ora in dettaglio cosa avviene in questo
:
Osserviamo che il passaggio dei valori delle variabili e l'allocazione dinamica delle variabili locali permettono di chiamare la stessa routine più volte. A seconda della natura della funzione, ed in particolare della struttura delle variabili statiche, non tutte le funzioni possono essere rientranti. Nella libreria standard C si possono talvolta trovare le versioni specificamente rientranti (e meno efficienti) di funzioni che normalmente non lo sono. Una pratica piuttosto comune che rende una funzione non rientrante è ad esempio l'allocazione dinamica di un'area temporanea che viene riutilizzata, ma questo sarà chiaro dopo la discussione sull'allocazione dinamica della memoria.
Che fare allora se una funzione deve modificare una o più
variabili, e non è pratico utilizzare solo il suo valore di ritorno?
Occorre fornire alla funzione un sistema per accedere all'indirizzo
della variabile (nella memoria virtuale del processo, nel caso di Unix).
Una variabile che contiene l'indirizzo di un'altra variabile si chiama
puntatore a variabile. Si continua a dibattere sull'utilità
e sulla necessità
o meno dell'accesso ai puntatori da parte di un linguaggio di programmazione
di alto livello. Una delle caratteristiche più apprezzate
del C è tuttavia quella di
essere il miglior assembler in circolazione, di essere cioè
in grado di replicare efficacemente le strutture del linguaggio macchina
sottostante. Per garantire questa funzionalità, diciamo così, di livello
più basso, è essenziale disporre di un meccanismo efficace per accedere
alla memoria.
Non solo, per controllare direttamente i meccanismi di allocazione
dinamica della memoria (altra caratteristica fondamentale del C, che permette
un enorme salto di qualità nella robustezza dei programmi), è
altrettanto necessario disporre di un sistema per accedere in modo
sintatticamente chiaro alla memoria stessa.
L'esistenza di tipi di variabile specifici
per descrivere i puntatori, con i relativi meccanismi di manipolazione,\
sono una caratteristica fondamentale del C. Forse però costituiscono
anche l'ostacolo più difficile da superare in vista del
suo efficace utilizzo. Parlare e pensare
in termini di puntatori (e di puntatori a puntatori...) è fonte di lunghi e
intricati malintesi.
Dato un qualsiasi tipo di variabile (vedremo più avanti che in C si possono
anche definire nuovi tipi di variabile), ad esempio (int),
per descrivere
un puntatore ad una variabile di quel tipo basta aggiungere
un asterisco: (int *).
A bruciapelo: sizeof(int) e sizeof(int *) sono
uguali o diversi?
Verifichiamo la risposta in questo
,
dove viene utilizzato l'operatore unario &, la cui
funzione è ottenere l'indirizzo di una variabile.
Come si fa allora a rendere una funzione in grado di modificare una
variabile? Ecco:
.
In questo esempio, oltre all'operatore &, viene usato
un altro operatore unario, *, che svolge la funzione
esattamente opposta: dato un puntatore, ottiene il relativo valore.
Un'altra comune fonte di confusione, che richiede lunghissimo tempo per essere assimilata, è legata a questa affermazione:
Un array in C equivale ad un puntatore associato ad un'area di memoria pre-allocata.
Il campo in cui questa associazione ha
le conseguenza più pervasive è quello delle
stringhe di caratteri, che finora abbiamo sempre considerato come
semplici vettori di char, (char[]).
Nel nostro primo esempio di funzione, l'array di caratteri
ricevuto dalla funzione era dichiarato come (char *). Abbiamo
detto che alla funzione viene passato l'indirizzo del primo elemento
dell'array. Essendo l'array composto di tanti char,
una variabile che contiene l'indirizzo di un'altra variabile di tipo
(char) non può che essere di tipo (char *).
Qual è allora la differenza fra queste due dichiarazioni?
char message[20];
char *message;
Nel primo caso, message punta ad una sequenza di 20
caratteri che è stata allocata staticamente. Nel secondo caso,
il puntatore non viene inizializzato, per cui punta a un
mucchio di spazzatura, ed è meglio non usarlo. Diverso è il caso di:
char *message="Il mio messaggio";
In questo caso message viene inizializzato, e punta alla
costante di stringa specificata. Attenzione: non si può modificare
una costante!
Ai puntatori dedicheremo ancora la gran parte della prossima lezione.
D: Perché in Fortran non ci si preoccupava dell'esistenza dei puntatori eppure le funzioni e le subroutine potevano tranquillamente modificare le variabili?
R: Perché tipicamente nel Fortran vengono sempre passati alle subroutine e alle funzioni puntatori a tutte le variabili.
Conoscendo questa caratteristica fondamentale,
ed un paio di altre cosette, descritte
per esempio in questo documento (tratto
dal manuale di Solaris), è possibile
effettuare il link di programmi scritti parte in C e parte in altri linguaggi
come il Fortran.
Per chi volesse sperimentare, ecco il precedente esempio di chiamata a
subroutine, spezzato in una parte C,
,
e una parte Fortran,
.
Per la compilazione su Linux (con g77):
f77 -g -c ex4_f.f cc -o ex4 ex4.c ex4_f.o -lg2c -lm
Per aggiungere un po' di brio agli esempi che prenderemo in considerazione d'ora in poi, possiamo usare un piccolo insieme di routine grafiche (basate direttamente sulla libreria X). Sono state sviluppate appositamente per questo scopo didattico, e questo sottintende anche la mancanza di molte funzionalità offerte normalmente da una libreria grafica. Il codice sorgente delle routine si trova qui, ed ora commenteremo brevemente le chiamate disponibili.
Nota: tutte le funzioni, nello stile delle funzioni di
libreria standard C, restituiscono un codice di stato intero.
In questo modo non richiedono necessariamente un prototipo, e
questo, anche se è cattiva pratica, semplifica per il momento un po'
le cose. Tutte le funzioni per
le quali il compilatore non trova un prototipo sono infatti supposte
restituire un int.
Un codice di stato maggiore o uguale
a zero indica che la chiamata ha avuto successo, mentre un codice di
stato minore di zero (tipicamente -1) indica che la chiamata è
fallita per qualche ragione.
Una delle funzionalità mancanti è una gestione
più accurata degli errori....
int XHLP_open_display(char *display);
display (nel consueto formato hostaddress:0),
oppure, se display è NULL (il valore "zero",
quando è associato ad un puntatore, si preferisce esprimerlo
formalmente così), viene utilizzato il contenuto della variabile
d'ambiente DISPLAY. L'identità della connessione
con il display viene conservata in una variabile globale.
int XHLP_close_display();
XHLP_open_display.
int XHLP_open_window(int sx, int sy, char *name);
sx per sy punti.
Se name è diverso da NULL, viene utilizzato come
titolo della finestra. Questa funzione restituisce un numero
identificativo della finestra appena creata, oppure -1 in caso di
errore.
int XHLP_close_window(int id);
id.
int XHLP_set_color(int id, char *colorname);
id. Il nome
del colore (colorname) può essere scelto nella tabella
/usr/lib/X11/rgb.txt, oppure espresso nel
formato #rrggbb dove rr, gg e
bb sono le componenti di rosso, verde e blu espresse in
esadecimale.
int XHLP_draw_line(int id,int x1,int y1,int x2,int y2);
id, fra le coordinate
(x1,y1) e (x2,y2). Le coordinate sono relative
all'angolo superiore sinistro della finestra, e sono espresse in punti.
int XHLP_plot(int id,int x,int y);
id, alle coordinate (x,y).
int XHLP_print(int id,int x,int y,char *text);
text alle coordinate (x,y)
della finestra id. Viene usato il
font di default. Le coordinate (x,y) sono quelle
dell'angolo in basso a sinistra del rettangolo che contiene la stringa.
int XHLP_clear_window(int id);
id.
int XHLP_get_click(int id,int *rx,int *iry);
id,
e restituisce
in (rx,ry) le coordinate alle quali si trovava il puntatore
quando è stato premuto il tasto. All'interno della finestra specificata
e finché non viene premuto il tasto, la forma del cursore si trasforma
in una crocetta.
Un'ultima cosa: come fare a prendere questo codice sorgente e
trasformarlo in una "libreria", che possa, come abbiamo visto, essere
inclusa con -l ?
Per creare una libreria dinamica:
cc -shared xhelper.c -o libxhelper.so
Per creare una libreria statica:
cc -c xhelper.c
ar crv libxhelper.a xhelper.o
Esercizio: Scrivere o immaginare un programma che richiede le caratteristiche (raggio e coordinate del centro) di una circonferenza e la disegna sulla finestra grafica.