Wann sollten Zeiger in C auf NULL überprüft werden?


18

Zusammenfassung :

Sollte eine Funktion in C immer prüfen, ob sie keinen NULLZeiger dereferenziert ? Wenn nein, wann ist es angebracht, diese Prüfungen zu überspringen?

Details :

Ich habe einige Bücher über Programmierinterviews gelesen und frage mich, wie hoch der Grad der Eingabevalidierung für Funktionsargumente in C ist. Offensichtlich muss jede Funktion, die Eingaben von einem Benutzer entgegennimmt, eine Validierung durchführen, einschließlich der Überprüfung auf einen NULLZeiger, bevor er dereferenziert wird. Aber was ist mit einer Funktion in derselben Datei, die Sie nicht über Ihre API verfügbar machen möchten?

Zum Beispiel erscheint folgendes im Quellcode von git:

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Wenn dies der Fall *graphist, NULLwird ein Nullzeiger dereferenziert, was möglicherweise zum Absturz des Programms führt, möglicherweise jedoch zu einem anderen unvorhersehbaren Verhalten. Andererseits ist die Funktion staticund so hat der Programmierer möglicherweise die Eingabe bereits validiert. Ich weiß nicht, ich habe es nur zufällig ausgewählt, weil es ein kurzes Beispiel in einem in C geschriebenen Anwendungsprogramm war. Ich habe viele andere Stellen gesehen, an denen Zeiger verwendet werden, ohne nach NULL zu suchen. Meine Frage ist allgemein nicht spezifisch für dieses Codesegment.

Ich sah eine ähnliche Frage im Zusammenhang mit der Ausnahmebehandlung . Für eine unsichere Sprache wie C oder C ++ gibt es jedoch keine automatische Fehlerweitergabe für nicht behandelte Ausnahmen.

Andererseits habe ich in Open-Source-Projekten (wie im obigen Beispiel) viel Code gesehen, der vor der Verwendung keine Überprüfung von Zeigern durchführt. Ich frage mich, ob jemand über Richtlinien nachdenkt, wann eine Funktion überprüft werden soll, anstatt davon auszugehen, dass die Funktion mit korrekten Argumenten aufgerufen wurde.

Ich interessiere mich allgemein für diese Frage zum Schreiben von Produktionscode. Mich interessiert aber auch der Kontext von Programmierinterviews. Beispielsweise tendieren viele Algorithmus-Lehrbücher (wie CLR) dazu, die Algorithmen ohne jegliche Fehlerprüfung im Pseudocode darzustellen. Dies ist zwar gut, um den Kern eines Algorithmus zu verstehen, aber offensichtlich keine gute Programmierpraxis. Daher möchte ich keinem Interviewer mitteilen, dass ich die Fehlerprüfung übersprungen habe, um meine Codebeispiele zu vereinfachen (wie es in einem Lehrbuch der Fall sein könnte). Aber ich möchte auch nicht erscheinen, um ineffizienten Code mit übermäßiger Fehlerprüfung zu erzeugen. Zum Beispiel graph_get_current_column_colorkönnte das geändert worden sein, um *graphauf null zu prüfen, aber es ist nicht klar, was es tun würde, wenn *graphnull wäre, außer dass es es nicht dereferenzieren sollte.


7
Wenn Sie eine Funktion für eine API schreiben, bei der Aufrufer die Innereien nicht verstehen sollen, ist dies einer der Bereiche, in denen Dokumentation wichtig ist. Wenn Sie dokumentieren, dass ein Argument ein gültiger Zeiger ungleich NULL sein muss, liegt die Überprüfung in der Verantwortung des Aufrufers.
Blrfl


Wenn man bedenkt, dass die Frage und die meisten Antworten im Jahr 2017 geschrieben wurden, wird in keiner der Antworten das Problem zeitreisender undefinierter Verhaltensweisen aufgrund der Optimierung von Compilern angesprochen?
Rwong

Im Fall von API-Aufrufen, die gültige Zeigerargumente erwarten, frage ich mich, welchen Wert es hat, nur auf NULL zu testen. Jeder ungültige Zeiger, der dereferenziert wird, ist genauso schlecht wie NULL und segfault.
PaulHK

Antworten:


15

Ungültige Nullzeiger können entweder durch Programmiererfehler oder durch Laufzeitfehler verursacht werden. Laufzeitfehler sind etwas, das ein Programmierer nicht beheben kann, z. B. ein Fehler mallocaufgrund von wenig Arbeitsspeicher oder wenn das Netzwerk ein Paket fallen lässt oder der Benutzer etwas Dummes eingibt. Programmiererfehler werden durch einen Programmierer verursacht, der die Funktion falsch verwendet.

Die allgemeine Faustregel, die ich gesehen habe, ist, dass Laufzeitfehler immer überprüft werden sollten, aber Programmiererfehler müssen nicht jedes Mal überprüft werden. Nehmen wir an, ein idiotischer Programmierer hat direkt angerufen graph_get_current_column_color(0). Beim ersten Aufruf tritt ein Seg-Fehler auf. Sobald Sie den Fehler behoben haben, wird der Fix dauerhaft kompiliert. Es muss nicht jedes Mal überprüft werden, wenn es ausgeführt wird.

In einigen Fällen, insbesondere in Bibliotheken von Drittanbietern, wird assertanstelle einer ifAnweisung ein angezeigt , um nach Programmierfehlern zu suchen . Auf diese Weise können Sie die Prüfungen während der Entwicklung kompilieren und im Produktionscode weglassen. Ich habe auch gelegentlich unentgeltliche Überprüfungen gesehen, bei denen die Quelle des möglichen Programmierfehlers weit vom Symptom entfernt ist.

Natürlich kann man immer jemanden finden, der pedantischer ist, aber die meisten mir bekannten C-Programmierer bevorzugen weniger überfüllten Code gegenüber Code, der etwas sicherer ist. Und "sicherer" ist ein subjektiver Begriff. Ein offensichtlicher Fehler während der Entwicklung ist einem subtilen Korruptionsfehler im Feld vorzuziehen.


Die Frage ist etwas subjektiv, aber dies schien vorerst die beste Antwort zu sein. Vielen Dank an alle, die über diese Frage nachgedacht haben.
Gabriel Southern

1
In iOS gibt malloc niemals NULL zurück. Wenn es keinen Speicher findet, fordert es Ihre Anwendung auf, zuerst Speicher freizugeben, und fragt dann das Betriebssystem (das andere Apps auffordert, Speicher freizugeben und möglicherweise zu beenden). Wenn es immer noch keinen Speicher gibt, wird Ihre App beendet . Keine Überprüfungen erforderlich.
gnasher729

11

Kernighan & Plauger schrieben in "Software Tools", dass sie alles überprüfen würden, und unter Bedingungen, von denen sie glaubten, dass sie tatsächlich niemals eintreten könnten, brachen sie mit der Fehlermeldung "Can't happen" ab.

Sie berichten, dass sie schnell demütigt sind, wenn sie gesehen haben, dass "Can't happen" auf ihren Terminals erscheint.

Sie sollten den Zeiger IMMER auf NULL prüfen, bevor Sie ihn dereferenzieren (versuchen). IMMER . Die Menge an Code, die Sie duplizieren, um nach NULL-Werten zu suchen, die nicht vorkommen, und die Prozessorzyklen, die Sie "verschwenden", werden durch die Anzahl der Abstürze bezahlt, die Sie nicht mehr als durch einen Absturzspeicherauszug debuggen müssen. wenn du so viel Glück hast.

Wenn der Zeiger innerhalb einer Schleife unveränderlich ist, reicht es aus, ihn außerhalb der Schleife zu überprüfen. Anschließend sollten Sie ihn in eine lokale Variable mit eingeschränktem Gültigkeitsbereich "kopieren", die von der Schleife verwendet wird und die entsprechenden const-Dekorationen hinzufügt. In diesem Fall MÜSSEN Sie sicherstellen, dass jede vom Schleifenkörper aufgerufene Funktion die erforderlichen Konstantendekorationen auf den Prototypen enthält, GANZ ABWÄRTS. Wenn Sie dies nicht tun oder nicht können (z. B. aufgrund eines Lieferantenpakets oder eines hartnäckigen Mitarbeiters), müssen Sie dies jedes Mal auf NULL prüfen , da COL Murphy ein unheilbarer Optimist war und daher jemand mitmachen wird um es zu zappen, wenn Sie nicht suchen.

Wenn Sie sich in einer Funktion befinden und der Zeiger nicht NULL sein soll, sollten Sie ihn überprüfen.

Wenn Sie es von einer Funktion erhalten und es nicht NULL sein soll, sollten Sie es überprüfen. malloc () ist dafür besonders berüchtigt. (Nortel Networks, das inzwischen nicht mehr existiert, hatte einen hart und schnell geschriebenen Codierungsstandard. Ich musste an einem Punkt einen Absturz debuggen, der auf malloc () zurückgeführt wurde, wobei ein Nullzeiger zurückgegeben wurde und der idiotische Codierer sich nicht die Mühe machte, dies zu überprüfen es, bevor er es schrieb, weil er wusste, dass er viel Gedächtnis hatte ... Ich sagte einige sehr böse Dinge, als ich es endlich fand.)


8
Wenn Sie sich in einer Funktion befinden, für die ein Nicht-NULL-Zeiger erforderlich ist, die Sie jedoch trotzdem überprüfen und die NULL-Funktion ausführen, wie geht es weiter?
Detly

1
@detly entweder aufhören, was Sie tun, und einen Fehlercode zurückgeben, oder eine Behauptung auslösen
James

1
@ James - nicht gedacht assert, sicher. Ich mag die Fehlercode-Idee nicht, wenn Sie über das Ändern des vorhandenen Codes sprechen, um NULLÜberprüfungen einzuschließen.
Detly

10
@detly Sie werden nicht sehr weit kommen als ein C-Entwickler, wenn Sie Fehlercodes nicht mögen
James

5
@ JohnR.Strohm - das ist C, es ist Behauptungen oder nichts: P
detly

5

Sie können die Prüfung überspringen, wenn Sie sich irgendwie davon überzeugen können, dass der Zeiger möglicherweise nicht null sein kann.

Normalerweise werden Nullzeigerprüfungen in Code implementiert, bei denen erwartet wird, dass Null als Indikator dafür erscheint, dass ein Objekt derzeit nicht verfügbar ist. Null wird als Sentinel-Wert verwendet, um beispielsweise verknüpfte Listen oder sogar Arrays von Zeigern zu beenden. Der argvVektor der übergebenen Zeichenfolgen mainmuss durch einen Zeiger mit Null abgeschlossen werden, ähnlich wie eine Zeichenfolge durch ein Nullzeichen: argv[argc]ein Nullzeiger. Darauf können Sie sich beim Parsen der Befehlszeile verlassen.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

Die Situationen für die Überprüfung auf Null sind also solche, in denen es sich um einen erwarteten Wert handelt. Die Nullprüfungen implementieren die Bedeutung des Nullzeigers, z. B. das Stoppen der Suche in einer verknüpften Liste. Sie verhindern, dass der Code den Zeiger dereferenziert.

In einer Situation, in der ein Nullzeigerwert nicht von Entwurf erwartet wird, ist es nicht sinnvoll, danach zu suchen. Wenn ein ungültiger Zeigerwert auftritt, wird er mit hoher Wahrscheinlichkeit als nicht null angezeigt, was auf tragbare Weise nicht von gültigen Werten unterschieden werden kann. Zum Beispiel ein Zeigerwert, der durch Lesen eines nicht initialisierten Speichers erhalten wird, der als Zeigertyp interpretiert wird, ein Zeiger, der durch eine schattige Konvertierung erhalten wird, oder ein Zeiger, der außerhalb der Grenzen inkrementiert wird.

Informationen zu einem Datentyp wie graph *: Dies könnte so gestaltet werden, dass ein Nullwert ein gültiger Graph ist: etwas ohne Kanten und ohne Knoten. In diesem Fall müssen sich alle Funktionen, die einen graph *Zeiger benötigen, mit diesem Wert befassen, da es sich bei der Darstellung von Diagrammen um einen korrekten Domänenwert handelt. Andererseits graph *könnte a ein Zeiger auf ein containerähnliches Objekt sein, das niemals null ist, wenn wir einen Graphen halten. Ein Nullzeiger könnte uns dann mitteilen, dass "das Diagrammobjekt nicht vorhanden ist, wir es noch nicht zugeordnet haben oder es freigegeben haben, oder dass dies derzeit kein zugeordnetes Diagramm ist". Die letztere Verwendung von Zeigern ist ein kombinierter Boolescher Wert / Satellit: Wenn der Zeiger nicht null ist, wird "Ich habe dieses Schwesterobjekt" angezeigt und dieses Objekt bereitgestellt.

Wir können einen Zeiger auf null setzen, auch wenn wir ein Objekt nicht freigeben, um einfach ein Objekt von einem anderen zu trennen:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

Das überzeugendste Argument, von dem ich weiß, dass ein Zeiger an einem bestimmten Punkt nicht null sein kann, ist, diesen Punkt in "if (ptr! = NULL) {" und ein entsprechendes "}" zu setzen. Darüber hinaus befinden Sie sich im Gebiet der formellen Überprüfung.
John R. Strohm

4

Lassen Sie mich der Fuge noch eine Stimme hinzufügen.

Wie viele der anderen Antworten sage ich - prüfen Sie an dieser Stelle nicht; Es liegt in der Verantwortung des Anrufers. Aber ich habe eine Grundlage, auf der ich aufbauen kann, anstatt auf einfacher Zweckmäßigkeit (und C-Programmier-Arroganz).

Ich versuche Donald Knuths Grundsatz zu folgen, Programme so fragil wie möglich zu gestalten. Wenn irgendetwas schief geht, lassen Sie es groß abstürzen , und das Referenzieren eines Nullzeigers ist normalerweise eine gute Möglichkeit, dies zu tun. Die allgemeine Idee ist ein Absturz oder eine Endlosschleife ist weitaus besser als die Erstellung falscher Daten. Und es erregt die Aufmerksamkeit der Programmierer!

Das Referenzieren von Nullzeigern (insbesondere für große Datenstrukturen) führt jedoch nicht immer zu einem Absturz. Seufzer. Das ist richtig. Und hier fallen Asserts ins Spiel. Sie sind einfach, können Ihr Programm sofort zum Absturz bringen (und die Frage beantworten: "Was soll die Methode tun, wenn sie auf eine Null stößt?") Und können für verschiedene Situationen ein- und ausgeschaltet werden (ich empfehle) NICHT ausschalten, da es für Kunden besser ist, einen Absturz zu erleiden und eine kryptische Nachricht zu sehen, als schlechte Daten zu haben.

Das sind meine zwei Cent.


1

Ich überprüfe im Allgemeinen nur, wenn ein Zeiger zugewiesen ist. Dies ist im Allgemeinen das einzige Mal, dass ich tatsächlich etwas dagegen tun und möglicherweise wiederherstellen kann, wenn er ungültig ist.

Wenn ich zum Beispiel ein Handle für ein Fenster bekomme, überprüfe ich, ob es gleich null ist, und tue etwas gegen die Nullbedingung, aber ich werde nicht jedes Mal prüfen, ob es null ist Ich benutze den Zeiger in jeder Funktion, an die der Zeiger übergeben wird, sonst hätte ich Berge von doppeltem Fehlerbehandlungscode.

Funktionen wie können graph_get_current_column_colorwahrscheinlich nichts Nützliches für Ihre Situation tun, wenn sie auf einen schlechten Zeiger stößt. Daher überlasse ich die Überprüfung auf NULL ihren Aufrufern.


1

Ich würde sagen, dass es auf Folgendes ankommt:

  1. Ist die CPU-Auslastung kritisch? Jede Überprüfung auf NULL dauert einige Zeit.
  2. Wie hoch ist die Wahrscheinlichkeit, dass der Zeiger NULL ist? Wurde es gerade in einer früheren Funktion verwendet. Könnte der Wert des Zeigers geändert worden sein.
  3. Ist das System präventiv? Bedeutet das, dass eine Aufgabenänderung passieren und den Wert ändern könnte? Könnte ein ISR hereinkommen und den Wert ändern?
  4. Wie eng ist der Code gekoppelt?
  5. Gibt es einen automatischen Mechanismus, der automatisch nach NULL-Zeigern sucht?

CPU-Auslastung / Quotenzeiger ist NULL Jedes Mal, wenn Sie nach NULL suchen, dauert es eine Weile. Aus diesem Grund versuche ich, meine Prüfungen darauf zu beschränken, wo der Zeiger seinen Wert hätte ändern können.

Präventives System Wenn Ihr Code ausgeführt wird und eine andere Task ihn unterbrechen und möglicherweise den Wert ändern könnte, ist eine Überprüfung sinnvoll.

Eng gekoppelte Module Wenn das System eng gekoppelt ist, ist es sinnvoll, dass Sie mehr Prüfungen durchführen. Ich meine damit, dass es Datenstrukturen gibt, die von mehreren Modulen gemeinsam genutzt werden. Ein Modul könnte etwas unter einem anderen Modul ändern. In diesen Situationen ist es sinnvoll, öfter zu überprüfen.

Automatische Überprüfungen / Hardware-Unterstützung Als letztes muss berücksichtigt werden, ob die Hardware, auf der Sie ausgeführt werden, über einen Mechanismus verfügt, der nach NULL sucht. Insbesondere beziehe ich mich auf die Seitenfehlererkennung. Wenn Ihr System eine Seitenfehlererkennung hat, kann die CPU selbst nach NULL-Zugriffen suchen. Persönlich halte ich dies für den besten Mechanismus, da er immer ausgeführt wird und nicht darauf angewiesen ist, dass der Programmierer explizite Überprüfungen vornimmt. Es hat auch den Vorteil, dass der Overhead praktisch Null ist. Wenn dies verfügbar ist, empfehle ich es, das Debuggen ist etwas schwieriger, aber nicht allzu schwierig.

Um zu testen, ob es verfügbar ist, erstellen Sie ein Programm mit einem Zeiger. Stellen Sie den Zeiger auf 0 und versuchen Sie, ihn zu lesen / schreiben.


Ich weiß nicht, ob ich einen Segfault als automatische NULL-Prüfung einstufen würde. Ich bin damit einverstanden, dass ein CPU-Speicherschutz hilft, sodass ein Prozess den Rest des Systems nicht so stark schädigt, aber ich würde ihn nicht als automatischen Schutz bezeichnen.
Gabriel Southern

1

Meiner Meinung nach ist es eine gute Sache, Eingaben (dh Vor- / Nachbedingungen) zu validieren, um Programmierfehler zu erkennen, aber nur, wenn dies zu lauten und widerwärtigen, aufsehenerregenden Fehlern führt, die nicht ignoriert werden können. asserthat in der Regel diesen Effekt.

Alles, was darunter leidet, kann ohne sorgfältig aufeinander abgestimmte Teams zum Albtraum werden. Und natürlich sind alle Teams im Idealfall sehr sorgfältig koordiniert und unter strengen Maßstäben vereinheitlicht, aber die meisten Umgebungen, in denen ich gearbeitet habe, haben das bei weitem nicht geschafft.

Nur als Beispiel habe ich mit einigen Kollegen zusammengearbeitet, die der Meinung waren, dass man religiös auf das Vorhandensein von Nullzeigern prüfen sollte, und deshalb eine Menge Code wie diesen gestreut haben:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... und manchmal einfach so, ohne dass ein Fehlercode zurückgegeben oder gesetzt wird. Und das in einer jahrzehntelangen Codebasis mit vielen erworbenen Plugins von Drittanbietern. Es war auch eine Codebasis, die mit vielen Fehlern behaftet war, und oft waren Fehler, die nur sehr schwer auf Grundursachen zurückzuführen waren, da sie dazu neigten, an Orten, die weit von der unmittelbaren Ursache des Problems entfernt waren, abgestürzt zu sein.

Und diese Praxis war einer der Gründe dafür. Es ist eine Verletzung einer festgelegten Vorbedingung der obigen move_vertexFunktion, einen Nullpunkt an sie zu übergeben, aber eine solche Funktion hat es einfach stillschweigend akzeptiert und nichts als Antwort darauf getan. In der Regel hat ein Plugin also einen Programmierfehler, der dazu führt, dass es null an diese Funktion weitergibt, sie nur nicht erkennt, nur viele Dinge später ausführt und das System irgendwann abbricht oder abstürzt.

Das eigentliche Problem hierbei war jedoch die Unfähigkeit, dieses Problem leicht zu erkennen. Also habe ich einmal versucht zu sehen, was passieren würde, wenn ich den obigen analogen Code in einen verwandle assert:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... und zu meinem Entsetzen stellte ich fest, dass diese Behauptung auch beim Starten der Anwendung nach links und rechts fehlschlug. Nachdem ich die ersten Anrufseiten repariert hatte, machte ich noch einige Dinge und bekam dann eine Schiffsladung mit mehr Behauptungsfehlern. Ich machte weiter, bis ich so viel Code geändert hatte, dass ich meine Änderungen rückgängig machte, weil sie zu aufdringlich geworden waren und diese Nullzeigerprüfung widerwillig beibehalten hatten, anstatt zu dokumentieren, dass die Funktion das Akzeptieren eines Nullscheitelpunkts zulässt.

Aber das ist die Gefahr, wenn auch im schlimmsten Fall, Verstöße gegen die Vor- / Nachbedingungen nicht leicht erkennbar zu machen. Sie können dann im Laufe der Jahre eine Schiffsladung Code ansammeln, die gegen diese Vor- / Nachbedingungen verstößt, während Sie unter dem Radar der Tests fliegen. Meiner Meinung nach können solche Nullzeigerprüfungen außerhalb eines offensichtlichen und abscheulichen Behauptungsfehlers tatsächlich weit, weit mehr schaden als nützen.

In Bezug auf die wesentliche Frage, wann Sie auf Null-Zeiger prüfen sollten, möchte ich großzügig behaupten, dass ein Programmiererfehler erkannt werden soll, ohne dass dies stumm und schwer zu erkennen ist. Wenn es sich nicht um einen Programmierfehler handelt und sich der Kontrolle des Programmierers entzogen hat, wie z. B. ein Speichermangel, ist es sinnvoll, nach Null zu suchen und die Fehlerbehandlung zu verwenden. Darüber hinaus ist es eine Entwurfsfrage und basiert darauf, was Ihre Funktionen als gültige Vor- / Nachbedingungen betrachten.


0

Eine Übung besteht darin, die Nullprüfung immer durchzuführen, es sei denn, Sie haben sie bereits geprüft. Wenn also eine Eingabe von Funktion A () an B () übergeben wird und A () den Zeiger bereits validiert hat und Sie sicher sind, dass B () nirgendwo anders aufgerufen wird, kann B () A () vertrauen bereinigte die Daten.


1
... bis in 6 Monaten jemand kommt und etwas mehr Code hinzufügt, der B () aufruft (möglicherweise unter der Annahme, dass derjenige, der B () geschrieben hat, sicher richtig auf NULLs überprüft hat). Dann bist du beschissen, oder? Grundregel - Wenn für die Eingabe einer Funktion eine ungültige Bedingung vorliegt, prüfen Sie diese, da die Eingabe außerhalb der Kontrolle der Funktion liegt.
Maximus Minimus

@ mh01 Wenn Sie nur zufälligen Code zerschlagen (dh Annahmen treffen und keine Dokumentation lesen), dann glaube ich nicht, dass zusätzliche NULLPrüfungen viel bewirken werden. Denken Sie darüber nach: Jetzt B()prüft NULLund ... macht was? Rückkehr -1? Wenn der Anrufer nicht überprüft NULL, welches Vertrauen können Sie haben, dass er den -1Rückgabewert-Fall überhaupt bearbeiten wird?
Detly

1
Das ist die Verantwortung des Anrufers. Sie sind für Ihre eigene Verantwortung verantwortlich, einschließlich der Tatsache, dass Sie keinen willkürlichen / nicht erkennbaren / potenziell nicht überprüften Eingaben vertrauen. Ansonsten bist du in Cop-out-Stadt. Wenn der Anrufer nicht überprüft, hat der Anrufer es vermasselt. Sie haben nachgesehen, Ihr eigener Hintern ist bedeckt, Sie können jedem, der den Anrufer angeschrieben hat, sagen, dass Sie zumindest alles richtig gemacht haben.
Maximus Minimus
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.