File Header Innestati in Linguaggio C

Un file header, in linguaggio C, può includere, a sua volta, altri file header. Questa pratica è detta inclusione innestata o inclusione annidata.

In questo articolo vedremo come includere file header in altri file header in linguaggio C, e come risolvere il problema dell'inclusione multipla che da tale pratica può derivare.

In particolare, studieremo lo schema delle guardie di inclusione, un meccanismo che permette di proteggere i file header dall'inclusione multipla.

Inclusioni innestate

Il meccanismo di inclusione dei file header in linguaggio C non ha limiti.

Possiamo, infatti, includere file header in altri file header attraverso la direttiva di precompilazione #include.

A prima vista questa pratica può sembrare alquanto strana. Tuttavia, nella realtà essa è molto adoperata.

Vediamo un esempio. Supponiamo di voler realizzare un programma che effettua calcoli geometrici, ad esempio un programma in grado di calcolare le aree di varie figure geometriche dati in input i valori delle loro dimensioni.

Potremmo realizzare un file sorgente geometria.c che contiene le funzioni per il calcolo delle aree delle varie figure geometriche, e un file header geometria.h che contiene i prototipi delle funzioni definite in geometria.c:

// geometria.h
float area_quadrato(float lato);
float area_poligono_regolare(float base, float apotema, int n_lati);
float area_cerchio(float raggio);

Il file sorgente geometria.c conterrà le definizioni delle funzioni:

// geometria.c
#include "geometria.h"

float area_quadrato(float lato) {
    return lato * lato;
}

float area_poligono_regolare(float base, float apotema, int n_lati) {
    return base * apotema * n_lati / 2;
}

float area_cerchio(float raggio) {
    return 3.14159 * raggio * raggio;
}

Nel file geometria.c abbiamo adoperato, per calcolare l'area del cerchio, un valore letterale corrispondente ad un'approssimazione di \pi.

Potremmo voler definire un altro file di intestazione il cui scopo è quello di raccogliere tutte le costanti matematiche utilizzate nel nostro programma, di cui la costante \pi è solo un esempio.

// costanti.h
#define PI 3.14159

In questo caso, possiamo includere il file costanti.h nel file geometria.h:

// geometria.h
#include "costanti.h"

float area_quadrato(float lato);
float area_poligono_regolare(float base, float apotema, int n_lati);
float area_cerchio(float raggio);

In questo modo, il file geometria.h conterrà i prototipi delle funzioni e le costanti matematiche utilizzate nel file geometria.c:

// geometria.c
#include "geometria.h"

float area_quadrato(float lato) {
    return lato * lato;
}

float area_poligono_regolare(float base, float apotema, int n_lati) {
    return base * apotema * n_lati / 2;
}

float area_cerchio(float raggio) {
    return PI * raggio * raggio;
}

Potremmo voler aggiungere una funzione che verifica se dati i lati di un rettangolo esso sia un quadrato:

// geometria.h
#include "costanti.h"

float area_quadrato(float lato);
float area_poligono_regolare(float base, float apotema, int n_lati);
float area_cerchio(float raggio);
int is_quadrato(float lato1, float lato2);
// geometria.c
#include "geometria.h"

/* ... */

int is_quadrato(float lato1, float lato2) {
    return lato1 == lato2;
}

Ma, la funzione is_quadrato alla fine restituisce solo 1 o 0, ossia valori booleani che possiamo definire in un nuovo file header boolean.h:

// boolean.h
typedef int BOOL;
#define VERO 1
#define FALSO 0
// geometria.h
#include "costanti.h"
#include "boolean.h"

float area_quadrato(float lato);
float area_poligono_regolare(float base, float apotema, int n_lati);
float area_cerchio(float raggio);
BOOL is_quadrato(float lato1, float lato2);
// geometria.c
#include "geometria.h"

/* ... */

BOOL is_quadrato(float lato1, float lato2) {
    return lato1 == lato2 ? VERO : FALSO;
}

Infine, possiamo includere il file geometria.h nel file sorgente principale del nostro programma:

// main.c
#include <stdio.h>
#include "geometria.h"

int main() {
    /* PROGRAMMA PRINCIPALE */
}

Il risultato finale è mostrato in figura:

Esempio di File Header Innestati
Figura 1: Esempio di File Header Innestati

Come si può osservare abbiamo che entrambi i file sorgenti geometria.c e main.c includono il file header geometria.h, che a sua volta include i file header costanti.h e boolean.h.

In questo modo, possiamo definire le funzioni e le costanti matematiche in file separati, mantenendo il codice ben organizzato e facilmente manutenibile.

Il linguaggio C non pone limiti al numero di inclusioni innestate.

Definizione

File Header Annidati

Un file header può includere altri file header, che a loro volta possono includere altri file header, e così via. Questa pratica è detta inclusione innestata o annidata.

Tradizionalmente, l'inclusione annidata non era vista di buon occhio dai programmatori C. Addirittura, le prime versioni del linguaggio non permettevano l'inclusione annidata. Tuttavia, con l'avvento di compilatori più moderni e potenti, l'inclusione annidata è diventata una pratica comune e accettata.

Il problema dell'inclusione multipla

L'inclusione annidata vista sopra espone, tuttavia, i nostri programmi ad un problema noto come inclusione multipla.

Per comprendere di cosa si tratta, analizziamo un esempio.

Supponiamo di voler creare un programma che effettua calcoli di geometria analitica. Questo programma utilizza punti nello spazio bidimensionale, e quindi definiamo una struttura Punto che rappresenta una coppia di coordinate in un file header punto.h. Sempre in questo file header definiamo una funzione distanza che calcola la distanza tra due punti.

// punto.h
typedef struct {
    float x;
    float y;
} Punto;

float distanza(Punto p1, Punto p2);

Il file sorgente punto.c conterrà le definizioni delle funzioni:

// punto.c
#include "punto.h"

float distanza(Punto p1, Punto p2) {
    /* Implementazione */
}

In un altro file header rettangolo.h definiamo una struttura Rettangolo che rappresenta un rettangolo nel piano cartesiano, e una funzione area che calcola l'area del rettangolo. Questo file header include il file header punto.h per poter utilizzare la struttura Punto.

// rettangolo.h
#include "punto.h"

typedef struct {
    Punto vertice1;
    Punto vertice2;
} Rettangolo;

float area(Rettangolo r);

Il file sorgente rettangolo.c conterrà le definizioni delle funzioni:

// rettangolo.c
#include "rettangolo.h"

float area(Rettangolo r) {
    /* Implementazione */
}

Il file sorgente principale main.c dovrà adoperare sia le funzioni definite in punto.c che in rettangolo.c. Pertanto, includerà entrambi i file header punto.h e rettangolo.h.

// main.c
#include <stdio.h>

#include "punto.h"
#include "rettangolo.h"

int main() {
    /* PROGRAMMA PRINCIPALE */
}

Scritto in questo modo, il programma non può essere compilato!

Per comprendere il perché dobbiamo visualizzare la struttura congiunta dei vari file sorgenti e file header inclusi:

Esempio di Problema di Inclusione Multipla
Figura 2: Esempio di Problema di Inclusione Multipla

Il problema sta nel fatto che, se seguiamo i collegamenti tra i file header, vediamo che il file punto.h è incluso due volte nel file main.c. La prima inclusione avviene direttamente, basta guardare la freccia rossa diretta nell'immagine. La seconda inclusione avviene indirettamente, attraverso l'inclusione del file punto.h nel file rettangolo.h. Per rendersene conto basta seguire la seconda freccia rossa che parte da main.c, raggiunge rettangolo.h e da lì arriva a punto.h.

Fintanto che il file punto.h contiene solo dichiarazioni di variabili, funzioni e macro, non ci sono problemi. Il problema sorge quando nei file header sono contenute dichiarazioni di tipi.

Infatti, se proviamo a compilare main.c con il compilatore gcc otteniamo il seguente errore:

$ gcc main.c punto.c rettangolo.c -o main
In file included from rettangolo.h:1,
                 from main.c:5:
punto.h:1:8: error: redefinition of ‘struct Punto’
    1 | typedef struct {
      |        ^~~~~~
punto.h:1:8: note: originally defined here
    1 | typedef struct {
      |        ^~~~~~

In altre parole, il compilatore ci dice che la struttura Punto è stata definita due volte, una volta nel file punto.h e una volta nel file rettangolo.h che include punto.h.

Per risolvere il problema bisogna proteggere i file header dall'inclusione multipla. In realtà, non sarebbe necessario proteggere tutti i file header, ma solo quelli che contengono dichiarazioni di tipi. Tuttavia, è una buona pratica applicare la protezione a tutti i file header così che se in un secondo momento vengono aggiunte dichiarazioni di tipi non ci si debba preoccupare di aggiungere le protezioni.

Per proteggere un file header è sufficiente racchiudere l'intero file tra due direttive di precompilazione #ifndef e #endif. Tali direttive prendono il nome di Guardie di Inclusione.

Ad esempio, per proteggere il file punto.h possiamo scrivere:

// punto.h
#ifndef PUNTO_H
#define PUNTO_H

typedef struct {
    float x;
    float y;
} Punto;

float distanza(Punto p1, Punto p2);

#endif

Quando questo file viene incluso la prima volta quello che accade è:

  1. La direttiva #ifndef PUNTO_H è valutata come vera, perché la macro PUNTO_H non è stata definita.
  2. La direttiva #define PUNTO_H definisce la macro PUNTO_H.
  3. Il contenuto del file viene incluso.

Quando il file viene incluso la seconda volta:

  1. La direttiva #ifndef PUNTO_H è valutata come falsa, perché la macro PUNTO_H è già stata definita.
  2. Il contenuto del file non viene incluso.

In questo modo, il problema dell'inclusione multipla è risolto.

Il nome della macro, che nel nostro caso era PUNTO_H non ha un'importanza particolare. Tuttavia, è una buona pratica utilizzare un nome che rifletta il nome del file header, in modo da evitare conflitti con altre macro definite in altri file header.

Alcuni schemi comuni di macro usate a questo scopo sono:

  • NOMEFILE_H
  • NOMEFILE_HEADER
  • NOMEFILE_H_INCLUDED
  • _NOMEFILE_H_

L'importante è essere consistenti nell'utilizzo del nome della macro.

Definizione

Inclusione Multipla e Guardie di Inclusione

L'inclusione multipla è un problema che si verifica quando un file header è incluso più volte in un file sorgente o in altri file header. Questo problema si manifesta quando il file header contiene dichiarazioni di tipi, come strutture o enumerazioni.

Lo schema da adoperare per proteggere un file header dall'inclusione multipla è quello di usare delle direttive di precompilazione che prendono il nome di Guardie di Inclusione.

Le guardie di inclusione sono definite come segue:

#ifndef NOMEFILE_H
#define NOMEFILE_H

/* Contenuto del file header */

#endif

dove NOMEFILE_H rimanda al nome del file header.

In Sintesi

  • Il meccanismo di inclusione dei file header in linguaggio C non ha limiti. Possiamo includere file header in altri file header attraverso la direttiva di precompilazione #include.
  • L'inclusione annidata è una pratica comune e accettata in C. Tuttavia, espone i nostri programmi al problema dell'inclusione multipla.
  • L'inclusione multipla è un problema che si verifica quando un file header è incluso più volte in un file sorgente o in altri file header. Questo problema si manifesta quando il file header contiene dichiarazioni di tipi, come strutture o enumerazioni.
  • Per proteggere un file header dall'inclusione multipla si adopera uno schema di guardie di inclusione, che consiste nell'usare delle direttive di precompilazione #ifndef, #define e #endif.

Adesso che sappiamo come includere file header in altri file header, nella prossima lezione riassuntiva vedremo uno schema generale di realizzazione di file header che ha una validità di applicazione in molti contesti.