Ricerca…


introduzione

Nella moderna C, i file di intestazione sono strumenti cruciali che devono essere progettati e utilizzati correttamente. Consentono al compilatore di eseguire il controllo incrociato delle parti compilate in modo indipendente di un programma.

Le intestazioni dichiarano tipi, funzioni, macro ecc. Che sono necessari ai consumatori di una serie di servizi. Tutto il codice che utilizza una di queste strutture include l'intestazione. Tutto il codice che definisce queste strutture include l'intestazione. Ciò consente al compilatore di verificare che gli usi e le definizioni corrispondano.

introduzione

Ci sono un certo numero di linee guida da seguire quando si creano e si usano i file di intestazione in un progetto C:

  • Idemopotence

    Se un file di intestazione è incluso più volte in un'unità di traduzione (TU), non dovrebbe interrompere le build.

  • Self-contenimento

    Se hai bisogno delle strutture dichiarate in un file di intestazione, non dovresti includere esplicitamente nessun altro header.

  • minimalità

    Non dovresti essere in grado di rimuovere alcuna informazione da un'intestazione senza causare fallimenti di build.

  • Includi cosa usi (IWYU)

    Di maggiore preoccupazione per C ++ rispetto a C, ma comunque importante anche in C. Se il codice in una TU (chiamiamolo code.c ) utilizza direttamente le caratteristiche dichiarate da un'intestazione (chiamatelo "headerA.h" ), quindi code.c dovrebbe #include "headerA.h" direttamente, anche se la TU include un'altra intestazione (chiamiamola "headerB.h" ) che al momento include "headerA.h" .

Occasionalmente, ci potrebbero essere buoni motivi per rompere una o più di queste linee guida, ma entrambi dovresti essere consapevole del fatto che stai infrangendo la regola ed essere consapevole delle conseguenze di ciò prima di romperlo.

idempotence

Se un particolare file di intestazione è incluso più di una volta in un'unità di traduzione (TU), non dovrebbero esserci problemi di compilazione. Questo è chiamato 'idempotence'; le tue intestazioni dovrebbero essere idempotenti. Pensa a quanto sarebbe difficile la vita se dovessi assicurarti che #include <stdio.h> stato incluso solo una volta.

Esistono due modi per ottenere l'idempotence: header guards e #pragma once directive.

Guardie di testa

Le protezioni dell'intestazione sono semplici e affidabili e conformi allo standard C. Le prime righe senza commento in un file di intestazione dovrebbero essere del formato:

#ifndef UNIQUE_ID_FOR_HEADER
#define UNIQUE_ID_FOR_HEADER

L'ultima riga di non commento deve essere #endif , facoltativamente con un commento dopo di esso:

#endif /* UNIQUE_ID_FOR_HEADER */

Tutto il codice operativo, comprese altre #include direttive, dovrebbe essere tra queste linee.

Ogni nome deve essere unico. Spesso, viene utilizzato uno schema di nomi come HEADER_H_INCLUDED . Alcuni codici meno #ifndef BUFSIZ utilizzano un simbolo definito come intestazione (ad esempio #ifndef BUFSIZ in <stdio.h> ), ma non è affidabile come un nome univoco.

Un'opzione consisterebbe nell'utilizzare un hash MD5 (o altro) generato per il nome della guardia dell'intestazione. Evitare di emulare gli schemi utilizzati dalle intestazioni di sistema che utilizzano frequentemente i nomi riservati all'implementazione - i nomi iniziano con un trattino basso seguito da un altro carattere di sottolineatura o da una lettera maiuscola.

Il #pragma once direttiva

In alternativa, alcuni compilatori supportano la direttiva #pragma once che ha lo stesso effetto delle tre linee mostrate per le guardie di intestazione.

#pragma once

I compilatori che supportano #pragma once includono MS Visual Studio e GCC e Clang. Tuttavia, se la portabilità è un problema, è preferibile utilizzare le protezioni intestazione o utilizzare entrambe. I compilatori moderni (quelli che supportano C89 o successivi) devono ignorare, senza commenti, pragma che non riconoscono ("Qualsiasi pragma di questo tipo che non è riconosciuto dall'implementazione viene ignorato") ma le vecchie versioni di GCC non erano così indulgenti.

Self-contenimento

Le intestazioni moderne dovrebbero essere autarchiche, il che significa che un programma che deve utilizzare le funzionalità definite da header.h può includere quell'intestazione ( #include "header.h" ) e non preoccuparsi se altre intestazioni devono essere incluse per prime.

Raccomandazione: i file di intestazione dovrebbero essere autonomi.


Regole storiche

Storicamente, questo è stato un argomento moderatamente controverso.

Nel corso di un altro millennio, gli standard AT & T Indian Hill C e gli standard di codifica hanno dichiarato:

I file di intestazione non devono essere nidificati. Il prologo per un file di intestazione dovrebbe, quindi, descrivere quali altre intestazioni devono essere #include d perché l'intestazione sia funzionale. In casi estremi, in cui è necessario includere un numero elevato di file di intestazione in diversi file di origine, è accettabile inserire tutti i #include s comuni in un file di inclusione.

Questa è l'antitesi del self-contenimento.

Regole moderne

Tuttavia, da allora, l'opinione ha teso nella direzione opposta. Se un file sorgente deve utilizzare le strutture dichiarate da un'intestazione header.h , il programmatore dovrebbe essere in grado di scrivere:

#include "header.h"

e (soggetto solo ad avere i percorsi di ricerca corretti impostati sulla riga di comando), tutti gli header necessari per i prerequisiti saranno inclusi da header.h senza bisogno di ulteriori intestazioni aggiunte al file sorgente.

Ciò fornisce una migliore modularità per il codice sorgente. Protegge inoltre la fonte dall'enigma "indovina perché questa intestazione è stata aggiunta" che si verifica dopo che il codice è stato modificato e violato per un decennio o due.

Gli standard di codifica Goddard Space Flight Center (GSFC) della NASA per C sono uno degli standard più moderni, ma ora è un po 'difficile da rintracciare. Dichiara che le intestazioni dovrebbero essere autonome. Fornisce anche un modo semplice per garantire che le intestazioni siano autonome: il file di implementazione per l'intestazione dovrebbe includere l'intestazione come prima intestazione. Se non è autonomo, quel codice non verrà compilato.

La motivazione fornita da GSFC include:

§2.1.1 L'intestazione include la logica

Questo standard richiede che l'intestazione di un'unità contenga le istruzioni #include per tutte le altre intestazioni richieste dall'intestazione dell'unità. Inserire #include per l'intestazione dell'unità prima nel corpo dell'unità consente al compilatore di verificare che l'intestazione contenga tutte le istruzioni #include richieste.

Un disegno alternativo, non consentito da questo standard, non consente di #include istruzioni #include nelle intestazioni; tutti gli #includes sono fatti nei file del corpo. I file di intestazione dell'unità devono quindi contenere istruzioni #ifdef che controllano che le intestazioni richieste siano incluse nell'ordine corretto.

Un vantaggio del design alternativo è che l'elenco #include nel file body è esattamente l'elenco delle dipendenze necessario in un makefile e questo elenco viene controllato dal compilatore. Con la progettazione standard, è necessario utilizzare uno strumento per generare l'elenco delle dipendenze. Tuttavia, tutti gli ambienti di sviluppo consigliati dalla filiale forniscono uno strumento del genere.

Uno svantaggio principale del design alternativo è che se l'elenco di intestazioni richiesto di un'unità cambia, ogni file che utilizza quell'unità deve essere modificato per aggiornare l'elenco di istruzioni #include . Inoltre, l'elenco di intestazioni richiesto per un'unità di libreria del compilatore può essere diverso su target diversi.

Un altro svantaggio della progettazione alternativa è che i file di intestazione della libreria del compilatore e altri file di terze parti devono essere modificati per aggiungere le istruzioni #ifdef richieste.

Quindi, l'auto-contenimento significa che:

  • Se un'intestazione header.h bisogno di una nuova intestazione nidificata extra.h , non è necessario controllare ogni file sorgente che utilizza header.h per vedere se è necessario aggiungere extra.h .
  • Se un'intestazione header.h non deve più includere un'intestazione specifica notneeded.h , non è necessario controllare ogni file sorgente che utilizza header.h per vedere se è possibile rimuovere notneeded.h modo sicuro (ma vedere Includi ciò che si usa .
  • Non è necessario stabilire la sequenza corretta per includere le intestazioni pre-requisito (che richiede un ordinamento topologico per eseguire correttamente il lavoro).

Controllo di auto-contenimento

Vedi Collegamento a una libreria statica per uno script chkhdr che può essere utilizzato per testare l'idempotenza e l'autocontenimento di un file di intestazione.

minimalità

Le intestazioni sono un meccanismo di verifica della coerenza fondamentale, ma dovrebbero essere il più possibile ridotte. In particolare, ciò significa che un'intestazione non dovrebbe includere altre intestazioni solo perché il file di implementazione avrà bisogno delle altre intestazioni. Un'intestazione deve contenere solo le intestazioni necessarie per un consumatore dei servizi descritti.

Ad esempio, un'intestazione di progetto non dovrebbe includere <stdio.h> meno che una delle interfacce di funzione non utilizzi il tipo FILE * (o uno degli altri tipi definiti esclusivamente in <stdio.h> ). Se un'interfaccia utilizza size_t , l'intestazione più piccola che è sufficiente è <stddef.h> . Ovviamente, se è inclusa un'altra intestazione che definisce size_t , non è necessario includere anche <stddef.h> .

Se le intestazioni sono minime, mantiene anche il tempo di compilazione al minimo.

È possibile ideare intestazioni il cui unico scopo è includere molte altre intestazioni. Queste raramente risultano essere una buona idea a lungo termine perché pochi file sorgente avranno effettivamente bisogno di tutte le funzionalità descritte da tutte le intestazioni. Ad esempio, potrebbe essere ideato un <standard-ch> che includa tutti gli header C standard - con attenzione dato che alcune intestazioni non sono sempre presenti. Tuttavia, pochissimi programmi utilizzano effettivamente le funzionalità di <locale.h> o <tgmath.h> .

Includi cosa usi (IWYU)

Il progetto Includi cosa utilizzi di Google o IWYU garantisce che i file di origine includano tutte le intestazioni utilizzate nel codice.

Supponiamo che un file sorgente source.c includa un'intestazione arbitrary.h che a sua volta include casualmente freeloader.h , ma il file sorgente utilizza anche esplicitamente e in modo indipendente le strutture da freeloader.h . Tutto va bene per cominciare. Quindi un giorno arbitrary.h viene cambiato in modo che i suoi clienti non freeloader.h più bisogno delle funzionalità di freeloader.h . All'improvviso source.c interrompe la compilazione perché non soddisfa i criteri IWYU. Poiché il codice in source.c utilizzava esplicitamente le funzionalità di freeloader.h , avrebbe dovuto includere ciò che utilizzava - avrebbe dovuto esserci un esplicito #include "freeloader.h" nella sorgente. (L' idempionalità avrebbe assicurato che non c'era un problema.)

La filosofia IWYU massimizza la probabilità che il codice continui a compilare anche con modifiche ragionevoli apportate alle interfacce. Chiaramente, se il tuo codice chiama una funzione che viene successivamente rimossa dall'interfaccia pubblicata, nessuna quantità di preparazione può impedire che le modifiche diventino necessarie. Questo è il motivo per cui le modifiche alle API vengono evitate quando possibile e perché ci sono cicli di deprecazione su più versioni, ecc.

Questo è un problema particolare in C ++ perché le intestazioni standard possono essere incluse tra loro. Il file di origine file.cpp potrebbe includere un'intestazione header1.h che su una piattaforma include un'altra intestazione header2.h . file.cpp potrebbe risultare utilizzare anche le funzionalità di header2.h . Inizialmente questo non sarebbe un problema: il codice verrebbe compilato perché header1.h include header2.h . Su un'altra piattaforma, o un aggiornamento della piattaforma corrente, header1.h potrebbe essere rivisto in modo che non includa più header2.h , e quindi file.cpp smetterebbe di compilare come risultato.

IWYU individuerebbe il problema e raccomanderebbe che header2.h fosse incluso direttamente in file.cpp . Ciò garantirebbe che continui a compilare. Considerazioni analoghe si applicano anche al codice C.

Notazione e Miscellanea

Lo standard C dice che c'è ben poca differenza tra le #include <header.h> e #include "header.h" .

[ #include <header.h> ] ricerca una sequenza di luoghi definiti dall'implementazione per un'intestazione identificata univocamente dalla sequenza specificata tra i delimitatori < e > e causa la sostituzione di tale direttiva con l'intero contenuto dell'intestazione. Come vengono specificati i posti o l'intestazione identificata è definita dall'implementazione.

[ #include "header.h" ] causa la sostituzione di quella direttiva con l'intero contenuto del file sorgente identificato dalla sequenza specificata tra i delimitatori "…" . Il file di origine denominato viene cercato in un modo definito dall'implementazione. Se questa ricerca non è supportata, o se la ricerca fallisce, la direttiva viene rielaborata come se leggesse [ #include <header.h> ] ...

Pertanto, la forma con doppia citazione può apparire in più punti rispetto alla forma con parentesi angolare. Lo standard specifica per esempio che le intestazioni standard dovrebbero essere incluse tra parentesi angolari, anche se la compilazione funziona se si utilizzano invece le virgolette doppie. Allo stesso modo, standard come POSIX usano il formato parentesi angolare - e dovresti farlo anche tu. Prenota intestazioni a doppia quotazione per intestazioni definite dal progetto. Per le intestazioni definite esternamente (comprese le intestazioni di altri progetti su cui si basa il progetto), la notazione a parentesi angolare è più appropriata.

Nota che dovrebbe esserci uno spazio tra #include e l'intestazione, anche se i compilatori non accetteranno spazio. Gli spazi sono economici.

Un numero di progetti utilizza una notazione come:

#include <openssl/ssl.h>
#include <sys/stat.h>
#include <linux/kernel.h>

Dovresti considerare se usare quel controllo del namespace nel tuo progetto (è probabilmente una buona idea). Dovresti evitare i nomi usati dai progetti esistenti (in particolare, sia sys che linux sarebbero scelte sbagliate).

Se lo usi, il tuo codice dovrebbe essere attento e coerente nell'uso della notazione.

Non usare la notazione #include "../include/header.h" .

I file di intestazione dovrebbero raramente definire le variabili. Sebbene manterrai le variabili globali al minimo, se hai bisogno di una variabile globale, la dichiarerai in un'intestazione e la definirai in un file sorgente adatto, e quel file di origine includerà l'intestazione per verificare la dichiarazione e la definizione e tutti i file sorgente che usano la variabile useranno l'intestazione per dichiararlo.

Corollario: non dichiarerai le variabili globali in un file sorgente: un file sorgente conterrà solo definizioni.

I file di intestazione dovrebbero dichiarare raramente funzioni static , con la notevole eccezione delle funzioni static inline che saranno definite nelle intestazioni se la funzione è necessaria in più di un file sorgente.

  • I file di origine definiscono le variabili globali e le funzioni globali.
  • I file di origine non dichiarano l'esistenza di variabili o funzioni globali; includono l'intestazione che dichiara la variabile o la funzione.
  • I file di intestazione dichiarano variabile globale e funzioni (e tipi e altro materiale di supporto).
  • I file di intestazione non definiscono variabili o funzioni eccetto static funzioni inline ( static ).

Riferimenti incrociati



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow