6
Tutorial C++ Programmierung Realisierung einer doppelt-verketteten Liste von Björn Philippe Thöne Liebe Joycer, vor kurzem entstand im Rahmen meiner Tutorentä- tigkeit (Universität Duisburg-Essen) ein Aufsatz über die Programmierung der sogenannten doppelt ver- ketteten Liste, einer wichtigen und klassischen, dynamischen Datenstruktur in der Informatik. n diesem Artikel werde ich für alle, die diese Struktur noch nicht kennen, genauer erläutern, was sich da- hinter verbirgt. Für die Umsetzung wurde C++ gewählt, da es in der heutigen Softwareentwicklung als die am häufigsten verwendete Programmiersprache angesehen werden kann. Auch wenn nahezu alle Microsoft-Produkte darin entwickelt wurden (aber auch große Teile von Unix / Linux) ;-), ist diese Sprache in Punkto Flexibilität, Portierbarkeit und Geschwindigkeit unschlagbar und besitzt zudem durch ihre zum Teil asketisch kurzen und manchmal etwas kryptisch anmutenden Quell- texte eine gewisse Anziehungskraft. Ich hoffe, dass dieser Beitrag für den Leser ausrei- chend Licht ins Dunkel der C++ Programmierung als auch in die Struktur der doppelt verketteten Liste bringt. Einzige Voraussetzung für den Genuss der folgenden Zeilen sind Grundkenntnisse in der C- und C++-Programmierung und der OOP (Objekt- orientierten Programmier)-Konzepte. Für Anregungen und Nachfragen stehe ich gerne per e-Mail unter [email protected] zur Verfü- gung. Damit stürzen wir uns jetzt in das Vergnügen. Prolog Wozu sollte man eine DLL (double-linked list) selbst programmieren? Die Frage, weshalb man Klassen zur Verwaltung doppelt verkettete Listen einmal selbst entwickeln sollte, ist zunächst auf dem folgen- den Hintergrund gut nachvollziehbar: Der Ansi-C++ Standard enthält bereits die von HP entwickelte STL (Standard Template Library) Bibliothek, die u.a. eine komfortable, umfassende und stabile Listenverwal- tung bietet und mit einer großen Zahl von Methoden ausgestattet ist. Dennoch sprechen viele Gründe dafür, zumindest einmal im Leben eines Programmierers eine solche Struktur selbst in die Hand zu nehmen: das tiefere Verständnis, wie Listen gespei- chert und Daten darin organisiert werden; die Möglichkeit, die Datenstruktur zu modifi- zieren und neue Algorithmen zur Verarbei- tung der Daten zu implementieren; der effizientere Umgang mit Listen und das Wissen, für welche Einsatzgebiete sich eine DLL lohnt; die hundertprozentige Kontrolle (und damit Verantwortung) über das Geschehen; das Interesse am Programmieren und an kom- plexeren Datenstrukturen der Informatik. Die doppelt-verkettete Liste Die doppelt-verkettete Liste ist eine wichtige Standard- Datenstruktur der Informatik und gehört natürlich zu der Gruppe der Listen. Listen sind dynamische Datenstrukturen, deren Größe zu Beginn nicht feststeht und die sich zur Laufzeit än- dern kann. Eine Liste besteht, wie der Name bereits erkennen lässt, aus einer Zahl von Daten-Elementen, die (ähnlicher einer Liste von Zeilen auf dem Papier von oben nach unten angeordnet) linear miteinander verknüpft sind. Bei der Implementierung einer Listenstruktur in eine eigene Anwendung verhält sich die Liste zumeist wie eine undurchschaubare Black Box. Nachdem die Liste einmalig angelegt wurde, können durch zur Verfügung gestellte Funktionen (in C++ Me- thoden) Elemente hinzugefügt, gelöscht, verschiede- nen Kriterien abgerufen, vertauscht und noch weitere Operationen durchgeführt werden. Häufig bieten Listen die Möglichkeit der Sortierung. Die Kenntnis der internen Organisation der Daten ist für die Nutzung der Liste im Prinzip nicht nötig. Wenn wir nicht auf fertige Lösungen zurückgreifen wollen, müssen wir uns damit jedoch auseinanderset- zen. Insbesondere die Art der Verknüpfung der Listen- elemente entscheidet über die Fähigkeiten der Liste. Hierbei kann man zwei Arten unterscheiden: Einfach-verkettete Listen Doppelt-verkettete Listen Die einfach verkettete Liste besteht aus Listenele- menten, die die folgenden Komponenten aufweisen: Daten mit beliebiger, aber bekannter Struktur; Verweis (Zeiger) auf das nächste Element der Liste. Es existiert für jede einfach-verkettete Liste zudem immer ein Verweis (im Folgenden in Hinblick auf die Implementation in C nur noch Zeiger genannt) auf das erste Element (root) der Liste. Zu Beginn ist die Liste leer, d.h. root zeigt definitions- gemäß auf NULL (nicht vorhanden bzw. definiert). Dies ermöglicht uns folgende Operationen auf einfache Weise durchzuführen: Durchlaufen der Liste von Anfang zum Ende; Anhängen und Löschen eines Elementes.

Dopplet Verkettete Liste

Embed Size (px)

Citation preview

Page 1: Dopplet Verkettete Liste

Tutorial C++ Programmierung Realisierung einer doppelt-verketteten Liste

von Björn Philippe Thöne

Liebe Joycer, vor kurzem entstand im Rahmen meiner Tutorentä-tigkeit (Universität Duisburg-Essen) ein Aufsatz über die Programmierung der sogenannten doppelt ver-ketteten Liste, einer wichtigen und klassischen, dynamischen Datenstruktur in der Informatik. n diesem Artikel werde ich für alle, die diese Struktur noch nicht kennen, genauer erläutern, was sich da-hinter verbirgt. Für die Umsetzung wurde C++ gewählt, da es in der heutigen Softwareentwicklung als die am häufigsten verwendete Programmiersprache angesehen werden kann. Auch wenn nahezu alle Microsoft-Produkte darin entwickelt wurden (aber auch große Teile von Unix / Linux) ;-), ist diese Sprache in Punkto Flexibilität, Portierbarkeit und Geschwindigkeit unschlagbar und besitzt zudem durch ihre zum Teil asketisch kurzen und manchmal etwas kryptisch anmutenden Quell-texte eine gewisse Anziehungskraft. Ich hoffe, dass dieser Beitrag für den Leser ausrei-chend Licht ins Dunkel der C++ Programmierung als auch in die Struktur der doppelt verketteten Liste bringt. Einzige Voraussetzung für den Genuss der folgenden Zeilen sind Grundkenntnisse in der C- und C++-Programmierung und der OOP (Objekt-orientierten Programmier)-Konzepte. Für Anregungen und Nachfragen stehe ich gerne per e-Mail unter [email protected] zur Verfü-gung. Damit stürzen wir uns jetzt in das Vergnügen.

Prolog Wozu sollte man eine DLL (double-linked list) selbst programmieren? Die Frage, weshalb man Klassen zur Verwaltung doppelt verkettete Listen einmal selbst entwickeln sollte, ist zunächst auf dem folgen-den Hintergrund gut nachvollziehbar: Der Ansi-C++ Standard enthält bereits die von HP entwickelte STL (Standard Template Library) Bibliothek, die u.a. eine komfortable, umfassende und stabile Listenverwal-tung bietet und mit einer großen Zahl von Methoden ausgestattet ist. Dennoch sprechen viele Gründe dafür, zumindest einmal im Leben eines Programmierers eine solche Struktur selbst in die Hand zu nehmen:

• das tiefere Verständnis, wie Listen gespei-chert und Daten darin organisiert werden;

• die Möglichkeit, die Datenstruktur zu modifi-zieren und neue Algorithmen zur Verarbei-tung der Daten zu implementieren;

• der effizientere Umgang mit Listen und das Wissen, für welche Einsatzgebiete sich eine DLL lohnt;

• die hundertprozentige Kontrolle (und damit Verantwortung) über das Geschehen;

• das Interesse am Programmieren und an kom-plexeren Datenstrukturen der Informatik.

Die doppelt-verkettete Liste Die doppelt-verkettete Liste ist eine wichtige Standard-Datenstruktur der Informatik und gehört natürlich zu der Gruppe der Listen. Listen sind dynamische Datenstrukturen, deren Größe zu Beginn nicht feststeht und die sich zur Laufzeit än-dern kann. Eine Liste besteht, wie der Name bereits erkennen lässt, aus einer Zahl von Daten-Elementen, die (ähnlicher einer Liste von Zeilen auf dem Papier von oben nach unten angeordnet) linear miteinander verknüpft sind. Bei der Implementierung einer Listenstruktur in eine eigene Anwendung verhält sich die Liste zumeist wie eine undurchschaubare Black Box. Nachdem die Liste einmalig angelegt wurde, können durch zur Verfügung gestellte Funktionen (in C++ Me-thoden) Elemente hinzugefügt, gelöscht, verschiede-nen Kriterien abgerufen, vertauscht und noch weitere Operationen durchgeführt werden. Häufig bieten Listen die Möglichkeit der Sortierung. Die Kenntnis der internen Organisation der Daten ist für die Nutzung der Liste im Prinzip nicht nötig. Wenn wir nicht auf fertige Lösungen zurückgreifen wollen, müssen wir uns damit jedoch auseinanderset-zen. Insbesondere die Art der Verknüpfung der Listen-elemente entscheidet über die Fähigkeiten der Liste. Hierbei kann man zwei Arten unterscheiden:

• Einfach-verkettete Listen • Doppelt-verkettete Listen

Die einfach verkettete Liste besteht aus Listenele-menten, die die folgenden Komponenten aufweisen:

• Daten mit beliebiger, aber bekannter Struktur; • Verweis (Zeiger) auf das nächste Element der

Liste. Es existiert für jede einfach-verkettete Liste zudem immer ein Verweis (im Folgenden in Hinblick auf die Implementation in C nur noch Zeiger genannt) auf das erste Element (root) der Liste. Zu Beginn ist die Liste leer, d.h. root zeigt definitions-gemäß auf NULL (nicht vorhanden bzw. definiert). Dies ermöglicht uns folgende Operationen auf einfache Weise durchzuführen:

• Durchlaufen der Liste von Anfang zum Ende; • Anhängen und Löschen eines Elementes.

Page 2: Dopplet Verkettete Liste

Da jedoch immer nur der Nachfolger eines Elemen-tes bekannt ist, sind einige wichtige Operationen nur aufwendig (z.B. durch das Zwischenspeichern von Zeigern) zu realisieren:

• rückwärst Durchlaufen der Liste; • Taschen, Sortieren von Elementen; • Einfügen und Löschen eines Elementes an

beliebiger Stelle. Diese Operationen sind beim zweiten, oben aufge-führten Listentyp einfacher zu realisieren. Die doppelt verkettete Liste besteht analog zu der einfach verketteten List aus Listenelementen mit den folgenden Komponenten:

• Daten mit beliebiger, aber bekannter Struktur • Zeiger auf das vorherige Element der Liste • Zeiger auf das nächste Element der Liste

Zudem sind der Anfang (root) und das Ende (end) der Liste stets bekannt. Mit dieser Struktur und deren Implementation in C++ mittels einer dafür vorgese-henen Klasse wollen wir uns nun näher beschäftigen.

Vorüberlegungen für C++ Bisher haben wir die Liste und die in ihr gespeicher-ten Daten abstrakt betrachtet. Für die jetzt folgende Umsetzung in C++ müssen wir dies konkretisieren. Zu Beginn müssen wir für die Daten eine geeignete Struktur wählen. Aus didaktischen Gründen eignet sich für eine Beispielanwendung der Datentyp String, d.h. eine Zeichenkette. Ein String ist in C jedoch kein elementarer Datentyp. Aber die Funktionen der C-Laufzeitbibliothek <string.h> verstehen unter einem String eine beliebig lange Folge von Zeichen des Typs char, die durch den Wert 0 begrenzt wird. Dem wollen wir uns anschließen. Um eine Listenstruktur mit diesem Datentyp realisie-ren zu können, verwenden wir zwei Klassen, die uns den nötigen Speicher und die Funktionen (Methoden) dynamisch zur Verfügung stellen:

• Die Listenelementklasse (list_element_string) • Die Listenklasse (list_of_string)

In der klassischen objektorientierten Programmierung (OOP), also auch in C++, werden Daten und die darauf angewendeten Algorithmen nicht mehr von-einander getrennt betrachtet. Dies hat zu Folge, dass Objekte der obigen Klassen beides in sich tragen.

Die Klasse list_element_string Die Klasse list_element_string stellt uns den nötigen Speicherplatz und die Methoden zum Zugriff auf wichtige Informationen zur Verfügung.

Da die Variablen von Zugriffen von außen geschützt sind, kann nur mit Hilfe der Methoden der Klasse auf die Objektinhalte zugegriffen werden. Kommen wir nun zum ersten Quelltextabschnitt, in dem wir die Klasse definieren und die Methodenrümpfe aufführen: class list_element_string { char *data; list_element_string *previous; list_element_string *next; public: list_element_string(char *string);// Konstruktor list_element_string(); // Destruktor void set_previous(list_element_string *pointer); void set_next (list_element_string *pointer); list_element_string *get_next(); list_element_string *get_previous(); void set_data(char *string); char *get_data(); }

Betrachten wir die Klassendefinition schrittweise:

• Entsprechend der obigen Ausführungen über doppelt verkettete Listen zeigt data auf unseren String, next und previous zeigen auf das Vor-gänger- bzw. Nachfolgerelement.

• Konstruktor: Beim Anlegen eines neuen Ob-jekts der Klasse list_element_string mittels new muss ein Zeiger auf die neu anzulegenden Zei-chenkette - unsere Daten - übergeben werden.

• Destruktor: Wird das Element gelöscht, muss der reservierte Speicher freigegeben werden.

Die folgenden, von außen zugreifbaren (public) Me-thoden gewähren uns Zugriff auf die klasseninter-nen Daten der Objekte: • get_previous() liefert einen Zeiger auf das

nächste Element, set_previous() setzt den Zei-ger auf das vorhergehende Element.

• get_data() liefert einen Zeiger auf den gespei-cherten String, set_data() legt den neu überge-benen String unter data im Element.

Alle Methoden werden später von unserer zweiten Klasse list_of_string, die die Listenverwaltung über-nimmt, aufgerufen. Der vollständige Quelltext der Me-thoden sieht wie folgt aus (mit kurzen Erläuterungen): list_element_string(char *string) { previous = NULL; next = NULL; data = (char*) malloc((strlen(string)+1)); if (data != NULL) strcpy(data,string); }

Der Konstruktor allokiert Speicher der Länge des Strings + 1 für die Abschlusskennung des Strings (das 0-Byte). Wenn der Speichervorgang erfolgreich war, dann wird der Quellstring in den Datenbereich des Listenelements kopiert. Die Zeiger werden mit NULL initialisiert. Das ist enorm wichtig! Viele schwer auffind-bare Fehler beruhen auf „wilden“ Zeigern!

Page 3: Dopplet Verkettete Liste

~list_element_string() { if (data != NULL) free(data); }

Der Destruktor gibt den allokierten Speicher für den String wieder frei. Die Freigabe des übrigen Spei-chers für das Objekt übernimmt C++. void set_previous(list_element_string *pointer) { previous = pointer; }

Die Methode set_previous() setzt den internen Zeiger zum nächsten Element auf den übergebenen Zeiger. void set_next(list_element_string *pointer) { next = pointer; }

set_next() verhält sich analog zu set_previous(). void set_data(char *string) { if (data != NULL) free(data); data = (char *) malloc( (strlen(string)+1) ); if (data != NULL) strcpy(data, string); }

set_data() überprüft zunächst, ob bereits Daten vor-liegen. Diese werden dann zuvor frei gegeben. Sollte dies vergessen werden, addieren sich im Laufe der Zeit immer mehr Speicherbereiche auf, die nicht mehr zur Laufzeit freigegeben werden können. char *get_data() { return(data); }

get_data() übergibt den lokalen Zeiger auf den Da-tenstring an die aufrufende Funktion / Methode. list_element_string *get_next() { return(next); }

get_next() übergibt den lokalen Zeiger auf das nächs-te Listenelement (NULL falls nicht vorhanden). list_element_string *get_previous() { return(previous); }

get_previous() übergibt den lokalen Zeiger auf das vorige Listenelement (NULL falls nicht vorhanden). Damit wäre die Klasse list_element_string abgehan-delt. Der interessantere Teil - die Organisation der Listenelemente - folgt im nächsten Abschnitt.

Die Klasse list_of_string Die Klasse list_of_string leistet die eigentliche Arbeit. Sie organisiert die Listenelemente und stellt einen Satz von Methoden zur Verfügung, die es dem An-wender erlauben, komfortabel mit der Liste umgehen

zu können, ohne sich um die Verwaltung zu bemühen. Richten wir wieder einen Blick auf die Klassendefinition und die Methoden. Es werden im Hinblick auf die Kom-plexität des Beispiels nur die wichtigsten Operationen einer Listenverwaltung integriert. Auf Sortieralgorith-men wie z.B. den Quick-Sort wird bewusst verzichtet. Aber dem engagierten Leser dürfte es nicht schwer fallen, basierend auf den folgenden Methoden fehlende Operationen zu ergänzen: class list_of_string { int list_size; list_element_string *root; list_element_string *end; public: list_of_string() // Konstruktor ~list_of_string() // Destruktor void add(char *string); int erase(list_element_string *element); void erase_first(); void erase_last(); list_element_string *get_root(); list_element_string *get_end(); list_element_string *get_element_by_number (int number); list_element_string *get_element_by_data (char *string); int get_number_by_data(char *string); int get_size(); }

Kommen wir zur Beschreibung der Methoden:

• Variablen: Jede neu angelegte Liste bedarf der Kenntnis des ersten und des letzten Elements (s.o.). Die Listengröße d.h. die Anzahl der Ele-mente der Liste, ist ein häufig benötigter Wert. Anstelle des langsamen Durchzählens kann man daher stattdessen besser einen Zähler list_size mitlaufen lassen.

• Der Konstruktor initalisiert die Liste und setzt die Startwerte fest.

• Der Destruktor muss alle Listenelemente lö-schen, um so den gesamten verwendeten Speicherplatz freizugeben.

• Die Methode add() fügt ein Element in die Liste ein, dessen Daten durch den Zeiger string auf eine Zeichenkette gekennzeichnet ist.

• Die Methode erase() löscht das Element, das mittels eines Zeigers übergeben wird. Die Me-thoden erase_first() und erase_last() sind ein-fache, davon abgeleitete Sonderfälle.

• get_root() liefert einen Zeiger auf den Anfang der Liste zurück, get_end() entsprechend das Ende der Liste.

• get_element_by_number() liefert einen Zeiger auf das durch number referenzierte Element.

• get_element_by_data() sucht in der Liste nach dem übergebenen String. Ist er identisch mit dem Inhalt eines Listenelements, wird die Nummer des Elements in der Liste übergeben.

• get_size() liefert die Anzahl der Listenelemente Mit Hilfe dieser Methoden lässt sich eine Liste von Strings komfortabel verwalten. Zwei kurze Beispiele zur Demonstration werden wir in den beiden folgenden Abschnitten kennen lernen.

Page 4: Dopplet Verkettete Liste

Zunächst der Quelltext der Methoden mit ergänzen-den Erläuterungen (spaltenbedingte Umbrüche sind zu entfernen): list_of_string() { list_size = 0; root = NULL; end = NULL; }

Der Konstruktor initialisiert die Zeiger und Werte mit NULL bzw. 0. ~list_of_string() { list_element_string *walk_through_list, *element_to_delete; walk_through_list = root; // Deleting all elements of the list while(walk_through_list!=NULL) { element_to_delete = walk_through_list; walk_through_list = walk_through_list->get_next(); delete element_to_delete; }

Der Destruktor löscht nacheinander alle Elemente der Liste. Die Hilfsvariable element_to_delete spei-chert den Zeiger auf das zu löschende Element zwi-schen, da vor dem eigentlichen Löschvorgang das nachfolgende Element noch bestimmt werden muss. void add(char *string) { list_element_string *new_element; if(root == NULL) // first element of list { root = new list_element_string(string); end = root; if (root != NULL) list_size++; } else { if (end->get_previous() == NULL) { new_element = new list_element_string(string); new_element->set_previous(root); // previous element is start new_element->set_next(NULL); // next element is NULL root->set_next(new_element); // end of list is the new element end = new_element; list_size++; } else { new_element = new list_element_string(string); new_element->set_previous(end); // previous element is start new_element->set_next(NULL); // next element is NULL end->set_next(new_element); end = new_element; // end of list is the new element list_size++; } } }

Zur korrekten Zeiger-Zuweisung der Zeiger root und end muss die obige Methode zum Hinzufügen von Elementen Fallunterscheidungen für die folgenden Situationen aufweisen:

• Die Liste ist leer d.h. root = NULL • Nur ein Element ist in der Liste, d.h. root und

end sind identisch • Die Liste enthält zwei oder mehr Elemente

int erase(list_element_string *element) { if (element == NULL) return(0); // ERROR if (element == end) // if it is the last element { if (element == root) // root = end => only one element left { root = NULL; end = NULL; delete element; list_size--; } else // deleting the last element { end = element->get_previous(); end->set_next(NULL); delete element; list_size--; } } else // not the last element { if (element == root) // if it is the first element { root = element->get_next(); delete element; list_size--; } else // out of the middle part of list { element->get_next()-> set_previous(element->get_previous()); element->get_previous()-> set_next(element->get_next()); delete element; list_size--; } } return(-1); // ERASE OK }

Ähnlich verhält es sich bei der erase()-Methode. Auch hier muss in Abhängigkeit von der Position und der Anzahl der vorhandenen Listenelemente unterschied-lich verfahren werden. void erase_first() { erase(root); }

Hier wird auf die bereits definierte Methode erase() zugegriffen und der nur lokal bekannte Zeiger root übergeben. Alternativ kann man außerhalb der Klasse listobject->erase(listobject->get_root()); aufrufen kön-nen (list_of_string *listobj = new list_of_string(string)) void erase_last() { erase(end); }

erase_last verhält sich analog zu erase_first(); list_element_string *get_root() { return(root); }

Der lokale Zeiger root wird als Argument übergeben. list_element_string *get_end() { return(end); }

Der lokale Zeiger end wird als Argument übergeben. list_element_string *get_element_by_number(int number) { list_element_string *walk_through_list; int count;

Page 5: Dopplet Verkettete Liste

count = 1; walk_through_list = root; while ((walk_through_list != NULL) && (count < number)) { walk_through_list = walk_through_list-> get_next(); count++; } return(walk_through_list); }

Die while-Schleife wird so lange durchlaufen, bis der Zeiger walk_through_list auf dem Element mit der Nummer number steht. Außerdem muss die Funktion gegen zu hohe Werte von number gesichert sein. In diesem Fall liefert die Funktion NULL. Alternativ hätte man eine if-Abfrage mit anschließen-der for-Schleife einsetzen können. list_element_string *get_element_by_data(char *string) { list_element_string *walk_through_list; walk_through_list = root; while (walk_through_list != NULL) { if (!strcmp(walk_through_list-> get_data(),string)) return(walk_through_list); walk_through_list = walk_through_list-> get_next(); } return(NULL); }

Die while-Schleife wird so oft durchlaufen, bis der Vergleich der Zeichenketten eine Übereinstimmung zeigt, was zur Übergabe des Zeigers auf das gefun-dene Listenelement führt. int get_number_by_data(char *string) { list_element_string *walk_through_list; int count; walk_through_list = root; count = 0; while (walk_through_list != NULL) { count++; if (!strcmp(walk_through_list-> get_data(),string)) return(count); walk_through_list = walk_through_list-> get_next(); } return(0); }

Hier ist der Schleifenaufbau vergleichbar mit der Methode get_number_by_data(), nur das der zurück-gegebene Wert die Anzahl und nicht ein Elementzei-ger ist. int get_size() { return(list_size); }

oder: int get_size() { unsigned int count; list_element_string *walk_through_list; count = 0; walk_through_list = start; while(walk_through_list!=NULL) { count++;

walk_through_list = walk_through_list-> get_next_element(); } return(count);*/ }

Die schnelle get_szie()-Methode liefert nur den lokalen Zähler list_size zurück. Die gleichwertige, wohl aber bedeutend langsamere Methode zählt die Liste durch. Sie wird hier nur als Beispiel, wie man ganz einfach eine Liste durchlaufen kann, aufgeführt. Abschließend können diese Klassendefinitionen z.B. noch in class_list_of_string.h ausgelagert werden, so dass man sie mit #include in einen beliebigen C++-Quelltext einbinden kann.

Beispiel I : Listendemo.cpp Das folgende Beispielprogramm ist weitestgehend selbsterklärend und testet die zuvor definierten Klassen auf Fehlerfreiheit mit verschiedenen Listenmanipulatio-nen. Kern des Programm ist eine generierte Liste mit sieben Elementen, die nach und nach gelöscht oder bearbeitet werden. Auch in diesem C++-Quelltextbeispiel sind die unge-wollten Zeilenumbrüche – verursacht durch die be-grenzte Spaltenbreite – zu entfernen. #include <cstring> #include <iostream> #include "class_list_of_string.h" using namespace std; void ZeigeGanzeListe (list_of_string *liste) { int i; list_element_string *element; cout << endl << "Anzahl der Listenelemente : " << liste->get_size() << endl << endl; if (liste->get_root() != NULL) cout << "Das erste Element der Liste heisst : " << liste->get_root()->get_data() << endl; if (liste->get_end() != NULL) cout << "Das letzte Element der Liste heisst : " << liste->get_end()->get_data() << endl; cout << endl << "Anzeigen aller Elemente (Methode 1):" << endl; cout << "[ANFANG] :"; element = liste->get_root(); while(element != NULL) { cout << " " << element->get_data() << " :"; element=element->get_next(); } cout << " [ENDE]" << endl; cout << "Anzeigen aller Elemente (Methode 2):" << endl; cout << "[ANFANG] :"; for (i=1; i<=liste->get_size(); i++) cout << " " << liste-> get_element_by_number(i)->get_data() << " :"; cout << " [ENDE]" << endl; cout << endl <<"--------------------------------------- -----------------------------------------"; } int main(void) {

Page 6: Dopplet Verkettete Liste

list_of_string *list; cout << "Lege neue Liste an." << endl; list = new list_of_string(); char Suchstring[]="E 4"; ZeigeGanzeListe(list); cout << "Fuege 7 Elemente hinzu." << endl; list->add("E 1"); list->add("E 2"); list->add("E 3"); list->add("E 4"); list->add("E 5"); list->add("E 6"); list->add("E 7"); ZeigeGanzeListe(list); cout << "Suche das Element mit Inhalt '" << Suchstring <<"' und gib es aus:" << endl; cout << "Verweis auf Element Nr. " << list->get_number_by_data(Suchstring) << " : "; if(list->get_element_by_data(Suchstring) != NULL) { cout << list-> get_element_by_data(Suchstring)-> get_data() << endl; } else cout << "Element " << Suchstring << " wurde nicht gefunden."; cout << "------------------------------------- -------------------------------------------"; cout << "Loesche das letze Element." << endl; list->erase(list->get_end()); ZeigeGanzeListe(list); cout << "Loesche das erste Element." << endl; list->erase(list->get_root()); ZeigeGanzeListe(list); cout << "Loesche nun das neue zweite Element." << endl; list->erase(list->get_element_by_number(2)); ZeigeGanzeListe(list); cout << "Benenne Element Nr. 2 um in 'umbenannt'." << endl; list->get_element_by_number(2)-> set_data("umbenannt"); ZeigeGanzeListe(list); cout << "Benenne Element 'umbenannt' um in 'benannt'." << endl; list->get_element_by_data("umbenannt")-> set_data("benannt"); ZeigeGanzeListe(list); cout << "Loesche erstes Element." << endl; list->erase_first(); ZeigeGanzeListe(list); cout << "Loesche letztes Element." << endl; list->erase_last(); ZeigeGanzeListe(list); cout << "\nLoesche Liste." << endl; delete list; cout << "Bitte Eingabetaste druecken"; cin.get(); return(0); }

Beispiel II : Datei_in_Liste.cpp Zu guter Letzt noch ein kleines Anwendungsbeispiel, wie es in vereinfachter Form z.B. in einem Editor vorkommen könnte: #include <cstdio> #include <iostream> #include "class_list_of_string.h" using namespace std; FILE *datei; char buffer[100000]; list_of_string *document; int main() { int i; long laenge1,laenge2; document = new list_of_string();

if( (datei = fopen("text.txt","r")) == NULL) cout << "Datei nicht erfolgreich geoeffnet." << endl; else { cout << "Datei erfolgreich geoeffnet." << endl; laenge2 = 0; fseek(datei, 0L, SEEK_END); /* Position 0 vom Ende */ laenge2 = ftell(datei); fseek(datei, 0L, SEEK_SET); /* Anfang */ while(!feof(datei)) { fgets(buffer,100000,datei); document->add(buffer); } fclose (datei); cout << "Datei erfolgreich geschlossen." << endl; } cout << "Dokumentinhalt:" << endl << endl; laenge1 = 0; for (i=1;i<=document->get_size();i++) { laenge1+=strlen(document-> get_element_by_number(i)->get_data()); cout << document->get_element_by_number(i)-> get_data(); } cout << endl << "Anzahl der Zeilen :" << document->get_size() << endl; cout << endl << "Anzahl der Zeichen laut Liste :" << laenge1 << endl; cout << endl << "Anzahl der Zeichen laut Datei :" << laenge2 << " (incl. Carriage Return / Linefeed)" # << endl; delete document; cin.get(); return(0); }

Es wird die Datei text.txt im ASCII-Modus geöffnet und sukzessive jede Zeile in jeweils ein Listenelement über-tragen. Die Datei wird geschlossen und die Liste wird nun elementweise d.h. Zeile für Zeile auf dem Bild-schirm ausgegeben. Abschließend wird die Dateilänge mit der Länge der aufsummierten Längen der Listenelemente verglei-chend ausgegeben. Wundern Sie sich nicht über die (mögliche) Diskrepanz der beiden Werte. Je nach Betriebsystem und Compi-ler schließt eine Zeile in einer Datei mit einer Kombina-tion aus Linefeed und Carriage Return ab, während in der Liste nur ein Zeichen pro Zeile gespeichert wird. Wenn ASCII-Texte eingelesen werden, gehen bei die-ser Methode in keinem Fall Daten verloren.

Schlusswort Ich hoffe die oben aufgeführten Beispiele bringen das Thema in verständlicher Form nahe. Sollten sich in das Tutorial trotz intensiver Suche noch Fehler eingeschlichen haben oder Fragen zur Imple-mentierung in verschiedenen C++-Umgebungen offen geblieben sein, freue ich mich über eine Nachricht. Wer die Quelltexte nicht von Hand übertragen möchte, der kann unter www.bjoern-thoene.de/cpp_listen.zip die obigen Demodateien als Quelltexte und ausführba-re *.EXE Dateien downloaden.