Terza lezione

Chiamata di funzioni

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.

Puntatori a variabili

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.

Puntatori e array

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?

  1. char message[20];
  2. 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.

La programmazione a linguaggi misti (cenni)

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

Una helper library grafica

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....

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.