namespace cpp {}

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


Action disabled: source
lernen:const

Political const correctness

Der Hauptzweck der DATA-Anweisung ist es, Namen für Konstanten festzulegen; anstatt an jeder Stelle im Programm, wo π vorkommt, 3.141592653589793 zu schreiben, kann man diesen Wert mit einer DATA-Anweisung einer Variablen PI zuweisen und diese dann anstelle der Langform verwenden. Dies vereinfacht auch die Änderung des Programms, falls sich der Wert von π ändern sollte.
— FORTRAN Manual der Firma Xerox
All this provides a significant additional form of type checking and safety in your programming. The use of so-called const correctness (the use of const anywhere you possibly can) can be a lifesaver for projects. Although you can ignore const and continue to use old C coding practices, it's there to help you.
— Bruce Eckel
It's hard at first, but using const really tightens up your coding style.
Const correctness grows on you.
— Todd Hoff

Die Konstant-Deklaration ist von politischer Tragweite. Ihr korrekter Einsatz hilft, saubere Entwürfe zu gestalten und ihre Datenintegrität zu sichern. Dieser Artikel erörtert das Warum und Wie für den Einsatz des Schlüsselwortes const in C++ in prozeduralen und objekt-orientierten Programmen.

Ode an ein Schlüsselwort

In Abwandlung eines Songs von Joan Baez:

const is just a five letter word. 

Na und? Mir geht es mit dem Wort wie einem Alkoholiker mit dem Suchtmittel: ich habe kein Problem damit, sondern ohne. Ich will zu erklären versuchen, warum. Ich hoffe, nachher geht es Ihnen wie mir. Seien Sie nicht konstant, verändern Sie sich! Es wird künftigen Quelltexten gut tun.

C lernt von C++

Nein, Sie haben sich nicht verlesen. Das Schlüsselwort const wurde 1983 in C++ und 1984 in den Standard der Vorgängersprache C übernommen [D&E]. Eingeführt wurde es, um unabsichtliche Änderungen von Werten zu erschweren. Des Öfteren kommen Zahlen im Quelltext vor, die man besser an einer Stelle definiert, weil sie im Programm zwar als unveränderlich betrachtet werden, aber sich von Version zu Version auch mal ändern können:

int const ARRAYSIZE = 10;
double const PI = 4.0*atan(1.0);

(siehe obiges Xerox-Zitat [PI]). Solche globalen "magischen" Zahlen im Quelltext zu suchen und zu übersehen, ist ein Alptraum jedes Wartungsprogrammierers. C-Makrokonstanten in C++ zu verwenden ist zwar möglich, aber es gibt Bestrebungen, dies künftig zu ächten:

#define ARRAYSIZE 10
#define PI (4.0*atan(1.0))    /* wird jedes Mal neu berechnet ! */

Dankbare Abnehmer für Konstanten sind Felder:

  float array[ARRAYSIZE];
 
  std::cout << "Eingabe von " << ARRAYSIZE << " Werten:\n";
  for (int i = 0; i < ARRAYSIZE; i++)
  {
    std::cout << "Wert " << (i+1) << " : "; 
    std::cin >> array[i];
  }
  float sum = 0;
  for (int i = 0; i < ARRAYSIZE; i++)
  {
    sum += array[i];
    array[i] = sum;
  }
  for (int i = 0; i < ARRAYSIZE; i++)
  {
    std::cout << "Partialsumme " << (i+1) << " = " << array[i] << '\n';
  }

Bisher mussten in C und müssen in C++ die Feldgrößen konstante, beim Übersetzen festliegende Ausdrücke sein. Künftige C++-Standards können evtl. die Festlegungen des neuen C-Sprachstandards ISO 9899:1999 übernehmen und zur Laufzeit variabel festlegbare Feldgrößen erlauben (wie g++ dies schon tut). Was wird dann wohl mit dem Schleifenende, wenn sich der Wert ARRAYSIZE zwischendurch auch, natürlich unabsichtlich, ändern kann?

Konstante Variablen?

Variablen sind Speicherzellen, deren Wert sich durch Zuweisungen oder Operationen ändern kann. Konstanten ändern sich nicht:

int n = 10;           // Variablendefinition
int const SIZE = 10;  // const Variable

Der direkte Änderungsversuch erzeugt einen Übersetzungsfehler. C-Programmierer sagen trotzdem zur const-Variablen ungern Konstante, weil sie

  • wissen, wie man const umgehen kann,
  • der Drang, das zu tun, beliebig groß werden kann und
  • die Bezeichnung Konstante aus historischen Gründen vom Präprozessor belegt ist.

(Ich werde schlechten Stil an dieser Stelle nicht unterstützen und nicht verraten, wie const zu umgehen ist. Die kriminelle Energie, das herauszufinden, muss schon jeder selbst aufbringen.)

Aber ist das nicht ein Widerspruch in sich: konstante Variable? Das Schlüsselwort const meint, das der Programmierer den Wert nicht ändern will. Sehr wohl kann sich der Wert einer const-Variable aber aus Gründen ändern, die der Programmierer nicht beeinflussen kann, z.B. wenn die am seriellen Port angeschlossene Maus bewegt wird:

char const volatile* COM1port = 0x000003f8; // serial in port (DOS)

Der Port wird als flatterhaft (flüchtig = volatil) gekennzeichnet, damit der Compiler tatsächlich jedesmal den Inhalt abfragt. Compiler sind nämlich auch faul und können manche Zugriffe wegoptimieren, besonders wenn sie durch Compiler-Einstellungen (Optimierung) dazu aufgefordert werden.

Konstant gute Referenzen

Manche Objekte von zusammengesetzten (nutzerdefinierten) Datentypen sind echte Speicherfresser. Ihre Übergabe als Wertparameter kann sehr teuer werden:

struct Vec3 { double x, y, z; };
 
void ausgabe(Vec3 v); // muss 3 x 8 = 24 Byte kopieren !!

Aus diesem Grund wurden extra für C++ die Referenzen erfunden, die es ermöglichen, (verdeckt) nur die Adresse einer Variable zu übergeben:

void ausgabe2(Vec3& v);  // nur 4 Byte Adresse

Nur haben Referenzparameter wie die (in C üblicherweise) mittels Zeiger übergebenen Parameter die unangenehme Eigenschaft, sich auch innerhalb der Prozedur ändern zu können. Diese Änderung wirkt dann zurück ins Hauptprogramm, wie in alten FORTRAN-Zeiten. Um entscheiden zu können, ob

  Vec3 v = { 1, 2, 3 }; 
  ausgabe2(v);
  // immer noch (1,2,3) ?

den Wert von v ändert, müsste man den Quelltext von ausgabe2() kontrollieren, und zwar jedesmal, wenn man wieder neu übersetzt; schließlich könnte einer inzwischen den Code von ausgabe2() geändert haben. Dies ist nicht praktikabel, abgesehen davon, dass Quelltext von Bibliotheken dem Nutzer auch als Geschäftsgeheimnis vorenthalten werden kann und wird. Bei der folgenden Funktion darf der Nutzer darauf vertrauen, dass der übergebene Wert nicht geändert wird:

void ausgabe3(Vec3 const& v); // freiwillige Selbstverpflichtung

Der Implementierer von ausgabe3() gibt bekannt, dass er v nicht ändern will. Eine Analogie zur Dateiorganisation des Betriebssystems ist an der Stelle vielleicht nützlich: const wirkt wie das Schreibschutzattribut (oder entzogenes Schreibrecht) für den Nutzer der const-Variablen. Referenzen entsprechen Verküpfungen (oder soft links) auf anderswo stehende Daten.

Ändert der Implementierer das konstante v unabsichtlich, wird er durch eine Fehlermeldung des Compilers deutlich darauf hingewiesen. Will er es absichtlich tun, muss er unweigerlich kriminell werden und die zugesicherte Schnittstelle verletzen, z.B. durch einen Typecast. Tut er es und liegt dann v beim Aufruf im Nur-Lese-Bereich des Programmspeichers, riskiert der Anwender den Abschuss des Programms durch das stets wachende Betriebssystem. Dieses —- ich meine nicht die DOSe und deren graphische Oberfläche — versteht an dieser Stelle keinerlei Spaß:

segmentation fault, core dump. 

Deutsche Fensterer kennen (und lieben) die allgemeine Schutzverletzung. Manchmal ist Kontrolle eben besser. (Nur: Wozu ist dann Vertrauen gut?) Bei solchen Betrachtungen darf man schon mal ethischen bis religiösen Eifer zeigen: Programme in einem Multi-Tasking-System müssen sich wie Menschen auch an soziale Regeln halten, soll das Ganze funktionieren. Und leider kann man sich nicht immer darauf verlassen. Die Kontrolle ihrerseits muss bezahlt werden, mit Zeit, Geld und Resourcen.

Konstanten und Zeiger

Eine schwierige Frage: Was bedeuten

char*
const char*
char* const
const char* const

Eine einfachere Frage: Was ist (vom Namen abgesehen) der Unterschied der nächsten Zeilen?

  char s1[] = "Hallo";
  char *s2  = "Ballo";

Antwort: Der Zeiger s2 darf versetzt werden, der Feldname s1 dagegen nicht.

  s2 = s1; // erlaubt
  s1 = s2; // Fehler! Ein Feld kann nicht umziehen!

Feldnamen können wie unveränderliche Zeiger betrachtet werden (obwohl sie es genau genommen auch nicht sind). Die Anfangswertbelegung von s2 ist darüber hinaus auch noch gefährlich:

  s2[1] = 'e';

führt zu einem Programmabsturz (segmentation fault) unter Unix, sofern s2 auf die Zeichenkette "Ballo" im Nur-Lese-Programmspeicherbereich zeigt. Der Inhalt von "Hallo" dagegen darf geändert werden.

Mit diesen Vor-Überlegungen kommen wir der Antwort auf die schwierige Frage näher. Das Schlüsselwort const kann in (für Anfänger verwirrender) Kombination mit Zeigern sogar mehrfach auftauchen:

        char *s3 = "schlecht";   // strcpy(s3, "peng"); => core dump!
  const char *s4 = "sicher";      
  char* const s5 = s1;           // zeigt immer auf den Feldanfang von s1 
  const char* const s6 = "ewig";

Zum Verstehen hilft, die Definition "von innen nach außen" zu lesen, d.h. vom definierten Namen ausgehend:

s6            const      *         char     const
s6 ist ein konstanter Zeiger auf Zeichen-Konstanten

mit dem unveränderlichen Inhalt "ewig". Durch das näherstehende const ist der Zeiger s6 eingefroren. Er darf weder woanders hinzeigen noch wandern, nicht mal wackeln. Das weiter entfernte const verbietet außerdem die Änderung des Zeichenketteninhaltes.

Die Konstant-Deklaration hat Folgen, die den Umgang zunächst erschweren und dazu verführen, const generell wegzulassen. Der Compiler weigert sich bei dieser Anweisung:

  char* s7 = s4; // Fehler: kann const char* nicht in char* umwandeln

Aus gutem Grund, denn nachfolgend könnte mit

  s7[0] = 'k';

sich jemand ins Fäustchen lachen und die (ersehnte) Schutzverletzung doch herbeiführen.

const und OOP

In der objektorientierten Programmierweise (OOP) nimmt die Bedeutung von const weiter zu, da es hier sogar in noch mehr Zusammenhängen auftauchen kann und sollte:

  • nicht veränderbare Rückgabewerte von Methoden,
  • nicht veränderbare Datenwerte in Objekten (konstante Attribute),
  • nicht veränderbare Objekte und
  • Methoden, die den Zustand von Objekten nicht ändern (wollen).

Beim Erlernen einer neuen Technik macht man selten alles sogleich richtig. Die objektorientierte Denkweise für sich genommen ist schon schwierig genug. Die Syntax für die Klassen-Definition und für die -Implementation ist neu. Dabei kommt ein Aspekt wie der korrekte Einsatz des unscheinbaren Wörtchens const leicht unter die Räder. Aus Fehlern lernt man (hoffentlich, falls überhaupt). Dazu müssen wir erstmal welche machen.

Die offene Hintertür

Schaffen Sie auch Ihr Geld zur Bank? Eröffnen Sie ein Konto und lassen Sie Ihr mühsam Erspartes einschließen:

class Konto
{
public:
  Konto(char inhabername[40], int anfangseinzahlung);
  char* inhaber() { return name;  }
  int  guthaben() { return saldo; }
  // ...
private:
  int  saldo;
  char name[40]; 
};
 
Konto vielGeld("du", 10000);

Ist Ihr Geld sicher? An den Saldo kommt niemand heran. Der liegt im privaten Bereich, auch der Inhabername ist privat. Wirklich? Probieren wir es:

  strcpy(vielGeld.inhaber(), "ich");
  std::cout << vielGeld.inhaber() << '\n'; // Jetzt ist es meins!

In der Klasse steht eine Hintertür offen. Die Methode inhaber() liefert einen Zeiger als Haken oder Diederich ins Innere ihres Geldtresors, der alles ändern darf. Ohne Fehlermeldung, ohne Hindernis. Fünf unscheinbare Buchstaben am Ergebnistyp hätten uns aufgehalten:

class Konto
{
public:
  char const* inhaber() { return name; }
  // ...
};
 
 // ...
 strcpy(vielGeld.inhaber(), "ich"); // Fehler! 

indem der Compiler beklagt, dass der von inhaber() gelieferte const char* nicht in char* umwandelbar ist, wie strcpy(ziel, quelle) das von der Zielzeichenkette erwartet.

Konstante Attribute

In den Geschäftsbedingungen der Bank sind Übereignungen möglicherweise gar nicht vorgesehen. Der Inhaber des Kontos sollte nicht wechseln dürfen, vorbeugend gegen Geldwäsche und Spendenskandale. Der Name des Inhabers wäre als konstant anzugeben:

class Konto
{
public:
  Konto(char const inhabername[40], int anfangseinzahlung)
  std::string inhaber() { return name; }
  // ...
private:
  std::string const name; 
  // ...
};

Dann ist ein std::string besser geeignet, denn wie sollte der Name mit strcpy() in ein konstantes char-Feld geschrieben werden? (Es geht, aber erfordert const-Betrügereien.) Konstante Attribute können nicht im Konstruktorrumpf zugewiesen werden. Sie erhalten ihre Werte vor dem Betreten des Konstruktors in der Initialisiererliste nach dem Doppelpunkt:

Konto::Konto(char const inhabername[40], int anfangseinzahlung)
: name(inhabername, 39), saldo(anfangseinzahlung)
{
}

Das hindert gleichzeitig den Implementierer der Klasse daran, in irgendeiner Methode (un)absichtliche Änderungen am Namen vorzunehmen.

Konstante Feldparameter

Der übernommene Inhabername wurde auch gleich noch als const vereinbart. Gewöhnen Sie sich an, Feldparameter mit Eingabedaten als konstant zu kennzeichnen! Damit wird die Gefahrenquelle gestopft, in der ersten Version der Konto-Klasse bei strcpy() die Reihenfolge der Parameter zu verwechseln:

Konto::Konto(char inhabername[40], int anfangseinzahlung)
{
  strcpy( inhabername, name ); // Peng!
  saldo = anfangseinzahlung;
}

Ich weiß, das passiert echten Programmierern nicht. Wirklich. Niemals. Nur Anfängern. Sie merken (vielleicht) beim Test zur Laufzeit, dass hier etwas schiefgelaufen ist. Vielleicht stürzt das Testprogramm ab. Möglicherweise wundern Sie sich nur, dass das Konto im Schweizer-Banken-Stil namenlos bleibt. Ungünstigstenfalls lauert der Fehler solange unentdeckt, bis er entsprechend Murphys Gesetz mit maximalem Erfolg zuschlagen kann. (Murphys Gesetz wurde schließlich auch nicht von Murphy entdeckt, sondern von einem anderen Mann gleichen Namens.)

Das kleine Wörtchen const lässt den Compiler sofort beim Übersetzen Krach schlagen. Es erspart stundenlange erfolglose, nervenaufreibende Fehlersuche und Debuggersitzungen.

(Zeitweise) konstante Objekte

Sie wollen Ihren Kontoauszug holen. Sie könnten sich dazu eine Kopie des Kontos anlegen (sic!), den Auszug drucken, das (farb)kopierte Geld abheben und anschließend verjubeln:

void auszug(Konto konto)
{
  std::cout << "Name des Inhabers: " << konto.inhaber()
            << "Guthaben in Taler: " << konto.guthaben() << '\n';
  /*
  int falschgeld = konto.abheben( konto.guthaben() );
  std::cout << "Wir geben gleich aus soviel " << falschgeld << '\n';
  */
}

Das originale Konto ist immer noch voll. Den auskommentierten Teil habe ich nur eingefügt, um zu verdeutlichen: Als Erbe von C zeigen Funktionsparameter in C++ Wertverhalten, verwirklicht durch Kopien. Referenzen müssen in C++ extra gekennzeichnet werden:

void auszug(Konto& konto)
{
  std::cout << "Name des Inhabers: " << konto.inhaber()
            << "Guthaben in Taler: " << konto.guthaben() << '\n';
  /*
  int echtesgeld = konto.abheben( konto.guthaben() );
  std::cout << "Wir geben gleich aus soviel " << echtesgeld << '\n';
  */
}

Der auskommentierte Teil würde die Kunden sehr ärgerlich machen, weil deren Geld dann wirklich verjubelt würde. Durch Abfragen sollte Ihr Kontostand nicht niedriger werden. Obwohl: Manche Kreditinstitute lassen sich das Ausdrucken noch extra bezahlen, trotzdem sie schon einen Teil der Kapitalerträge aus der Verfügung über Ihr Guthaben einstreichen. Verbitten Sie sich solchen Unfug:

void auszug(Konto const& konto)
{
   std::cout << "Name des Inhabers: " << konto.inhaber()
             << "Guthaben in Taler: " << konto.guthaben() << '\n';
}

Nanu? Wieso geht das nicht? Der Compiler meint, dass er von einem konstanten Konto weder den Inhaber noch das Guthaben abfragen kann. Daran ändert sich auch nichts, wenn die Funktionsnamen in getInhaber() und getGuthaben() umbenannt werden.

Beim Klassenentwurf legen Sie fest, was welche Methode tun soll: einzahlen(betrag) und abheben(betrag) werden den Saldo vermutlich ändern wollen. Methoden mit get…(), hat..(), has…(), ist…() oder is…() im Namen (deutsch, englisch, oder grauenhaft dänglisch) fragen nur Werte ab oder berechnen diese. Für ändernde (schreibende) Methoden wählen manche Designer gern Namen wie set…(), setze…() usw.

Den C++-Compiler rühren solche Moden nicht. Er muss den Quelltext nicht verstehen, nur übersetzen und dabei auf Korrektheit prüfen. Dazu gehört, dass auch die Konstantheit gewahrt bleibt. Notfalls dadurch, dass der Quelltext nicht übersetzt wird. Spätestens hier rudern Anfänger freiwillig zurück in den Abgrund und lassen alle konstanten Vorsätze fahren. Hauptsache, es übersetzt und wird lauffähig. Egal wie. Vergiss const. Alles für die Katz. Wirklich? Lassen Sie sich nicht so ins Bockshorn jagen. Nicht aufgeben, weiterlesen.

Konstante Methoden

Die Unterscheidung in lesende Methoden (accessors) und schreibende Methoden (mutators) ist richtig und wichtig. Wie die Funktionen benannt werden, ist eine Frage des Stils, nicht der Korrektheit. Der Entwickler der <iostream>-Bibliothek, Jerry Schwarz, hat sich nicht an set…() und get…() gehalten. Es ist klar, dass die Methode ohne Parameter nur einen Wert zurückgeben (holen) kann, während die gleichnamige Methode mit Parameter einen Wert übernimmt, der die Einstellung ändern kann:

  int stellen = std::cout.precision();  
  std::cout.precision(10);              // Wir wollen's genauer als 6 Ziffern!

Vorsilben sind in solchen Sprachen notwendig, die das Überladen von Funktionsnamen nicht beherrschen. Aus dieser überlieferten Erfahrung stammt wohl die Praxis mit den Vorsilben. In den modernen OOP-Sprachen ist sie genauso diskussionswürdig wie die (sogenannte ungarische) Simonyi-Notation. Befürworter sagen, die Vorsilben erhöhten die Lesbarkeit, indem deutlich (unmissverständlich?) geschrieben steht, welche Rolle die Methode spielt.

Kritiker finden, die Lesbarkeit sei herabgesetzt, da istFormale szsetVorsilben den szgetTextfluss istEmpfindlich setStören. istStimmts? Der dauernde Groß-/klein-Wechsel erschwert Lesen und Schreiben gleichermaßen. Sehr_lange_zusammengesetzte_Bezeichner können besser mit Unterstrichen verkoppelt werden. Zudem können Bezeichner auch lügen. Vielleicht muss eine Methode sich im Verlaufe ihrer Entwicklung Schreibzugriffen öffnen oder verschließen. Wird dann get…() / set…() überall geändert? Es ist zu befürchten, nein. Schließlich findet Software-Entwicklung fast immer unter Zeitdruck statt.

Wer garantiert dann die Korrektheit? Dies ist eine Aufgabe, die nicht fehlerbehafteten und allzu toleranzwilligen menschlichen Wesen überlassen bleiben kann. Zumal es zuverlässige Werkzeuge gibt, die const-Korrektheit überwachen. Compiler.

Woher soll aber der Compiler "wissen", dass die Funktion guthaben() oder getGuthaben() nicht ihr Guthaben verändert? Sie müssen es ihm verständlich machen. Mit den Schlüsselwort const an der richtigen Stelle. Studieren Sie z.B. auch die Definitionen der <iostream>-Klassen. Finden sie das const:

class Konto
{
public:
  std::string inhaber() const { return name;  }
  int        guthaben() const { return saldo; }
  //...
};

Das hinter der schließenden runden Klammer stehende Schlüsselwort gehört, genau wie die Typen der Parameterliste mit zum Funktionsnamen. Wird die const-Methode außerhalb der Klassenschnittstelle implementiert, muss das const wieder mit aufgeführt werden:

int Konto::guthaben() const 
{
  return saldo; 
}

Wird es vergessen, entstehen ein Compilerfehler (implementierte Funktion guthaben() ohne const in der Klassenschnittstelle Konto nicht aufgeführt) und ein Linkerfehler (Methode guthaben() const nicht implementiert). Es ist möglich, dass es zwei Methoden gibt, die sich nur im nachgestellten const unterscheiden. Ein wichtiges Beispiel ist der Indexoperator operator[].

Konstanter Zustand

Die const-Markierung von Methoden versichert den Nutzer nach außen, dass die Methoden nichts am Inhalt des Objektes ändern. Bei konstanten Objekten können nur solche const-Methoden aufgerufen werden.

Der andere Aspekt ist aber genauso wichtig: Was nach außen versichert wird, muss nach innen garantiert und kontrolliert werden. Durch const markierte Methoden dürfen keinen Attributwert des Objektes ändern, zu dem sie gehören. Zudem dürfen aus const-Methoden heraus nur ebensolche als const deklarierte Methoden aufgerufen werden, die den Zustand des Objektes nicht ändern, damit die Konstantheit gewahrt bleibt. Setzt man const ein, sind manche Verirrungen wie die folgende schnell entdeckt. Sie stammt (etwas abgewandelt und zugespitzt) aus Prüfungsarbeiten angehender Programmierer:

double Ware::getBruttopreis()
{
  return netto += netto*mehrwertsteuersatz/100.0;
}

Sie werden Ihren Augen nicht trauen. Die Ware wird jedesmal, wenn Sie aufs Preisschild schauen, um 16 Prozent teurer! Bei dieser Inflation können wir den Maastricht-Kriterien ade sagen. Halten Sie die Preise konstant:

double Ware::getBruttopreis() const
{
  return netto + netto*mehrwertsteuersatz/100.0;
}

Das Erhöhen des Nettopreises mit += wird in einer const-Methode vom Compiler beanstandet. Da kann etwas nicht stimmen. Der Fehler wird erkannt, bevor er im ausführbaren Code wirksam werden kann. Lassen Sie den Compiler mit über die Qualität des Quelltextes wachen. Der 21. Ratschlag von Scott Meyers [Meyers] lautet:

Use const whenever possible.

Leider ist es mir nicht gelungen, mich kurz zu fassen. Ich war schon in der Schule wegen langer Aufsätze gefürchtet. Ich tröste mich (und hoffentlich Sie auch) mit dem Hinweis, dass Bruce Eckel [Eckel] dem Schlüsselwort const ein ganzes Buchkapitel gewidmet hat. Sie sollten es lesen. Es ist es wert.

Quellen

Weiterführende Literatur:

  • [Stroustrup] Bjarne Stroustrup: Die C++ Programmiersprache. 3. Aufl. Addison-Wesley (1998) S.102-105.
lernen/const.txt · Zuletzt geändert: 2020-07-27 09:28 von 127.0.0.1

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki