Verkettete Listen - Zeiger in Strukturen
-
-
Erstmal ein großen Lob an dieses Forum und seine Mitglieder.
Leider habe ich das Buch von Jürgen Wolf schon durch.
Aber dank dem Buch von Herrn Erlenkötter konnte ich die meisten Kapitel überfliegen. Nur das Kapitel 13 und 14 mit eben diesen Verketteten Listen fand ich sehr verwirrend.Was haltet ihr von diesem Buch:
C Lernen und professionell anwenden | ISBN: 9783826695049Das würde ich gerne als nächstes durcharbeiten.
Das man den .Operator bei direkten Zugriff auf das Objekt benutzt und den
->Operator bei Zugriff über einen Zeiger das war mir klar.
Meine Frage bezog sich darauf, wenn innerhalb der Struktur ein Zeiger steht.a1.next ist ja vergleichbar mit *ptr (wenn keine Struktur verwendet wird),oder?
Bei dem Befehl a2->next sind ja zwei Zeiger im Spiel. Zeigt dann der Zeiger *a2 auf das selbe auf das *next zeigt?
Allgemeine Frage zu Zeigern und Strukturen:
Warum liefern beide Eingabe das selbe Ergebnis?
struct lagerverwaltung *ptr = &test;
oder
struct lagerverwaltung *ptr;
ptr=&test;Wenn ich dasselbe ohne Strukturen machen, kommt es zu einem Programmabsturz:
int zahl;
int *ptr;
zahl = 20;*ptr=&zahl;
Danke und Gruß
Daniel
-
Achte auf den Dereferenzierungsoperator *
Du machst eben nicht dasselbe.
int zahl; // zahl ist eine int-Variable int *ptr; // So wie es da steht mit dem * davor ist es auch wie eine int-Variable. // der Zeiger ist ptr *ptr=&zahl; // hier willst du der int-Variablen eine Adresse zuweisen //daher ohne * ptr=&zahl // ptr bekommt die Adresse von zahl zugewiesen
Der -> kommt ins Spiel, wenn du über einen Zeiger auf ein struct-Element zugreifen willst. Dabei spielt es keine Rolle von welchem Typ dieses Element ist.
struct lagerverwaltung *ptr, test; ptr=&test; test.artikelnummer = 4711; // Zugriff über Variablennamen ptr->artikelnummer = 4711; // Zugriff über Pointer test.next = malloc(....); // bzw. ptr->next = malloc(....); // Da next ein Zeiger ist kommt hinter das next dann der -> test.next->next = NULL; ptr->next->next = NULL;
-
Ok, ich denke ich habe es verstanden.
Mir war nicht klar das folgendes die selbe Schreibweise ist.
int zahl;
int *ptr;
ptr=&zahlund
int zahl;
int *ptr=&zahl;struct lagerverwaltung *ptr, test; ptr=&test; test.artikelnummer = 4711; // Zugriff über Variablennamen ptr->artikelnummer = 4711; // Zugriff über Pointer test.next = malloc(....); // bzw. ptr->next = malloc(....); // Da next ein Zeiger ist kommt hinter das next dann der -> test.next->next = NULL; ptr->next->next = NULL;
Zeile 11 und 12 verwirrt mich etwas.
Müsste für diese Schreibweise nicht noch eine Struktur innerhalb der Struktur sein?
Mit ptr->next greife ich mit Hilfe des Pointers ptr auf den Pointer next zu.
Aber was macht ptr->next-> oder test.next->next?
-
daniel2345 schrieb:
Aber was macht ptr->next-> oder test.next->next?
next
ist ein Zeiger auf einestruct lagerverwaltung
und wie du jetzt weißt, greift man mit -> auf die Elemente einer struct zu.Du greifst also über den next Zeiger auf die nächste struct zu (und da ebenfalls auf den next Zeiger.
(ptr ist der Zeiger, daher das -> dahinter. next ist auch ein Zeiger, daher auch ein ->)Daher kommt der Begriff "verkettete Liste"
daniel2345 schrieb:
Müsste für diese Schreibweise nicht noch eine Struktur innerhalb der Struktur sein?
Nein. Dann wäre die Schreibweise
ptr->next.next
Du kannst zwar eine struct als Element in einer struct einbauen, aber nicht die struct due du gerade definierst. Das wäre eine Endlosdefinition.Der Typ links vom (vor dem) -> bzw. . ist entscheidend.
-
Vielen Dank, ich denke wieder was gelernt.
Heißt das, ich könnte theoretisch ewig so weitermachen, wenn ich genügend nachfolgende Strukturen habe.
ptr->next->next->next-next würde also z.B. auf den next Zeiger auf einer nachfolgenden Struktur - und zwar auf die vierte - zugreifen?
Wird das in der Praxis auch so verwendet?
Oder kann man auch einfacher auf z.B. die vierte Struktur zugreifen?
-
Diese Listen können beliebig lang werden.
In der Praxis merkst du dir den Anfang der Liste und hangelst du dich immer weiter.so kommst du z.B. zum letzten Element der Liste
ptr = ListenAnfang; while (ptr->next != NULL) ptr = ptr->next; // hier zeigt dann ptr auf das letzte Element der Liste und du kannst dann z.B. neue anhängen
-
Endlich verstanden.
Leider ist das in meinem Buch nicht gut erklärt.Ich hoffe ich darf noch eine letzte Frage zu dem Buch von Jürgen Wolf stelle, danach lege ich es auch ganz bestimmt weg.
Es handelt sich um die Funktion void load lagerverwaltung().
Warum wird nach der IF-Anweisung if(first==NULL) in Zeile 19 Speicher reserviert und ab Zeile 29 Daten ausgegeben?
Wenn first==NULL ist, ist doch die Liste leer und es sind keine Daten vorhanden?Wird mit der Funktion fread in Zeile 17 in jedem Schleifendurchlauf der while Schlefe immer nur eine gespeicherte Struktur ausgegeben oder werden sofort alle gespeicherten Strukturen innerhalb eines Schleifendurchlaufs ausgegeben?
Laut meinem Verständnis wird immer nur ein Datensatz einer Struktur ausgegeben, aber ich verstehe dann die Zuweisung in Zeile 37 nicht.
lager_ptr=first; somit würde doch immer die selbe Struktur ausgegeben werden.
Immer die Struktur, die auf first folgt.(Zeile 49: lager_ptr = lager_ptr->next)void load_lagerverwaltung() { FILE *f; char file_name[255]; struct lagerverwaltung *lager_ptr, lager_daten; printf("Welchen Datensatz wollen Sie laden : "); scanf("%s",file_name); fflush(stdin); f = fopen(file_name, "r"); if(f == NULL) { printf("Fehler beim oeffnen von %s\n",file_name); return; } while(fread(&lager_daten,sizeof(struct lagerverwaltung),1,f)) { if(first == NULL) { first = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); if(first == NULL) { printf("Speicherplatzmangel!!!\n"); exit(0); /* Programm beenden */ } else { first->artikelnummer = lager_daten.artikelnummer; strcpy(first->artikelbezeichnung, lager_daten.artikelbezeichnung); first->anzahl_artikel = lager_daten.anzahl_artikel; first->next = NULL; } } else { lager_ptr=first; while(lager_ptr->next != NULL) lager_ptr = lager_ptr->next; lager_ptr->next = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); if(lager_ptr->next == NULL) { printf("Speicherplatzmangel!!!\n"); exit(0); /* Programm beenden */ } else { lager_ptr = lager_ptr->next; lager_ptr->artikelnummer = lager_daten.artikelnummer;; strcpy(lager_ptr->artikelbezeichnung, lager_daten.artikelbezeichnung); lager_ptr->anzahl_artikel = lager_daten.anzahl_artikel; lager_ptr->next = NULL; } } } printf("\nDatensatz geladen\n\n"); }
Hier nochmal das ganze Beispielprogramm.
[#include <stdio.h> #include <stdlib.h> #include <string.h> struct lagerverwaltung { long artikelnummer; char artikelbezeichnung[100]; int anzahl_artikel; struct lagerverwaltung *next; }; struct lagerverwaltung *first = NULL; void insert_lagerverwaltung(long art_nr, char art_tit[], int anz_art) { struct lagerverwaltung *lager_ptr; if(first == NULL) { first = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); if(first == NULL) { printf("Speicherplatzmangel!!!\n"); exit(0); /* Programm beenden */ } else { first->artikelnummer = art_nr; strcpy(first->artikelbezeichnung, art_tit); first->anzahl_artikel = anz_art; first->next = NULL; } } else { lager_ptr=first; while(lager_ptr->next != NULL) lager_ptr = lager_ptr->next; lager_ptr->next = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); if(lager_ptr->next == NULL) { printf("Speicherplatzmangel!!!\n"); exit(0); /* Programm beenden */ } else { lager_ptr = lager_ptr->next; lager_ptr->artikelnummer = art_nr; strcpy(lager_ptr->artikelbezeichnung, art_tit); lager_ptr->anzahl_artikel = anz_art; lager_ptr->next = NULL; } } printf("\nNeuer Artikel hinzugefuegt\n\n"); } void insert_sort_lagerverwaltung(long art_nr, char art_tit[], int anz_art) { struct lagerverwaltung *lager_ptr1, *lager_ptr2; /* Zuerst wird überprüft, ob sich überhaupt ein Element in der Liste befindet. Falls nicht, rufen Sie die Funktion insert_lagerverwaltung() mit dem Argumenten art_nr , art_tit, anz_art auf */ if(first == NULL) insert_lagerverwaltung(art_nr, art_tit, anz_art); else { /*Als nächstes wird nach dem Element gesucht,das größer als das neue Element ist und fügen es an der entsprechenden Stellen ein */ lager_ptr1 = first; /* Hier durchlaufen Sie die Liste so lange, bis Sie ein Element finden, das größer ist als art_nr oder Sie am Ende der Liste angekommen sind und somit die Funktion insert_lagerverwaltung() aufrufen */ while( (lager_ptr1 != NULL) && (art_nr > lager_ptr1->artikelnummer) ) /* solange nicht am Ende angekommen UND einzufügender Artikel ist größer als der aktuelle */ lager_ptr1 = lager_ptr1->next; if(lager_ptr1 == NULL) insert_lagerverwaltung(art_nr, art_tit, anz_art); /* Hinten anhängen */ else if(lager_ptr1 == first) { first = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); first->artikelnummer = art_nr; strcpy(first->artikelbezeichnung, art_tit); first->anzahl_artikel = anz_art; first->next = lager_ptr1; } else { /* Hier postitionieren Sie den zweiten Zeiger nun ein Position vor dem ersten */ lager_ptr2 = first; while(lager_ptr2->next != lager_ptr1) lager_ptr2 = lager_ptr2->next; lager_ptr1 = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); lager_ptr1->artikelnummer = art_nr; strcpy(lager_ptr1->artikelbezeichnung, art_tit); lager_ptr1->anzahl_artikel = anz_art; lager_ptr1->next = lager_ptr2->next; lager_ptr2->next = lager_ptr1; } } } void output_lagerverwaltung() { struct lagerverwaltung *lager_ptr; if(first == NULL) printf("Es sind kein Daten zum Ausgeben vorhanden!!\n"); else { lager_ptr = first; while(lager_ptr != NULL) { printf("Artikelnummer : %ld\n",lager_ptr->artikelnummer); printf("Artikelbezeichnung : %s",lager_ptr->artikelbezeichnung); printf("Anzahl Artikel : %d\n\n",lager_ptr->anzahl_artikel); lager_ptr = lager_ptr->next; } } } void read_lagerverwaltung(void) { long an; char at[100]; int aa; printf("Artikelnummer : "); scanf("%ld",&an); fflush(stdin); printf("Artikelbezeichnung : "); fgets(at, sizeof(at), stdin); printf("Anzahl d. Artikel : "); scanf("%d",&aa); insert_sort_lagerverwaltung(an,at,aa); } void delete_lagerverwaltung(long art_nr) { struct lagerverwaltung *lager_ptr1; struct lagerverwaltung *lager_ptr2; /* Die logische erste Überprüfung ist ... */ if(first != NULL) /* Überhaupt etwas in der Liste ? */ { /* Ist das erste Element gleich das gesuchte ? */ if(first->artikelnummer == art_nr) { /* Das erste Element ist das gesuchte ! */ lager_ptr1 = first->next; free(first); first=lager_ptr1; printf("\nElement mit Art.-Nummer %ld wurde geloescht\n\n",art_nr); } else { /* Also nicht das erste Element .... */ lager_ptr1=first; /* Irgendwo muss man ja beginnen ... */ while(lager_ptr1->next != NULL) { /*So lange man nicht am Ende der Liste ist */ lager_ptr2=lager_ptr1->next; if(lager_ptr2->artikelnummer == art_nr) { /* Das Element wurde schon gefunden */ lager_ptr1->next = lager_ptr2->next; free(lager_ptr2); printf("\nElement mit Art.-Nummer %ld wurde geloescht\n\n",art_nr); break; /* Zum Abbrechen der while() - Schleife */ } lager_ptr1=lager_ptr2; /* Das Element wurde noch nicht gefunden! */ }/*Ende while*/ } /*Ende else*/ } /*Ende if(first!= NULL)*/ else printf("\nEs sind keine Daten zum loeschen vorhanden!!!\n\n"); } void search_lagerverwaltung(char bez[]) { struct lagerverwaltung *lager_ptr1; if(first == NULL) printf("\nDie Liste ist leer!\n\n"); else { lager_ptr1 = first; while(lager_ptr1 != NULL) { if(strcmp(lager_ptr1->artikelbezeichnung, bez) == 0) { printf("Artikelnummer : %ld\n",lager_ptr1->artikelnummer); printf("Artikelbezeichnung : %s",lager_ptr1->artikelbezeichnung); printf("Anzahl Artikel : %d\n\n",lager_ptr1->anzahl_artikel); return; /* Funktion beenden */ } lager_ptr1 = lager_ptr1->next; } } printf("\nSuche erfolglos\n\n"); } void save_lagerverwaltung() { FILE *f; char file_name[255]; struct lagerverwaltung *lager_ptr; printf("Unter welchem Namen wollen Sie die Datei speichern : "); scanf("%s",file_name); fflush(stdin); f = fopen(file_name, "w"); if(f == NULL) { printf("Fehler beim oeffnen von %s\n",file_name); return; } /* lager_ptr auf das erste Element */ lager_ptr=first; if(lager_ptr == NULL) { printf("\nEs gibt nichts zum Speichern\n\n"); return; } /* Nun wird Element für Element in der Liste gespeichert, bis lager_ptr auf NULL zeigt und somit alle Daten gespeichert hat */ while(lager_ptr != NULL) { fwrite(lager_ptr,sizeof(struct lagerverwaltung),1,f); lager_ptr = lager_ptr->next; } printf("\nDaten erfolgreich gespeichert\n\n"); } void load_lagerverwaltung() { FILE *f; char file_name[255]; struct lagerverwaltung *lager_ptr, lager_daten; printf("Welchen Datensatz wollen Sie laden : "); scanf("%s",file_name); fflush(stdin); f = fopen(file_name, "r"); if(f == NULL) { printf("Fehler beim oeffnen von %s\n",file_name); return; } while(fread(&lager_daten,sizeof(struct lagerverwaltung),1,f)) { if(first == NULL) { first = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); if(first == NULL) { printf("Speicherplatzmangel!!!\n"); exit(0); /* Programm beenden */ } else { first->artikelnummer = lager_daten.artikelnummer; strcpy(first->artikelbezeichnung, lager_daten.artikelbezeichnung); first->anzahl_artikel = lager_daten.anzahl_artikel; first->next = NULL; } } else { lager_ptr=first; while(lager_ptr->next != NULL) lager_ptr = lager_ptr->next; lager_ptr->next = (struct lagerverwaltung *)malloc(sizeof(struct lagerverwaltung)); if(lager_ptr->next == NULL) { printf("Speicherplatzmangel!!!\n"); exit(0); /* Programm beenden */ } else { lager_ptr = lager_ptr->next; lager_ptr->artikelnummer = lager_daten.artikelnummer;; strcpy(lager_ptr->artikelbezeichnung, lager_daten.artikelbezeichnung); lager_ptr->anzahl_artikel = lager_daten.anzahl_artikel; lager_ptr->next = NULL; } } } printf("\nDatensatz geladen\n\n"); } int main() { int abfrage; long art_nr; char search[100]; do{ printf("<1> Neue Daten einlesen\n"); printf("<2> Alle Daten ausgeben\n"); printf("<3> Ein Element in der Liste loeschen\n"); printf("<4> Element suchen\n"); printf("<5> Datensatz laden\n"); printf("<6> Datensatz speichern\n"); printf("<7> Ende\n\n"); printf("Ihre Auswahl : "); scanf("%d",&abfrage); fflush(stdin); /* Bei Probleme mit scanf() die Funktion getchar() anstatt fflush(stdin) verwenden !!! */ switch(abfrage) { case 1 : read_lagerverwaltung(); break; case 2 : output_lagerverwaltung(); break; case 3 : printf("Artikelnummer : "); scanf("%ld",&art_nr); delete_lagerverwaltung(art_nr); break; case 4 : printf("Welchen Artikel suchen Sie : "); fgets(search, sizeof(search), stdin); search_lagerverwaltung(search); break; case 5 : load_lagerverwaltung(); break; case 6 : save_lagerverwaltung(); break; case 7 : printf("Bye\n"); break; default : printf("Falsche Eingabe!!!\n"); } }while(abfrage != 7); return 0; } ]
-
Grundsätzlich erstmal wird in der Funktion nichts ausgegeben sondern eingelesen.
Die Funktion heißt load.... (laden....) und in der Funktion wirdfread
(File read: Datei lesen) aufgerufen.Die Liste wird also von der Festplatte in den Arbeitsspeicher gelesen.
Es wird jeweils ein Datensatz von der Platte gelesen (Zeile 17), Speicher besorgt (Zeile 21 bzw 41) und die Daten in diesen Speicher kopiert (Zeile 29ff und 50ff). Dazwischen ist noch Fehlerbehandlung.
DirkB schrieb:
In der Praxis merkst du dir den Anfang der Liste
Das wäre das first.
Wenn der auf NULL zeigt gibt es noch keine Liste und du brauchst die Sonderbehandlung für das erste Element der Liste.Das was bei
lager_ptr = lager_ptr->next;
passiert ist so ähnlich wieint a= 10 while (a<10) { a=a+1; // hier prinf("a=%d\n",a); }
Wird hier immer nur 1 ausgegeben? Denn die folgt ja auf die 0.
Die Funktion sucht jedesmal das Ende der Liste, was eigentlich nicht nötig ist.
-
Ja wenn first == NULL, dann ist die Liste leer
Zeile 17 .. ich weiß nicht warum hier nicht einfach die struktur mit *first = lagerdaten kopiert wird, dort muss dann nur next angepasst werden (das gleiche gilt weiter unten).
zu Zeile 37 dort wird jedesmal vom Anfang zum Ende der Liste durchlaufen um den geladenen Datensatz hinten anzuhängen.
Ich finde das eher unschön einfacher währe es Vorne anzuhängen statt hinten, oder einen zeiger auf das ende der Liste, oder den letzten next zeiger zu haben, um dann hinten anzuhängen ohne durchzulaufen.Was mir beim überfliegen auffällt:
schreibe besser:ptr = malloc(sizeof(*ptr)); //in C99 sind sogar solche Kommentare erlaubt! fread(&lager_daten, sizeof(lager_daten), 1, file_out);
, denn der Cast ist überflüssig und, dann kannst du denn Typ vob ptr/lager_daten ändern ohne das du alles nach mallocs,.... absuchen musst um auch dort den Typ zu ändern
Das Verhalten von fflush(stdin) ist undefiniert siehe: http://www.c-plusplus.net/forum/viewtopic.php?t=39349
vorsicht bei scanf("%s", str); es kann zu bufferoverflows kommen besserchar str[60]; scanf("%59s", str);
Oder du benutzt fgets. Aber das hat leider den Nachteil, dass es das '\n' mit einliest. Aber da könntest du dir ja eine passende Funktion schreiben, die das regelt.
int main(void) // (void) für keine Argumente / () heißt beliebig viele Argumente
Tipp schau dir mal perror() an ist manchmal ganz praktisch
-
Noch ein paar Bemerkungen:
Funktionen die einfach so das Programm beenden (durch exit) würden dir als Nutzer auch nicht gefallen.
Die Funktion load... soll die Liste laden und wenn ein Fehler auftritt, muss sie diesen an die aufrufende Funktion melden. Dafür kann man den Rückgbewert der Funktion nutzen.int load_lagerverwaltung() {.... if(f == NULL) { printf("Fehler beim oeffnen von %s\n",file_name); // Eigentlich nicht nötig. Die Funktion soll die Daten laden und nicht mit dem Nutzer quatschen. // Kann in eine Debug-Ausgabe return -1; // Fehler beim öffnen } ... if(lager_ptr->next == NULL) { printf("Speicherplatzmangel!!!\n"); // Wie oben // exit(0); /* Programm beenden */ NEEEEIN return -2; //Es ist ein Fehler beim Speicher aufgetreten } .... printf("\nDatensatz geladen\n\n"); // Wie oben return 0; // Alles OK }
Dann kannst du als Programmierer selber entscheiden (oder den Nutze fragen) was passieren soll.
-
Mal sehen, ob ich das verstanden habe.
1. Mit der while Schleife und fread wird bei jedem Schleifendurchlauf eine Liste der Stuktur bzw. eine Struktur in den Arbeitsspeicher geladen
(Zeile 17)2. Wenn first==NULL ist die Liste im Arbeitsspeicher leer und nicht die Liste die auf der Festplatte liegt. Es wird die erste Liste der Struktur in den Arbeitsspeicher geladen und an das Ende der Liste wieder der NULL Zeiger
angehängt (Zeile 19-32)3. Wenn first!=NULL ist wird dem Zeiger lager_ptr mit lager_ptr=first die Adresse der ersten Liste übergeben (Zeile 37).
4. Dann wird Speicher für die nächste Liste reserviert (Zeile 41).
5. Jetzt wird dem Zeiger lager_ptr die Adresse des reservierten Speichers übergeben und die nächste Liste in den Arbeitsspeicher geladen.
Dann wird das Ende der Liste wieder mit dem NULL-Zeiger markiert. (Zeile 49-53).6. Beim nächsten Durchlauf der while-Schleife würde dann die nächste Liste ausgegeben, usw.
Stimmt das so ungefähr?
Dank euch habe ich auch meinen Denkfehler beseitgen können:
Ich dachte mit der Zuweisung lager_ptr=first und anschließender Zuweisung
lager_ptr=lager_ptr->next wird immmer die selbe Liste ausgegeben.
Aber durch die while Schleife in Zeile 39 wird die Zuweisung
lager_ptr=lager_ptr->next nicht nur einmal ausgeführt, sondern so lange bis das aktuell letzte Element in der Liste (markiert durch den NULL-Zeiger) gefunden wurde.
-
Begriffsverwirrung.
Du hast nur eine Liste. Die besteht aus einzelnen Einträgen/Elementen (jeweils eine ganze struct), die miteinander durch die next-Zeiger Verknüpft sind.
Und das einzige was da ausgegeben wird sind die Textmeldungen bei printf.
1. Es wird der Inhalt einer struct aus der Datei in eine lokale struct (lager_daten) eingelesen. (Zeile 17)
Wenn das fehlschlägt, geht es bei 4. weiter2a. Wenn die Liste noch leer ist, wird Speicher besorgt, der Inhalt von
lager_daten
in diesen Speicher kopiert und der next Zeiger auf NULL gesetzt.2b. Wenn die Liste schon einen Eintrag hat, wird das Ende der Liste gesucht (Zeile 39, 40), Speicher besorgt, auf den neuen Speicher verwiesen (Zeile 49), den Inhalt von
lager_daten
in diesen Speicher kopiert und der next Zeiger auf NULL gesetzt.3. Weiter bei 1.
4. Fertig.
Der NULL-Zeiger dient als Endekennung.
-
Ok, ich dachte weil es Verkettete Listen heißt, nennt man jede ganze Struct eine Liste.
Nochmals vielen Dank für die Erklärungen.
-
daniel2345 schrieb:
Ok, ich dachte weil es Verkettete Listen heißt, nennt man jede ganze Struct eine Liste.
Es heißt verkettete Liste, verkettete Listen ist die Mehrzahl.
Eine verkettete Liste besteht aus miteinander verketteten Elementen.