Moduli in C: Header &co. - STB1019/SkullOfSummer GitHub Wiki
fonte c-modules
In progetti complessi, dove occorre implementare diverse funzionalità per poter offrire un set specifico di servizi, risulta comodo suddividere in più file l'intero programma. Tali suddivisioni prendono il nome di moduli; ogni modulo rappresenta una funzionalità ed le funzionalità devono essere del tutto indipendenti dalle altre.
Ciascun modulo è dotato di un'interfaccia definita dal file header.h
ed un ulteriore file module.c
(da ora in poi rispettivamente header e body,ndr). L'header ha il compito di dichiarare variabili e funzioni utilizzati nel body (con opportune convenzioni).
All'header spetta il compito di rendere accessibile a chiunque ne faccia richiesta, un determinato set di funzioni (non-static), dichiarandone il prototipo. L'header può anche esporre delle variabili del modulo che vengono poi definite esplicitate nel body. Invece, le costanti pubbliche sono dichiarate in toto all'interno dell'header.
Nell'header devono essere messe tutte le dichiarazioni di variabili e funzioni. Nello specifico vanno inseriti nell'header:
-
le guardie: gli header sono copiati-incollati dal preprocessore in molte "*.c", quindi è utile un sistema per evitare di inserire nello stesso file "*.c" lo stesso header più volte;
-
le dichiarazioni di funzioni pubbliche: tutte le funzioni che vuoi che il tuo modulo offra al mondo (quindi funzioni "pubbliche") devono avere il loro prototipo nell'header;
-
le dichiarazioni di variabili di modulo con extern: se il tuo modulo fornisce una variabile al mondo (per esempio
stdio.h
offre le variabilistdout
,stdin
estderr
) allora nell'header devono essere presenti. Tuttavia è necessario dichiararle con la reserved wordextern
; -
le definizioni di macro utili all'esterno: per esempio se il tuo modulo gestisce buffer di una certa dimensione massima, una macro che potrebbe essere definita con
#define
nell'header è la dimensione massima (per esempio#define MAX_SIZE 100
); -
typedef di
struct
,enum
,union
: nel caso in cui è ammissibile che un programmatore esterno, nell'usare il tuo modulo, abbia necessità di creare variabili di quel tipo. Per esempio se pensi che un programmatore esterno dovrà dichiarare variabili di tipofoo
dove:typedef struct foo { int a; unsigned long b; } foo;
allora devi mettere la definizione di
foo
nell'header;
Viceversa nel body:
- tutte le definizioni delle funzioni che hai definito nel header, senza static;
- tutte le definizioni di funzioni "private" di cui il tuo modulo ha bisogno per fare il suo lavoro, definita con static: queste funzioni saranno visibili solo dal modulo stesso e nessun altro;
- tutte le definizioni di variabili globali di modulo private, marcate con static;
- tutte le definizioni delle variabili dichiarate con extern nell'header;
- eventuali MACRO che il tuo modulo usa: le macro qui definite saranno visibili ed usabili solo dal modulo stesso;
- tutte le definizioni di tipi (
struct
,enum
,union
) che userai solo all'interno del body;
L'uso del modulo da parte di terzi, deve limitarsi alla sola conoscenza del suo header. Il Body deve rimanere celato (pertanto ciascuna dichiarazione deve essere il più esplicativa possibile). Di seguito, suggeriamo un modello per l'header non dissimile da quanto visto nella programmazione O.O.
Il modello è stato realizzato da Umberto Salsi.
/**
* Skeleton example of a C module. Illustrates the general structure of a
* module's interface.
* @copyright 2008 by icosaedro.it di Umberto Salsi
* @license as you wish
* @author Umberto Salsi <[email protected]>
* @version 2008-04-23
* @file
*/
#ifndef module1_H
#define module1_H
/*
* System headers required by the following declarations
* (the implementation will import its specific dependencies):
*/
#include <stdlib.h>
#include <math.h>
/*
* Application specific headers required by the following declarations
* (the implementation will import its specific dependencies):
*/
#include "module2.h"
#include "module3.h"
/* Set EXTERN macro: */
#ifdef module1_IMPORT
#define EXTERN
#else
#define EXTERN extern
#endif
/* Constants declarations here. */
/* Types declarations here. */
/* Global variables declarations here. */
/* Function prototypes here. */
#undef module1_IMPORT
#undef EXTERN
#endif
/**
* Skeleton example of a C module. Illustrates the general structure of a
* module's interface.
* @copyright 2008 by icosaedro.it di Umberto Salsi
* @license as you wish
* @author Umberto Salsi <[email protected]>
* @version 2008-04-23
* @file
*/
Estremamente utile per spiegare le funzionalità del modulo, ma ancor di più per tool di documentazione (Doxygen, ndr).
#ifndef module1_H
#define module1_H
...
#endif
Non è dato sapere quante volte un header venga incluso all'interno dello stesso programma. Siccome nel modulo sono a volte definiti anche dei typedef (di cui è ammessa al più una dichiarazione per translation unit), è bene assicurarsi che uno stesso header venga incluso al più una volta in una singola translation unit.
#include <stdlib.h>
#include <math.h>
#include "module2.h"
#include "module3.h"
Talvolta è necessario inserire librerie (o solo alcuni loro moduli) di sistema, oppure presenti nel nostro programma. Questo non mina assolutamente l'indipendenza tra i moduli, piuttosto incentiva il loro riuso.
#ifdef module1_IMPORT
#define EXTERN
#else
#define EXTERN extern
#endif
...
#undef module1_IMPORT
#undef EXTERN
La macro EXTERN è stata introdotta per evitare di riscrivere la dichiarazione e la definizione della variabili di modulo: in questo modo si possono evitare errori di copia-incolla dei protototipi.
L'idea è che la macro module1_IMPORT
sia definita soltanto nell'implementazione del modulo. Se ipotizziamo che nell'header ci sia:
EXTERN int foo;
L'implementazione quindi vedrà solo:
int foo;
Ossia una definizione della variabile. Viceversa un altro codice sorgente "*.c" non avrà definito la macro module1_IMPORT
e quindi vedrà la riga di foo
così fatta:
extern int foo;
Pertanto utilizzando solamente l'header, e supponendo che non venga definita la macro prima dell'inclusione stessa dell'header, le variabili e funzioni pubbliche avranno l'attributo extern
; pertanto la loro definizione sarà collocata altrove (ed è compito del linker indicare quest'ultima posizione).
extern
permette di separare la dichiarazione dalla definizione di una variabile. Per le funzioni, invece, il prototipo è la dichiarazione della stessa, mentre la sua definizione è l'implementazione del corpo della funzione.
extern type_var name_var;
L'attributo extern
permette di dichiarare una variabile, tale per cui il compilatore è a conoscenza di questo identificatore ma la sua definizione è altrove (e spetterà al linker collegare la dichiarazione con la sua corrispettiva definizione). In altre parole, l'attributo extern
permette di dichiarare una variabile la cui allocazione in memoria viene definita da terzi (un ulteriore modulo).
A supporto, la dichiarazione è ciò di cui ha bisogno il compilatore, mentre la definizione è ciò di cui necessita il linker (Riferimento) (Riferimento).
Ovviamente, se durante la fase di linking (dopo il compilatore) non dovesse esserci nessuna area di memoria allocata per la dichiarazione, spetterà al linker sollevare l'eccezione.
Per le funzioni, la loro dichiarazione (con il corrispettivo prototipo) non beneficia dell'uso dell'extern
perché tale reserved word è implicita nel prototipo.
Ricordati che è possibile avere più dichiarazioni (di variabili) e prototipi di funzione, ma sempre una ed una sola loro definizione.
Un buon modo di vedere la reserved word extern per le variabili è:
- scrivere
int foo;
è come dire al compilatore: "Sappi che ora devi allocare un numero sufficiente di byte in memoria tale da contenere unint
e che questa nuova area di memoria allocata avrà come nome "foo"; - scrivere
extern int foo;
è come dire al compilatore: "Sappi che, da qualche parte, esiste qualcuno (file "*.c", translation unit) che definisce una variabile chiamatafoo
: io qui non la definisco, la uso soltanto, ma so che qualcun altro la definirà per me!";
Durante l'implementazione, non è sempre facile mantenere uno stile corretto e coerente; specie durante la suddivisione dei moduli per le varie cartelle del nostro programma. Pertanto, ecco in soccorso l'articolo scritto da Michael Barr:
-
Un header, un modulo Solo così è possibile mantenere l'indipendenza tra i moduli. Pertanto ogni modulo deve implementare una singola funzionalità del sistema.
-
static
è la prima opzione Le variabili "private", non accessibili al di fuori del modulo devono implementare l'attributostatic
nel body (il file*.c
) e non nell'header. -
Prototipi di funzioni pubbliche Come in altri linguaggi, è buona norma adottare una nomenclatura completa (ma prolissa), per le funzioni pubbliche (ovvero quelle prive dell'attributo
static
). Ad es. nel file header io.h, potrebbe aver protipi comeio_build()
,io_cast_integer()
,io_display_refresh()
oio_pointer_reader()
, etc. -
Black Box Non devono esistere riferimenti ad altri moduli impliciti; il solo ed unico modo per recuperare funzioni di altri moduli è attraverso l'inclusione del rispettivo file header.
-
Non solo prototipi Per alcune funzioni dette Funzioni
inline
conviene esplicitarne il corpo direttamente nel file header, purché si rispettino le seguenti indicazioni (suggerimento da parte di Michael Barr):- La funzione è utilizzata solo ed esclusivamente all'interno di altre funzioni prototipate dal file header.
- La funzione è privata
- La funzione rende più snello e facile da leggere il codice
-
extern
con cautela Prima di considerare l'uso dell'attributoextern
conviene rivedere se la progettazione del software è corretta (specie se siamo programmatori inesperti, ndr). L'attributoextern
risulta estremamente comodo per rendere rendere le variabili del modulo (dichiarate nell'header), fruibili all'esterno.
Commento Come Studenti, riteniamo di riflettere attentamente prima di utilizzare
extern
a cuor leggero al fine di aumentare la qualità di progettazione del software. Uno dei casi in cui è opportuno utilizzare tale attributo, sia dovuto all'inclusione di moduli il cui stesso inserimento nel programma in progettazione, potrebbe comportare malfunzionamenti e/o problematiche di compatibilità (o permessi) specifici.
-
Dati strutturati L'header deve contenere solo il riferimento alla struttura, senza che vi sia alcuna specifica di questa. Pertanto non vi sarà mai
struct { .. } asd;
, lasciando che la specifica sia data in input da terzi. Piuttosto, nell'header sarà opportuno inserire un suo sinonimotypedef struct foo mod_k_tipo
, rispettando la convenzione di indicare l'appartenenza. Nel caso di enum, per il medesimo principio delle funzioni di supporto, si suggerisce di dichiarare direttamente nell'header, anziché demandare a terzi.
Utilizzando nell'header la dichiarazione:
typedef struct foo mod_k_tipo
non sarà possibile creare alcuna variabile di tipo mod_k_tipo
ma al più variabili di tipo mod_k_tipo*
(Riferimento). Questi tipi sono chiamati tipi opachi (opaque types) perché sono accessibili solo tramite puntatori e funzioni di accesso (tipo getter e setter) ma la cui attuale implementazione è sconosciuta (potrebbe essere una struct
di 10 campi come una struct
di 100000). Non è possibile dereferenziare o allocare nello stack una variabile di tipo opaco. Quindi non è possibile fare:
typedef struct opaque_t opaque_t;
opaque_t* var;
(*a).a; //sta dereferenziando!
var->a; //sta dereferenziando!
opaque_t b; //sta allocando byte sullo stack!