Sie sind auf Seite 1von 124

Programmiersprache C++

3. Jahrgang

Dipl.-Ing. Heinrich Dornmayr MBA


Zeiger
• Ein Zeiger beinhaltet nicht einen Variablenwert,
sondern zeigt nur auf die Adresse, wo der
Variablenwert (das Objekt) hinterlegt ist.

Z.B.: char c = 'a';


char* p = &c; // p (Zeiger auf char)
// enthält die Adresse von c

p: &c * … Inhaltsoperator
(Dereferenzierungs-
c: 'a' operator)

& … Adressoperator
Zeiger
• Weitere Beispiele:

int* pi; // Zeiger auf int


char** ppc; // Zeiger auf Zeiger auf char
int* ap[15]; // Feld von 15 Zeigern auf int
int* f(char*); // Funktion mit einem char*-
// Argument; liefert einen Zeiger
// auf int
Zeiger: Beispiel
int x = 1, y = 2; /* zwei initialisierte integer-Variablen */
int* ip; /* nicht-initialisierte Zeigervariable auf integer */
ip = &x; /* Bildung eines Adresswertes und Zuweisung */
y = *ip; /* Bildung eines Zeigerinhalts u. Zuweisung an Variable*/
x = ip; /* Fehler!!!*/
*ip = 3; /* Änderung eines Zeigerinhalts per Zuweisung */

• Bemerkung:
– Ein Zeiger ist eine Variable.
– Sein Wert muss irgendwo im Speicher gespeichert
werden.
Zeiger: Beispiel
• Annahme:
– Der Wert von x ist im Speicher an der Adresse 100
gespeichert,
– y an der Adresse 200 und
– ip an der Adresse 1000.
Zeiger: Beispiel
Zeiger: Beispiel
Zeiger: Beispiel
Zeiger: Beispiel
Zeiger: Beispiel
-> … Strukturzeiger
Zeiger dereferenzieren

• Neben den typischen Zeigeroperatoren (*,->,&)


können auch arithmetische Operatoren ( ++ -- + -)
und der Vergleichsoperator (==) auf Zeiger
angewendet werden.
• Addiert man aber 2 zu einem Zeiger, so wird
dieser nicht um 2 Byte erhöht, sondern um 2 mal
die Länge des Datentyps, auf den der Zeiger
definiert ist ("Zeigerarithmetik").
• Da es normal nicht sinnvoll ist, einen Zeiger quer
durch den Hauptspeicher zu addieren, wird diese
Methode bei Arrays angewendet.
Zeiger
• Navigieren in Feldern:

void fi(char v[ ])
{
for (int i=0; v[i]!=0; i++) cout << v[i];
}

äquivalent mit Zeiger:

void fp(char v[ ])
{
for (char* p = v; *p!=0; p++) cout << *p;
}
Zeiger
• Anwendungsbeispiel voriger Funktionen:

int main()
{

char f[]={'Z','e','i','g','e','r','\0'};

fi(f);
cout << endl;
fp(f);
cout << endl << endl;
}
Zeiger
• Ausgabe:
Zeiger
• Warum also Zeiger?
– Das Übergeben von Daten wird stark beschleunigt, da
nur eine Adresse anstatt der komplette Datensatz
übergeben wird.
– Es lassen sich auf schnelle Art und Weise Felder
verwalten und manipulieren.
– Auch viele Funktionen liefern einen Zeiger als
Funktionswert zurück (Rückgabe von mehreren
Werten).
– Behandlung komplexer Datenstrukturen wie z.B.
verkettete Listen.
Zeiger: Übungsbeispiel
• Schreibe ein Unterprogramm, welches "Werte"
vertauscht (Swap-Funktion selbstgemacht):

– Version 1: für Array-Werte


Aufruf:tausche_A (feld,i,j)

– Version 2: für integer-Variablen


Aufruf:tausche_I (a,b)

– Version 3: mit Zeiger auf integer-Variablen Aufruf:


tausche_P (&a,&b)
Zeiger: Übungsbeispiel (C-Stil)
void tausche_P(int *px,int *py) int main()
{ {
int temp; int a = 1, b = 2;
temp = *px; tausche_P(&a,&b);
*px = *py; cout << "a=" << a;
*py = temp; cout << "b=" << b;
} cout << "\n\n";
return 0;
}
Referenzen
• Die Sprache C kennt bei Funktionen nur die
Wertübergabe (call by value).
• Soll ein Unterprogramm eine Variable x des
Hauptprogramms verändern, muss ihm die
Adresse &x in Form eines Zeigers übergeben
werden, und das Unterprogramm muss den
Zeiger bei jedem Zugriff dereferenzieren.
• Dies funktioniert auch unter C++ ist aber
generell umständlich und fehleranfällig.
Referenzen
• C++ hat deshalb den Sprachumfang um
Referenzen erweitert.
• In technischer Hinsicht sind Referenzen nichts
anderes als Zeiger auf Variablen.
• Es gibt aber einige wesentliche Unterschiede:
– eine Referenz muss initialisiert (Wert zuweisen) werden,
– die Adresse, auf die eine Referenz zeigt, kann nicht
verändert werden,
– Referenzen werden mit der gleichen Syntax eingesetzt
wie „echte“ Variablen,
– es gibt kein Analogon zum Nullzeiger.
Referenzen

int i = 1;
int& r = i; // r und i beziehen sich auf denselben
int
int x = r; // x = 1

r = 2; // i = 2
Referenzen
• Referenzen werden hauptsächlich verwendet,
um Argumente an Funktionen zu übergeben
(call-by-reference).
• Gegenüber der Wertübergabe hat das den
großen Vorteile, dass die übergebenen
Variablen verändert werden können.
• Gegenüber der Verwendung von Zeigern ist
neben der einfacheren Syntax der Hauptvorteil,
dass die Existenz der übergebenen Variable
garantiert ist. (Ein Zeiger könnte dagegen
nirgendwohin oder auf einen bereits wieder
freigegebenen Speicherbereich zeigen.)
Referenzen
• Wo das Referenzierungszeichen (&) steht bleibt
dem Benutzer überlassen.
• Alle folgenden Zeilen haben also dieselbe
Bedeutung:

void tausche(int &rx, int &ry)


void tausche(int & rx, int & ry)
void tausche(int& rx, int& ry)
void tausche(int&rx, int&ry)
Referenzen: Übungsbeispiel
void tausche_R(int &rx,int &ry) int main()
{ {
int temp; int a = 1, b = 2;
temp = rx; tausche_R(a,b);
rx = ry; cout << "a=" << a;
ry = temp; cout << "b=" << b;
} cout << "\n\n";
return 0;
}
Strukturen
• Ein Feld ist die Zusammenfassung von
Elementen desselben Typs.
• Eine Struktur (struct) ist die Zusammenfassung
von verschiedenen Typen.
struct: einfaches Beispiel
struct adress
{
char nam[20];
char vornam[20];
int alter;
};
struct: einfaches Beispiel
• Dies definiert einen neuen Typ adress.
• Man beachte den Strichpunkt am Ende!!!
– Dies ist eine der wenigen Stellen in C++, an der ein
Strichpunkt nach einer geschwungenen Klammer
notwendig ist, daher wird er häufig vergessen!
• Variablen vom Typ adress können genau wie
andere Variablen deklariert werden.
• Auf die einzelnen Elemente kann durch den
Punktoperator (.) zugegriffen werden:
struct: einfaches Beispiel
adress IK;
adress AK = {{'A','d','l','e','r','\0'},{'K','u','r','t','\0'},{20}};
IK = AK;
AK.vornam[4] = 'i';
AK.vornam[5] = '\0';
IK.alter = 22;
cout << "Der Vorname von AK ist: " << AK.vornam << endl;
cout << "AK ist " << AK.alter << " Jahre alt.\n";
cout << "Der Vorname von IK ist: " << IK.vornam << endl;
cout << "IK ist " << IK.alter << " Jahre alt.\n\n";
struct: einfaches Beispiel
struct: einfaches Beispiel
• In C++ können Instanzen von Strukturen ohne
das zusätzliche Schlüsselwort struct erzeugt
werden.
• C++ erzeugt bei der Deklaration einer Struktur
intern einen neuen Datentyp, dessen Namen
identisch zum Namen der Struktur ist.

z.B.: adress IK;


oder
struct adress IK;
struct: Zeiger als Strukturelemente
struct adresse
{
char* name;
char* vorname;
char* strasse;
int nummer;
int PLZ;
char* ort;
};
struct: Zeiger als Strukturelemente
adresse RW;
RW.name = "Wahnsinn";
RW.vorname = "Rainer";
cout << "RW steht fuer: " << RW.vorname << " " <<
RW.name << endl << endl;
struct: Zeiger als Strukturelemente
struct: Feld vom Typ "struct"
adresse freunde[10];
freunde[0].name = "Bolika";
freunde[1].name = "Wahnsinn";
freunde[2].name = "Schweiss";
freunde[0].vorname = "Anna";
freunde[1].vorname = "Rainer";
freunde[2].vorname = "Axel";
cout << "Meine besten Freunde sind: \n";
cout << freunde[0].vorname << " " << freunde[0].name << endl;
cout << freunde[1].vorname << " " << freunde[1].name << endl;
cout << freunde[2].vorname << " " << freunde[2].name << endl << endl;
struct: Feld vom Typ "struct"
struct: Übungsbeispiel
• Adressverwaltung:

– Namen und Adressen müssen eingegeben werden


können.
– Namen und Adressen aller eingegebenen Personen
müssen auch wieder ausgegeben werden können.
– Verwendung von <string> (siehe mögliche Lösung).

– Das Löschen von Einträgen muss nicht möglich sein.


– Schreibe das Programm noch ohne Funktionen.
struct: Übungsbeispiel
struct: Übungsbeispiel
• Mögliche Lösung:

#include <string>

struct adresse
{
string name;
string vorname;
int PLZ;
string ort;
};
struct: Übungsbeispiel
• Mögliche Lösung:

adresse freunde[99];

for (i=0;i<anzahl;i++)
{
cout << freunde[i].vorname << " ";
cout << freunde[i].name << endl;
cout << freunde[i].PLZ << " ";
cout << freunde[i].ort << endl << endl;
}
struct: verschachtelte Strukturen
• Definition der Strukturen:

struct adresse
{
int PLZ;
char* ort;
};

struct person
{
char* name;
char* vorname;
adresse wohnen;
int alter;
};
struct: verschachtelte Strukturen
• Deklaration der Strukturen lehrer und einem
Feld von Strukturen schueler[]:

person lehrer, schueler[20];


struct: verschachtelte Strukturen
• Initialisierung der Strukturen:
lehrer.name = "Dornmayr";
lehrer.vorname = "Heinrich";
lehrer.wohnen.PLZ = 1180;
lehrer.wohnen.ort = "Wien";
lehrer.alter = 43;

schueler[0].name = "Meier";
schueler[0].vorname = "Hans";
schueler[0].wohnen.PLZ = 1160;
schueler[0].wohnen.ort = "Wien";
schueler[0].alter = 15;
struct: verschachtelte Strukturen
• Ansprechen von Strukturelementen:

cout << "Der Lehrer wohnt in ";


cout << lehrer.wohnen.ort << ".\n\n";
cout << "Der Schueler ist ";
cout << schueler[0].alter << " Jahre alt.\n\n\n\n";
struct: verschachtelte Strukturen
struct: Zeiger auf Strukturen
• Auf Strukturelemente wird häufig über Zeiger
mittels des -> Operators (Strukturzeiger
dereferenzieren) zugegriffen.
• Zwei Zugriffsvarianten:
p sei ein Zeiger auf lehrer
(*p).name
oder
p->name
struct: Zeiger auf Strukturen
• Beispiel:

struct koordinate
{
int x,y;
};
struct: Zeiger auf Strukturen
• Direkte Verwendung einer Struktur:

koordinate oben;
oben.x = 0;
oben.y = 15;

cout << "Die Koordinaten von OBEN sind: ";


cout << oben.x << " " << oben.y << "\n\n";
struct: Zeiger auf Strukturen
• Zeiger auf eine Struktur:

koordinate* p;
p = &oben; // Zeiger muß initialisiert werden, da er zunächst nirgends
hinzeigt!!!

(*p).x = 3; // oder: p->x


(*p).y = 7; // oder: p->y

cout << "Die Koordinaten von P sind: ";


cout << p->x << " " << p->y << "\n\n";
struct: Zeiger auf Strukturen
struct: … als Argumente und
Ergebnisse einer Funktion

struct adresse
{
char* name;
char* vorname;
int PLZ;
char* ort;
};

adresse aktuelle = {"Huber","Kurti",1170,"Wien"};


struct: … als Argumente und
Ergebnisse einer Funktion

adresse aktuelle_setzen(adresse naechste)


{
adresse vorher = aktuelle;
aktuelle = naechste;
return vorher;
}
struct: … als Argumente und
Ergebnisse einer Funktion

adresse neu = {"Sorglos","Susi",1160,"Wien"};

cout << aktuelle.name << endl;


cout << neu.name << endl << endl;

adresse alte = aktuelle_setzen(neu);

cout << aktuelle.name << endl;


cout << alte.name << endl << endl;
struct: … als Argumente und
Ergebnisse einer Funktion
Strukturen
• Eine Struktur ist eine einfache Form einer
Klasse (ein Sonderfall).
• Eine Klasse ist eine Verbindung von Daten und
Funktionen.
Unions
• Eine union fasst ähnlich wie ein struct mehrere
Elemente unterschiedlichen Typs zu einem
neuen Typ zusammen:
union MyUnion
{
int i;
char c;
};

• Abgesehen von einem anderen Schlüsselwort,


bestehen zwischen Unions und Strukturen keine
syntaktischen Unterschiede.
Unions
• Der Unterschied liegt in der Art und Weise, wie
mit dem Speicherplatz der Daten umgegangen
wird.
• Bei einer Union belegen alle Elemente
denselben Speicherplatz!
• Unions kann man zum Platzsparen bei
Datenstrukturen benutzen, in denen zwei
Elemente nie gleichzeitig sinnvolle Werte haben
können.
• Eine union hat die Größe des größten Elements.
Speicherplatzverwaltung
• Es gibt drei fundamentale Arten, Speicherplatz
in C++ zu verwenden:
– Statischer Speicherplatz,
in dem ein Objekt vom Linker für die Dauer des Programms
angelegt wird (z.B. globale Variablen).
– Automatischer Speicherplatz / Stack,
in dem Funktionsargumente und lokale Variablen angelegt
werden. Diese Art von Speicher wird automatisch erzeugt
und wieder zerstört (daher die Bezeichnung).
– Freispeicher / Heap ("Haufen")
aus dem Speicherplatz für Objekte explizit (mit new)
angefordert werden kann und wo ein Programm diesen
Speicherplatz, wenn er nicht mehr gebraucht wird, (mit
delete) wieder freigeben kann.
Dynamische Speicherverwaltung
• Bisher waren Datenobjekte (Variable), die in
einem Programm definiert werden, sind
statische Objekte.
• Das heißt, dass ihre Anzahl und Größe (also der
Speicherbedarf) zum Zeitpunkt der Übersetzung
festliegen muss.
• Bei manchen Algorithmen ergibt sich das
Problem, dass die Größe eines Objektes (z. B.
Arrays) bzw. die Anzahl der Objekte erst zur
Programmlaufzeit angegeben werden kann.
Dynamische Speicherverwaltung
• Bisherige Notlösung: Definition statisch
angelegter Objekte mit Maximalgröße.
• Nachteile:
– Zum einen wird hier oft Speicher verschwendet
– und zum anderen reich mitunter der verfügbare
Speicher doch nicht aus.
• Die Lösung liegt in einer dynamischen
Speicherallokation bei Auftreten des
Platzbedarfs, d. h. Definition der Objekte zur
Laufzeit (→ dynamisch).
Dynamische Speicherverwaltung
• Dynamisch allokierte Objekte werden nicht über
einen Namen, sondern nur über ihre Adresse
angesprochen.
• Diese Adresse entsteht bei der Speicherbelegung
("Allokation") und wird normalerweise einer
Zeiger-Variablen zugewiesen.
Dynamische Speicherverwaltung
• Eine statische Variable, etwa ein Array, hat einfach
die Form:

• Bei dynamischer Allokation geht man


folgendermaßen vor:
– Es wird also zuerst eine Zeigervariable entsprechenden
Typs definiert, die aber noch keinen sinnvollen Wert
besitzt.
– Dann wird für das Objekt, auf das die Zeigervariable
verweisen soll, ausreichend Speicher allokiert und nun
der Zeigervariablen die Adresse dieses Speichers
zugewiesen.
Dynamische Speicherverwaltung
• new
– Der Befehl new fordert neuen Speicher an.
– Um auf ihn zugreifen zu können, liefert new einen
Zeiger darauf zurück.
– Damit new weiß, wie viel Speicher angefordert
werden soll, folgt dem Befehl der gewünschte
Datentyp.
– Der neu angeforderte Speicher kann gleich initialisiert
werden. Dazu wird der Initialisierungswert in
Klammern hinter dem Typ angegeben.
int* intZeiger = new int(2);
– Die Variable, auf die intZeiger zeigt, wird gleich nach
ihrer Erzeugung mit dem Wert 2 belegt.
Dynamische Speicherverwaltung
• Speicherlecks
– Der angeforderte Speicher muss irgendwann wieder
freigegeben werden.
– Insbesondere, wenn über den Zeiger neuen Speicher
angefordert wird, bevor der alte freigegeben wurde,
irren Speicherreste durch den Hauptspeicher, auf die
nicht mehr zugegriffen werden kann.
– Da dies durchaus mit einem Auto vergleichbar ist, das
Öl verliert, spricht man von einem Speicherleck
(memory leak).
Dynamische Speicherverwaltung
• Delete
– Dient zum Freigeben von Speicher.
– Dem Befehl folgt der Zeiger, der auf den
freizugebenden Speicher weist:
delete floatZeiger;
– Nach der Speicherfreigabe durch delete empfiehlt es
sich, die Zeigervariable auf 0 zu setzen.
• Damit wird verhindert, dass an einer anderen Stelle
versehentlich eine weitere Freigabe erfolgt.
• Und es führt dazu, dass eine weitere Verwendung dieses
Zeigers zum Auslesen oder Beschreiben des Speichers
sofort zu einem Fehler führt und damit auffindbar wird.
Dynamische Speicherverwaltung
• Speicherknappheit:
– Konnte kein Speicher alloziert werden, dann wird vom
Allokator die Ausnahme bad_alloc geworfen.
– Mit try-catch können derartige Ausnahmen
abgefangen und behandelt werden und somit
entsprechende Fehlermeldungen ausgegeben
werden.
Dynamische Speicherverwaltung
• Übungsbeispiel:
int* ip = 0; // Zeiger definieren und sichern
float* fp = 0;

ip = new int; // Speicherplatz anfordern


fp = new float;

*ip = 7; // Speicher wird initialisiert


*fp = 7.5; // d.h. ip und fp werden verwendet

cout << *ip << endl; // Inhalte ausgeben


cout << *fp << endl << endl;

delete ip; // Freigabe des Speichers


delete fp;

ip = 0; // Zeiger sichern
fp = 0;
Dynamische Speicherverwaltung
• Übungsbeispiel:
– Lege eine Struktur dynamisch an!
– Verwende dazu die schon bekannte Struktur
"Koordinate".
– Weise den beiden Koordinaten Werte zu, gib diese
dann aus.
– Anschließend ist der Speicher wieder frei zu geben
und die Zeiger zu "sichern".
Dynamische Speicherverwaltung
koordinate* p = 0;
p = new koordinate;
p->x = 1;
p->y = 2;

cout << "x-Koordinate: " << p->x << endl;


cout << "y-Koordinate: " << p->y << endl;

delete p;
p=0;
Dynamische Datenstrukturen
• Dynamische Datenstrukturen ändern ihre Struktur
und den von ihnen belegten Speicherplatz
während der Programmausführung.
• Sie sind aus einzelnen Elementen, den so
genannten 'Knoten', aufgebaut, zwischen denen
üblicherweise eine bestimmte Nachbarschafts-
Beziehung (Verweise) besteht.
• Die Dynamik liegt im Einfügen neuer Knoten,
Entfernen vorhandener Knoten und Änderung der
Nachbarschaftsbeziehung.
Dynamische Datenstrukturen
• Der Speicherplatz für einen Knoten wird erst bei
Bedarf zur Programmlaufzeit allokiert.
• Es handelt sich hierbei um Strukturen, die als
'Listen' oder 'Bäume' bezeichnet werden.
• Die Beziehung der einzelnen Knoten
untereinander wird sinnvollerweise über Zeiger
hergestellt, die jeweils auf den Speicherort des
"logischen Nachbarn" zeigen.
• Jeder Knoten wird daher neben den jeweils zu
speichernden Nutzdaten mindestens einen Zeiger
auf die jeweiligen "Nachbar"-Knoten enthalten.
Dynamische Datenstrukturen
• Die wichtigsten dieser "verketteten
Datenstrukturen" oder auch "selbstbezüglichen
(rekursiven) Datenstrukturen" sind :
– Lineare Listen
• einfach verkette Listen
• doppelt verkette Listen

– Bäume
• Binärbäume (zwei Nachfolger/Nachbarn)
• Vielweg-Bäume (mehr als zwei Nachfolger/Nachbarn)
Dynamische Datenstrukturen
• Die wichtigsten Operationen mit dynamischen
Datenstrukturen sind:
– Erzeugen eines neuen Elements
– Einfügen eines Elements
– Entfernen eines Elements
– Suchen eines Elements
Dynamische Datenstrukturen
• Im Folgenden werden wir uns mit dem Aufbau
verketteter Datenstrukturen mittels rekursiver
Strukturen beschäftigen.
Listen
• Eine Liste ist eine Sammlung von sequenziell
angeordneten Elementen.
• Eine Liste kann ihren Benutzern eine Vielzahl
unterschiedlicher Operationen anbieten.
• Typisch für Listen ist, dass
– die Liste nicht auf eine bestimmte Größe festgelegt ist
und Elemente nach Belieben eingefügt und
herausgenommen werden können, sowie dass
– zum Zugriff auf ein Listenelement die Liste vom
Anfang an durchlaufen werden muss. (Im Gegensatz
zu Feldern gibt es keinen "Listen-Index").
Verkettete Listen
• Das Konzept "Verkettete Liste" bezieht sich auf
die Implementierung einer Liste in Form von
einzelnen Knoten, die über Zeiger miteinander
verbunden sind.
• Verkettete Listen können in unterschiedlicher Art
realisiert werden:
– Einfach verkettete Listen: Bei ihnen enthält jeder
Knoten neben dem Listenelement einen Verweis auf
den Nachfolgerknoten.
– Doppelt verkettete Listen: Jeder Knoten beinhaltet
einen Verweis auf den Nachfolger und den
Vorgänger.
Einfach verkettete Liste
Doppelt verkettete Liste
Einfach verkettete Liste
• Die Basis einer verketteten Liste ist eine
Struktur, die einerseits die eigentlichen Daten
und andererseits einen Zeiger enthält, um auf
das nächste Element der Liste zu verweisen:
Einfach verkettete Liste
• Etwas verblüffend ist die Verwendung des Typs
ListenKnoten innerhalb der Deklaration des Typs
ListenKnoten.
• Dem Compiler muss an dieser Stelle das
genaue Aussehen des Typs ListenKnoten noch
nicht bekannt sein, da hier lediglich ein Zeiger
darauf definiert wird.
• Ein Zeiger ist aber immer gleich groß, ganz
gleich, auf was er zeigt.
Einfach verkettete Liste

node old

Anfang
Einfach verkettete Liste
• Die Variable Anfang ist ein Zeiger auf den Typ
ListenKnoten und bildet die Basis für den Zugriff
auf die verkettete Liste vom Programm aus.
• Über den Anfang erreicht man den ersten
Listenknoten.
• Dort enthält das Element next den Verweis auf den
nächsten Listenknoten. So kann sich das
Programm durch die Liste hangeln, bis next einmal
0 ist. Damit wird das Ende der Liste angezeigt.
• Ist die gesamte Liste leer, muss die Variable
Anfang 0 enthalten.
Einfach verkettete Liste
• Ein neuer Listenknoten wird durch Aufruf von
new erzeugt.
• Dabei muss darauf geachtet werden, dass der
Zeiger next gleich korrekt gesetzt wird.
• Die Variable old ist ein Zeiger auf einen zu
löschenden Listenknoten.
Einfach verkettete Liste
• Übungsbeispiel:
– Fülle eine einfach verkette Liste vom Typ
ListenKnoten mit Zahlen – solange bis 0 eingegeben
wird.
– Füge die jeweils neuen Elemente am Anfang ein.
– Gib anschließend die Liste in umgekehrter
Reihenfolge aus und lösche die ausgegebenen
Elemente.
Einfach verkettete Liste
Einfach verkettete Liste
Einfach verkettete Liste
ListenKnoten* Anfang = 0;

node

Anfang

data NULL
NULL

ListenKnoten* node = new ListenKnoten;


node->data = Inhalt;
node->next = Anfang;
Anfang = node;
Einfach verkettete Liste

node

Anfang

data NULL data next

ListenKnoten* node = new ListenKnoten;


node->data = Inhalt;
node->next = Anfang;
Anfang = node;
Einfach verkettete Liste

node

Anfang

data next data NULL

old

old = Anfang;
Anfang = Anfang->next;
delete old;
Einfach verkettete Liste
• Übungsbeispiel:
– Die Funktionsweise des vorhergehenden Programms
soll erhalten bleiben.
– Schreibe nun aber folgende Funktionen zum
Bearbeiten der Liste:
void ausgabe (void)
void loesche_alles (void)
void voranstellen (int daten)
Einfach verkettete Liste
Einfach verkettete Liste
Einfach verkettete Liste
Einfach verkettete Liste
• Übungsbeispiel:
– Erweitere das Programm um eine Menüführung.
– Beispiel siehe nächste Folie:
Einfach verkettete Liste
Einfach verkettete Liste
• Übungsbeispiel:
– Erweitere das Programm um die Funktion:
void anhaengen (int daten)

– Auch das Menü ist um folgenden Eintrag zu


erweitern:
h ... Elemente anhaengen
Einfach verkettete Liste
Einfach verkettete Liste

node node node node

Anfang

data next data next data next data next


data NULL

node
ListenKnoten* node = Anfang;
while(node->next != NULL) node = node->next;
node->next = new ListenKnoten; data NULL
node = node->next;
node->data = daten;
node->next = NULL;
Einfach verkettete Liste
• Übungsbeispiel:
– Erweitere das Programm um die Funktion:
void sortiert_einfuegen (int daten)

– Auch das Menü ist um folgenden Eintrag zu


erweitern:
s ... Elemente sortiert einfügen
Einfach verkettete Liste
Einfach verkettete Liste
help node

Anfang

data next data next data next data NULL

while (node != NULL && node->data < daten) node = node->next;



node
ListenKnoten* help = Anfang;
while (help->next != node) help = help->next;
node = new ListenKnoten;
node->data = daten;
node->next = help->next;
help->next = node; data next
Einfach verkettete Liste
• Übungsbeispiel:
– Die Funktionsweise des vorhergehenden Programms
soll erhalten bleiben.
– Ersetze die globale Variable 'Anfang' durch eine
lokale Variable 'start' im Hauptprogramm und
adaptiere die Funktionen derart, dass nun auch dieser
Zeiger übergeben wird.
Einfach verkettete Liste
Einfach verkettete Liste
Einfach verkettete Liste
Einfach verkettete Liste
Einfach verkettete Liste
Doppelt verkettete Liste
• Wir wandeln nun die einfach verkettete Liste in
eine doppelt verkettete Liste um.
– D.h. Wir erweitern die Struktur um das Element:

ListenKnoten* previous;

• Zusätzlich führen wir einen Zeiger auf das Ende


der Liste ein, damit nicht beim Anhängen eines
Elements die gesamte Liste durchlaufen werden
muss:
ListenKnoten* end = NULL;
Doppelt verkettete Liste
Doppelt verkettete Liste

start end

next

previous
Doppelt verkettete Liste
• Übungsbeispiel:
– Adaptiere das Programm inklusive aller Funktionen
für eine doppelt verkettete Liste.

– Achte auch auf den Einsatz des Zeigers auf das Ende
der Liste (z.B. bei der Funktion "anhaengen").
Doppelt verkettete Liste

Noch kein Element vorhanden.

Neues Element ist größtes Element.

Neues Element ist kleinstes Element.


Neues Element ist irgendwo dazwischen.
Doppelt verkettete Liste
Doppelt verkettete Liste
Doppelt verkettete Liste
Doppelt verkettete Liste
• Übungsbeispiel:
– Erweitere das Programm um die Funktion:
void verkehrt_ausgeben (ListenKnoten* Ende)

– Auch das Menü ist um folgenden Eintrag zu


erweitern:
k ... Alle Elemente verkehrt ausgeben
Doppelt verkettete Liste
Doppelt verkettete Liste
• Übungsbeispiel:
– Erweitere das Programm um die Möglichkeit ein
beliebiges Element zu löschen und den
dementsprechenden Menüeintrag:

e ... Loesche Element


Doppelt verkettete Liste
Doppelt verkettete Liste
Doppelt verkettete Liste
Doppelt verkettete Liste
• Übungsbeispiel:
– Adaptiere das Programm für folgende verschachtelte
Struktur:

struct datum
{
int tag;
int monat;
int jahr;
};
Doppelt verkettete Liste
• Übungsbeispiel:

struct angestellter
{
char name[20];
char vorname[20];
datum geburt;
datum eingestellt;
float gehalt;
angestellter* next;
angestellter* previous;
};
Doppelt verkettete Liste
• Übungsbeispiel - Hinweise:
– Befehle für Stringmanipulation verwenden (strncpy,
strcmp).
– #include <string> nicht vergessen!
Tipps zur Arbeit mit Listen
• Darauf achten, dass Zeiger immer auf einen
gültigen Speicherbereich (Adresse) zeigen.
– Häufiges Missverständnis: Mit zeiger2=zeiger1 wird
kein Wert an zeiger2 übergeben, sondern nur die
Adresse auf die zeiger1 verweist.

• Aussagekräftige Namen für Zeiger verwenden!


Z.B.: next, previous, start, end.
Unter anfang->next=end kann man sich mehr
vorstellen als unter a->n=e.
Tipps zur Arbeit mit Listen
• Häufigster Fehler ist ein Zeiger, der auf einen
unerlaubten Speicherplatz zeigt.
Es lohnt sich den Programmablauf immer wieder
am Papier zu überprüfen, um ihn besser
nachvollziehen zu können.
"Ein Bild sagt mehr als 1000 Worte!"
• Häufiges überprüfen des Programms und aller
möglichen Sonderfälle, denn alles, was schief
gehen kann, wird irgendwann einmal schief
gehen!

Das könnte Ihnen auch gefallen