Brauche Hilfe beim Abspeichern von Daten

Folgendes Problem ist für Profis wahrscheinlich leicht lösbar. Ich mache das aber zum ersten Mal und bin etwas überfordert. Das untenstehende Programm liest mit Benutzerabfragen 12 Umsätze ein, speichert diese temporär, berechnet nach Eingabe aller Umsätze den Gesamtumsatz, damit jedem einzelnen Umsatz ein prozentualer Anteil am Gesamtumsatz zugeordnet werden kann. Das macht das untenstehende Programm auch einwandfrei.

Ich möchte nun, dass die Daten nicht mehr am Bilschrirm ausgegeben werden, sondern in der Datei C:\umsatz.dat gespeichert werden. Ich habe schon in Tutorials nachgelesen und da was von ASSIGN und REWRITE gelesen, aber ich kriege es nicht hin. Daher wäre ich für jede Hilfe dankbar.

PROGRAM Umsaetze;
USES crt;

type tumsaetze = array[1…12] of record
umsatz : real;
prozent : real;
end;

var i : integer;
umsatz, GESAMT : real;
einlesen : tumsaetze;
BEGIN
clrscr;
Gesamt:= 0;
FOR i:= 1 TO 12 DO
BEGIN
WRITE (‚Bitte geben Sie den ‚,i,‘. Umsatz ein:‘);
READLN (einlesen[i].Umsatz);
END;
FOR i:= 1 TO 12 DO
BEGIN
Gesamt := Gesamt+einlesen[i].Umsatz;
END;
FOR i:= 1 TO 12 DO
BEGIN
einlesen[i].Prozent:= einlesen[i].Umsatz *100 /Gesamt;
END;

FOR i:= 1 TO 12 DO
BEGIN
WRITELN (‚Umsatz ‚,i,‘ betrug ‚,einlesen[i].Umsatz:1:2,‘‘);
WRITELN (‚Prozentualer Anteil: ‚,einlesen[i].Prozent:1:2,‘‘);
END;
READKEY;
END.

Hi Marc,

ich hab Dein Programm mal etwas aufgepeppt. Guck es Dir an, und versuche, die Änderungen zu verstehen. Wenn Du Fragen dazu hast, dann stelle sie einfach hier.

Gruß
Martin

(die „~“-Zeichen bitte mit „Suchen&Ersetzen“ im Pascal-Editor entfernen; ich habe sie eingefügt, weil der Wer-Weiss-Was-Postingtext-Prozessor alle Leerzeilen killt)

PROGRAM Umsaetze;
~
USES crt;
~
~
CONST
 FILENAME = 'C:\UMSAETZE.TXT';
~
 N = 12;
~
~
TYPE
 TUmsatz
 = RECORD
 Betrag : real;
 Prozent: real;
 END;
~
~
VAR Umsatz: ARRAY[0..N-1] OF TUmsatz;
~
~
(\* \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* \*)
~
PROCEDURE Eingabe;
~
VAR
 i: INTEGER;
~
begin
 FOR i:= 0 TO N-1 DO
 BEGIN
 WRITE ('Bitte geben Sie den ',i+1,'. Umsatz ein: ');
 READLN (Umsatz[i].Betrag);
 END;
~
 WRITELN;
 WRITELN
end;
~
~
(\* \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* \*)
~
PROCEDURE Berechnung;
~
VAR
 i: INTEGER;
 g: REAL; (\* Gesamt \*)
~
begin
 g:= 0;
~
 FOR i:= 0 TO N-1 DO
 BEGIN
 g := g+Umsatz[i].Betrag;
 END;
~
 FOR i:= 0 TO N-1 DO
 BEGIN
 Umsatz[i].Prozent:= Umsatz[i].Betrag\*100/g;
 END
end;
~
~
(\* \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* \*)
~
PROCEDURE AusgabeAufScreen;
~
VAR i: INTEGER;
~
begin
 FOR i:= 0 TO N-1 DO
 BEGIN
 WRITELN ('Umsatz ',i+1,' betrug ',Umsatz[i].Betrag:1:2,'');
 WRITELN ('Anteil: ', Umsatz[i].Prozent:1:2, ' %');
 WRITELN
 END;
end;
~
~
(\* \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* \*)
~
PROCEDURE AusgabeInDatei;
~
VAR
 f: TEXT;
 i: INTEGER;
~
begin
 Assign(f, FILENAME);
~
 ReWrite(f);
~
 FOR i := 0 TO N-1 DO
 begin
 WRITELN (f, 'Umsatz ',i+1,' betrug ',Umsatz[i].Betrag:1:2,'');
 WRITELN (f, 'Anteil: ', Umsatz[i].Prozent:1:2, ' %');
 WRITELN (f, '')
 end;
~
 Close(f)
end;
~
~
(\* \*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*\* \*)
~
BEGIN
 clrscr;
~
 Eingabe;
 Berechnung;
 AusgabeAufScreen;
 AusgabeInDatei;
~
 READKEY
END.

Hallo Martin.

Erst einmal vielen Dank, dass du dir die Arbeit gemacht hast. Es hat mich wirklich weitergebracht.

Da ist noch eine Kleinigkeit, aber das wird wohl schnell gehen. Ich möchte die Daten (also hier nur das Record) so abspeichern, dass sie auch von anderen Programmen gelesen und bearbeitet werden könnten. Ich denke, dass das nur *.dat Dateien können (bin mir da aber auch nicht sicher). Ich denke, dafür müsste die Variable f in Procedure AusgabeinDatei anders definiert werden, aber ich weiß nicht wie?

Da sind noch 2 allgemeine Fragen, die mir beim Lesen deines Codes aufgefallen sind:

Du lässt alle Schleifen von 0 bis Konstante-1 ablaufen. Was für Vorteile hat das?
Muss ich alle Dateipfade, deren zugehörigen Daten ich zu verwenden gedenke zu Beginn als Konstante definieren, oder würde es auch genügen im ASSIGN Befehl den Dateipfad zu nennen?

Nochmals vielen Dank,

Marc

[Bei dieser Antwort wurde das Vollzitat nachträglich automatisiert entfernt]

Hi Marc,

Ich möchte die Daten (also hier nur das Record) so
abspeichern, dass sie auch von anderen Programmen gelesen und
bearbeitet werden könnten. Ich denke, dass das nur *.dat
Dateien können (bin mir da aber auch nicht sicher).

Nein, das hat mit der Dateiendung nichts zu tun.
Die meisten Programme verstehen Dateien, die Text enthalten. Genau dies tut die (mit VAR f:Text;…) angelegte Datei, es handelt es sich also um eine Textdatei. Du könntest diese Datei einfach mal im Editor öffenen.

Ich denke,
dafür müsste die Variable f in Procedure AusgabeinDatei anders
definiert werden, aber ich weiß nicht wie?

Nein, das brauchst du nicht. Wenn du willst, dass die Datei mit ‚dat‘ endet, dann ändere den Dateinamen (‚C:\UMSAETZE.TXT‘ in ‚C:\UMSAETZE.DAT‘). Das ändert nichts daran, dass es sich um eine normale Textdatei handelt.

Du kannst auch eine „Typisierte Datei“ erstellen mit

VAR
 f: File of TUmsatz;
...
 FOR i := 0 TO N-1 DO
 begin
 WRITELN (f, Umsatz[i]);
 end;
...

aber dann kannst du die Daten nur mit deinem Programm wieder lesen, weil andere dieses Dateiformat nicht kennen.

Da sind noch 2 allgemeine Fragen, die mir beim Lesen deines
Codes aufgefallen sind:

Du lässt alle Schleifen von 0 bis Konstante-1 ablaufen. Was
für Vorteile hat das?

Keine. Konventionsgemäß beginnen Indizes immer bei Null. Wenn man sich daran hält, kommt man nicht durcheinander, was nun bei Null und was bei Eins beginnt. Das wäre der einzige Vorteil.

Muss ich alle Dateipfade, deren zugehörigen Daten ich zu
verwenden gedenke zu Beginn als Konstante definieren, oder
würde es auch genügen im ASSIGN Befehl den Dateipfad zu
nennen?

Ja, das würde reichen. Du kannst auch im Programm nach einem Dateinamen fragen (zB mit ReadLn) und diesen in einer String-Variable speichern und diese Variable im ASSIGN Befehl einsetzen (dann kann der Anwender zur Laufzeit bestimmen, in welche Datei die daten gespeichert werden sollen).
Gruß
Jochen

Hallo Marc,

die Sache mit dem Dateiformat (Stichwort „typisierte Datei“) hast Du ja schon von Jochen erklärt bekommen.

Du lässt alle Schleifen von 0 bis Konstante-1 ablaufen. Was
für Vorteile hat das?

Ein einstelliger Dezimalzähler kann 10 verschiedene Zustände annehmen, nämlich „0“, „1“, „2“, „3“, „4“, „5“, „6“, „7“, „8“ und „9“. Somit „zählt“ er nicht von 1 bis 10, sondern von 0 bis 9. Ein zweistelliger Dezimalzähler kann 100 verschiedene Zustände annehmen, und zwar „00“, „01“, „02“… „98“, „99“. Er zählt also von 0 bis 99.
Computer funktionieren mit dualen Zählern (Basis 2), aber auch die zählen von Null ab. Von 0 bis 255 zählt ein Byte-Zähler (acht Bits), von 0 bis 65535 zählt ein 16-Bit-Zähler, und von 0 bis 1 kann ein einstelliger Dualzähler „zählen“.

Fazit: Die „natürliche“ Art zu zählen ist, bei Null anzufangen, nicht bei Eins, wie es die Menschen gemeinhin tun. Alle (guten) Programmierer halten sich deshalb an die sinnvolle (!) Konvention, Schleifen immer von „0“ bis „N – 1“ laufen zu lassen. Wenn eine Schleife zwölf mal durchlaufen werden soll, dann sollte dies

also nicht so:

FOR i := 1 TO 12 DO ...

und auch nicht so:

FOR i := 0 TO 11 DO ...

sondern so programmiert werden:

CONST N = 12;
...
FOR i := 0 TO N-1 DO ...

Diesen Stil solltest Du Dir frühzeitig angewöhnen. Auf den ersten Blick sieht das sicher etwas „seltsam“ aus, aber Du wirst es schnell verinnerlicht haben. Fortan wird Dir dann ein „FOR k := 1 TO 1000 DO“ in fremdem Code sofort ins Auge springen („Hoppla, da war ein Laie am Werk!“). Irgendwann wirst Du vielleicht auch auf Funktionen aus Codebibliotheken zurückgreifen; spätestens dann sollte Dir in Fleisch und Blut übergegangen sein, daß das erste Element eines Arrays den Index Null hat („a[0]“) und das n-te Element den Index n – 1 („a[n–1]“).

Muss ich alle Dateipfade, deren zugehörigen Daten ich zu
verwenden gedenke zu Beginn als Konstante definieren, oder
würde es auch genügen im ASSIGN Befehl den Dateipfad zu
nennen?

Der Compiler hat nichts dagegen, wenn Du den Dateipfad direkt in den Assign-Befehl schreibst (wie Du durch Ausprobieren leicht feststellen kannst), aber Du solltest etwas dagegen haben!

Warum? Nehmen wir an, Du möchtest den Benutzer des Programms noch durch eine Meldung informieren, daß die Ausgabe auch in die besagte Datei geschrieben wurde. Das sähe dann so aus:

begin
 Assign(f, 'UMSAETZE.TXT');
 ReWrite(f);
 FOR ... TO ... DO 
 begin
 ...
 end;
 Close(f);
 WriteLn('Ausgabe wurde in die Datei "UMSATZE.TXT" geschrieben!");
end;

Vielleicht gibt es noch weitere Aktionen, an denen die Datei „UMSAETZE.TXT“ beteiligt ist. Niemand verbietet Dir, den Namen jedes mal „direkt“ dorthin zu schreiben, wo er gebraucht wird. Nur: Dein Programm umfaßt eventuell irgendwann mal 4000 (oder auch 40000) Codezeilen, und 29 mal steht darin „verstreut“ an allen möglichen und unmöglichen Stellen der String „UMSAETZE.TXT“. Es ist unterdessen auch so leistungsfähig geworden, daß der Name „UMSATZE.TXT“ gar nicht mehr paßt, und Du ihn in „BILANZ.TXT“ abändern willst. Dann bleibt Dir nichts anderes übrig, als Deinen Code nach allen 29 Einträge zu durchforsten, um sie dann alle zu ändern. Klar, das geht auch automatisch mit Suchen&Ersetzen. Aber: 4 von den 29 Einträgen werden sich vielleicht in einer anderen Datei befinden (der Code eines Programms kann auf mehrere Dateien aufgeteilt werden, z. B. Include-Dateien oder Units), und Du kannst nicht ausschließen, einen davon aus irgendeinem „ganz dummen“ Grund zu übersehen. Und schwupps, hast Du einen hübschen Bug im Programm. Was Du möglicherweise erstmal gar nicht gar bemerken wirst. Wenn sich ein Benutzer Deines Programs dann drei Wochen später beschwert, daß da was nicht stimmt, und Du anfangen mußt, über den Grund dafür nachzudenken (während Du schon am nächsten Projekt arbeitest), hast Du die „banale“ Suchen&Ersetzen-Aktion natürlich längst vergessen.

Erfahrene Programmierer wissen, daß „direktes Hineinschreiben“ von z. B. Dateinamen in Code Fehler der beschriebenen Art geradezu provozieren. Und wer sich mal über ein (zwei, drei…) Stündchen mit mühsamer, lästiger und frustrierender Fehlersuche verschwendete Zeit ärgern durfte (und intelligent genug ist, daraus zu lernen), wird künftig nie wieder

begin
 Assign(f, 'UMSAETZE.TXT');
 ReWrite(f);
 FOR ... TO ... DO 
 begin
 ...
 end;
 Close(f);
 WriteLn('Ausgabe wurde in die Datei "UMSATZE.TXT" geschrieben!");
end;

sondern immer

CONST 
 FILNAME = 'UMSAETZE.TXT';
~
begin
 Assign(f, FILENAME);
 ReWrite(f);
 FOR ... TO ... DO 
 begin
 ...
 end;
 Close(f);
 WriteLn('Ausgabe wurde in die Datei '+FILENAME+' geschrieben!");
end;

schreiben. In der zweiten, „guten“ Version ist die Gefahr durch 29 verstreute Dateinamen prinzipiell eliminiert. Und Du hast es auch viel leichter, möchtest Du statt „UMSAETZE.TXT“ mal „BILANZ.TXT“ haben: Die Änderung einer einzigen Zeile (der CONST-Zeile) genügt. Ein minimaler Aufwand bringt hier einen Riesengewinn in punkto Sicherheit, und das ist gar nicht hoch genug einzuschätzen. Programmieren ist nämlich von Haus aus ein so fehleranfälliges Geschäft, daß man einfach jede Möglichkeit, Fehler (und damit möglicherweise eine unnötige und sehr teure spätere Fehlersuche) von vornherein zu vermeiden, unter allen Umständen ausnutzen muß. Dateinamen aus Code „herauszuhalten“ zählt deshalb zu den Selbstverständlichkeiten guten Programmierens. Profis gehen übrigens noch viel weiter: Sie halten sogar alle Zahlenkonstanten – bis auf „0“ und „1“ – peinlich sorgfältig aus ihrem Code heraus. Sie würden also nicht schreiben (nur als Beispiel):

Jahreslohn := 220 \* Tageslohn;

sondern

CONST 
 ARBEITSTAGPROJAHR = 220;
...
Jahreslohn := ARBEITSTAGPROJAHR \* Tageslohn;

Und noch etwas: Es gibt keine untere Grenze für die Größe eines Programms, unterhalb derer sich das Beherzigen dieses Tips nicht lohnen würde. Der Einwand „Mein Gott, mein Proggi hat lächerliche 200 Zeilen, da werde ich doch nicht mit „CONST FILENAME = …“ anfangen“, gilt nicht! Es ist auch bei einem Programm mit 10 (ja, 10!) Zeilen sinnvoll.

Vielleicht noch ein paar Tips zur Code-Stilistik:

  • Schalte das Syntax-Highlighting Deines Editors ein. Bewährt hat sich unter anderem folgendes Farbschema:

Schlüsselwörter grün, Strings + Zahlkonstanten rot, Kommentare hellgrau, Inline-Assemblercode weiß oder pink, alles andere gelb. Hintergrund dunkelblau.

Rotdarstellung von Strings und Zahlkonstanten hat den Vorteil, daß man es sich dann zur Aufgabe machen kann, alles (bis auf „0“ und „1“) was rot wird, in CONST-Teile „auszulagern“.

  • Schreibe die Pascal-Schlüsselwörter durchgehend groß:

    PROGRAM, UNIT, CONST, VAR, FOR, TO, DO, PROCEDURE, FUNCTION…

Ausgenommen:

begin
end
  • Schreibe Konstanten (= alles, was mit „CONST“ vereinbart wurde) ebenfalls durchgehend groß.

  • Schreibe Namen von Variablen und Namen von Prozeduren und Funktionen gemischt: WriteLn, Assign, AusgabeInDatei, Berechnen.

  • Aufeinanderfolgende Prozeduren und Funktionen kann das Auge viel besser voneinander abgrenzen, wenn man ihm mit Trennern wie

    (* *************************************************** *)

dabei hilft.

  • Keine Leerzeichen bei Klammern:

    „(a, b, c, d)“; nicht: „( a, b, c, d )“

  • nach Kommas ein Leerzeichen:

    „(a, b, c, d)“; nicht: „(a,b,c,d)“

  • vor und nach Zuweisung „:=“ ein Leerzeichen:

    „a := b“; nicht: „a:=b“

  • bei Vergleichsoperatoren kein Leerzeichen:

    „IF (a=b) OR (cd) THEN…“; nicht: „IF (a = b) OR (c d) THEN…“

  • bei Rechenzeichen kann man Leerzeichen verwenden oder nicht:

    „p-q+r-s“, „a*x*x*x + b*x*x + c*x + d“

  • Einrücktiefe genau zwei Zeichen. Das Einrücken sollte so vorgenommen werden:

    FOR i := 0 TO N-1 DO
    begin
    TueIrgendwas(i);
    TueNochIrgendwasAnderes(i)
    BeendeTuen(i)
    end;

Nicht:

FOR i := 0 TO N-1 DO
 begin
 TueIrgendwas(i);
 TueNochIrgendwasAnderes(i)
 BeendeTuen(i)
 end 

und nicht:

FOR i := 0 TO N-1 DO
begin
 TueIrgendwas(i);
 TueNochIrgendwasAnderes(i)
 BeendeTuen(i)
end 

und auch nicht (brrr… schrecklich):

FOR i := 0 TO N-1 DO
 begin
 TueIrgendwas(i);
 TueNochIrgendwasAnderes(i)
 BeendeTuen(i);
 FOR k := 0 TO TOTALCOUNT-1 DO
 begin
 TueWasNeues(i, k)
 ... 
 end
 end 

Manche Programmierer pflegen folgenden Einrückstil:

FOR i := 0 TO N-1 DO begin
 TueIrgendwas(i);
 TueNochIrgendwasAnderes(i)
 BeendeTuen(i)
end;

Darüber kann man sich streiten. Mir gefällt es überhaupt nicht, weil ich finde, daß das „begin“ es nicht verdient hat, in die hinterletzte Ecke geschrieben zu werden, wo es doch mit dem „end“ logisch auf derselben Stufe steht. Ich sehe jedenfalls keinen Grund, es nicht auch optisch auf dieselbe Stufe zu stellen:

FOR i := 0 TO N-1 DO
 begin
 TueIrgendwas(i);
 TueNochIrgendwasAnderes(i)
 BeendeTuen(i)
 end;

So, das soll mal genügen (sorry, so viel wollte ich eigentlich gar nicht schreiben).

Mit freundlichem Gruß
Martin

Danke euch beiden!
Vielen Dank für die zahlreichen Tipps und Erklärungen. Im Moment komme ich recht gut voran, hoffentlich geht das so weiter, damit ich im nächsten Februar wirklich zu 100% auf meine Klausur vorbereitet bin, die im Anspruch bis zu den Pointern geht.

Ich hab vor Jahren schon ein wenig mit QBasic gearbeitet, das fand ich wirklich viel leichter als Pascal.

Hallo Martin,

wollte eigentlich nur was zu dem Thema For … to Schleife sagen. Ich finde es etwas krass, jeden als Laien abzustempeln der die For Schleife mit 1 anfängt und nicht mit 0. Außerdem, denke ich, läßt sich das so pauschal gar nicht sagen. Zum Beispiel bei Dynamischen Array’s. Oder wenn ich in einem Programm zehn oder mehr Zählschleifen hab, würde ich nicht für jede extra ein Konstante programmieren.

MFG Murdoc

Hallo Murdoc,

Ich finde es etwas krass, jeden als Laien abzustempeln
der die For Schleife mit 1 anfängt und nicht mit 0.

mhh, fühlst Du Dich angesprochen? :wink: Also nochmal: Computer zählen von Haus aus von Null ab (ich hoffe, Du stimmst mir darin zu), und es hat Vorteile, das auch so zu programmieren. Beispiel 1: Dein Target ist ein 8-Bit-Microcontroller. Schon ist Essig mit „FOR i := 1 TO 256 DO …“, denn 256 paßt nicht mehr in einen 8-Bit-Zähler. Möchtest Du alle 256 Zustände durchlaufen, geht’s mit „FOR i := 0 TO 255 DO …“ und nur so. Beispiel 2: Eine fundamentale Klasse von Delphi ist „TList“. Sie ist so etwas wie ein dynamisches Array. Zitat aus der Doku: „The Index parameter indicates the index of the object, where 0 is the index of the first object […]“.

Das erste Element eines Arrays a gehört in a[0], das n-te in a[n–1], und eine Schleife dazu läuft gemäß „FOR k := 0 TO n–1 DO Tueirgendwas mit a[k]“. Immer! Es so zu programmieren ist nicht schwer und tut nicht weh, erspart aber unnötige Umgewöhnung und/oder Verrenkereien, so man z. B. mal eine Codebibliothek benutzen oder einen Mikrocontroller programmieren will (aber welcher Laie will das schon? :wink:).

Oder wenn ich in einem
Programm zehn oder mehr Zählschleifen hab, würde ich nicht für
jede extra ein Konstante programmieren.

Nein, natürlich nicht für jede Schleife extra, sondern je eine „Zählerendstandskonstante“ für alle Schleifen, die denselben Zähler bemühen! Es ist doch so, daß man zwar i. a. eine Unmenge Schleifen in einem Programm hat, aber nur relativ wenige „Anzahl-Konstanten“, weil viele Schleifen vom selben Typ sind. Diese wenigen Konstanten fühlen sich dann in einem CONST-Teil wohl, weil sie dort ein „Gesicht“, sprich einen aussagekräftigen Namen bekommen, und weil sie dort alle zusammen sein können.

Grüße
Martin