Tracks
/
C++
C++
/
Lehrplan
/
Header-Dateien
He

Header-Dateien in C++

2 Übungen

Über Header-Dateien

In C++ werden Deklarationen oft von Definitionen getrennt. Deklarationen werden in sogenannten Header-Dateien gruppiert, während die jeweiligen Implementierungen in Quelldateien liegen. Du kannst dir die Header-Dateien wie eine API vorstellen. Die Header-Datei sagt dir, was eine Codebasis zu bieten hat, ohne auf die Details des Wie einzugehen.

Header und Quelldatei

Die gebräuchlichste Dateiendung für Header-Dateien ist .h. Manche Projekte verwenden .hpp oder lassen die Endung ganz weg.

Die Definitionen stehen in einer separaten .cpp-Datei. Um die Teile wieder zusammenzuführen, beginnt die Quelldatei damit, die jeweilige Header-Datei einzubinden.

Wenn du eine Bibliothek namens „quick_math" schreiben willst, die eine Funktion „super_root" anbietet, die du oft verwenden möchtest, sehen die Dateien so aus:

// A file named quick_math.h
#pragma once

namespace quick_math {

double super_root(double x, int n);

}
// A file named quick_math.cpp
#include <cmath>

#include "quick_math.h"

double quick_math::super_root(double x, int n) {
    while (n) {
        x = std::sqrt(x), --n;
    }
    return x;
}

Wenn du einen Header einbinden musst, der nur für die Implementierung nötig ist, wird die entsprechende #include-Zeile nur in der Quelldatei gebraucht. Alles, was im Header eingebunden ist, steht auch in der .cpp-Datei zur Verfügung, wie die string-Bibliothek im folgenden Beispiel. Achtung: Das ; wird nach der Deklaration in der Header-Datei gebraucht, aber nicht nach der Definition in der Quelldatei.

Note

Viele C++-Übungen auf Exercism beginnen mit zwei fast leeren Dateien: Header und Quelldatei. Du musst die *_test.cpp-Datei ansehen, um die Namen und Namensräume der erwarteten Funktionen zu sehen, um die Übung zu lösen.

Klassen und Header

Klassen können sehr komplex werden, und ihr Verhältnis zur Aufteilung in Header und Quelldatei ist vielleicht verwirrend. Eine mögliche Aufteilung ist, alle Implementierungsdetails in der Quelldatei zu lassen und alle Deklarationen und Membervariablen in den Header zu schreiben:

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0);
    void give_water();
    std::string get_name();
    int get_size();
    void start_next_day();
};
}  // namespace robots
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"

robots::Flower::Flower(std::string name, int size) {
    this->name = "Robotica " + name;
    this->size = size;
}

void robots::Flower::start_next_day() {
    if (!needs_water) ++size;
    needs_water = true;
}

std::string robots::Flower::get_name() { return name; }

int robots::Flower::get_size() { return size; }

Wenn der Header als API-Übersicht dient, schaut man genau dort nach Informationen wie Standardwerten. Der Standardwert des size-Parameters des Konstruktors wird deshalb im Header festgelegt und nicht in der Implementierung. Den Definitionen in der Quelldatei werden der Namensraum robots und der Klassentyp Flower vorangestellt.

Eine weitere Aufteilungsmöglichkeit ist eine Header-only-Bibliothek, die überhaupt keine .cpp-Datei hat:

// A file named robot_flower.h
#pragma once

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0) {
        this->name = "Robotica " + name;
        this->size = size;
    }
    void give_water() { needs_water = false; }
    std::string get_name() { return name; }
    int get_size() { return size; }
    void start_next_day() {
        if (!needs_water) ++size;
        needs_water = true;
    }
};
}  // namespace robots

Projekte verwenden vielleicht Kombinationen dieser Aufteilungen, und es wird viel darüber diskutiert, was für den jeweiligen Anwendungsfall am besten passt.

Include Guards

Vielleicht ist dir die Zeile #pragma once in der obigen Beispiel-Header-Datei aufgefallen. Das nennt man einen Include-Guard. Er sorgt dafür, dass der Inhalt der Datei während der Kompilierung nur einmal eingebunden wird, um Fehler zu vermeiden. Es gibt noch eine andere, komplexere Variante eines Include-Guards, die mit #ifndef beginnt und mit #endif endet und weiter unten beschrieben wird.

Advanced

Vorwärtsdeklarationen

C++-Code wird prozedural ausgewertet. Wenn du eine Funktion verwenden willst, muss sie dem Compiler in dem Moment bekannt sein, in dem du sie verwendest. Manchmal ist es nicht möglich, die Definitionen im Quellcode in einer linearen Reihenfolge anzuordnen. Schau dir das folgende Beispiel an:

int myFunction(int n) {
    if (n < 10) {
        return n;
    } else {
        return myOtherFunction(n / 10);
    }
}

int myOtherFunction(int m) { return myFunction(m / 2); }

Wenn myFunction definiert wird, kennt der Compiler myOtherFunction noch nicht. Leider lässt sich das Problem der wechselseitigen Referenz nicht durch Vertauschen der Reihenfolge lösen.

C++ bietet Vorwärtsdeklarationen, damit der Compiler myFunction und myOtherFunction kennt, bevor sie definiert werden. Der Compiler geht davon aus, dass die Definition später nach der Deklaration folgt. Das nächste Beispiel zeigt, wie eine Vorwärtsdeklaration für Funktionen verwendet wird.

int myFunction(int n);       // Forward declaration of myFunction
int myOtherFunction(int m);  // Forward declaration of myOtherFunction

// Definition of myFunction
int myFunction(int n) {
    if (n < 0) {
        return 0;
    } else {
        return myOtherFunction(n - 2);
    }
}

// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }

Include Guards mit ifndef

Es spielt keine Rolle, ob dieselbe Datei innerhalb eines Projekts mehrfach eingebunden wird. Header-Dateien sollten keine Definitionen enthalten. Ein vollständiges Projekt darf dieselbe Definition nicht mehr als einmal enthalten. Das nennt man die „One Definition Rule". Der Compiler setzt sie durch.

Mit Include-Guards lässt sich leicht vermeiden, dass dieselbe Definition versehentlich mehrfach eingebunden wird. Sie entstehen durch ein besonderes Verfahren während der Kompilierungsphasen. Das Ziel ist, eine Datei nur dann einzubinden, wenn eine bestimmte Variable noch nicht gesetzt ist, und sie dann zu setzen, sobald die Datei eingebunden ist. Oft wird der Name dieser Variable als Variante des Dateinamens gewählt. Eine andere Methode ist die Erzeugung einer UUID, um das Risiko zu verringern, den Namen versehentlich zweimal zu verwenden. Die Syntax siehst du unten, wobei MY_HEADER_FILE_H als Variable dient.

#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H

// file content

#endif

Das Problem mit #pragma once ist, dass Pragmas kein offizieller Teil der Sprache C++ sind und die Implementierung von Compiler zu Compiler abweicht. Viele große Projekte sind zur einfacheren Pragma-Methode übergegangen, aber einige sind noch vorsichtig.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab

Lerne Header-Dateien