Suche…


Einführung

In modernen C sind Header-Dateien entscheidende Werkzeuge, die korrekt entworfen und verwendet werden müssen. Sie ermöglichen es dem Compiler, unabhängig kompilierte Teile eines Programms zu überprüfen.

Header deklarieren Typen, Funktionen, Makros usw., die von den Verbrauchern einer Reihe von Einrichtungen benötigt werden. Der gesamte Code, der eine dieser Funktionen verwendet, enthält den Header. Der gesamte Code, der diese Funktionen definiert, enthält den Header. Dadurch kann der Compiler prüfen, ob die Verwendungen und Definitionen übereinstimmen.

Einführung

Es gibt eine Reihe von Richtlinien, die beim Erstellen und Verwenden von Header-Dateien in einem C-Projekt zu beachten sind:

  • Idemopotenz

    Wenn eine Headerdatei mehrmals in einer Übersetzungseinheit (TU) enthalten ist, sollte sie die Builds nicht beschädigen.

  • Selbstbeherrschung

    Wenn Sie die in einer Header-Datei deklarierten Einrichtungen benötigen, müssen Sie keine anderen Header explizit angeben.

  • Minimalität

    Sie sollten keine Informationen aus einem Header entfernen können, ohne dass Builds fehlschlagen.

  • Einschließen, was Sie verwenden (IWYU)

    Für C ++ mehr Bedeutung als für C, aber auch für C wichtig. Wenn der Code in der TU (nennen wir es code.c ) direkt die Eigenschaften verwendet , die durch einen Header deklariert (nennen wir es "headerA.h" ), dann code.c sollte #include "headerA.h" direkt, auch wenn die TU umfasst Ein weiterer Header (nennen Sie es "headerB.h" ), der momentan "headerA.h" .

Gelegentlich gibt es gute Gründe, eine oder mehrere dieser Richtlinien zu brechen. Sie sollten jedoch beide wissen, dass Sie die Regel brechen, und sich der Konsequenzen dessen bewusst sein, bevor Sie sie brechen.

Idempotenz

Wenn eine bestimmte Header-Datei mehr als einmal in einer Übersetzungseinheit (TU) enthalten ist, sollten keine Kompilierungsprobleme auftreten. Dies wird als "Idempotenz" bezeichnet. Ihre Header sollten idempotent sein. Stellen Sie sich vor, wie schwierig das Leben wäre, wenn Sie dafür sorgen müssten, dass #include <stdio.h> nur einmal aufgenommen wurde.

Es gibt zwei Möglichkeiten, um Idempotenz zu erreichen: Header Guards und die Direktive #pragma once .

Kopfschutz

Kopfschutzvorrichtungen sind einfach und zuverlässig und entsprechen dem C-Standard. Die ersten Nicht-Kommentarzeilen in einer Header-Datei sollten folgende Form haben:

#ifndef UNIQUE_ID_FOR_HEADER
#define UNIQUE_ID_FOR_HEADER

Die letzte Nichtkommentarzeile sollte #endif , optional mit einem Kommentar:

#endif /* UNIQUE_ID_FOR_HEADER */

Der gesamte Betriebscode, einschließlich anderer #include Direktiven, sollte zwischen diesen Zeilen stehen.

Jeder Name muss eindeutig sein. Häufig wird ein Namensschema wie HEADER_H_INCLUDED verwendet. Ein älterer Code verwendet ein Symbol, das als Header Guard definiert ist (z. B. #ifndef BUFSIZ in <stdio.h> ), aber es ist nicht so zuverlässig wie ein eindeutiger Name.

Eine Option wäre die Verwendung eines generierten MD5-Hashes (oder eines anderen) für den Header-Guard-Namen. Sie sollten es vermeiden, die Schemata zu emulieren, die von Systemköpfen verwendet werden, die häufig für die Implementierung reservierte Namen verwenden - Namen, die mit einem Unterstrich beginnen, gefolgt von einem weiteren Unterstrich oder einem Großbuchstaben.

Die #pragma once Direktive

Alternativ unterstützen einige Compiler die Direktive #pragma once , die dieselbe Wirkung wie die drei Zeilen für Header-Guards hat.

#pragma once

Zu den Compilern, die #pragma once gehören MS Visual Studio und GCC und Clang. Wenn die Portabilität jedoch ein Problem darstellt, sollten Kopfschutzvorrichtungen oder beides verwendet werden. Moderne Compiler (diejenigen, die C89 oder höher unterstützen) müssen unangemessen ignorierte Pragmas ignorieren ("Ein solches Pragma, das von der Implementierung nicht erkannt wird, wird ignoriert"), aber alte Versionen von GCC waren nicht nachsichtig.

Selbstbeherrschung

Moderne Header sollten in sich geschlossen sein, was bedeutet, dass ein Programm, das die von header.h definierten header.h verwenden muss, diesen Header ( #include "header.h" ) enthalten kann und sich nicht darum kümmern muss, ob andere Header zuerst #include "header.h" müssen.

Empfehlung: Header-Dateien sollten in sich geschlossen sein.


Historische Regeln

Historisch war dies ein leicht kontroverses Thema.

Nach einem weiteren Jahrtausend gaben die AT & T Indian Hill C-Standards für Stil und Kodierung Folgendes an:

Header-Dateien sollten nicht verschachtelt sein. Der Prolog für eine Header-Datei sollte daher beschreiben, welche anderen Header #include d sein müssen, damit der Header funktionsfähig ist. In extremen Fällen, in denen eine große Anzahl von Header-Dateien in mehreren verschiedenen Quelldateien enthalten ist, ist es zulässig, alle gängigen #include Dateien in einer Include-Datei zu speichern.

Dies ist der Gegensatz von Selbstbeherrschung.

Moderne Regeln

Seitdem tendiert die Meinung jedoch in die entgegengesetzte Richtung. Wenn eine Quelldatei die von einem Header header.h deklarierten header.h , kann der Programmierer header.h schreiben:

#include "header.h"

und (vorbehaltlich der Einstellung der richtigen Suchpfade in der Befehlszeile) werden alle erforderlichen Kopfzeilen von header.h ohne dass weitere Kopfzeilen in die Quelldatei header.h werden müssen.

Dies bietet eine bessere Modularität für den Quellcode. Sie schützt die Quelle auch vor dem Rätsel "Vermutung, warum dieser Header hinzugefügt wurde", das entsteht, nachdem der Code für ein oder zwei Jahrzehnte geändert und gehackt wurde.

Der Kodierungsstandard der NASA Goddard Space Flight Center (GSFC) für C ist einer der moderneren Standards - er ist jedoch etwas schwer zu finden. Es besagt, dass Header in sich abgeschlossen sein sollten. Es bietet auch eine einfache Möglichkeit, um sicherzustellen, dass Header in sich geschlossen sind: Die Implementierungsdatei für den Header sollte den Header als ersten Header enthalten. Wenn es nicht eigenständig ist, wird der Code nicht kompiliert.

Die Begründung des GSFC beinhaltet:

§ 2.1.1.1 Header-Include-Begründung

Dieser Standard erfordert, dass der Header einer Unit #include Anweisungen für alle anderen Header enthält, die für den Unit-Header erforderlich sind. Durch das Platzieren von #include für den Einheitenheader im Einheitenkörper kann der Compiler überprüfen, ob der Header alle erforderlichen #include Anweisungen enthält.

Ein alternatives Design, das in dieser Norm nicht zulässig ist, erlaubt keine #include Anweisungen in Headern. Alle #includes sind in den Body-Dateien. Unit-Header-Dateien müssen dann #ifdef-Anweisungen enthalten, die überprüfen, ob die erforderlichen Header in der richtigen Reihenfolge enthalten sind.

Ein Vorteil des alternativen Designs besteht darin, dass die #include Liste in der Hauptdatei genau die Abhängigkeitsliste ist, die in einem Makefile benötigt wird. Diese Liste wird vom Compiler geprüft. Bei der Standardausführung muss ein Werkzeug zum Generieren der Abhängigkeitsliste verwendet werden. Alle von der Branche empfohlenen Entwicklungsumgebungen bieten jedoch ein solches Werkzeug.

Ein Hauptnachteil des alternativen Designs besteht darin, dass bei Änderungen der erforderlichen Header-Liste einer Einheit jede Datei, die diese Unit verwendet, bearbeitet werden muss, um die #include Anweisungsliste zu aktualisieren. Außerdem kann sich die erforderliche Headerliste für eine Compiler-Bibliothekseinheit auf verschiedenen Zielen unterscheiden.

Ein weiterer Nachteil des alternativen Designs besteht darin, dass die Header-Dateien der Compiler-Bibliothek und andere Dateien von Drittanbietern geändert werden müssen, um die erforderlichen #ifdef Anweisungen hinzuzufügen.

Selbstbeherrschung bedeutet also:

  • Wenn für einen Header header.h ein neuer verschachtelter Header extra.h , müssen Sie nicht jede Quelldatei, die header.h verwendet, header.h , ob Sie extra.h hinzufügen extra.h .
  • Wenn ein Header header.h nicht länger einen bestimmten Header notneeded.h , müssen Sie nicht jede Quelldatei, die header.h verwendet, header.h , ob notneeded.h sicher entfernt werden kann .
  • Sie müssen nicht die richtige Reihenfolge für das Einfügen der erforderlichen Header festlegen (dies erfordert eine topologische Sortierung, um die Aufgabe ordnungsgemäß auszuführen).

Selbstbeherrschung prüfen

Siehe Verknüpfen mit einer statischen Bibliothek für ein Skript- chkhdr , mit dem die Idempotenz und die Selbstbeschränkung einer Header-Datei chkhdr können.

Minimalität

Header sind ein wichtiger Mechanismus zur Überprüfung der Konsistenz, sollten jedoch so klein wie möglich sein. Das bedeutet insbesondere, dass ein Header keine anderen Header enthalten sollte, nur weil die Implementierungsdatei die anderen Header benötigt. Ein Header sollte nur die Header enthalten, die für einen Verbraucher der beschriebenen Dienste erforderlich sind.

Beispielsweise sollte ein Projektheader <stdio.h> nicht enthalten, es sei denn, eine der Funktionsschnittstellen verwendet den Typ FILE * (oder einen der anderen, nur in <stdio.h> definierten Typen). Wenn ein Interface size_t , ist der kleinste Header <stddef.h> . Wenn ein anderer Header enthalten ist, der size_t definiert, ist es nicht erforderlich, auch <stddef.h> .

Wenn die Kopfzeilen minimal sind, wird auch die Übersetzungszeit auf ein Minimum reduziert.

Es ist möglich, Header zu entwickeln, deren einziger Zweck darin besteht, viele andere Header einzubeziehen. Dies stellt sich auf lange Sicht selten als eine gute Idee heraus, da nur wenige Quelldateien alle von den Kopfzeilen beschriebenen Einrichtungen benötigen. Beispielsweise könnte ein <standard-ch> entworfen werden, der alle Standard-C-Header enthält - mit Vorsicht, da einige Header nicht immer vorhanden sind. Allerdings verwenden nur wenige Programme die Funktionen von <locale.h> oder <tgmath.h> .

Einschließen, was Sie verwenden (IWYU)

Das Google Include Your You Use- Projekt oder IWYU stellt sicher, dass die Quelldateien alle im Code verwendeten Header enthalten.

Angenommen , eine Quelldatei source.c einen Header enthält arbitrary.h die zufälligerweise wiederum enthält freeloader.h , aber die Quelldatei auch explizit und unabhängig verwendet die Einrichtungen von freeloader.h . Alles ist gut zu beginnen. Dann wird eines Tages arbitrary.h geändert, so dass seine Kunden die Einrichtungen von freeloader.h nicht mehr benötigen. Plötzlich hört source.c Kompilieren auf, weil es die IWYU-Kriterien nicht erfüllt. Da der Code in source.c ausdrücklich die Einrichtungen verwendet freeloader.h , hätte es enthalten , was es verwendet - es sollte eine explizite gewesen #include "freeloader.h" in der Quelle zu. ( Idempotenz hätte sichergestellt, dass es kein Problem gab.)

Die IWYU-Philosophie maximiert die Wahrscheinlichkeit, dass Code trotz vernünftiger Änderungen an Schnittstellen weiterhin kompiliert wird. Wenn Ihr Code eine Funktion aufruft, die später von der veröffentlichten Schnittstelle entfernt wird, kann keine Vorbereitung ausreichend sein, damit Änderungen nicht erforderlich werden. Aus diesem Grund werden Änderungen an APIs nach Möglichkeit vermieden, und es gibt Abwertungszyklen für mehrere Releases usw.

Dies ist ein besonderes Problem in C ++, da Standardkopfzeilen sich gegenseitig einschließen dürfen. Die file.cpp könnte einen Header header1.h , der auf einer Plattform einen anderen Header header2.h . file.cpp könnte auch die Möglichkeiten von header2.h . Dies wäre kein Problem zunächst sein - würde der Code kompiliert , da header1.h enthält header2.h . Auf einer anderen Plattform oder einem Upgrade der aktuellen Plattform könnte header1.h überarbeitet werden, sodass diese header2.h file.cpp nicht mehr enthält. header2.h diesem file.cpp würde file.cpp das Kompilieren beenden.

IWYU würde das Problem erkennen und empfehlen, header2.h direkt in file.cpp . Dies würde sicherstellen, dass es weiterhin kompiliert wird. Analoge Überlegungen gelten auch für C-Code.

Notation und Verschiedenes

Der C-Standard sagt, dass es sehr wenig Unterschiede zwischen den Schreibweisen #include <header.h> und #include "header.h" .

[ #include <header.h> ] durchsucht eine Sequenz implementierungsdefinierter Stellen nach einem Header, der durch die angegebene Sequenz zwischen den Trennzeichen < und > eindeutig identifiziert wird, und bewirkt, dass diese Direktive durch den gesamten Inhalt des Headers ersetzt wird. Wie die Bereiche angegeben werden oder wie der Header identifiziert wird, ist durch die Implementierung definiert.

[ #include "header.h" ] bewirkt, dass diese Direktive durch den gesamten Inhalt der Quelldatei ersetzt wird, die in der angegebenen Reihenfolge zwischen den Trennzeichen "…" . Die benannte Quelldatei wird implementierungsdefiniert gesucht. Wenn diese Suche nicht unterstützt wird oder die Suche fehlschlägt, wird die Direktive erneut verarbeitet, als ob sie [ #include <header.h> ] lesen würde.

Daher kann die doppelt zitierte Form an mehr Stellen aussehen als in der eckigen Klammer. In der Norm ist beispielhaft festgelegt, dass die Standardheader in spitzen Klammern enthalten sein sollten, obwohl die Kompilierung funktioniert, wenn Sie stattdessen doppelte Anführungszeichen verwenden. In ähnlicher Weise verwenden Standards wie POSIX das spitze Klammerformat - und das sollten Sie auch. Reservieren Sie doppelte Anführungszeichen für vom Projekt definierte Header. Bei extern definierten Kopfzeilen (einschließlich Kopfzeilen aus anderen Projekten, auf die sich Ihr Projekt stützt) ist die Notation der spitzen Klammern am besten geeignet.

Beachten Sie, dass zwischen #include und dem Header ein Leerzeichen stehen sollte, obwohl die Compiler dort kein Leerzeichen akzeptieren. Räume sind billig.

Einige Projekte verwenden eine Notation wie:

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

Sie sollten überlegen, ob Sie diese Namespace-Steuerung in Ihrem Projekt verwenden möchten (dies ist wahrscheinlich eine gute Idee). Sie sollten sich von den Namen fernhalten, die in vorhandenen Projekten verwendet werden (insbesondere wären sowohl sys als auch linux eine schlechte Wahl).

Wenn Sie dies verwenden, sollte Ihr Code bei der Verwendung der Notation vorsichtig und konsistent sein.

Verwenden Sie nicht #include "../include/header.h" .

Header-Dateien sollten selten, wenn überhaupt Variablen definiert werden. Obwohl Sie globale Variablen auf ein Minimum beschränken, müssen Sie sie, wenn Sie eine globale Variable benötigen, in einem Header deklarieren und in einer geeigneten Quelldatei definieren. Diese Quelldatei enthält den Header, um die Deklaration und Definition zu überprüfen und alle Quelldateien, die die Variable verwenden, verwenden den Header, um sie zu deklarieren.

Folgerung: Sie deklarieren keine globalen Variablen in einer Quelldatei - eine Quelldatei enthält nur Definitionen.

Header-Dateien sollten selten static Funktionen deklarieren, mit der bemerkenswerten Ausnahme von static inline Funktionen, die in Headern definiert werden, wenn die Funktion in mehr als einer Quelldatei benötigt wird.

  • Quelldateien definieren globale Variablen und globale Funktionen.
  • Quelldateien erklären nicht das Vorhandensein globaler Variablen oder Funktionen. Sie enthalten den Header, der die Variable oder Funktion deklariert.
  • Header-Dateien deklarieren globale Variablen und Funktionen (und Typen und anderes unterstützendes Material).
  • Header-Dateien definieren keine Variablen oder Funktionen außer ( static ) inline Funktionen.

Querverweise



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow