Wieviel taugt dieses Programm?

Hallo zusammen,

Ich habe heute an einer alten Klausuraufgabe für Pascal gewerkelt. Leider kann ich nicht direkt überprüfen, wie erfolgreich ich dabei war, da mit eine nötige .dat Datei nicht zur Verfügung stand.

Es geht um folgenden Sachverhalt:

In der Datei punkte.dat sind die Ergebnisse einer Klausur gespeichert. Diese umfasste 4 Aufgaben mit 2 Teilaufgaben. Für jeden Studenten werden die Punkte für jede Teilaufgabe erfasst.

Entwickeln Sie ein Pascal Programm, das für jeden Studenten seine erreichte Gesamtpunktezahl ermittelt und ausgibt. Abschließend soll auch die Durchschnittspunktzahl aller erzielten Gesamtpunkte ausgegeben werden. Das Programm soll so flexibel sein, dass es auch korrekt arbeitet, wenn die vorgegebenen Konstanten verändert werden. Der Datentyp tStudent entspricht dem Muster, nach dem die Datensätze in der Datei punkte.dat gespeichert wurden.

Benutzen Sie folgende Grundlage für Ihr Programm. Falls Sie weitere Dateitypen oder Variablen benötigen, so fügen Sie diese hinzu:

PROGRAM Auswertung;

CONST

Aufgaben = 4;
Teilaufg = 3;

TYPE

TPunkte = ARRAY [1..Aufgaben, 1..Teilaufg] OF REAL;
TStudent = RECORD
 Matrikel\_Nr: LongInt;
 Punkte : Tpunkte;

VAR 

Student : tStudent;
PunkteDat : FILE OF tStudent;

BEGIN

END.

Die Ausgabe auf dem Bildschirm könnte so aussehen

Matrikel-Nr.: 12345 Punkte: 59.5
Matrikel-Nr.:54321 Punkte: 47.0
Und so weiter
Durchschnittspunktzahl der Klausur: 56.5

Mein Versuch sieht dann wie folgt aus. Syntaktisch offenbar fehlerfrei, aber die Funktionalität ist fraglich:

PROGRAM Einlesen;
uses crt;

CONST Aufgaben = 4;
 Teilaufg = 2;
 FILENAME = 'c:\punkte.dat';

TYPE tPunkte = ARRAY[1..Aufgaben, 1..Teilaufg] OF REAL;
 tStudent = RECORD
 Matrikel\_NR: LongInt;
 Punkte: tPunkte;
 END;

VAR Student: tStudent;
PunkteDat: File OF tStudent;
i: INTEGER;
zaehler : INTEGER;
punkt : REAL;

BEGIN
 ASSIGN(PunkteDat, FILENAME);
 RESET(PunkteDat);
 WHILE NOT EOF DO
 begin
 READ (PunkteDat, Student);
 end;
 CLOSE (PunkteDat);

 zaehler := 0;
 punkt := 0;

 WHILE NOT EOF DO
 begin
 WRITELN ('Matrikel-Nr.: ',Student.Matrikel\_Nr,''); 
 WRITELN('Punkte',Student.Punkte[1,1]+Student.Punkte[1,2]+Student.Punkte[2,1]+Student.Punkte[2,2]+Student.Punkte[3,1]+Student.Punkte[3,2]+Student.Punkte[4,1]+Student.Punkte[4,2],'');
 zaehler := zaehler +1;
 punkte:=punkt+Student.Punkte[1,1]+Student.Punkte[1,2]+Student.Punkte[2,1]+Student.Punkte[2,2]+Student.Punkte[3,1]+Student.Punkte[3,2]+Student.Punkte[4,1]+Student.Punkte[4,2];
 end;
 WRITELN ('Durchschnittspunktzahl der Klausur: ',punkte / zaehler,'');
 END.

Hauptproblem war dabei für mich das Zusammenrechnen der Punkte, das ich ziemlich unhandlich gemacht habe. Bin für kompaktere Vorschläge ratbar und bitte auch um weitere konstruktive Anmerkungen.

Vielen Dank,

Marc Borsos

kleiner Fehler
Ich hab mich einmal bei der Konstanten „Teilaufg“ verschrieben. Es ist 2 und nicht 3.

Marc Borsos

Nicht viel…
Hallo Marc,

ich habe den Code nur mal kurz überflogen. So wie es aussieht, wird dabei nichts sinnvolles rauskommen.

> [...]  
> BEGIN  
> ASSIGN(PunkteDat, FILENAME);  
> RESET(PunkteDat);  
> WHILE NOT EOF DO  
> begin  
> READ (PunkteDat, Student);  
> end;  
> CLOSE (PunkteDat);

Was sollte das bringen? Du hast die Datei bis zum Ende gelesen.
In Student steht jetzt noch der letzte eingelesene Satz. Alle anderen sind „verpufft“.

> zaehler := 0;  
> punkt := 0;  
>   
> WHILE NOT EOF DO  
> begin  
> [...]

Die Datei ist bereits geschlossen. Eine erneute Schleife greift daher ins leere.
Du müsstest also die beiden Schleifen zusammenfassen, also lesen und Bildschirmausgabe in einer Schleife.
Die Berechnung selbst muß natürlich anders ablaufen:

Programm soll so flexibel sein, dass es auch korrekt arbeitet, wenn die vorgegebenen Konstanten verändert werden.

Das heißt nämlich, dass die Berechnung mit Hilfe der Konstanten in einer geschachtelten Schleife erfolgen muß.
In etwa so:

Var
 SummePunkte : Real;
 i, j : Integer
Begin
[...]
 SummePunkte := 0;
 For i := 1 to Aufgaben do
 For j := 1 to Teilaufg do
 SummePunkte := SummePunkte + Student.Punkte[i,j]
[...]

hth
Christian

Wie sieht es jetzt aus?
Hallo an Christian und die anderen,

Es kommt zwar spät, aber hier ist eine komplette Neuaflage des Programms. Dabei habe ich eine dynamische Datenstruktur verwendet und die Punkteauslesung vereinfacht, was mir Christian ja dankenswerterweise erläutert hat.
Wie schon beim letzten Mal gilt: Das Programm ist syntaktisch fehlerfrei, aber das muss ja lange nichts bedeuten.

Das wo ich mir unsicher bin, ist das Bezeichnen des Listenendes mit -1 im Abschnitt der Initialisierung der Liste. Das habe ich aus einem anderen Programm übernommen, aber ich bin mir unsicher, ob das hier klappt.
Zudem hab ich Zweifel, ob die Ausgabe der einzelnen Punktzahlen so richtig funktioniert.

Danke für Eure Kommentare.

Marc Borsos

PROGRAM Einlesen;
uses crt;

CONST Aufgaben = 4;
 Teilaufg = 2;
 FILENAME = 'c:\punkte.dat';

TYPE tPunkte = ARRAY [1..Aufgaben, 1..Teilaufg] OF REAL;
 tStudent = RECORD
 Matrikel\_NR: LongInt;
 Punkte: tPunkte;
 END;

 pListe = ^tListe;
 tListe = RECORD
 content : tStudent;
 next: pListe;
 END;

VAR Student: tStudent; {Dateieintrag}
 PunkteDat: File OF tStudent; {Dateivariable}
 FirstListItem : pListe; {Erster Eintrag der Liste}
 AktListItem : pListe; {Aktueller Eintrag in der Liste}
zaehler : INTEGER;
punkteeinzeln : REAL;
punktesumme: REAL;
i: INTEGER;
j: INTEGER;

BEGIN
 zaehler := 0;
 punktesumme := 0;

 {Datei öffnen}
 ASSIGN(PunkteDat, FILENAME);
 RESET(PunkteDat);

 {Liste initialisieren}
 New(FirstListItem);
 AktListItem := FirstListItem;
 AktListItem^.content.Matrikel\_Nr := -1; { Menge -1 markiert das Ende der Liste}

 {Daten auslesen}
 while NOT EOF(PunkteDat) DO
 begin
 Read(PunkteDat,Student);
 AktListItem^.content := Student;

 { Ausgeben des Eintrags aus der Liste }
 WRITELN('Matrikel-Nr.: ',AktListItem^.content.Matrikel\_NR,'');
 punkteeinzeln := 0;
 FOR i := 1 TO Aufgaben DO
 FOR j:= 1 TO Teilaufg DO
 punkteeinzeln := punkteeinzeln + Student.Punkte[i,j];
 WRITELN('Punkte: ',punkteeinzeln,'');
 punktesumme := punktesumme+punkteeinzeln;
 zaehler := zaehler + 1;

 { Speicher für den nächsten Listeneintrag reservieren }
 New(AktListItem^.Next);
 AktListItem := AktListItem^.Next;
 AktListItem^.content.Matrikel\_NR := -1;
 end;

 {Speicher freigegeben}
 while FirstListItem^.content.Matrikel\_NR \> -1 do
 begin
 AktListItem := FirstListItem^.Next;
 Dispose(FirstListItem);
 FirstListItem := AktListItem;
 end;

 {Datei schließen}
 CLOSE (PunkteDat);

 {Ausgabe Durchscnittspunkte}
 WRITELN ('Durchschnittspunktzahl der Klausur: ',punktesumme / zaehler:1:2,'');

END.

Hallo Marc,

Es kommt zwar spät, aber hier ist eine komplette Neuaflage des
Programms. Dabei habe ich eine dynamische Datenstruktur
verwendet

nein, Du baust zwar eine auf, aber Du _verwendest_ sie gerade _nicht_. Es gibt zwei Möglichkeiten:


Datei öffnen
In Schleife: Datensatz einlesen + DS verarbeiten (Punkte-Addition)
Datei schließen
Ende.


Datei öffnen
In Schleife: DS einlesen und damit verkettete Liste konstruieren
Datei schließen

In Schleife: DS in der verketteten Liste abklappern, dabei DS verarbeiten

In Schleife: DS in der verk. Liste abklappern und allozierten Speicher freigeben
Ende.

Ein Progamm nach Möglichkeit 1 würde quasi nur aus einem Teil (einer Schleife) bestehen, und zu seiner Realisierung wäre überhaupt keine verkettete Liste nötig. Ein Programm nach Möglk. 2 würde dagegen aus drei Teilen (drei Schleifen) bestehen. Im ersten würde die verk. Liste erzeugt, im zweiten einmal zur Verarbeitung der Daten durchlaufen, und im dritten Teil zerstört werden.

Dein Programm arbeitet nach (1), aber Du sollst (2) realisieren. Abgesehen davon, daß nur (2) das Programmieren einer verk. Liste erfordert, was Du ja üben sollst, ist (2) auch die universellere und „sauberere“ Alternative.

Das Programm ist syntaktisch
fehlerfrei, aber das muss ja lange nichts bedeuten.

Ja.

Das wo ich mir unsicher bin, ist das Bezeichnen des
Listenendes mit -1 im Abschnitt der Initialisierung der Liste.

Das Listenende soltest Du dadurch kennzeichnen, daß Du den „next“-Zeiger des letzten Elements auf „NIL“ setzt. „NIL“ ist die einzige POINTER-Konstante; sie ist mit der Adresse 0 belegt. Wenn ein POINTER auf „NIL“ zeigt, bedeutet das definitionsgemäß, daß er _nirgendwohin_ zeigt. Der Test, ob das letzte Listenelement erreicht ist, lautet „IF (Item.next=NIL) THEN …“.

Zu der -1-Sache kann ich nur sagen: Von „Trick-Programmierung“ mit speziellen Bedeutungen spezieller Zahlen à la „die Eingabe von für die Jahreszahl bedeutet, daß der Kundendatensatz gelöscht werden soll“ ist generell abzuraten. In den 70ger Jahren mag so etwas seine Berechtigung gehabt haben; Paradigma: Spare jedes Byte teuren Speicherplatz, das Du einsparen kannst. Diese Zeiten sind jedoch vorbei und das Paradigma ist heute ein anderes: Programmiere maximal sauber, um die Gefahr einer teuren späteren Fehlersuche zu minimieren, und schreibe Software, die so wiederverwendbar, erweiterbar und sicher wie möglich ist, damit komplexe Projekte überhaupt noch beherrschbar sind.

Zudem hab ich Zweifel, ob die Ausgabe der einzelnen
Punktzahlen so richtig funktioniert.

Du könntest einfach auch ein zweites Programm schreiben, daß Dir eine Test-Studenten-Datei erzeugt. Das wäre übrigens bei weitem nicht so anspruchsvoll wie das Lese-Programm (Dateien generieren ist immer easy, Aus-Dateien-Lesen dagegen meistens „knifflig“).

So, Vorwarnung: gleich werde ich mich bei Dir unbeliebt machen…

PROGRAM Einlesen;

Wieso heißt Dein Programm „Einlesen“, es wird doch nicht nur was eingelesen, sondern auch was berechnet und ausgegeben?

uses crt;

CONST Aufgaben = 4;
Teilaufg = 2;
FILENAME = ‚c:\punkte.dat‘;

TYPE tPunkte = ARRAY [1…Aufgaben, 1…Teilaufg] OF REAL;
tStudent = RECORD
Matrikel_NR: LongInt;
Punkte: tPunkte;
END;

pListe = ^tListe;
tListe = RECORD
content : tStudent;
next: pListe;
END;

Wieso nennst Du den zweiten Record „tListe“, wo er doch nur ein Element (Item) der Liste (= Gesamtheit aller Items) ist?

Warum wählst Du für „Content“ und „Next“ so nichtssagende Namen?

VAR Student: tStudent; {Dateieintrag}

Gut!

PunkteDat: File OF tStudent; {Dateivariable}

Nicht so gut! Warum „PunkteDat“ und nicht z. B. „StudentDataFile“?

FirstListItem : pListe; {Erster Eintrag der Liste}
AktListItem : pListe; {Aktueller Eintrag in der Liste}

Wieso ist First_ListItem_ und Akt_ListItem_ nicht vom Typ „pListItem“?

zaehler : INTEGER;

Wieso läßt Du den Variablennamen „zaehler“ nicht darüber Auskunft geben, was die Variable zählt?

punkteeinzeln : REAL;
punktesumme: REAL;
i: INTEGER;
j: INTEGER;

BEGIN
zaehler := 0;
punktesumme := 0;

{Datei öffnen}
ASSIGN(PunkteDat, FILENAME);
RESET(PunkteDat);

{Liste initialisieren}
New(FirstListItem);
AktListItem := FirstListItem;
AktListItem^.content.Matrikel_Nr := -1; { Menge -1
markiert das Ende der Liste}

{Daten auslesen}
while NOT EOF(PunkteDat) DO
begin
Read(PunkteDat,Student);
AktListItem^.content := Student;

Und next möchtest Du nicht setzen?

{ Ausgeben des Eintrags aus der Liste }
WRITELN(‚Matrikel-Nr.:
‚,AktListItem^.content.Matrikel_NR,‘‘);
punkteeinzeln := 0;
FOR i := 1 TO Aufgaben DO
FOR j:= 1 TO Teilaufg DO
punkteeinzeln := punkteeinzeln +
Student.Punkte[i,j];
WRITELN(‚Punkte: ‚,punkteeinzeln,‘‘);
punktesumme := punktesumme+punkteeinzeln;
zaehler := zaehler + 1;

{ Speicher für den nächsten Listeneintrag reservieren }
New(AktListItem^.Next);
AktListItem := AktListItem^.Next;
AktListItem^.content.Matrikel_NR := -1;
end;

{Speicher freigegeben}
while FirstListItem^.content.Matrikel_NR > -1 do

Besser: WHILE NOT (…ListItem^.Next=NIL) DO
siehe Bemerkung oben

begin
AktListItem := FirstListItem^.Next;
Dispose(FirstListItem);
FirstListItem := AktListItem;
end;

{Datei schließen}
CLOSE (PunkteDat);

{Ausgabe Durchscnittspunkte}
WRITELN (‚Durchschnittspunktzahl der Klausur: ‚,punktesumme
/ zaehler:1:2,‘‘);

END.

Anmerkung 1: So wie ich das sehe verkettest Du die ListeItems überhaupt nicht! Bitte denk selbst darüber nach.

Anmerkung 2: Stimmts, die Kritik zu den Variablennamen fandest Du nörgelig? :wink: Nicht böse sein, bitte. Jeder, der Software schreibt, kennt die Qual des ständigen „Sich-gute-Namen-Ausdenken-Müssens“ nur zu gut. Oft bekommen Variablen auch erstmal nur „Scherznamen“, die dann später, wenn klarer geworden ist, was die entsprechende Variable oder das entsprechende Objekt (bei objektorientierter Programmierung) für Aufgaben erfüllt, in einen „richtigen“ Namen umgewandelt werden. Hier ist aber auch schon der entscheidende Punkt: _Wenn die Aufgaben klarer geworden sind_. Das Sich-Ausdenken von richtig guten (des _einen_ richtig guten?), d. h. aussagekräftigen Namen, und das Sich-darüber-klar-werden, was eine Variable/ein Objekt tut und was nicht, ist eng miteinander verknüpft. Die Suche nach dem richtigen Namen ist deshalb so schwierig, weil dahinter eine elementare Frage steckt, die Frage _„Was ist es?“_. Hast Du Klarheit über die Bedeutung einer „Entität“ (vornehmes Wort für „Ding“) gewonnen, dann hast Du automatisch auch einen guten Namen, und bist auch der Lösung des Gesamtproblems ein Stück näher gekommen, weil Du etwas _verstanden_ hast. Daher mein Tip: Nimm das „Richtige-Namen-Problem“ ernst (wenn Du gut werden willst). Bemühe Dich, die Was-ist-es-Fragen bestmöglich zu knacken, schreib die Antworten in die Namen, und lerne dabei jedesmal ein Stück mehr über das Wesen des Problems. Beim Namen-Geben geht es nicht um „schönes Aussehen“, sondern um Verstehen.

Abschließend ein Zitat aus einem dicken Schinken zur professionellen C+±Programmierung: Die Auswahl guter Namen ist eine Kunst. Dem kann selbst ich nichts mehr hinzufügen :wink:.

Gruß
Martin

Hallo Marc,

folgende Idee kam mir gerade (keine Ahnung, warum ich die nicht gleich hatte): Versuch es mit einem Programm, das eine Textdatei zeilenweise in eine verkettete Liste einliest. Dies ist, wie ich mich zu erinnern glaube, auch die Standard-Basis-Aufgabe jedes Pascalkurses, wenn man beim Thema „verkettete Listen“ angelangt ist. Der Riesenvorteil wäre, daß Du dann das Problem los wärst, Dein Programm nicht testen zu können. Wenn Du dieses Programm zu Laufen gebracht hast, ist die Erweiterung auf das Studenten-Programm ein Klacks, denn die dann notwendigen Änderungen betreffen bloß die Struktur des „content“-Record-Typs, aber nicht die „kniffligen“ Operationen mit der Liste und den Zeigern.

Ich glaube, mit diesem Weg würden Deine Erfolgschancen auf ein Vielfaches steigen.

Gruß
Martin

Hallo Martin,

Danke für Deine Hinweise. Ich habe in meiner neuen Fassung einige Variablennamen geändert, aber nicht „punktedat“ (Name ist zwar nicht so toll, ist aber aus Aufgabenstellung vorgegeben und da will ich nicht unbedingt was ändern) und „next“ (hab bisher noch keine verkettete Liste gesehen, die nicht das „next“ benutzt hat, eine Änderung würde mich hier mehr verwirren als es nutzen würde).

Ich hätte noch zwei Fragen:

Du schreibst, dass ich „next nicht setzen möchte“. Ich dachte eigentlich, dass ich mit dem Abschnitt „Speicher für den nächsten Listeneintrag reservieren“ (AktListItem := AktListItem^.Next) alles nötige getan hätte.
Ich bin mir daher nicht so ganz sicher, wass ich bei aktlistitem^.next setzen sollte?

Hab ich das richtig verstanden, dass ich das Ende der Liste erkennbar machen könnte, einerseits indem .Matrikel_Nr = -1 gesetzt wird ODER andererseits .next = NIL gesetzt wird?

Danke,

Marc Borsos

Hallo Marc,

einige Variablennamen geändert, aber nicht „punktedat“ (Name
ist zwar nicht so toll, ist aber aus Aufgabenstellung
vorgegeben und da will ich nicht unbedingt was ändern)

„KlausurDataFile“ würde es nach meinem Empfinden am besten ausdrücken, aber wenn der Name in der Aufgabenstellung steht, dann würde ich selbst auch keinen anderen nehmen.

und
„next“ (hab bisher noch keine verkettete Liste gesehen, die
nicht das „next“ benutzt hat, eine Änderung würde mich hier
mehr verwirren als es nutzen würde).

Dann ist das auch OK.

Du schreibst, dass ich „next nicht setzen möchte“. Ich dachte
eigentlich, dass ich mit dem Abschnitt „Speicher für den
nächsten Listeneintrag reservieren“ (AktListItem :=
AktListItem^.Next) alles nötige getan hätte.
Ich bin mir daher nicht so ganz sicher, wass ich bei
aktlistitem^.next setzen sollte?

Ja, jetzt habe ich gesehen, wo Du AktListItem^.Next setzt, nämlich in der Zeile „New(AktListItem^.Next)“ (klar?). Im Kontext der von Dir gewählten Ende-der-Liste-Markierung mit dem „-1“ ergibt das auch Sinn. Möglicherweise läuft Dein Programm also vollständig korrekt so. Allerdings: Das „MatrikelNr := -1“ ist – sorry – einfach nicht gut. Du kannst Dir leicht überlegen (bzw. hast das schon getan), daß Deine Liste immer ein Item mehr enthält als Datensätze in der Datei vorhanden sind. Das letzte Item hat dabei keine Nutzdaten, sondern enthält bloß die „-1“ als Ende-Marker. Der Inhalt des „next“-Zeigers dieses Items bleibt sogar unbestimmt (er wird nirgendwo gesetzt). Wenn diese Liste mal in einem anderen Programm zum Einsatz kommen würde, könnte es sein, daß sie wenige, aber sehr große Datensätze zu verwalten hätte, z. B. Bilddateien mit je 10 MB. Eine Liste, die 3 Bilder enthält, würde dann nicht 30 MB wertvollen RAM-Speicher allozieren, sondern 40 MB. Eine bild-leere Liste würde 10 MB belegen. Außerdem müßtest Du Dir überlegen, in welcher Form Du eine Bilddatei die „-1“ repräsentieren lassen willst (bei Matrikelnummern ist es ja einfach). Die „-1“-Sache ist durchaus problematisch.

Was ist zu tun? Stell Dir die Liste einfach mal bildlich vor. Siehst Du die _natürliche_ Form für das Listenende? Der „next“-Zeiger des letzten Elements zeigt einfach „nirgendwohin“! Ist die Liste leer, dann zeigt bereits FirstListItem nach „nirgendwohin“! Daß es großen Sinn macht, einen Zeiger nirgendwohin zeigen zu lassen, haben auch die Erfinder von Programmiersprachen erkannt, und deshalb findet sich in allen (ernsthaften) Programmiersprachen eine entsprechende Möglichkeit (informationstheoretisch gesehen ist das Nirgendwohinzeigen eines Zeigers sogar das Elementarste, was überhaupt passieren kann). In Pascal gibt es dafür „NIL“. Damit kannst Du genau zwei Sachen machen:

  1. Du kannst jeden Pointer auf NIL setzen: p := NIL,
    und
  2. Du kannst jeden Pointer abfragen, ob er auf NIL, d. h. „nirgendwohin“ zeigt: IF (p=NIL) THEN…; WHILE NOT (p=NIL) DO…

Mehr braucht auch niemand! Damit kannst Du die Liste so implementieren, daß sie dem oben Erläuterten entspricht. Die Notwendigkeit eines „Spezielle-Zahl-mit-spezieller-Bedeutung“-Konstrukts wie MatrikelNr := -1 entfällt automatisch (daß solche „Tricks“ auch den Paradigmen moderner Softwareentwicklung entgegenstehen, habe ich ja schon angesprochen). Und eine Liste mit 23 Studenten hätte auch nur 23 Items, und nicht 24. Langer Rede kurzer Sinn: Die optimale Realisierung so einer verketteten Liste verwendet NIL.

Vielleicht reizt es Dich ja, Dein Programm umzuschreiben. Sollte das mit der „-1“ in dem von Dir erwähnten Buch stehen, wäre das eher ein Beispiel dafür, daß auch Buchautoren nicht immer das Optimum erreichen.

Hab ich das richtig verstanden, dass ich das Ende der Liste
erkennbar machen könnte, einerseits indem .Matrikel_Nr = -1
gesetzt wird ODER andererseits .next = NIL gesetzt wird?

Es sind zwei Alternativen, die sich ausschließen.

Übrigens: Die Bezeichnung „NIL“ hat nichts mit einem Fluß in Ägypten zu tun, sondern ist die Abkürzung von „not in list“. Mit „next=NIL“ ist also für das letzte Listenelement „next not in list“ – wie sinnfällig, nicht wahr?

Gruß
Martin

Hallo Marc und Martin,

zunächst einmal, das Progrämmchen, das als Basis für diese Anwendung von Marc dient Stammt von mir und ist ein leicht umgeschriebenes Programm, das ich, als ich mein Programmierdiplom machte mit einer Note 1 quittiert bekam. Das ist allerdings schon 14 Jahre her.
Das mit der -1 hatte aus Speichergründen damals Gültigkeit. Zudem sollte dieses Progrämmchen nur als Abguckdemo für Marc dienen, das Marc dieses kleine Demo gleich als der Weisheit letzter Schluß ansieht war nicht geplant.

Nun zu dir Martin,

du hast absolut recht mit deiner Behauptung, das -1 als Listenende heute passe ist. Ich habe das mit -1 auch nur zum besseren Verständniss eingebracht (Du hättest das erste Programm von Marc sehen sollen, dann wüsstest du, das Marc noch ziemlich am Anfang seiner Programmiererkarriere steht und ich wollte ihn nicht gleich überfordern.)

Ja, jetzt habe ich gesehen, wo Du AktListItem^.Next setzt,
nämlich in der Zeile „New(AktListItem^.Next)“ (klar?).

Du solltest dir Quelltexte genauer ansehen, bevor du Tipps abgibst, die nicht korrekt sind und andere verwirren könnten.

Im
Kontext der von Dir gewählten Ende-der-Liste-Markierung mit
dem „-1“ ergibt das auch Sinn. Möglicherweise läuft Dein
Programm also vollständig korrekt so. Allerdings: Das
„MatrikelNr := -1“ ist – sorry – einfach nicht gut. Du kannst
Dir leicht überlegen (bzw. hast das schon getan), daß Deine
Liste immer ein Item mehr enthält als Datensätze in der Datei
vorhanden sind. Das letzte Item hat dabei keine Nutzdaten,
sondern enthält bloß die „-1“ als Ende-Marker. Der Inhalt des
„next“-Zeigers dieses Items bleibt sogar unbestimmt (er wird
nirgendwo gesetzt). Wenn diese Liste mal in einem anderen
Programm zum Einsatz kommen würde,

Was von meiner Seite nie geplant war, ich dachte Marc würde auf Grundlage meines Demos ein eigenes Programm entwickeln.

könnte es sein, daß sie
wenige, aber sehr große Datensätze zu verwalten hätte, z. B.
Bilddateien mit je 10 MB. Eine Liste, die 3 Bilder enthält,
würde dann nicht 30 MB wertvollen RAM-Speicher allozieren,
sondern 40 MB. Eine bild-leere Liste würde 10 MB belegen.

wer mit Verketteten listen Arbeitet, kann sich wohl auch für den letzten, noch nicht verwendeten Listeneintrag den Speicher für das Bild dynamisch reservieren, wenn er benötigt wird.

Außerdem müßtest Du Dir überlegen, in welcher Form Du eine
Bilddatei die „-1“ repräsentieren lassen willst (bei
Matrikelnummern ist es ja einfach). Die „-1“-Sache ist
durchaus problematisch.

Wie gesagt, die -1 war nur zur Verdeutlichung.

Was ist zu tun? Stell Dir die Liste einfach mal bildlich vor.
Siehst Du die _natürliche_ Form für das Listenende? Der
„next“-Zeiger des letzten Elements zeigt einfach
„nirgendwohin“!

Da hast du recht, da aber in meinem Demo über -1 geprüft wurde ob das Listenende erreicht ist, ist das unerheblich.

Ist die Liste leer, dann zeigt bereits
FirstListItem nach „nirgendwohin“!

Ich betone hier nochmal, lies den Quelltext, bevor du unhaltbare Tipps abgibst, die andere verwirren und zudem schlicht Falsch sind.
Du hast die Programmzeilen

{Liste initialisieren}
New(FirstListItem);

übersehen.

Wo zeigt hier der erste Listeneintrag nach nirgendwo, wenn Speicher für den ersten Eintrag reserviert wurde?

Daß es großen Sinn macht,
einen Zeiger nirgendwohin zeigen zu lassen, haben auch die
Erfinder von Programmiersprachen erkannt, und deshalb findet
sich in allen (ernsthaften) Programmiersprachen eine
entsprechende Möglichkeit (informationstheoretisch gesehen ist
das Nirgendwohinzeigen eines Zeigers sogar das Elementarste,
was überhaupt passieren kann). In Pascal gibt es dafür „NIL“.
Damit kannst Du genau zwei Sachen machen:

  1. Du kannst jeden Pointer auf NIL setzen: p := NIL,
    und
  2. Du kannst jeden Pointer abfragen, ob er auf NIL, d. h.
    „nirgendwohin“ zeigt: IF (p=NIL) THEN…; WHILE NOT (p=NIL)
    DO…

Das ist korrekt

Mehr braucht auch niemand! Damit kannst Du die Liste so
implementieren, daß sie dem oben Erläuterten entspricht. Die
Notwendigkeit eines
„Spezielle-Zahl-mit-spezieller-Bedeutung“-Konstrukts wie
MatrikelNr := -1 entfällt automatisch (daß solche „Tricks“
auch den Paradigmen moderner Softwareentwicklung
entgegenstehen, habe ich ja schon angesprochen). Und eine
Liste mit 23 Studenten hätte auch nur 23 Items, und nicht 24.
Langer Rede kurzer Sinn: Die optimale Realisierung so einer
verketteten Liste verwendet NIL.

Vielleicht reizt es Dich ja, Dein Programm umzuschreiben.
Sollte das mit der „-1“ in dem von Dir erwähnten Buch stehen,
wäre das eher ein Beispiel dafür, daß auch Buchautoren nicht
immer das Optimum erreichen.

Das war kein Buch sonder ein Demo von mir, das (Hier wiederhole ich mich) nicht zum benutzen sondern als Denkanstoss gedacht war.

Hab ich das richtig verstanden, dass ich das Ende der Liste
erkennbar machen könnte, einerseits indem .Matrikel_Nr = -1
gesetzt wird ODER andererseits .next = NIL gesetzt wird?

Es sind zwei Alternativen, die sich ausschließen.

Durchaus nicht, es kommt immer auf die Problemstellung an.

Übrigens: Die Bezeichnung „NIL“ hat nichts mit einem Fluß in
Ägypten zu tun, sondern ist die Abkürzung von „not in list“.
Mit „next=NIL“ ist also für das letzte Listenelement „next not
in list“ – wie sinnfällig, nicht wahr?

Was ich nur bestätigen kann.

Gruß
Mike

P.S. Marc du solltest denke ich nicht die Anwendungen anderer dazu benutzen um gute Noten zu erhalten, sondern dich besser mit deinen eigenen Federn schmücken.

Hallo Michael,

Das mit der -1 hatte aus Speichergründen damals Gültigkeit.

du hast absolut recht mit deiner Behauptung, das -1 als
Listenende heute passe ist. Ich habe das mit -1 auch nur zum
besseren Verständniss eingebracht

auf die Gefahr hin, daß Du Dich noch mehr über mich aufregen wirst :wink:: Ich habe Marc zum besseren Verständnis gerade die NIL-Geschichte nahezubringen versucht. „Es gibt ein Nirgendwohinzeigen von Zeigern“ und „die natürliche Form eines Listenendes ist dergestalt, daß der next-Zeiger des letzten Elements nach nirgendwohin zeigt“: Das – und wie ich finde nur das – ist es, was es zum wirklich besten Verständnis einer verketteten Liste zu verstehen gibt (falls Du anderer Meinung bist: Es ist nicht gegen Dich persönlich gerichtet). Keine Frage: Daß Zeiger die Fähigkeit besitzen, nach Nirgendwohin zu zeigen, hört sich für einen Anfänger im ersten Moment sicher seltsam an. Daraus jedoch den Schluß zu ziehen, daß erst der fortgeschrittene Programmierlehrling mit NIL konfrontiert werden sollte, halte ich für fatal. NIL überfordert niemanden, denn es ist nichts „kompliziert“ daran – im Gegenteil. Die Teilnehmer eines Programmierkurses sollten deshalb tunlichst noch in derselben Stunde NIL erklärt bekommen, in der das Kapitel Zeiger aufgeschlagen wird. Ohne NIL müssen notwendigerweise Krücken wie das „-1“ konstruiert werden. Die unerfahrenen Teilnehmer werden jedoch glauben, dies sei bereits die „richtige“ Implementierung: „Aha, so programmiert man!“ verstehen sie. Wenn dann irgendwann später noch die andere Möglichkeit vorgestellt wird: „Aha, mit NIL könnte man es auch machen!“. Ich bin der unumstößlichen Überzeugung, daß eine „NIL-Vorenthaltung“ didaktisch nur Schaden, aber keinen Nutzen bringt.

(Du hättest das erste
Programm von Marc sehen sollen, dann wüsstest du, das Marc
noch ziemlich am Anfang seiner Programmiererkarriere steht und
ich wollte ihn nicht gleich überfordern.)

Ja, jetzt habe ich gesehen, wo Du AktListItem^.Next setzt,
nämlich in der Zeile „New(AktListItem^.Next)“ (klar?).

Du solltest dir Quelltexte genauer ansehen, bevor du Tipps
abgibst, die nicht korrekt sind und andere verwirren könnten.

OK, da hätte ein zweiter Blick nicht geschadet, das gebe ich zu. Aber wie Du auch weißt, ist das mit fremdem Code immer so eine Sache. An dieser Stelle kann ich dann auch nur Deinen Rat an Marc unterstreichen: (@Marc:smile: Versuche unbedingt, ein eigenes Programm zu schreiben. Am besten eins, das Du auch testen kannst. Text zeilenweise in eine verkettete Liste Einlesen wäre eine dafür hervorragend geeignete Aufgabenstelleung, weil die erforderlichen Test-Inputs kein Problem sind (übrigens solltest Du das Programm dann nicht nur mit 5-zeiligen und 1-zeiligen Textdateien testen, sondern auch mit einer 0-zeiligen. Selbstverständlich darf auch letztere für Dein Programm kein Problem sein.). Durch Studieren fremder Lösungen kann man zwar auch etwas lernen, aber das einzig wahre und echte Lernen wird Dir nur beim Eigene-Lösung-Finden zuteil (und das gilt nicht nur fürs Programmieren).

könnte es sein, daß sie
wenige, aber sehr große Datensätze zu verwalten hätte, z. B.
Bilddateien mit je 10 MB. Eine Liste, die 3 Bilder enthält,
würde dann nicht 30 MB wertvollen RAM-Speicher allozieren,
sondern 40 MB. Eine bild-leere Liste würde 10 MB belegen.

wer mit Verketteten listen Arbeitet, kann sich wohl auch für
den letzten, noch nicht verwendeten Listeneintrag den Speicher
für das Bild dynamisch reservieren, wenn er benötigt wird.

Wie was „wenn er benötigt wird“? Der Speicher für das letzte Listenelement wird doch unabhängig von irgendwas immer alloziert. Die Größe des Datenteils ist ja statisch. Oder möchtest Du den Speicher für „Content“ nur dann allozieren, wenn es nicht um das letzte Listenelement geht? Nun, dann mußt Du eben dies erstmal feststellen können. Dazu könntest Du natürlich etwa ein weiteres BOOLEAN-Feld einführen und so setzen, daß es nur beim letzten (n+1 -ten) Datenelement TRUE ist. Funktionieren würde es, sicher.

Was ist zu tun? Stell Dir die Liste einfach mal bildlich vor.
Siehst Du die _natürliche_ Form für das Listenende? Der
„next“-Zeiger des letzten Elements zeigt einfach
„nirgendwohin“!

Da hast du recht, da aber in meinem Demo über -1 geprüft wurde
ob das Listenende erreicht ist, ist das unerheblich.

Wo zeigt hier der erste Listeneintrag nach nirgendwo, wenn
Speicher für den ersten Eintrag reserviert wurde?

Dieser Abschnitt bezieht sich nicht auf Dein Programm, sondern ich stelle Marc hier die alternative Implementierung mit NIL vor. Vielleicht hätte ich das mit einem einleitenden Satz vorwegschicken sollen.

Das ist korrekt

Wenigstens was :wink:

Das war kein Buch sonder ein Demo von mir, das (Hier
wiederhole ich mich) nicht zum benutzen sondern als
Denkanstoss gedacht war.

Na ja, das konnte ich ja nicht wissen.

Hab ich das richtig verstanden, dass ich das Ende der Liste
erkennbar machen könnte, einerseits indem .Matrikel_Nr = -1
gesetzt wird ODER andererseits .next = NIL gesetzt wird?

Es sind zwei Alternativen, die sich ausschließen.

Durchaus nicht, es kommt immer auf die Problemstellung an.

Du kannst nicht beides gleichzeitig machen. Eine Liste nach der „-1“-Ende-Markier-Methode hat n+1 Items für n Datensätze, eine Liste mit „NIL“-Ende-Markierung n Items. An letztere kann man auch kein weiteres Element dranhängen, weil man dazu den letzten Zeiger von NIL auf das n+1-Item setzen müßte, und dann wäre er nicht mehr NIL, und könnte nicht mehr zur n-Abfrage herangezogen werden.

Gruß zurück
Martin

Hallo Martin,

erstmal, ich rege mich sicher nicht über dich auf,
unterschidliche Meinung zu haben ist eher förderlich.

Da du so auf NIL herumreites erwähne ich hier nochmals, das du recht hast. Zum besseren Verständiss, ich verdiene mit Anwendungsentwicklung meine Brötchen und habe eines meiner alten Studienprogramme kurz umgestalltet. Das bedingt natürlich, dass ich nicht Stunden, sondern ein paar Minuten investiert habe um einem Anfänger einen Denkanstoß zu geben. Hätte ich das Programm mit sämtlichen Fehlerabfanroutinen und nach modernstem Stand der „Programmiertechnik“ geschrieben, so hätte ich das sicher nicht kostenlos gemacht. Bei deinem Wissen um programmierung brauche ich dir warscheinlich nicht sagen, was ein Programmiererstunde kostet.

Die Teilnehmer eines Programmierkurses sollten
deshalb tunlichst noch in derselben Stunde NIL erklärt
bekommen, in der das Kapitel Zeiger aufgeschlagen wird.

Da stimme ich dir zu, aber ich habe ein Demo zu verketteten Listen zugeschickt und nicht zu den Grundlagen der Zeigerprogrammierung.

OK, da hätte ein zweiter Blick nicht geschadet, das gebe ich
zu. Aber wie Du auch weißt, ist das mit fremdem Code immer so
eine Sache. An dieser Stelle kann ich dann auch nur Deinen
Rat an Marc unterstreichen: (@Marc:smile: Versuche unbedingt, ein
eigenes Programm zu schreiben. Am besten eins, das Du
auch testen kannst. Text zeilenweise in eine verkettete Liste
Einlesen wäre eine dafür hervorragend geeignete
Aufgabenstelleung, weil die erforderlichen Test-Inputs kein
Problem sind (übrigens solltest Du das Programm dann nicht nur
mit 5-zeiligen und 1-zeiligen Textdateien testen, sondern auch
mit einer 0-zeiligen. Selbstverständlich darf auch letztere
für Dein Programm kein Problem sein.). Durch Studieren
fremder Lösungen kann man zwar auch etwas lernen, aber das
einzig wahre und echte Lernen wird Dir nur beim
Eigene-Lösung-Finden zuteil (und das gilt nicht nur fürs
Programmieren).

Du hast den Nagel auf den Kopf getroffen.

könnte es sein, daß sie
wenige, aber sehr große Datensätze zu verwalten hätte, z. B.
Bilddateien mit je 10 MB. Eine Liste, die 3 Bilder enthält,
würde dann nicht 30 MB wertvollen RAM-Speicher allozieren,
sondern 40 MB. Eine bild-leere Liste würde 10 MB belegen.

wer mit Verketteten listen Arbeitet, kann sich wohl auch für
den letzten, noch nicht verwendeten Listeneintrag den Speicher
für das Bild dynamisch reservieren, wenn er benötigt wird.

Wie was „wenn er benötigt wird“? Der Speicher für das letzte
Listenelement wird doch unabhängig von irgendwas immer
alloziert. Die Größe des Datenteils ist ja statisch. Oder
möchtest Du den Speicher für „Content“ nur dann allozieren,
wenn es nicht um das letzte Listenelement geht? Nun, dann mußt
Du eben dies erstmal feststellen können. Dazu könntest Du
natürlich etwa ein weiteres BOOLEAN-Feld einführen und so
setzen, daß es nur beim letzten (n+1 -ten) Datenelement TRUE
ist. Funktionieren würde es, sicher.

Immer langsam, du sprachst von Bilddaten, aber Bilder haben nie alle eine Große von z.B. 10MB, vielmehr hat das eine 9,75MB das andere 8,93MB etc. Also würde ich so einen Rekord für die Listenelemente implementieren

type
 PListItem = ^TListItem;
 TListItem = record
 BildDaten : Pointer;
 Next : PListItem;
 end;

den Speicher würde ich dann nicht über New sondern über GetMem(ListItem^.BildDaten,SizeOf(„Bilddatei“)) reservieren.

Dieser Abschnitt bezieht sich nicht auf Dein Programm, sondern
ich stelle Marc hier die alternative Implementierung mit NIL
vor. Vielleicht hätte ich das mit einem einleitenden Satz
vorwegschicken sollen.

Ach so, hab ich in den falschen Hals gekrigt :wink:

Du kannst nicht beides gleichzeitig machen. Eine Liste nach
der „-1“-Ende-Markier-Methode hat n+1 Items für n Datensätze,
eine Liste mit „NIL“-Ende-Markierung n Items. An letztere
kann man auch kein weiteres Element dranhängen, weil man dazu
den letzten Zeiger von NIL auf das n+1-Item setzen müßte, und
dann wäre er nicht mehr NIL, und könnte nicht mehr zur
n-Abfrage herangezogen werden.

Auf diesen Punkt näher einzugehen, würde hier jeden Rahmen sprengen und hat auch mit dem eigentlichen Problem nichts zu tun, aber sei versichert, das ich durchaus beide Praktiken gleichzeitig nutze. Das Zauberwort hierbei ist „Mehrfach verkettete Liste“

Gruß
Mike

Hallo Mike,

unterschidliche Meinung zu haben ist eher förderlich.

da bin ich mir Dir derselben Meinung :wink:

habe eines meiner
alten Studienprogramme kurz umgestalltet. Das bedingt
natürlich, dass ich nicht Stunden, sondern ein paar Minuten
investiert habe um einem Anfänger einen Denkanstoß zu geben.

Ja, ist schon angekommen. Ich habe Dein Programm für ein ausgefeiltes Lehrstück gehalten und damit unwissentlich etwas kritisiert, das für mich etwas anderes zu sein schien, als es tatsächlich war. Ich schlage vor, wir schieben die Schuld – Achtung, nicht ernst gemeint! – einfach auf Marc (ähem… :wink:), der vergaß, eine kurze Bemerkung über die Herkunft und die ursprüngliche Intention des Objekts seiner Fragen zu verlieren.

Was Du im technischen Teil Deines Postings sagst, verstehe ich, aber ich möchte es mal so dahingestellt sein lassen, damit die Diskussion nicht ausufert. Ich hoffe, das ist OK für Dich.

Mit freundlichem Gruß
Martin