Was sind die Vorteile von nullptr?


163

Dieser Code macht konzeptionell dasselbe für die drei Zeiger (sichere Zeigerinitialisierung):

int* p1 = nullptr;
int* p2 = NULL;
int* p3 = 0;

Was sind also die Vorteile der Zuweisung von Zeigern nullptrgegenüber der Zuweisung von Werten NULLoder 0?


39
Zum einen nimmt eine überladene Funktion die Version intund void *wählt bei der Verwendung nicht die intVersion aus . void *nullptr
Chris

2
Nun f(nullptr)ist anders als f(NULL). In Bezug auf den obigen Code (Zuweisung zu einer lokalen Variablen) sind jedoch alle drei Zeiger genau gleich. Der einzige Vorteil ist die Lesbarkeit des Codes.
Balki

2
Ich bin dafür, dies zu einer FAQ zu machen, @Prasoon. Vielen Dank!
sbi

1
NB NULL ist historisch gesehen nicht garantiert 0, sondern wie oc C99, ähnlich wie ein Byte nicht unbedingt 8 Bit lang war und wahr und falsch architekturabhängige Werte waren. Diese Frage konzentriert sich auf nullptrden Unterschied zwischen 0 undNULL
awiebe

Antworten:


180

In diesem Code scheint es keinen Vorteil zu geben. Beachten Sie jedoch die folgenden überladenen Funktionen:

void f(char const *ptr);
void f(int v);

f(NULL);  //which function will be called?

Welche Funktion wird aufgerufen? Natürlich soll hier angerufen werden f(char const *), aber in Wirklichkeit f(int)wird angerufen! Das ist ein großes Problem 1 , nicht wahr?

Die Lösung für solche Probleme besteht also darin, Folgendes zu verwenden nullptr:

f(nullptr); //first function is called

Das ist natürlich nicht der einzige Vorteil von nullptr. Hier ist noch einer:

template<typename T, T *ptr>
struct something{};                     //primary template

template<>
struct something<nullptr_t, nullptr>{};  //partial specialization for nullptr

Da in der Vorlage der Typ von nullptrabgeleitet wird nullptr_t, können Sie Folgendes schreiben:

template<typename T>
void f(T *ptr);   //function to handle non-nullptr argument

void f(nullptr_t); //an overload to handle nullptr argument!!!

1. In C ++ NULList definiert als #define NULL 0, so ist es im Grunde int, deshalb f(int)wird aufgerufen.


1
Wie Mehrdad festgestellt hatte, sind solche Überlastungen ziemlich selten. Gibt es andere relevante Vorteile von nullptr? (Nein, ich fordere nicht)
Mark Garcia

2
@MarkGarcia, Dies könnte hilfreich sein: stackoverflow.com/questions/13665349/...
chris

9
Ihre Fußnote scheint rückwärts zu sein. NULLwird vom Standard verlangt, einen integralen Typ zu haben, und deshalb wird er normalerweise als 0oder definiert 0L. Ich bin mir auch nicht sicher, ob mir diese nullptr_tÜberladung gefällt , da sie nur Aufrufe mit nullptr, nicht mit einem Nullzeiger eines anderen Typs abfängt, wie z (void*)0. Aber ich kann glauben, dass es einige Verwendungszwecke hat, auch wenn Sie nur einen eigenen einwertigen Platzhaltertyp definieren müssen, der "keine" bedeutet.
Steve Jessop

1
Ein weiterer Vorteil (obwohl zugegebenermaßen geringfügig) kann sein, dass er nullptreinen genau definierten numerischen Wert hat, während dies bei Nullzeigerkonstanten nicht der Fall ist. Eine Nullzeigerkonstante wird in den Nullzeiger dieses Typs konvertiert (was auch immer das ist). Es ist erforderlich, dass zwei Nullzeiger desselben Typs identisch verglichen werden, und die boolesche Konvertierung verwandelt einen Nullzeiger in false. Sonst ist nichts erforderlich. Daher ist es für einen Compiler (albern, aber möglich) möglich, zB 0xabcdef1234oder eine andere Zahl für den Nullzeiger zu verwenden. Andererseits nullptrist eine Konvertierung in eine numerische Null erforderlich.
Damon

1
@DeadMG: Was ist falsch in meiner Antwort? das f(nullptr)wird nicht die beabsichtigte Funktion aufrufen? Es gab mehr als eine Motivation. Viele andere nützliche Dinge können die Programmierer in den kommenden Jahren selbst entdecken. Man kann also nicht sagen, dass es nur eine wahre Verwendung von gibt nullptr.
Nawaz

87

C ++ 11 nullptrwird als NullZeigerkonstante bezeichnet. Es verbessert die Typensicherheit und löst mehrdeutige Situationen im Gegensatz zur vorhandenen implementierungsabhängigen Nullzeigerkonstante NULL. Um die Vorteile von verstehen zu können nullptr. Wir müssen zuerst verstehen, was ist NULLund welche Probleme damit verbunden sind.


Was ist NULLgenau?

Pre C ++ 11 NULLwurde verwendet, um einen Zeiger darzustellen, der keinen Wert hat, oder einen Zeiger, der auf nichts Gültiges zeigt. Entgegen der weit verbreiteten Vorstellung NULList in C ++ kein Schlüsselwort . Es ist eine Kennung, die in Standardbibliotheksüberschriften definiert ist. Kurz gesagt, Sie können nicht verwenden, NULLohne einige Standardbibliotheksheader einzuschließen. Betrachten Sie das Beispielprogramm :

int main()
{ 
    int *ptr = NULL;
    return 0;
}

Ausgabe:

prog.cpp: In function 'int main()':
prog.cpp:3:16: error: 'NULL' was not declared in this scope

Der C ++ - Standard definiert NULL als ein implementierungsdefiniertes Makro, das in bestimmten Standardbibliotheksheaderdateien definiert ist. Der Ursprung von NULL ist von C und C ++ hat ihn von C geerbt. Der C-Standard hat NULL als 0oder definiert (void *)0. In C ++ gibt es jedoch einen subtilen Unterschied.

C ++ konnte diese Spezifikation nicht akzeptieren. Im Gegensatz zu C ist C ++ eine stark typisierte Sprache (C erfordert keine explizite Umwandlung void*in einen beliebigen Typ, während C ++ eine explizite Umwandlung vorschreibt ). Dies macht die vom C-Standard angegebene Definition von NULL in vielen C ++ - Ausdrücken unbrauchbar. Beispielsweise:

std::string * str = NULL;         //Case 1
void (A::*ptrFunc) () = &A::doSomething;
if (ptrFunc == NULL) {}           //Case 2

Wenn NULL als definiert wäre (void *)0, würde keiner der obigen Ausdrücke funktionieren.

  • Fall 1: Wird nicht kompiliert, da eine automatische Umwandlung von void *bis erforderlich ist std::string.
  • Fall 2: Wird nicht kompiliert, da eine Umwandlung von void *zum Zeiger auf die Elementfunktion erforderlich ist.

Im Gegensatz zu C hat C ++ Standard den Auftrag, NULL als numerisches Literal 0oder zu definieren 0L.


Was ist also die Notwendigkeit für eine weitere Nullzeigerkonstante, wenn wir NULLbereits haben?

Obwohl das C ++ - Standardkomitee eine NULL-Definition ausgearbeitet hat, die für C ++ funktioniert, hatte diese Definition einen angemessenen Anteil an Problemen. NULL funktionierte für fast alle Szenarien gut genug, aber nicht für alle. Es gab überraschende und fehlerhafte Ergebnisse für bestimmte seltene Szenarien. Zum Beispiel :

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    doSomething(NULL);
    return 0;
}

Ausgabe:

In Int version

Natürlich scheint die Absicht zu sein, die Version aufzurufen, die char*als Argument verwendet wird, aber wie die Ausgabe zeigt, wird die Funktion intaufgerufen, die eine Version benötigt. Dies liegt daran, dass NULL ein numerisches Literal ist.

Da durch die Implementierung definiert ist, ob NULL 0 oder 0L ist, kann die Auflösung der Funktionsüberlastung zu großer Verwirrung führen.

Beispielprogramm:

#include <cstddef>

void doSomething(int);
void doSomething(char *);

int main()
{
  doSomething(static_cast <char *>(0));    // Case 1
  doSomething(0);                          // Case 2
  doSomething(NULL)                        // Case 3
}

Analyse des obigen Snippets:

  • Fall 1: Anrufe doSomething(char *)wie erwartet.
  • Fall 2: Aufrufe, doSomething(int)aber möglicherweise war die char*Version erwünscht, da 0IS auch ein Nullzeiger ist.
  • Fall 3: Wenn NULLdefiniert als 0, werden Aufrufe doSomething(int)angezeigt , wenn dies möglicherweise doSomething(char *)beabsichtigt war, was möglicherweise zu einem Logikfehler zur Laufzeit führt. Wenn NULLdefiniert als 0L, ist der Aufruf mehrdeutig und führt zu einem Kompilierungsfehler.

Abhängig von der Implementierung kann derselbe Code verschiedene Ergebnisse liefern, was eindeutig unerwünscht ist. Natürlich wollte das C ++ - Standardkomitee dies korrigieren, und das ist die Hauptmotivation für nullptr.


Was ist nullptrund wie vermeidet es die Probleme von NULL?

C ++ 11 führt ein neues Schlüsselwort ein nullptr, das als Nullzeigerkonstante dient. Im Gegensatz zu NULL ist sein Verhalten nicht implementierungsdefiniert. Es ist kein Makro, aber es hat einen eigenen Typ. nullptr hat den Typ std::nullptr_t. C ++ 11 definiert die Eigenschaften für nullptr entsprechend, um die Nachteile von NULL zu vermeiden. Um seine Eigenschaften zusammenzufassen:

Eigenschaft 1: Es hat einen eigenen Typ std::nullptr_tund
Eigenschaft 2: Es ist implizit konvertierbar und mit jedem Zeigertyp oder Zeiger-zu-Element-Typ vergleichbar, aber
Eigenschaft 3: Es ist nicht implizit konvertierbar oder mit integralen Typen vergleichbar, außer für bool.

Betrachten Sie das folgende Beispiel:

#include<iostream>
void doSomething(int)
{
    std::cout<<"In Int version";
}
void doSomething(char *)
{
   std::cout<<"In char* version";
}

int main()
{
    char *pc = nullptr;      // Case 1
    int i = nullptr;         // Case 2
    bool flag = nullptr;     // Case 3

    doSomething(nullptr);    // Case 4
    return 0;
}

Im obigen Programm

  • Fall 1: OK - Eigenschaft 2
  • Fall 2: Nicht in Ordnung - Eigenschaft 3
  • Fall 3: OK - Eigenschaft 3
  • Fall 4: Keine Verwirrung - Ruft char *Version, Eigenschaft 2 & 3 auf

Somit vermeidet die Einführung von nullptr alle Probleme des guten alten NULL.

Wie und wo solltest du verwenden nullptr?

Die Faustregel für C ++ 11 lautet: Beginnen nullptrSie einfach mit der Verwendung, wenn Sie in der Vergangenheit sonst NULL verwendet hätten.


Standardreferenzen:

C ++ 11 Standard: C.3.2.4 Makro NULL
C ++ 11 Standard: 18.2 Typen
C ++ 11 Standard: 4.10
Zeigerkonvertierungen C99 Standard: 6.3.2.3 Zeiger


Ich übe bereits Ihren letzten Rat, seit ich ihn kenne nullptr, obwohl ich nicht wusste, welchen Unterschied er wirklich zu meinem Code macht. Vielen Dank für die tolle Antwort und vor allem für die Mühe. Brachte mir viel Licht auf das Thema.
Mark Garcia

"In bestimmten Standard-Bibliotheks-Header-Dateien." -> warum nicht einfach von Anfang an "cstddef" schreiben?
mxmlnkn

Warum sollten wir zulassen, dass nullptr in einen Bool-Typ konvertierbar ist? Könnten Sie bitte näher darauf eingehen?
Robert Wang

... wurde verwendet, um einen Zeiger darzustellen, der keinen Wert hat ... Variablen haben immer einen Wert. Es kann Rauschen sein, oder 0xccccc....eine wertlose Variable ist ein inhärenter Widerspruch.
3Dave

"Fall 3: OK - Eigenschaft 3" (Zeile bool flag = nullptr;). Nein, nicht OK, ich erhalte den folgenden Fehler beim Kompilieren mit g ++ 6:error: converting to ‘bool’ from ‘std::nullptr_t’ requires direct-initialization [-fpermissive]
Georg

23

Die eigentliche Motivation ist hier die perfekte Weiterleitung .

Erwägen:

void f(int* p);
template<typename T> void forward(T&& t) {
    f(std::forward<T>(t));
}
int main() {
    forward(0); // FAIL
}

Einfach ausgedrückt ist 0 ein spezieller Wert , aber Werte können sich nicht über die Systemtypen verbreiten. Weiterleitungsfunktionen sind unerlässlich, und 0 kann nicht damit umgehen. Daher war es absolut notwendig einzuführen nullptr, wo der Typ das Besondere ist und der Typ sich tatsächlich ausbreiten kann. Tatsächlich musste das MSVC-Team nullptrvorzeitig einführen, nachdem es rvalue-Referenzen implementiert und diese Falle für sich entdeckt hatte.

Es gibt einige andere nullptrEckfälle, in denen das Leben leichter gemacht werden kann - aber es ist kein Kernfall, da eine Besetzung diese Probleme lösen kann. Erwägen

void f(int);
void f(int*);
int main() { f(0); f(nullptr); }

Ruft zwei separate Überladungen auf. Darüber hinaus berücksichtigen

void f(int*);
void f(long*);
int main() { f(0); }

Das ist nicht eindeutig. Mit nullptr können Sie jedoch angeben

void f(std::nullptr_t)
int main() { f(nullptr); }

7
Komisch. Die Hälfte der Antwort ist die gleiche wie bei den beiden anderen Antworten, was Ihrer Meinung nach "ganz falsche" Antworten sind !!!
Nawaz

Das Weiterleitungsproblem kann auch mit einem Cast gelöst werden. forward((int*)0)funktioniert. Vermisse ich etwas
jcsahnwaldt Reinstate Monica

5

Grundlagen von nullptr

std::nullptr_tist der Typ des Nullzeigerliterals nullptr. Es ist ein Wert vom Typ prvalue / rvalue std::nullptr_t. Es gibt implizite Konvertierungen von nullptr in null Zeigerwerte für jeden Zeigertyp.

Das Literal 0 ist ein int, kein Zeiger. Wenn C ++ 0 in einem Kontext betrachtet, in dem nur ein Zeiger verwendet werden kann, interpretiert es 0 widerwillig als Nullzeiger, aber das ist eine Fallback-Position. Die primäre Richtlinie von C ++ lautet, dass 0 ein int und kein Zeiger ist.

Vorteil 1 - Entfernen Sie Mehrdeutigkeiten beim Überladen von Zeiger- und Integraltypen

In C ++ 98 war die Hauptaussage, dass eine Überladung von Zeiger- und Integraltypen zu Überraschungen führen kann. Das Übergeben von 0 oder NULL an solche Überladungen wird niemals als Zeigerüberladung bezeichnet:

   void fun(int); // two overloads of fun
    void fun(void*);
    fun(0); // calls f(int), not fun(void*)
    fun(NULL); // might not compile, but typically calls fun(int). Never calls fun(void*)

Das Interessante an diesem Aufruf ist der Widerspruch zwischen der offensichtlichen Bedeutung des Quellcodes („Ich rufe Spaß mit NULL - dem Nullzeiger“) und seiner tatsächlichen Bedeutung („Ich rufe Spaß mit einer Art Ganzzahl auf - nicht der Null Zeiger").

Der Vorteil von nullptr ist, dass es keinen integralen Typ hat. Das Aufrufen der überladenen Funktion fun mit nullptr ruft die void * -Überladung (dh die Zeigerüberladung) auf, da nullptr nicht als etwas Integrales angesehen werden kann:

fun(nullptr); // calls fun(void*) overload 

Die Verwendung von nullptr anstelle von 0 oder NULL vermeidet somit Überraschungen bei der Überlastungsauflösung.

Ein weiterer Vorteil nullptrgegenüber der NULL(0)Verwendung von Auto für den Rückgabetyp

Angenommen, Sie stoßen in einer Codebasis auf Folgendes:

auto result = findRecord( /* arguments */ );
if (result == 0) {
....
}

Wenn Sie nicht wissen (oder nicht leicht herausfinden können), was findRecord zurückgibt, ist möglicherweise nicht klar, ob das Ergebnis ein Zeigertyp oder ein ganzzahliger Typ ist. Immerhin könnte 0 (gegen welches Ergebnis getestet wird) in beide Richtungen gehen. Wenn Sie andererseits Folgendes sehen,

auto result = findRecord( /* arguments */ );
if (result == nullptr) {
...
}

Es gibt keine Mehrdeutigkeit: Das Ergebnis muss ein Zeigertyp sein.

Vorteil 3

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

void lockAndCallF1()
{
        MuxtexGuard g(f1m); // lock mutex for f1
        auto result = f1(static_cast<int>(0)); // pass 0 as null ptr to f1
        cout<< result<<endl;
}

void lockAndCallF2()
{
        MuxtexGuard g(f2m); // lock mutex for f2
        auto result = f2(static_cast<int>(NULL)); // pass NULL as null ptr to f2
        cout<< result<<endl;
}
void lockAndCallF3()
{
        MuxtexGuard g(f3m); // lock mutex for f2
        auto result = f3(nullptr);// pass nullptr as null ptr to f3 
        cout<< result<<endl;
} // unlock mutex
int main()
{
        lockAndCallF1();
        lockAndCallF2();
        lockAndCallF3();
        return 0;
}

Das obige Programm wurde kompiliert und erfolgreich ausgeführt, aber lockAndCallF1, lockAndCallF2 und lockAndCallF3 haben redundanten Code. Es ist schade, solchen Code zu schreiben, wenn wir für all dies eine Vorlage schreiben können lockAndCallF1, lockAndCallF2 & lockAndCallF3. So kann es mit Vorlage verallgemeinert werden. Ich habe eine Vorlagenfunktion lockAndCallanstelle einer Mehrfachdefinition lockAndCallF1, lockAndCallF2 & lockAndCallF3für redundanten Code geschrieben.

Der Code wird wie folgt umgerechnet:

#include<iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
int f1(std::shared_ptr<int> spw) // call these only when
{
  //do something
  return 0;
}
double f2(std::unique_ptr<int> upw) // the appropriate
{
  //do something
  return 0.0;
}
bool f3(int* pw) // mutex is locked
{

return 0;
}

std::mutex f1m, f2m, f3m; // mutexes for f1, f2, and f3
using MuxtexGuard = std::lock_guard<std::mutex>;

template<typename FuncType, typename MuxType, typename PtrType>
auto lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) -> decltype(func(ptr))
//decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr)
{
        MuxtexGuard g(mutex);
        return func(ptr);
}
int main()
{
        auto result1 = lockAndCall(f1, f1m, 0); //compilation failed 
        //do something
        auto result2 = lockAndCall(f2, f2m, NULL); //compilation failed
        //do something
        auto result3 = lockAndCall(f3, f3m, nullptr);
        //do something
        return 0;
}

Detail - Analyse , warum Kompilation für gescheiterte lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)für nichtlockAndCall(f3, f3m, nullptr)

Warum Kompilierung lockAndCall(f1, f1m, 0) & lockAndCall(f3, f3m, nullptr)fehlgeschlagen?

Das Problem ist, dass bei der Übergabe von 0 an lockAndCall der Abzug des Vorlagentyps aktiviert wird, um den Typ herauszufinden. Der Typ 0 ist int, das ist also der Typ des Parameters ptr innerhalb der Instanziierung dieses Aufrufs von lockAndCall. Leider bedeutet dies, dass beim Aufruf von func in lockAndCall ein int übergeben wird, das nicht mit dem erwarteten std::shared_ptr<int>Parameter kompatibel ist f1. Die im Aufruf an übergebene 0 lockAndCallsollte einen Nullzeiger darstellen, aber was tatsächlich übergeben wurde, war int. Der Versuch, dieses int als a an f1 zu übergeben, std::shared_ptr<int>ist ein Typfehler. Der Aufruf von lockAndCallwith 0 schlägt fehl, da innerhalb der Vorlage ein int an eine Funktion übergeben wird, für die a erforderlich ist std::shared_ptr<int>.

Die Analyse für den Anruf NULList im Wesentlichen dieselbe. Bei der NULLÜbergabe an lockAndCallwird ein integraler Typ für den Parameter ptr abgeleitet, und ein Typfehler tritt auf, wenn ptr- ein int- oder int-ähnlicher Typ - an übergeben wird f2, der erwartet, dass a erhalten wird std::unique_ptr<int>.

Im Gegensatz dazu hat der Anruf nullptrkeine Probleme. Wenn an übergeben nullptrwird, lockAndCallwird der Typ für ptrals abgeleitet std::nullptr_t. Wenn an übergeben ptrwird, f3erfolgt eine implizite Konvertierung von std::nullptr_tnach int*, da std::nullptr_timplizit in alle Zeigertypen konvertiert wird.

Es wird empfohlen, wann immer Sie auf einen Nullzeiger verweisen möchten, nullptr zu verwenden, nicht 0 oder NULL.


4

Es gibt keinen direkten Vorteil, wenn nullptrSie die Beispiele so gezeigt haben.
Stellen Sie sich jedoch eine Situation vor, in der Sie zwei Funktionen mit demselben Namen haben. Ich nehme intund noch eineint*

void foo(int);
void foo(int*);

Wenn Sie mit foo(int*)NULL anrufen möchten, lautet der Weg wie folgt:

foo((int*)0); // note: foo(NULL) means foo(0)

nullptrmacht es einfacher und intuitiver :

foo(nullptr);

Zusätzlicher Link von Bjarnes Webseite.
Irrelevant, aber auf C ++ 11 Randnotiz:

auto p = 0; // makes auto as int
auto p = nullptr; // makes auto as decltype(nullptr)

3
Als Referenz decltype(nullptr)ist std::nullptr_t.
chris

2
@ MarkGarcia, soweit ich weiß, ist es ein ausgewachsener Typ.
Chris

5
@ MarkGarcia, es ist eine interessante Frage. cppreference hat : typedef decltype(nullptr) nullptr_t;. Ich denke, ich kann im Standard schauen. Ah, ich habe es
Chris

2
@DeadMG: Es gab mehr als eine Motivation. Viele andere nützliche Dinge können die Programmierer in den kommenden Jahren selbst entdecken. Man kann also nicht sagen, dass es nur eine wahre Verwendung von gibt nullptr.
Nawaz

2
@DeadMG: Aber Sie sagten, diese Antwort sei "ganz falsch", einfach weil sie nicht über "die wahre Motivation" spricht, über die Sie in Ihrer Antwort gesprochen haben. Nicht nur, dass diese Antwort (und auch meine) von Ihnen abgelehnt wurde.
Nawaz

4

Wie andere bereits gesagt haben, liegt der Hauptvorteil in Überlastungen. Und während explizite intÜberladungen im std::fillVergleich zu Zeigern selten sein können, sollten Sie Standardbibliotheksfunktionen wie (die mich in C ++ 03 mehr als einmal gebissen haben) berücksichtigen :

MyClass *arr[4];
std::fill_n(arr, 4, NULL);

Kompiliert nicht : Cannot convert int to MyClass*.


2

IMO ist wichtiger als diese Überlastungsprobleme: In tief verschachtelten Vorlagenkonstrukten ist es schwierig, den Überblick über die Typen nicht zu verlieren, und explizite Signaturen sind ein ziemliches Unterfangen. Je genauer Sie sich auf den beabsichtigten Zweck konzentrieren, desto besser wird für alles, was Sie verwenden, der Bedarf an expliziten Signaturen verringert, und der Compiler kann aufschlussreichere Fehlermeldungen erstellen, wenn etwas schief geht.

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.