2.1.2. Das LEDA-Regelwerk, Handle-Typen und die Klassifizierung der LEDA-Typen

Lernziele

LEDA-Regeln im Allgemeinen
Die LEDA-Regel „Definition mit Initialisierung durch Kopieren
Handle-Schema und Referenzzählung
string als handle-ähnlicher Typ
Klassifizierung des LEDA-Typen

Das LEDA-Regelwerk

Im letzten Programm werden die Strings s und t mit Initialisierung durch Kopieren definiert. Eine syntaktisch äquivalente Form für die Initialisierung von t ist die Definition

string t = "LEDA";

Diese Möglichkeit der Definition durch Kopieren gibt es für jeden LEDA-Typ. Das ist eine feste Regel, die immer gültig ist, nämlich die LEDA-Regel bzgl. Definition mit Initialisierung durch Kopieren.

Diese Regel entstammt einem Regelwerk, das beim Programmieren mit LEDA eingehalten werden muss, um syntaktisch und semantisch korrekte und effiziente LEDA-Programme zu schreiben. Die Regel, dass Definition mit Initialisierung durch Kopieren für jeden LEDA-Typ möglich ist, ist eine davon. Wir werden diese Regeln nach und nach kennen lernen. Wer mag, kann sich in Anhang B, Die goldenen LEDA-Regeln schon einmal einen Überblick verschaffen.

Handle-Typen und handle-ähnliche Typen

Was aber ist eine Kopie eines Strings? Was machen Anweisungen wie

t = s;

oder

string t(s);

genau? In Programmen, die mit vielen Stringvariablen arbeiten, tragen erfahrungsgemäß viele dieser Variablen denselben Wert, d. h. sie beziehen sich auf die gleiche Zeichenkette. Es wäre eine große Speicherplatzverschwendung, wenn jede Variable ein eigenes Objekt bezeichnen würde, d. h. einen separaten Bereich im Speicher. LEDA ist immer bemüht, eine möglichst kluge, d. h. speicher- und/oder platzsparende Implementierung eines Datentyps anzubieten, und arbeitet deshalb hier mit einem Handle-Schema und mit Referenzzählung (reference counting): Wenn die Stringvariable s der Variablen t durch die Anweisung t = s zugewiesen oder t mit einer Kopie von s initialisiert wird, so werden nicht etwa die einzelnen Zeichen vom physikalischen Speicherort von s an den Speicherort von t kopiert, sondern vielmehr „zeigen“ nun t und s auf denselben physikalischen Speicherbereich. In diesem Sinne sind sie „Griffe“ (engl.Handles“), über die auf den tatsächlichen Bereich zugegriffen wird. Den tatsächlichen String, d. h. die Folge der Zeichen, gibt es nur einmal im Speicher, und LEDA zählt mit, wie viele Handles sich darauf beziehen.

Erst wenn ein String z. B. durch einen Zugriff über [] verändert wird, werden die beiden Handles tatsächlich „entkoppelt“, d. h. es wird neuer Speicherplatz angefordert und eine Kopie angelegt. In diesem Sinne sind Strings von ihrer Implementierung her Zeigern sehr ähnlich, nur geschieht dank LEDA alles von selbst im Hintergrund; der Benutzer muss sich über die üblichen, bei der Verwendung von Zeigern und dynamischer Speicherallokation auftretenden Schwierigkeiten keine Sorgen machen.

Einige Typen von LEDA folgen diesem Handle-Schema mit Referenz-Zählung, so etwa der Typ point für Punkte in der Ebene oder der Typ integer für beliebig große ganze Zahlen. Diese Typen werden demnach als Handle-Typen bezeichnet.

Der Typ string ist aus verschiedenen implemetierungstechnischen Günden, auf die wir hier nicht eingehen wollen, kein echter Handle-Typ. Demgemäß wird die Klasse string auch nur als handle-ähnlicher Typ bezeichnet.

Klassifizierung der LEDA-Datentypen

Alle Datentypen von LEDA lassen sich hinsichtlich der Art und Weise, wie ein Objekt eines solchen Typs kopiert wird, und hinsichtlich der Frage, wie ein solches Objekt aus Teilobjekten aufgebaut ist oder in einem umfassendem Objekt vorkommen muss, in ein System einordnen, das in Anhang A, Klassifizierung der LEDA-Datentypen dargestellt ist. Diese Klassifizierung werden wir nach und nach kennen lernen. In ihr ist der Typ string als handle-ähnlicher Typ unter die Item-Typen eingeordnet, die eine Unterkategorie der primitiven Typen darstellen.

Umgehen des Handle-Schemas

Im Gegensatz zum Zuweisungsoperator legt der Operator +=, den wir im Palindromtest benutzt haben, um einen String umzudrehen, immer eine Kopie eines Strings an, ohne dass wir es merken. Dadurch wird beim Umdrehen eines Strings, wie wir es bisher implementiert haben, trotz des Handle-Schemas beim Anhängen jedes Zeichens des ursprünglichen Strings eine Kopie eines Strings angelegt. Eine unnötige Speicherverschwendung! Wie also können wir einen (langen) String speicherplatzeffizient umdrehen? Das folgende Programm beinhaltet zwei Tricks. Einen, um beliebig lange Satzpalindrome zu bilden; er ist der englischen Sprache zuzuschreiben. Und einen Programmiertrick, der die Kopierwut von += umgeht.

Filename: PalindromGenerator.C
LEDA-Benutzer ab Version 5.0: Beachten Sie, dass die Header-Dateien nun in Module (Unterverzeichnisse) partitioniert sind!
#include <LEDA/string.h>
#include <iostream>

using leda::string;
using std::cout;

string reverse_efficient(const string& s) {
  string result = s;

  int i = 0;
  int j = result.length() - 1;

  while(i < j) {
    char tmp = result[i];
    result[i] = result[j];
    result[j] = tmp;
    i++; j--;
  }
  
  return result;
}


int main() {
  string s;
  
  cin >> s;

  string s_quoted = "'" + s + "'";
  
  string r = reverse_efficient(s_quoted);

  string outstring = s_quoted + " sides reversed is " + r;

  cout << outstring << "\n";
  cout << reverse_efficient(outstring) << "\n";
}

Die Ausgabe des Programms bei der Eingabe Joachim ist

'Joachim' sides reversed is 'mihcaoJ'
'Joachim' si desrever sedis 'mihcaoJ'

und damit dürfte der erste Trick offenkundig sein.

Die Effizienz der Funktion reverse_efficient() liegt darin begründet, dass insgesamt nur eine Kopie angelegt wird, und zwar in dem Moment, in dem zum ersten Mal schreibend auf result[0] zugegriffen wird. Das Vertauschen der Zeichen geschieht innerhalb dieser Kopie durch zwei Indizes i und j. Dabei läuft i von vorne nach hinten und j von hinten nach vorne, bis sie sich in der Mitte treffen. Es werden jeweils die Zeichen an der Stelle i und j vertauscht.

Implementierung und weitere Informationen

Strings sind durch Zeichenarrays von C++ implementiert.

Wenn der String s aus n Zeichen besteht und der String t aus m Zeichen, so gilt: Alle Operationen, die ein Suche nach dem Teilstring t im String s beinhalten, benötigen Zeit O(n·m). (Die Bedeutung der O-Notation werden wir in Abschnitt 2.2.3 kennen lernen.) Ein Zugriff auf ein einzelnes Zeichen durch [] benötigt konstante Zeit. Alle anderen Operationen benötigen Zeit O(n).

Die Klasse string besitzt noch einige weitere Methoden. Auch können die oben vorgestellten Methoden teilweise mit anderen oder zusätzlichen Argumenten aufgerufen werden. Diese Möglichkeiten wollen wir hier nicht alle vorstellen. Stattdessen verweisen wir auf die entsprechende Manualseite der Klasse string

Tipps zum Gebrauch der Klasse string

  • Um Strings aus Werten anderer Typen wie etwa double aufzubauen, kann der Konstruktor

    string s(const char* format, ...);

    benutzt werden, der einen Formatstring wie die C-Funktion scanf() und dann eine Liste der in einen String zu wandelnden Objekte erwartet.

  • Die Klasse string sollte i. Allg. den C++-Strings vom Typ char[] vorgezogen werden.

  • Bei vielen zeitkritischen Ein-/Ausgabeoperationen kann der Typ char[] effizienter sein.

  • Ob die LEDA-Klasse leda::string oder die gleichnamige Klasse std::string der C++-Standardbibliothek verwendet wird, ist eine Frage des Geschmacks und der Gewohnheit. Jedoch ist, wie in Abschnitt “Der Namensraum leda” beschrieben, in jedem Fall auf den korrekten Namensraum zu achten, sonst weiß man nicht, welche Klasse man benutzt!

Übungen

Übung 1.

Wandeln Sie einen int-Wert und einen double-Wert in einen string um, ohne Funktionen der C- oder C++-Standardbibliothek zu benutzen.

Übung 2.

Schreiben Sie eine Funktion, die einen string, der nur aus Ziffern besteht, in einen int-Wert wandelt. Schreiben Sie dann eine Funktion, die testet, ob in einem string ein Zeichen mehrfach vorkommt.

Berechnen Sie mit Hilfe dieser Funktionen und obiger Funktion zum Umdrehen eines Strings die MAXNONREPMIRP-Zahl. Das ist die größte (MAX) Primzahl, die auch rückwärts gelesen eine Primzahl ist (MIRP), und in der sich keine Ziffer wiederholt (NONREP).