[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home] [volume] [parte]


Capitolo 582.   C: organizzazione dei file sorgenti

Quando si scrive un programma che non sia estremamente banale, diventa importante organizzare i file dei sorgenti in un modo gestibile. Se l'esperienza di programmazione da cui si proviene, quando ci si rivolge al C, è quella dei linguaggi interpretati, si può essere tentati di scrivere tutto il proprio programma in un file solo, ma questo approccio può essere controproducente. D'altra parte, per dividere il lavoro in più file, occorre che tale suddivisione abbia un senso pratico, conforme alla filosofia del linguaggio.

582.1   File di intestazione

La direttiva #include del precompilatore consente di incorporare un altro file, scritto secondo le regole del linguaggio, come se il suo contenuto facesse parte del file incorporante. Tradizionalmente questi file che vengono incorporati sono «file di intestazione», a cui si dà un'estensione diversa, .h, proprio per distinguerne lo scopo.

Un file di intestazione, perché sia utile e non serva a creare maggiore confusione, può contenere la dichiarazione di macro-variabili, di macro-istruzioni, di tipi derivati, di prototipi di funzione e di variabili pubbliche. Non conviene inserire il codice completo delle funzioni all'interno di un file di intestazione, perché queste verrebbero replicate inutilmente nei file-oggetto, ogni volta che viene incorporato il file stesso.

Se si rispetta questo principio, un file di intestazione può essere incorporato in diversi file, garantendo un uso uniforme di quanto dichiarato al suo interno, senza duplicazioni inutili nel risultato della compilazione, anche se ciò che contiene tale file viene usato solo parzialmente o non viene usato affatto.

Un file di intestazione deve contenere ciò che serve alla soluzione di un certo tipo di problematica, ben delimitata. In particolare, dovrebbe contenere tutti i prototipi delle funzioni che servono, o possono servire, per quel tale problema.

582.2   Funzioni pubbliche

Le funzioni che devono poter essere usate in varie parti del programma è bene siano pubbliche (come avviene in modo predefinito) e che siano descritte come prototipo in un file di intestazione appropriato. Per quanto possibile, le funzioni potrebbero essere scritte in file indipendenti, ovvero: un file distinto per ogni funzione.

Dal momento che le funzioni potrebbero avere bisogno di usare macro-variabili o macro-istruzioni definite nel file di intestazione che ne dichiara i prototipi, nei file di queste funzioni dovrebbe apparire l'inclusione del file di intestazione rispettivo.

582.3   Funzioni e variabili private

Le funzioni dichiarate con la parola chiave static sono visibili solo all'interno del file-oggetto in cui vanno a finire. Queste funzioni statiche sono utili in quanto vengono chiamate da una sola o da poche funzioni; in tal caso, questo gruppo di funzioni è costretto a convivere nello stesso file.

Lo stesso problema riguarda le variabili che devono essere utilizzate da più funzioni, ma che non devono essere visibili alle altre, perché anche in questo caso si rende necessario il mettere tale insieme nello stesso file.

582.4   Esempio di «stdlib.h»

Per comprendere il senso di quanto appena descritto in modo così sintetico, è utile osservare l'organizzazione della libreria C standard, anche se poi nella realtà i contenuti dei file che la compongono non sono sempre facili da interpretare. A ogni modo, qui viene proposto il caso di quella parte della libreria C che fa capo al file di intestazione stdlib.h.

Per cominciare, già dal nome del file scelto come esempio, va osservato che un file di intestazione realizzato in modo conforme alla filosofia del linguaggio rappresenta una «libreria» di qualcosa, anche se, per le funzioni, contiene solo i prototipi. Ecco, in breve, come potrebbe essere fatto il file stdlib.h, omettendo alcune porzioni ridondanti per i fini della spiegazione:

#ifndef _STDLIB_H
#define _STDLIB_H       1
#define NULL 0
typedef unsigned long int size_t;
typedef unsigned int wchar_t;
#include <limits.h>
typedef struct {int quot; int rem;} div_t;
...
#define RAND_MAX        INT_MAX
...
int   atoi    (const char *nptr);
...
int   rand    (void);
void  srand   (unsigned int seed);
void *malloc  (size_t size);
void *realloc (void *ptr, size_t size);
void  free    (void *ptr);
#define calloc(nmemb, size) (malloc ((nmemb) * (size)))
...
#endif

Si può osservare che l'interpretazione del contenuto del file è subordinata al fatto che la macro-variabile _STDLIB_H non sia già stata dichiarata, mentre altrimenti viene dichiarata. In pratica, con questo meccanismo, se per qualunque ragione un file si trova a incorporare più volte il file di intestazione, il compilatore considera quel contenuto solo la prima volta.

Nell'esempio si vedono dichiarazioni di macro-variabili, di macro-istruzioni (calloc() è, in questo caso, una macro-istruzione), di tipi di dati derivati. Secondo il buon senso, tutte queste cose devono servire alle funzioni di cui sono presenti i prototipi, ma soprattutto per ciò che riguarda i prototipi. Per esempio, la macro-variabile NULL viene dichiarata nel file di intestazione perché è il valore che potrebbe essere restituito da funzioni come malloc() e deve essere uniformato; il tipo derivato size_t viene dichiarato perché viene usato dalla funzione malloc() e da altre; il file limits.h viene incorporato perché definisce il valore della macro-variabile INT_MAX che in questo caso viene usato per definire RAND_MAX, la quale deve essere uniformata per l'uso con la funzione rand().

La funzione atoi() è utile per dimostrare in che modo mettere ogni funzione nel proprio file indipendente. Per esempio, quello che segue potrebbe essere il file atoi.c:

#include <stdlib.h>:
#include <ctype.h>:
int
atoi (const char *nptr)
{
    int i;
    int sign = +1;
    int n;
    
    for (i = 0; isspace (nptr[i]); i++)
      {
        ;       // Si limita a saltare gli spazi iniziali.
      }

    if (nptr[i] == '+')
      {
        sign = +1;
        i++;
      }
    else if (nptr[i] == '-')
      {
        sign = -1;
        i++;
      }

    for (n = 0; isdigit (nptr[i]); i++)
      {
        n = (n * 10) + (nptr[i] - '0');         // Accumula il valore.
      }

    return sign * n;
}

Come si vede, questa versione di atoi() si avvale delle funzioni isspace() e isdigit(), dichiarate nel file ctype.h che viene aggiunto di conseguenza all'elenco delle inclusioni. Questa inclusione non è stata fatta nel file di intestazione stdlib.h, perché l'uso delle funzioni isspace() e isdigit() è dovuto soltanto a una scelta realizzativa di atoi() e non perché la libreria stdlib.h dipenda necessariamente da ctype.h.

Per realizzare le funzioni rand() e srand() deve essere condivisa una variabile, la quale può essere nascosta prudentemente al resto del programma. Pertanto serve un file unico che incorpori entrambe le funzioni:

#include <stdlib.h>
static unsigned int _srand = 1; // Il rango di «_srand» deve essere
                                // maggiore o uguale a quello di
                                // «RAND_MAX» e di «unsigned int».
int
rand (void)
{
    _srand = _srand * 1234567 + 12345;
    return _srand % ((unsigned int) RAND_MAX + 1);
}

void
srand (unsigned int seed)
{
    _srand = seed;
}

582.5   Parametri delle macro-istruzioni

Quando si dichiara una macro-istruzione, si usano delle macro-variabili interne che rappresentano i parametri per la «chiamata» di questa specie di funzione. Dal momento che il codice che costituisce la macro-istruzione può avvalersi di altre macro-variabili già dichiarate e dato che di norma queste hanno nomi che utilizzano lettere maiuscole, è bene che quelle interne siano scritte con sole lettere minuscole. In pratica, conviene fare come nella macro-istruzione già apparsa nella sezione precedente:

:-)

#define calloc(nmemb, size) (malloc ((nmemb) * (size)))

Al contrario, facendo come nell'esempio successivo, il rischio che sia già stata dichiarata la macro-variabile SIZE oppure NMEMB è più alto:

:-(

#define calloc(NMEMB, SIZE) (malloc ((NMEMB) * (SIZE)))

582.6   Compilazione

I vari file con estensione .c possono essere compilati separatamente, per ottenere altrettanti file-oggetto da collegare successivamente (i file .h devono essere incorporati da file .c, pertanto non vanno compilati da soli). Per esempio, per un certo gruppo di file collocato in una certa directory, si potrebbe usare un file-make simile a quello seguente:

sorgenti = uno due tre
#
all: $(sorgenti)
#
clean:
        @rm *.o 2> /dev/null
#
$(sorgenti):
        @echo $@.c
        @gcc -Wall -Werror -o $@.o -c $@.c -I../include

In pratica, si presume che nella directory in cui si trova il file-make, ci siano i file uno.c, due.c e tre.c, per i quali si vogliono ottenere altrettanti file-oggetto, con l'estensione appropriata. Si presume anche che i file di intestazione a cui i sorgenti fanno riferimento si trovino nella directory ../include/.

Compilando in questo modo i file che contengono il minimo indispensabile (possibilmente una sola funzione per ciascuno), quando si verificano errori è più semplice concentrare l'attenzione per correggerli.

Quando si dispone dei file-oggetto si può passare al collegamento (link), ma anche in questa fase possono emergere dei problemi di tipo diverso: di solito si tratta di una funzione che viene chiamata, della quale esiste solo il prototipo e quindi non si trova in alcun file-oggetto. Naturalmente, il collegamento deve avvenire una volta sola, con tutti i file-oggetto che compongono il programma.

Figura 582.7. Indicazioni generali per la stesura di un insieme di file sorgenti ordinato.

organizzazione dei sorgenti


Appunti di informatica libera 2008 --- Copyright © 2000-2008 Daniele Giacomini -- <appunti2 (ad) gmail·com>


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome c_organizzazione_dei_file_sorgenti.htm

[successivo] [precedente] [inizio] [fine] [indice generale] [indice ridotto] [indice analitico] [home]

Valid ISO-HTML!

CSS validator!

Gjlg Metamotore e Web Directory