Warum benötigt C ++ eine separate Header-Datei?


138

Ich habe nie wirklich verstanden, warum C ++ eine separate Header-Datei mit den gleichen Funktionen wie in der CPP-Datei benötigt. Es macht das Erstellen und Umgestalten von Klassen sehr schwierig und fügt dem Projekt unnötige Dateien hinzu. Und dann gibt es das Problem, dass Header-Dateien eingeschlossen werden müssen, aber explizit überprüft werden muss, ob sie bereits enthalten sind.

C ++ wurde 1998 ratifiziert. Warum ist es so konzipiert? Welche Vorteile hat eine separate Header-Datei?


Zusatzfrage:

Wie findet der Compiler die CPP-Datei mit dem darin enthaltenen Code, wenn ich nur die H-Datei einbinde? Wird davon ausgegangen, dass die CPP-Datei denselben Namen wie die H-Datei hat, oder werden tatsächlich alle Dateien in der Verzeichnisstruktur durchsucht?


2
Wenn Sie nur eine einzelne Datei bearbeiten möchten, überprüfen Sie lzz (www.lazycplusplus.com).
Richard Corden

Antworten:


105

Sie scheinen nach der Trennung von Definitionen und Deklarationen zu fragen, obwohl es andere Verwendungszwecke für Header-Dateien gibt.

Die Antwort ist, dass C ++ dies nicht "braucht". Wenn Sie alles inline markieren (was für in einer Klassendefinition definierte Elementfunktionen ohnehin automatisch ist), ist die Trennung nicht erforderlich. Sie können einfach alles in den Header-Dateien definieren.

Die Gründe, die Sie möglicherweise trennen möchten , sind:

  1. Um die Bauzeiten zu verbessern.
  2. Zum Verknüpfen mit Code, ohne die Quelle für die Definitionen zu haben.
  3. Um zu vermeiden, dass alles "inline" markiert wird.

Wenn Ihre allgemeinere Frage lautet: "Warum ist C ++ nicht mit Java identisch?", Muss ich fragen: "Warum schreiben Sie C ++ anstelle von Java?" ;-p

Im Ernst, der Grund ist, dass der C ++ - Compiler nicht einfach in eine andere Übersetzungseinheit greifen und herausfinden kann, wie seine Symbole so verwendet werden, wie es Javac kann und tut. Die Header-Datei wird benötigt, um dem Compiler zu deklarieren, was zum Zeitpunkt der Verknüpfung voraussichtlich verfügbar sein wird.

So #includeist eine reine Textsubstitution. Wenn Sie alles in Header-Dateien definieren, erstellt der Präprozessor eine enorme Kopie und Einfügung jeder Quelldatei in Ihrem Projekt und speist diese in den Compiler ein. Die Tatsache, dass der C ++ - Standard 1998 ratifiziert wurde, hat nichts damit zu tun. Es ist die Tatsache, dass die Kompilierungsumgebung für C ++ so eng an der von C basiert.

Konvertieren meiner Kommentare zur Beantwortung Ihrer Folgefrage:

Wie findet der Compiler die CPP-Datei mit dem darin enthaltenen Code?

Zumindest nicht zu dem Zeitpunkt, an dem der Code kompiliert wird, der die Header-Datei verwendet hat. Die Funktionen, mit denen Sie verknüpfen, müssen noch nicht einmal geschrieben worden sein, unabhängig davon, ob der Compiler weiß, in welcher .cppDatei sie sich befinden. Alles, was der aufrufende Code zur Kompilierungszeit wissen muss, wird in der Funktionsdeklaration ausgedrückt. Zur Verbindungszeit stellen Sie eine Liste von .oDateien oder statischen oder dynamischen Bibliotheken bereit , und der Header ist ein Versprechen, dass die Definitionen der Funktionen irgendwo dort sein werden.


3
Hinzufügen zu "Die Gründe, die Sie möglicherweise trennen möchten, sind:" & Ich denke, die wichtigste Funktion von Header-Dateien ist: Trennen des Codestrukturdesigns von der Implementierung, weil: A. Wenn Sie in wirklich komplizierte Strukturen geraten, die viele Objekte betreffen Es ist viel einfacher, Header-Dateien zu durchsuchen und sich daran zu erinnern, wie sie zusammenarbeiten, ergänzt durch Ihre Header-Kommentare. B. Wenn sich eine Person nicht um die Definition der gesamten Objektstruktur kümmert und eine andere Person sich um die Implementierung kümmert, werden die Dinge organisiert. Insgesamt denke ich, dass es komplexen Code lesbarer macht.
Andres Canella

Auf einfachste Weise kann ich mir vorstellen, dass die Trennung von Header- und CPP-Dateien nützlich ist, wenn Schnittstelle und Implementierungen getrennt werden, was für mittlere und große Projekte wirklich hilfreich ist.
Krishna Oza

Ich wollte, dass dies in (2) "Link gegen Code ohne die Definitionen" aufgenommen wird. OK, Sie haben möglicherweise weiterhin Zugriff auf das Git-Repo mit den Definitionen in, aber der Punkt ist, dass Sie Ihren Code getrennt von den Implementierungen seiner Abhängigkeiten kompilieren und dann verknüpfen können. Wenn Sie buchstäblich nur die Schnittstelle / Implementierung in verschiedene Dateien trennen wollten, ohne sich darum kümmern zu müssen, diese separat zu erstellen, können Sie dies tun, indem Sie einfach foo_interface.h foo_implementation.h einschließen.
Steve Jessop

4
@AndresCanella Nein, tut es nicht. Es macht das Lesen und Verwalten von nicht Ihrem eigenen Code zu einem Albtraum. Um vollständig zu verstehen, was etwas im Code bewirkt, müssen Sie durch 2n Dateien anstelle von n Dateien springen. Dies ist einfach keine Big-Oh-Notation, 2n macht einen großen Unterschied im Vergleich zu nur n.
Błażej Michalik

1
Ich zweitens, dass es eine Lüge ist, dass Header helfen. Überprüfen Sie beispielsweise die Minix-Quelle. Es ist so schwer zu verfolgen, wo sie beginnt, wo die Steuerung übergeben wird, wo die Dinge deklariert / definiert werden. Wenn sie über separate dynamische Module erstellt wurde, ist sie verdaulich, wenn Sie einen Sinn für eine Sache haben und dann zu springen ein Abhängigkeitsmodul. Stattdessen müssen Sie den Überschriften folgen, und es macht das Lesen von Code, der auf diese Weise geschrieben wurde, zur Hölle. Im Gegensatz dazu macht nodejs klar, woher was kommt, ohne ifdefs, und Sie können leicht erkennen, woher was kommt.
Dmitry

90

C ++ macht es so, weil C es so gemacht hat. Die eigentliche Frage ist also, warum C es so gemacht hat. Wikipedia spricht ein wenig dazu.

Neuere kompilierte Sprachen (wie Java, C #) verwenden keine Vorwärtsdeklarationen. Bezeichner werden automatisch aus Quelldateien erkannt und direkt aus dynamischen Bibliothekssymbolen gelesen. Dies bedeutet, dass keine Header-Dateien benötigt werden.


12
+1 Trifft den Nagel auf den Kopf. Dies erfordert wirklich keine ausführliche Erklärung.
MSalters

6
Es hat mich nicht auf den Kopf getroffen :( Ich muss noch nachsehen, warum C ++ Forward-Deklarationen verwenden muss und warum es keine Bezeichner aus Quelldateien erkennen und direkt aus dynamischen Bibliothekssymbolen lesen kann und warum C ++ das getan hat Weg, nur weil C es so gemacht hat: p
Alexander Taylor

2
Und Sie sind ein besserer Programmierer dafür @AlexanderTaylor :)
Donald Byrd

66

Einige Leute halten Header-Dateien für einen Vorteil:

  • Es wird behauptet, dass es die Trennung von Schnittstelle und Implementierung ermöglicht / erzwingt / erlaubt - aber normalerweise ist dies nicht der Fall. Header-Dateien enthalten viele Implementierungsdetails (z. B. müssen Mitgliedsvariablen einer Klasse im Header angegeben werden, obwohl sie nicht Teil der öffentlichen Schnittstelle sind), und Funktionen können und werden häufig inline in der Klassendeklaration definiert im Header, wieder diese Trennung zu zerstören.
  • Es wird manchmal gesagt, dass es die Kompilierungszeit verbessert, da jede Übersetzungseinheit unabhängig verarbeitet werden kann. Und dennoch ist C ++ wahrscheinlich die langsamste Sprache, die es gibt, wenn es um Kompilierungszeiten geht. Ein Teil des Grundes sind die vielen vielen wiederholten Einschlüsse desselben Headers. Eine große Anzahl von Headern ist in mehreren Übersetzungseinheiten enthalten, sodass sie mehrmals analysiert werden müssen.

Letztendlich ist das Header-System ein Artefakt aus den 70er Jahren, als C entworfen wurde. Computer hatten damals nur sehr wenig Speicher, und es war einfach keine Option, das gesamte Modul im Speicher zu halten. Ein Compiler musste die Datei oben lesen und dann linear durch den Quellcode gehen. Der Header-Mechanismus ermöglicht dies. Der Compiler muss keine anderen Übersetzungseinheiten berücksichtigen, sondern nur den Code von oben nach unten lesen.

Und C ++ hat dieses System aus Gründen der Abwärtskompatibilität beibehalten.

Heute macht es keinen Sinn. Es ist ineffizient, fehleranfällig und überkompliziert. Es gibt viel bessere Möglichkeiten zur Trennung von Schnittstelle und Implementierung, wenn das war das Ziel.

Einer der Vorschläge für C ++ 0x war jedoch, ein geeignetes Modulsystem hinzuzufügen, mit dem Code ähnlich wie .NET oder Java in größere Module kompiliert werden kann, alles auf einmal und ohne Header. Dieser Vorschlag hat den Schnitt in C ++ 0x nicht geschafft, aber ich glaube, er gehört immer noch zur Kategorie "Wir würden das gerne später machen". Vielleicht in einem TR2 oder ähnlichem.


Dies ist die beste Antwort auf der Seite. Danke dir!
Chuck Le Butt

29

Nach meinem (eingeschränkten - ich bin normalerweise kein C-Entwickler) Verständnis ist dies in C verwurzelt. Denken Sie daran, dass C nicht weiß, was Klassen oder Namespaces sind, es ist nur ein langes Programm. Außerdem müssen Funktionen deklariert werden, bevor Sie sie verwenden.

Folgendes sollte beispielsweise einen Compilerfehler ergeben:

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Der Fehler sollte sein, dass "SomeOtherFunction nicht deklariert ist", weil Sie es vor seiner Deklaration aufrufen. Eine Möglichkeit, dies zu beheben, besteht darin, SomeOtherFunction über SomeFunction zu verschieben. Ein anderer Ansatz besteht darin, zuerst die Funktionssignatur zu deklarieren:

void SomeOtherFunction();

void SomeFunction() {
    SomeOtherFunction();
}

void SomeOtherFunction() {
    printf("What?");
}

Dies lässt den Compiler wissen: Schauen Sie irgendwo im Code nach, es gibt eine Funktion namens SomeOtherFunction, die void zurückgibt und keine Parameter akzeptiert. Wenn Sie also Code finden, der versucht, SomeOtherFunction aufzurufen, geraten Sie nicht in Panik und suchen Sie stattdessen danach.

Stellen Sie sich vor, Sie haben SomeFunction und SomeOtherFunction in zwei verschiedenen .c-Dateien. Sie müssen dann "SomeOther.c" in Some.c einschließen. Fügen Sie nun SomeOther.c einige "private" Funktionen hinzu. Da C keine privaten Funktionen kennt, wäre diese Funktion auch in Some.c verfügbar.

Hier kommen .h-Dateien ins Spiel: Sie geben alle Funktionen (und Variablen) an, die Sie aus einer .c-Datei exportieren möchten, auf die in anderen .c-Dateien zugegriffen werden kann. Auf diese Weise erhalten Sie so etwas wie einen öffentlichen / privaten Bereich. Sie können diese .h-Datei auch an andere Personen weitergeben, ohne Ihren Quellcode freigeben zu müssen - .h-Dateien funktionieren auch mit kompilierten .lib-Dateien.

Der Hauptgrund liegt also in der Bequemlichkeit, im Schutz des Quellcodes und in der leichten Entkopplung zwischen den Teilen Ihrer Anwendung.

Das war allerdings C. C ++ hat Klassen und private / öffentliche Modifikatoren eingeführt. Während Sie also immer noch fragen können, ob sie benötigt werden, erfordert C ++ AFAIK vor der Verwendung immer noch eine Deklaration von Funktionen. Viele C ++ - Entwickler sind oder waren auch C-Entwickler und haben ihre Konzepte und Gewohnheiten in C ++ übernommen - warum ändern, was nicht kaputt ist?


5
Warum kann der Compiler den Code nicht durchlaufen und alle Funktionsdefinitionen finden? Es scheint etwas zu sein, das ziemlich einfach in den Compiler zu programmieren wäre.
Marius

3
Wenn Sie haben die Quelle, die Sie haben oft nicht. Kompiliertes C ++ ist effektiv Maschinencode mit gerade genug zusätzlichen Informationen, um den Code zu laden und zu verknüpfen. Dann richten Sie die CPU auf den Einstiegspunkt und lassen sie laufen. Dies unterscheidet sich grundlegend von Java oder C #, wo der Code zu einem Zwischenbytecode kompiliert wird, der Metadaten zu seinem Inhalt enthält.
DevSolar

Ich vermute, dass dies 1972 für den Compiler eine ziemlich teure Operation gewesen sein könnte.
Michael Stum

Genau das, was Michael Stum gesagt hat. Als dieses Verhalten definiert wurde, war ein linearer Scan durch eine einzelne Übersetzungseinheit das einzige, was praktisch implementiert werden konnte.
Jalf

3
Yup - das Kompilieren auf einem 16 Bitter mit Tape Mass Torage ist nicht trivial.
MSalters

11

Erster Vorteil: Wenn Sie keine Header-Dateien haben, müssten Sie Quelldateien in andere Quelldateien aufnehmen. Dies würde dazu führen, dass die eingeschlossenen Dateien erneut kompiliert werden, wenn sich die eingeschlossene Datei ändert.

Zweiter Vorteil: Es ermöglicht die gemeinsame Nutzung der Schnittstellen, ohne den Code zwischen verschiedenen Einheiten (verschiedenen Entwicklern, Teams, Unternehmen usw.) zu teilen.


1
Bedeuten Sie, dass Sie beispielsweise in C # "Quelldateien in andere Quelldateien aufnehmen müssen"? Weil du es offensichtlich nicht tust. Für den zweiten Vorteil denke ich, dass das zu sprachabhängig ist: Sie werden keine .h-Dateien in zB Delphi
Vlagged

Sie müssen sowieso das gesamte Projekt neu kompilieren. Zählt also der erste Vorteil wirklich?
Marius

ok, aber ich denke nicht, dass ein Sprachfeature. Es ist praktischer, sich mit der C-Deklaration vor der Definition "Problem" zu befassen. Es ist wie ein berühmtes Sprichwort: "Das ist kein Fehler, der ein Feature ist" :)
Neuro

@Marius: Ja, das zählt wirklich. Das Verknüpfen des gesamten Projekts unterscheidet sich vom Kompilieren und Verknüpfen des gesamten Projekts. Wenn die Anzahl der Dateien im Projekt zunimmt, wird das Kompilieren aller Dateien sehr ärgerlich. @Vlagged: Du hast recht, aber ich habe c ++ nicht mit einer anderen Sprache verglichen. Ich habe verglichen, nur Quelldateien mit Quell- und Header-Dateien zu verwenden.
Erelender

C # enthält keine Quelldateien in anderen, aber Sie müssen trotzdem auf die Module verweisen - und das bringt den Compiler dazu, die Quelldateien abzurufen (oder in die Binärdatei zu reflektieren), um die von Ihrem Code verwendeten Symbole zu analysieren.
Gbjbaanb

5

Die Notwendigkeit von Header-Dateien ergibt sich aus den Einschränkungen, die der Compiler hat, um die Typinformationen für Funktionen und / oder Variablen in anderen Modulen zu kennen. Das kompilierte Programm oder die kompilierte Bibliothek enthält nicht die Typinformationen, die der Compiler benötigt, um an Objekte zu binden, die in anderen Kompilierungseinheiten definiert sind.

Um diese Einschränkung zu kompensieren, lassen C und C ++ Deklarationen zu, und diese Deklarationen können mithilfe der # include-Direktive des Präprozessors in Module aufgenommen werden, die sie verwenden.

Sprachen wie Java oder C # enthalten andererseits die Informationen, die für die Bindung in der Ausgabe des Compilers erforderlich sind (Klassendatei oder Assembly). Daher besteht keine Notwendigkeit mehr, eigenständige Deklarationen zu verwalten, die von Clients eines Moduls aufgenommen werden müssen.

Der Grund dafür, dass die Bindungsinformationen nicht in der Compilerausgabe enthalten sind, ist einfach: Sie werden zur Laufzeit nicht benötigt (zur Kompilierungszeit erfolgt eine Typprüfung). Es würde nur Platz verschwenden. Denken Sie daran, dass C / C ++ aus einer Zeit stammt, in der die Größe einer ausführbaren Datei oder Bibliothek eine große Rolle spielte.


Ich stimme mit Ihnen ein. Ich habe hier eine ähnliche Idee: stackoverflow.com/questions/3702132/…
smwikipedia

4

C ++ wurde entwickelt, um der C-Infrastruktur moderne Programmiersprachenfunktionen hinzuzufügen, ohne unnötig etwas an C zu ändern, das sich nicht speziell auf die Sprache selbst bezieht.

Ja, zu diesem Zeitpunkt (10 Jahre nach dem ersten C ++ - Standard und 20 Jahre nach dem Beginn der ernsthaften Zunahme der Nutzung) ist es leicht zu fragen, warum es kein geeignetes Modulsystem gibt. Offensichtlich würde jede neue Sprache, die heute entworfen wird, nicht wie C ++ funktionieren. Aber darum geht es in C ++ nicht.

Der Sinn von C ++ ist es, evolutionär zu sein, eine reibungslose Fortsetzung der bestehenden Praxis, nur neue Funktionen hinzuzufügen, ohne (zu oft) Dinge zu beschädigen, die für die Benutzergemeinschaft angemessen funktionieren.

Dies bedeutet, dass einige Dinge schwieriger sind (insbesondere für Leute, die ein neues Projekt starten) und einige Dinge einfacher sind (insbesondere für diejenigen, die vorhandenen Code pflegen) als andere Sprachen.

Anstatt zu erwarten, dass C ++ zu C # wird (was sinnlos wäre, da wir bereits C # haben), warum nicht einfach das richtige Werkzeug für den Job auswählen? Ich selbst bemühe mich, bedeutende Teile neuer Funktionen in einer modernen Sprache zu schreiben (ich verwende zufällig C #), und ich habe eine große Menge an vorhandenem C ++, das ich in C ++ behalte, weil es keinen wirklichen Wert hätte, es neu zu schreiben alles. Sie integrieren sich sowieso sehr gut, so dass es weitgehend schmerzlos ist.


Wie integrieren Sie C # und C ++? Durch COM?
Peter Mortensen

1
Es gibt drei Möglichkeiten, die "beste" hängt von Ihrem vorhandenen Code ab. Ich habe alle drei benutzt. Das, was ich am häufigsten benutze, ist COM, da mein vorhandener Code bereits darauf ausgelegt ist, so dass es praktisch nahtlos ist und für mich sehr gut funktioniert. An einigen seltsamen Stellen verwende ich C ++ / CLI, was eine unglaublich reibungslose Integration für jede Situation ermöglicht, in der Sie noch keine COM-Schnittstellen haben (und Sie können es vorziehen, vorhandene COM-Schnittstellen zu verwenden, selbst wenn Sie diese haben). Schließlich gibt es p / invoke, mit dem Sie grundsätzlich jede C-ähnliche Funktion aufrufen können, die von einer DLL verfügbar gemacht wird, sodass Sie jede Win32-API direkt von C # aus aufrufen können.
Daniel Earwicker

4

Nun, C ++ wurde 1998 ratifiziert, aber es war schon viel länger in Gebrauch, und die Ratifizierung legte in erster Linie die aktuelle Nutzung fest, anstatt eine Struktur aufzuerlegen. Und da C ++ auf C basiert und C Header-Dateien hat, hat C ++ diese auch.

Der Hauptgrund für Header-Dateien besteht darin, die separate Kompilierung von Dateien zu ermöglichen und Abhängigkeiten zu minimieren.

Angenommen, ich habe foo.cpp und möchte Code aus den Dateien bar.h / bar.cpp verwenden.

Ich kann "bar.h" in foo.cpp einschließen und dann foo.cpp programmieren und kompilieren, auch wenn bar.cpp nicht existiert. Die Header-Datei verspricht dem Compiler, dass die Klassen / Funktionen in bar.h zur Laufzeit vorhanden sind und alles enthält, was er bereits wissen muss.

Wenn die Funktionen in bar.h keine Körper haben, wenn ich versuche, mein Programm zu verknüpfen, wird es natürlich nicht verknüpft und ich erhalte eine Fehlermeldung.

Ein Nebeneffekt ist, dass Sie Benutzern eine Header-Datei geben können, ohne Ihren Quellcode preiszugeben.

Ein weiterer Grund ist, dass Sie, wenn Sie die Implementierung Ihres Codes in der * .cpp-Datei ändern, aber den Header überhaupt nicht ändern, nur die * .cpp-Datei kompilieren müssen, anstatt alles, was sie verwendet. Wenn Sie viel Implementierung in die Header-Datei einfügen, wird dies natürlich weniger nützlich.


3

Es wird keine separate Header-Datei mit den gleichen Funktionen wie in main benötigt. Es wird nur benötigt, wenn Sie eine Anwendung mit mehreren Codedateien entwickeln und eine Funktion verwenden, die zuvor nicht deklariert wurde.

Es ist wirklich ein Umfangsproblem.


1

C ++ wurde 1998 ratifiziert. Warum ist es so konzipiert? Welche Vorteile hat eine separate Header-Datei?

Tatsächlich sind Header-Dateien sehr nützlich, wenn Sie Programme zum ersten Mal untersuchen. Durch das Auschecken von Header-Dateien (nur mit einem Texteditor) erhalten Sie einen Überblick über die Architektur des Programms, im Gegensatz zu anderen Sprachen, in denen Sie zum Anzeigen von Klassen und ausgefeilte Tools verwenden müssen ihre Mitgliedsfunktionen.


1

Ich denke, der wahre (historische) Grund für Header-Dateien war, dass es für Compiler-Entwickler einfacher war ... aber dann bieten Header-Dateien Vorteile.
Überprüfen Sie diesen vorherigen Beitrag für weitere Diskussionen ...


1

Nun, Sie können C ++ perfekt ohne Header-Dateien entwickeln. Tatsächlich verwenden einige Bibliotheken, die intensiv Vorlagen verwenden, nicht das Paradigma der Header- / Codedateien (siehe Boost). In C / C ++ können Sie jedoch nichts verwenden, was nicht deklariert ist. Eine praktische Möglichkeit, damit umzugehen, ist die Verwendung von Header-Dateien. Außerdem profitieren Sie von der Freigabe der Benutzeroberfläche ohne Freigabe von Code / Implementierung. Und ich denke, es wurde von den C-Erstellern nicht ins Auge gefasst: Wenn Sie gemeinsam genutzte Header-Dateien verwenden, müssen Sie die berühmten verwenden:

#ifndef MY_HEADER_SWEET_GUARDIAN
#define MY_HEADER_SWEET_GUARDIAN

// [...]
// my header
// [...]

#endif // MY_HEADER_SWEET_GUARDIAN

Das ist nicht wirklich ein Sprachmerkmal, sondern ein praktischer Weg, um mit multipler Inklusion umzugehen.

Ich denke also, als C erstellt wurde, wurden die Probleme mit der Vorwärtsdeklaration unterschätzt, und jetzt, wenn wir eine Hochsprache wie C ++ verwenden, müssen wir uns mit solchen Dingen befassen.

Eine weitere Belastung für uns arme C ++ - Benutzer ...


1

Wenn der Compiler Symbole, die in anderen Dateien definiert sind, automatisch herausfinden soll, müssen Sie den Programmierer zwingen, diese Dateien an vordefinierten Speicherorten abzulegen (wie die Struktur von Java-Paketen die Ordnerstruktur des Projekts bestimmt). Ich bevorzuge Header-Dateien. Außerdem benötigen Sie entweder die von Ihnen verwendeten Bibliotheksquellen oder eine einheitliche Methode, um die vom Compiler benötigten Informationen in Binärdateien abzulegen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.