586.094 aktive Mitglieder*
4.248 Besucher online*
Kostenfrei registrieren
Einloggen Registrieren

Anwenderprogrammierung in ANSI-C, Programmierung eines nicht banalen Anwenderprogramms

Beitrag 16.02.2012, 13:02 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Kleine Anmerkung zu der Notation der Tastaturabfrage und eine Präzisierung folgender Aussage, warum das Programm "herausspringt". Das macht es natürlich nicht automatisch, sondern aus bestimmten Gründen.

void liestaste(struct s_taste *taste)
{
char c,d;
taste->lo=0;taste->hi=0;taste->st=0;
c=getch();
if ((int)c>0)
{
if ((int)c>31)taste->lo=(int)c;
else taste->st=(int)c;

Die Bezeichner taste->lo deuten darauf hin, daß hier mit einem Pointer zugegriffen wird. Zeigt ein Pointer auf eine Struktur, werden die einzelnen Felder nicht mit strukturbezeichner.feld, sondern mit pointer->feld gekennzeichnet. Das ist auf den ersten Blick ungewöhnlich, aber sehr hilfreich, weil man sofort sieht, was gemeint ist. Den Pointer erkennt man im Funktionskopf an dem *Operator.

Nun zu der Frage, wie das Programm "herausspringt".

Das Thema wurde hier schon mal angesprochen. Es liegt dran, daß man die Eingabe mit hinereinandergeschalteten SWITCH-Blöcken verwertet, also alles von oben nach unten bis in das Kellergeschoß durchfällt. Der tiefere Grund, warum man sowas macht wink.gif ist:

Man will das Kind nicht in ein Dutzend Windeln packen a la if/else if/else if, was das Programm unleserlich macht und nicht gerade der Stabilität dienlich ist.

Die Struktur:

lies ein Zeichen

1) switch den druckbaren Wert (char)
case sonstwas mach irgendwas

2)switch den nicht druckbaren Wert (der int vom char)
case sonstwas mach irgendwas

3)switch sonstige Parameter
case sonstwas mach irgendwas

Nehmen wir an, wir sind in einem Texteingabefeld.

Wenn der erste Switch-Block den Großbuchstaben H erkennt, fügt er ihn in den Text ein und das war es auch schon, bzw. WÄRE ES GEWESEN, wenn der Großbuchstabe H nicht weiter nach unten durchrutschen würde und im Switch-Block 2 ein zweites Mal interpretiert mit dem (int) 72 = PFEILOBEN, worauf dann eine weitere (ungewünschte) Reaktion des Programms erfolgt.

Man kann diese Dinge abfangen, indem man den Switch-Block 1 mit CONTINUE abschließt, allerdings, continue wäre dann an Bedingungen gebunden, was if-else-if zur Folge hätte, möglicherweise in jedem case-Verzweiger, was SCHxxx aussieht und zweitens ist eine solche Codierung natürlich im höchsten Maße LABIL. Es muß nur die continue-Zeile versehentlich gelöscht oder mit einer nicht-hinreichenden-Bedingung versehen sein, und das PRogramm entwickelt ein Eigenleben.

STABIL ist es, JEDEN WERT von oben bis unten durch alle Switch-Blöcke fallen zu lassen, ohne daß was anbrennt.

Das wurde bzgl. der Tastatureingabe nun so gelöst, daß wir differenzieren zwischen alphanumeric und Sonderzeichen, so daß die Abfrage nicht mehr auf das Zeichen geht, sondern auf die Differenzierung. Formulieren wir

1) switch (char)taste.lo

2) switch taste.st

3) switch taste.hi

Brennt da überhaupt nichts an.

Sprunganweisungen wie goto oder continue sind mit ALLERGROESSTER VORSICHT zu genießen, weil sie die Strukturierung des Programms aufweichen. Man rettet sich sozusagen mit Sprüngen vor den Schlaglöchern, anstelle daß man die Schlaglöcher beseitigt.

In dem Bereich befindet sich auch die sehr beliebte Methode (beliebt, weil keine unendlichen if/else/if Verzweiger), wenn ein Problem gelöst ist, mit return aus der Funktion rauszuspringen. Das gilt als SAUBER. Jeder macht es. Aber STABIL ist es nicht. Denn wie bei den Sprunganweisungen innerhalb der Funktion: wenn diese return-Anweisung bei einer Programmüberarbeitung an die falsche Stelle verrutscht ... es ist genauso eine Krücke.

Um die Programmierung überschaubar zu halten, meiner Meinung nach, ist es viel besser, die Lösung des Problems in kleine Blöcke mit geringer Verschachtelungstiefe aufzuteilen. In der Regel hat man dann solche Probleme überhaupt nicht.



Fehlerkorrektur: bei doppelt belegter Taste ist der erste Wert nicht immer 0, wie oben steht, sondern er ist 0 oder kleiner als 0. Das wird in der Funktion aber schon berücksichtigt.


Der Beitrag wurde von sharky bearbeitet: 16.02.2012, 13:14 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 16.02.2012, 17:01 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Erster Probedurchgang der Buchungsmaske

Nachdem das Programm soweit durchstruktuiert ist in Bausteine, ist die Inbetriebnahme wenig Aufwand. Man muß nur die einzelnen Bausteine in passender Kombination aufrufen.

Die eigentliche Datenerfassung ist dann PEANUTS.

Hier mal eine Folge von 9 Screenshots.

Abb1. zeigt den Bildschirm für Buchungssatz bearbeiten, unten die Navigationshilfe und im Dialog den genullten Datensatz. Dieser ist neu, erkenntlich daran, daß alle Felder auf 0 stehen, mit Ausnahme des Datums, das auf 1.1.12 steht.

Erster Schritt bei einem neuen Datensatz das Datumsfeld. Das muß nicht sein, wir können mit den PFeiltasten wahlfrei auf jedes Feld zugreifen, aber da der Datensatz leer ist, fangen wir einfach mal oben an und geben das Datum ein. Dann drücken wir, damit das übernommen wird, ENTER, Eingabetaste.

Und gelangen so in die Abb. 2, ein Datenfeld weiter.

Wir sehen, daß SCHWUPPS, im rechten Bildchirmbereich sich der Kontenrahmen SKR03 aufgebaut hat. Falls wir nicht wissen, welches Konto wir benötigen. Der steht aber nicht nur einfach so da, sondern, da er zu lang ist für den Bildschirm, kann man dort scrollen.

Wir haben für die Belegschaft belegte Brötchen gekauft und wollen die unter freiwillige soziale Aufwendungen verbuchen, wissen aber nicht die passende Kontonummer.

Deshalb drücken wir jetzt gem. Navigationshilfe unten F1, um rechts in das Feld SKR03 zu gelangen. Daß wir drüben sind, sehen wir unten im Mitteilungsfenster, wo nämlich nun die Navigationshilfe für den Kontenrahmen eingeblendet wird. (Abb. 3)

Wir scrollen das Fenster nun soweit, bis unsere gesuchte Konto-Nr. oben am Bildschirmrand erscheint. Und gehen mit ESCAPE wieder raus und landen links in unserem Buchungsfeld und geben die gesuchte KOnto-Nr. ein. Daß wir wieder zurück sind, erkennen wir daran, daß das gelbe Mitteilungsfenster keine Navigationshilfe mehr zeigt (hier übersprungen), und nach der Eingabe wird im Dialogfenster ohnehin der passende Text zu dem gewählten Konto gezeigt. Hier befinden wir uns bereits im nächsten Feld, dem Gegenkonto. (Abb 4)

Die Brötchen wurden bar bezahlt (Kasse). Jetzt tun wir mal so, als wüßten wir die Konto-Nr. für Kasse nicht, und gehen wieder rüber und scrollen das Gegenkonto. Das ist natürlich Quatsch, Konto und Kasse 1200 und 1000 kennt man, nur mal zu Demo, könnte ja auch ein ungewöhnliches Konto sein. Der Scroll ist in der Abb. 5 vollzogen, das gewählte Konto steht wieder oben am Bildschirmrand.

Das nächste Feld wäre dann der Betrag, was die Brötchen gekostet haben.

Stichwort KONTEXTSENSITIVE HILFESTELLUNG: Im Gegensatz zu vielen Windows-Anwendungen, die alle als eierlegende Säue programmiert wurden, und bei denen man sich endlos durchklicken muß (ich hasse das wie die Pest):

Wenn wir den Kontenrahmen brauchen, erscheint er automatisch, ohne daß wir klicken müssen. Brauchen wir ihn nicht mehr, ist er verschwunden, ohne daß wir ihn wegklicken müssen. Für den Betrag wird er nicht mehr benötigt, daher in Abb 6 GANZ AUTOMATISCH nicht mehr zu sehen.

Nun können bzw. sollten wir einen Kommentar eingeben, um die Ausgabe zuzuordnen. Das geschieht in Abb. 7.

Die Eingabe wird abgeschlossen mit der Umsatzsteuer-Option. Auf Lebensmittel liegt die ermäßigtte UST von 7 Prozent. Um das auszusuchen, wechselt das rechte Fenster

KONTEXTSENSITIV

in die Auswahl der USt-Optionen (Abb. 8)

Zugleich ist der Datensatz hier komplett und wir könnten mit F10 quittieren, woraufhin wir gefragt werden würden (unten im Mitteilungsfenster) ob wir ihn speichern wollen.

Wir können aber auch, bevor wir das tun, mit den Pfeiltasten noch durch die Felder "durchhuschen" und die eine oder andere Änderung durchführen. Wohlgemerkt, WAHLFREI, nicht von oben nach unten, sondern jedes Feld läßt sich frei anspringen und korrigieren, und jedesmal erscheint KONTEXSENSITIV im rechten Fenster die richtige Hilfe dazu.

Somit ist die Buchungsmaske, der Hauptbaustein des Programms, eigentlich fertig.

Wie schon gesagt, DIESELBE Buchungsmaske verwenden wir, wenn wir einen vorhandenen Datensatz editieren wollen. Dann erscheinen dort keine NULL-Werte, sondern die bereits gespeicherten.

Daß man DIESELBE Funktion benutzt und KEINEN KLON leitet sich aus dem Verbot von redundantem Code ab, ist sinnvoll und praktisch und erhöht die Stabilität des Programms, wie weiter oben schon mehr als oft genug gesagt wurde.

Das Programm ist, wie man sieht, im Design schlicht und unauffällig gehalten, verfügt statt über dröhende Oberfläche mit viel KLIMBIM vielmehr über das, worauf es ankommt, eine INTELLIGENTE und KONTEXTSENSITIVE und ANGENEHME Bedienerführung. Jedenfalls so, wie ich das als angenehm empfinde. Ist eben auch Geschmackssache. wink.gif

Der Beitrag wurde von sharky bearbeitet: 16.02.2012, 17:10 Uhr
Angehängte Datei(en)
Angehängte Datei  erster_durchlauf.jpg ( 108.46KB ) Anzahl der Downloads: 6
Angehängte Datei  erster_durchlauf2.jpg ( 243.94KB ) Anzahl der Downloads: 5
Angehängte Datei  erster_durchlauf3.jpg ( 262.34KB ) Anzahl der Downloads: 4
Angehängte Datei  erster_durchlauf4.jpg ( 251.83KB ) Anzahl der Downloads: 10
Angehängte Datei  erster_durchlauf5.jpg ( 272.37KB ) Anzahl der Downloads: 5
Angehängte Datei  erster_durchlauf6.jpg ( 121.83KB ) Anzahl der Downloads: 4
Angehängte Datei  erster_durchlauf7.jpg ( 127.46KB ) Anzahl der Downloads: 3
Angehängte Datei  erster_durchlauf8.jpg ( 157.33KB ) Anzahl der Downloads: 12
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 16.02.2012, 17:21 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Nun es ist klar, die Berechnung der enthaltenen Vorsteuer erfolgt automatisch, muß nicht eingegeben werden, gem. der gewählten UST-Option. Mit dem Eingabefeld Ust-Option ist daher die Eingabe beendet.

Die Ident-Nr für die Buchung wird vom Programm automatisch vergeben (ist hier noch nicht realisiert).

Bei der Herausrechnung des USt-Anteils muß man runden: US-amerikanisch werden 0.5 Cent abgerundet, in Europa aber aufgerundet. Hierzu ist eine eigene Rundungsfunktion erforderlich, sonst ergeben sich Abweichungen im Cent-Bereich.

Für die Umsatzsteuer-Optionen wurde ein eigener Fensterbereich definiert.Die Initialisierung dieses Fensters erfolgt beim Programmstart und sieht so aus:

void uebertrage_ust_optionen()
{
resetSCRBUF(USTO);

my_puts(USTO, "0 = ohne Steueroption",1,1);
my_puts(USTO, "1 = 7% aufzuteilen",1,3);
my_puts(USTO, " Konto 1561",1,4);
my_puts(USTO, "2 = 19% aufzuteilen",1,6);
my_puts(USTO, " Konto 1566",1,7);
my_puts(USTO, "3 = 7% sofort abziehbar",1,9);
my_puts(USTO, " Konto 1571",1,10);
my_puts(USTO, "4 = 19% sofort abziehbar",1,12);
my_puts(USTO, " Konto 1576",1,13);
my_puts(USTO, "5 = 7% abzufuehren",1,15);
my_puts(USTO, " Konto 1771",1,16);
my_puts(USTO, "0 = 19% abzufuehren",1,18);
my_puts(USTO, " Konto 1776",1,19);

}

Der Aufruf im laufenden Programm erfolgt dann mit der Befehlszeile:

showSCRBUF(USTO,0); // der parameter ,0 ist formal und hat, wenn das Fenster nicht scrollbar ist, keine Bedeutung

anstelle, wie gesehen, dem dort befindlichen Fenster SKR03, Kontenrahmen.

Der Aufwand ist eben nur diese eine Zeile.

Das ist der Vorteil, wenn das Programm stark formalisiert wurde und die Bausteine in sich abgeschottet und so selbstständig sind, daß man sie ohne weitere Voraussetzungen jederzeit aufrufen kann.

Der Beitrag wurde von sharky bearbeitet: 16.02.2012, 17:25 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 17.02.2012, 10:17 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Die Montage der Bausteine

Wenn die Funktionen gut strukturiert sind, d.h., ihre Daten gekapselt und die Aufgabenverteilung klar geregelt ist, dann ist die Inbetriebnahme eigentlich nur eine Montage von Bausteinen.

Sehen wir mal auf das Hauptmenü:

void hauptmenu()
{
struct s_taste taste;
char i;
char c;
do
{
clrSCRBUF(MENU);clrSCRBUF(DIAL);clrSCRBUF(BOX2);
my_puts(MENU,"Hauptmenue",50,1);
my_puts(DIAL,"Neue Buchungssaetze eingeben F1",15,2);
my_puts(DIAL,"Buchungen ueberarbeiten F2",15,4);
my_puts(DIAL,"Kontenjournal F3",15,6);
my_puts(DIAL,"Primanota F4",15,8);
my_puts(BOX2,"Beenden mit ESCAPE",20,2);
showSCRBUF(MENU,0); showSCRBUF(DIAL,0); showSCRBUF(BOX2,0);
liestaste(&taste);
if (taste.st==F1) buchung_neu();
if (taste.st==F2) buchung_edit();
}while (taste.st!=ESCAPE);

}

Da sind also bisher nur die Funktionen buchung_neu() und buchung_edit() in Betrieb gegangen.

Wenn der Datensatz neu ist, muß er mit Null-Werten u. Leerzeichen gefüllt werden, wird dann bearbeitet und an die Datei angehängt.

Ist der Datensatz schon gespeichert, muß er aus der Datei herausgesucht, bearbeitet und wieder an die Stelle der Datei geschrieben werden, wo er stand.

Der Unterschied liegt also in der Vorgabe der Daten sowie ihrem Speichermodus.

Der Aufbau ist so:

buchung_neu()
nulle einen neuen Datensatz aus
bearbeite ihn mit bsatz_edit()
hänge ihn an die Datei an

buchung_edit()
suche einen Datensatz aus der Datei
bearbeite ihn mit bsatz_edit()
überschreibe ihn in der Datei, wo er gestanden hat

Beide benutzen die (recht umfangreiche) Funktion bsatz_edit. Es ist aber klar, daß eben darum bsatz_edit() die Speicherung der Daten nicht übernehmen darf, damit sie als neutraler Baustein verfügbar bleibt. Man packt nicht soviel in eine Funktion rein, wie möglich, sondern soviel wie nötig und sinnvoll.

bsatz_edit hat den Funktionskopf:

int bsatz_edit(struct s_bsatz *data)

und liefert OK oder NOTOK zurück. Die Prüfung der Eingabe erfolgt zentral am Schluß, wenn der Anwender die Bearbeitung beenden will:

int pruefe_buchungssatz(struct s_bsatz *data)
{
int i;
int result=OK;
int error[5]={0,0,0,0,0};
if (pruefe_datum(data->datum)==NOTOK) error[0]=1;
if (data->konto_nr<=0||data->gkonto_nr<=0)error[1]=1;
if (data->konto_nr==data->gkonto_nr)error[2]=1;
if (data->ust_option>0&&data->ustkonto<=0)error[3]=1;
if (ist_null(data->brutto)==JA)error[4]=1;
for (i=0;i<5;i++)if(error[i]!=0)result=NOTOK;
if (result==NOTOK)
{
clrSCRBUF(MBOX);
if (error[0]==1)my_puts(MBOX,"Fehler: Datum",1,0);
if (error[1]==1)my_puts(MBOX,"Fehler: Nullkonto",1,1);
if (error[2]==1)my_puts(MBOX,"Fehler: Kontengleichheit",1,2);
if (error[3]==1)my_puts(MBOX,"Fehler: USt-Konto",1,3);
if (error[4]==1)my_puts(MBOX,"Fehler: Nullbetrag",1,4);
showSCRBUF(MBOX,0);
WAIT;
clrSCRBUF(MBOX);
clrSCR(MBOX);
}
return result;

Der Anwender erfährt also unten rechts im Info-Fenster, was nicht stimmt, und kann das korrigieren.

Die Funktionen _neu und _edit sind sehr knapp gehalten, hier die Funktion neuer-Datensatz:

void buchung_neu()
{
bsatz_ausnullen(&bsatz);
if (bsatz_edit(&bsatz)==OK)
{
if ((abfrage("Buchungssatz speichern? Abbruch= ESCAPE"))==JA)
{
if (speichern_anhaengen(&bsatz)==OK) meldung("Daten wurden gespeichert","","");
else meldung("Fehler beim Speichern des Datensatzes","","");
return;
}
else return;
}
else return;
} // fu

Entsprechend wie oben geschrieben hat die Funktion Datensatz-überarbeiten leichte Abweichungen davon:

void buchung_edit()
{
if (lies_bsatz(&bsatz)==NOTOK)return;
if (bsatz_edit(&bsatz)==OK)
{
if ((abfrage("Buchungssatz speichern? Abbruch= ESCAPE"))==JA)
{
if (speichern_ueberschreiben(&bsatz)==OK) meldung("Daten wurden gespeichert","","");
else meldung("Fehler beim Speichern des Datensatzes","","");
return;
}
else return;
}
else return;
}

Die Dateifunktionen dazu sehen wir uns einmal an:

int speichern_anhaengen(struct s_bsatz *data)
{
FILE *bdp;
bdp=fopen(dn_buchungen,"ab");
if (bdp==NULL)return NOTOK;
else if (fwrite(data,sizeof(struct s_bsatz),1,bdp)!=1)return NOTOK;
fclose(bdp);
LFD_NR+=1; // LFD_NR erst nach erfolgreichem Abspeichern erhöhen
return OK;
}

Die Datei wird also im append-Modus "a" geöffnet, das "b2 zeigt, daß binär gespeichert wird. Wir können auch, wie gesagt, im Modus "r+" zugreifen, stellen den Dateizeiger auf das Ende der Datei und wenn wir dann schreiben, hängt er auch hinten dran.

Die LFD_NR ist die Ident_nr für die Datensätze. Sie muß natürlich laufend erhöht werden.

Am Programmanfang muß man herausfinden, welche höchste laufende Nummer schon vergeben wurde, um Doppelbelegungen zu verhindern. Man kann sich keinesfalls drauf verlassen, daß die höchste Nummer immer am Dateiende steht!

!!!

Denn durch Editieren insbesondere der Datumsangaben verrutscht die Reihenfolge, und man hätte doppelte Nummern. Daher wird hier die Datei von vorn bis hinten durchsucht (wie oben getestet weniger als 1 Millisekunde) und die höchste gefundene Nummer um 1 erhöht:

bdp=fopen(dn_buchungen,"rb");
if (bdp==NULL)LFD_NR=1; // keine Datei gefunden
else
{
LFD_NR=1;
while (fread(&data,sizeof(struct s_bsatz),1,bdp)==1)
if (data.ident_nr>LFD_NR)LFD_NR=data.ident_nr;
fclose(bdp);
LFD_NR+=1; // neuer Wert 1 draufsetzen
}

Kommen wir jetzt zum Schreiben im Überschreibe-Modus.

Das ist etwas komplizierter als anzuhängen. Wir müssen erstmal die Stelle in der Datei finden, wo der alte Datensatz (Identifikation anhand der ident_nr) sitzt. Dabei wäre es möglich, daß er nicht vorhanden ist, wenn die Datei auf dem Rechner herumkopiert wurde oder man im falschen Verzeichnis sucht. Für diesen Fall gibt es eine Fehlermeldung und return, da ist nichts zu machen.

int speichern_ueberschreiben(struct s_bsatz *data)
{
int gefunden=NEIN;
int erfolg;
FILE *bdp;
struct s_bsatz buffer;
bdp=fopen(dn_buchungen,"r+b");
if (bdp==NULL)
{
meldung("Datei",dn_buchungen,"nicht gefunden");
return NOTOK;
}
while (fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1)
if (buffer.ident_nr==data->ident_nr)gefunden=JA;
if (gefunden==NEIN)
{
meldung("Datensatz innerhalb der Datei",dn_buchungen,"wurde nicht gefunden");
return NOTOK;
}
while (fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1&&buffer.ident_nr!=data->ident_nr);
fseek(bdp,(long)-1*sizeof(struct s_bsatz),SEEK_CUR);// 1 zurück
if (fwrite(data,sizeof(struct s_bsatz),1,bdp)==1) erfolg=OK;
else erfolg=NOTOK;
fclose(bdp);
return erfolg;
}

Den Witz der Sache habe ich rot markiert. Wir suchen erstmal von vorn bis hintenn durch, Ergebnis: Datensatz ist vorhanden oder nicht. Wir könnten bei dem Durchlauf natürlich auch mit Doppelbedingung suchen. Allerdings wird der Code dadurch nicht kompakter, weil wir EOF überprüfen müßten und der letzte Datensatz evtl. gar keine Daten mehr enthält, weil EOF errreicht wurde.

Da die Performance im Bereich weniger als 1 Millisekunde liegt, bei 5000 Datensätzen (!), muß man da nicht rumtricksen.

Im zweiten Durchgang suchen wir mit Doppelbedingung, und das Semikolon am Ende von fread(... ); ist eine LEERE ANWEISUNG. Es bedeutet, solange die Bedingung nicht erfüllt ist, suche weiter und mache gar nichts. Sobald die Bedingung aber erfüllt ist, bricht der Lesezugriff ab und wir stehen mit dem Dateizeiger an der richtigen Stelle.

Aber Vorsicht: Wir stehen natürlich, am Ende des Lesezugriffs des korrekten Datensatzes, HINTER diesem Datensatz. Würden wir jetzt den geänderten Inhalt überschreiben, hätten wir an die Stelle des nächsten Datensatzes geschrieben, also einen "unschuldigen" Datensatz gelöscht und den alten (verkehrten) Inhalt zzzgl. dem neuen Datensatz in der Datei.

Wir müssen den Dateizeiger vielmehr auf den Anfang des korrekten Datensatzes zurückstellen. Das geschieht mit:

fseek(bdp,(long)-1*sizeof(struct s_bsatz),SEEK_CUR);// 1 zurück

Es heißt, einen Block von der Größe des Datensatzes, gerechnet von der aktuellen (current) Position zurück (-1).

Was geschieht, wenn die Datei leer ist? Nun, dann wird die Bedingung fread(...) ==1 nicht erfüllt, wir haben gefunden natürlich auf NEIN und es passiert gar nichts weiter. Diese implizite Fehlerbehandlung ist eleganter, als wenn man alles in if/else if hineinpackt.

Zum Schluß noch eine Datenstruktur, um den ekligen Umgang mit den Umsatzsteuerkonten indiziert anzusprechen. Die Definition der Struktur erfolgt praktischerweise gleich mit der Initialisierung der 7 Varianten:

struct s_ustoptionen{
float ustproz;
int ustkonto;
char ustbez[50];
};

struct s_ustoptionen ustoption[7]={0.0,0,"ohne Steueroption",
7.0,1561," 7% aufzuteilen",
19.0,1566,"19% aufzuteilen",
7.0,1571," 7% sofort abziehbar",
19.0,1576,"19% sofort abziehbar",
7.0,1771," 7% abzufuehren",
19.0,1776,"19% abzufuehren"};


Das sieht sehr sperrig aus, bringt aber eine e
norme Code-Komprimierung an den betreffenden Stellen, weil man nicht mit switch sage und schreibe 7 Bedingungen untersuchen muß. Das heißt, der gesamte Switch-Block mit 7 Optionen kann durch folgende beiden Anweisungen ersetzt werden:

taste=hole_ust_option(&data->ust_option,bmdata[feld].es,bmdata[feld].ez);
data->ust_proz =ustoption[data->ust_option].ustproz;
data->ustkonto=ustoption[data->ust_option].ustkonto;

Diese Indizierungen sind von der Optik her wirklich nicht schön, aber wie gesagt die Alternative sind einige DINA4 Seiten mehr Code.





--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 17.02.2012, 10:55 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Es sind noch zwei Fehler im Code zu korrigieren:

Nach dem ersten Suchlauf muß der Dateizeiger wieder an den Dateianfang gesetzt werden mit:

fseek(bdp,0,SEEK_SET) // ANweisung fehlt

und die Datei muß auch dann, wenn der Datensatz nicht gefunden wurde, vor dem return geschlossen werden.

Die unterschiedliche Notation zwischen

struct s_bsatz buffer mit buffer.feldbezeichner

und

struct s_bsatz *data mit data->feldbezeichner

rührt daher, daß die Funktion call by reference aufgerufen wird. Die lokale Konstante buffer wird natürlich direkt mit .feldbezeichner angesprochen.

Würde man by value aufrufen, müßte man die Struktur bsatz global erklären, was aber aus beschriebenen Gründen eher Nachteile hat.

Beim Aufruf der Reference-Funktionen ergeben sich weitere Notationen, wenn die aufrufende Funktion selbst bei reference aufgerufen worden ist, nämlich:

mache_was(struct s_bsatz *data ...

ruft die Unterfunktion hole_kontonummer(int *kontonr

folgendermaßen auf:

hole_kontonummer(&data->konto_nr

Alse eine Adresse auf eine Pointer- Adresse gesetzt.

Das kann man sich als Unteradresse vorstellen:

*data ist der Ursprung der gesamten Struktur, während &data->konto_nr innerhalb dieses Adreßbereichs die Adresse des Feldes der Struktur angibt, also sozuagen innerhalb des Hauses das 2. Zimmer oben links.

Für char* Pointer gilt, daß der Adreßoperator nicht erlaubt ist:

hole_datum(char *datum

wird aufgerufen mit:

hole_datum(data->datum und nicht mit &data->datum.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 17.02.2012, 19:47 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Einen Datensatz editieren

Das ist eine sehr komplexe Aufgabe. Erstmal muß man ihn finden. Anhand welcher Kriterien? Datum? Betrag? Konto? Gegenkonto?

Auf jeden Fall müssen die Daten nach Suchkriterien sortiert auf den Bildschirm.

Eine unbekannte Anzahl von Datensätzen, also dynamische Liste? Oder Kopie einer Datei?

Kopie einer Datei, sortiert nach Suchkriterien, hat was. Wir haben dann praktisch dasselbe wie eine dynamische Liste liefert, und brauchen uns nicht selbst um die Organisation zu kümmern.

Für den Fall, daß man es so machen will, muß man sich in der Datei vor- und zurückbewegen.

Daher mal als Vorüberlegung ein TESTPROGRAMM.

Es untersucht die Funktionen FSEEK() und FTELL()

Fseek ist der Wunsch, irgendwohin zu springen. Ftell ist die Angabe, wo der Dateizeiger steht.

Der Test fällt etwas überraschend aus, das gleich vorweg.

Natürlich, wir können alles abfangen, ich will aber erstmal sehen, wie das ohne Abfangen funktioniert.

GRUNDKURS DATEIEN in ANSI-C:

Datensatz:

int anz_datensaetze=5;

struct s_data{
int nr;
char name[50];
};

struct s_data data;

Datei schreiben:

void schreibe_datei()
{
int i;
FILE *bdp;
bdp=fopen("test.bin","wb");
if (bdp==NULL) return;
for (i=0;i<anz_datensaetze;i++)
{
data.nr=i+1;
fwrite(&data,sizeof(struct s_data),1,bdp);
}
fclose(bdp);
}

Datei lesen:

void lies_datei()
{
FILE *bdp;
bdp=fopen("test.bin","rb");
if (bdp==NULL)return;
while (fread(&data,sizeof(struct s_data),1,bdp)==1);
fclose(bdp);
}

Dateigroesse auslesen:

int dateigroesse()
{
long size;
FILE *bdp;
bdp=fopen("test.bin","rb");
if (bdp==NULL) return 0;
fseek(bdp,0,SEEK_END);
size=ftell(bdp);
fclose(bdp);
return size;
}

Anzahl Datensätze ermitteln:

printf("Dateigroeesse %10i\n",zahl=dateigroesse());
printf("Anzahl Datensaetze %10i\n",(zahl=dateigroesse())/sizeof(struct s_data));

Klar, wir müssen die gelesenen Bytes durch sizeof(struct Datensatz dividieren = Anzahl Datensätze

Jetzt wird es interessant mit folgender Funktion:

void switchmal()
{
struct s_data buffer;
char c;
FILE *bdp;
bdp=fopen("test.bin","rb");
if (bdp==NULL) return;
int delta=0;
do
{
fseek(bdp,(long)delta*sizeof(struct s_data),SEEK_CUR);
printf("ftell= %4i\n", ftell(bdp)/sizeof(struct s_data));
if (fread(&buffer,sizeof(struct s_data),1,bdp)==1)
{
printf("Gelesener Wert ist %4i\n",buffer.nr);
fseek(bdp,(long) -1*sizeof(struct s_data),SEEK_CUR);
}
else printf("%s\n","Keine Daten");
c=getch();
if (c=='+')delta=1;
if (c=='-')delta=-1;
}while ((int)c!=27);
fclose(bdp);
}

Kommentar kommt gleich, muß mal mit den Kindern noch raus. wink.gif


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 17.02.2012, 22:53 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Bildschirmausgabe für ftell/fseek:

Es wird erstmal solange + eingegeben, bis wir über das Dateiende hinaus sind, dann wird mit - zurückgeblättert, bis wir über den Dateianfang in die andere Richtung hinaus sind. Es sollen also GRENZWERTE überschritten werden. Das Ergebnis:

Dateigroeesse 280
Anzahl Datensaetze 5
ftell= 0 Gelesener Wert ist 1 // hochblättern mit der + Taste
ftell= 1 Gelesener Wert ist 2
ftell= 2 Gelesener Wert ist 3
ftell= 3 Gelesener Wert ist 4
ftell= 4 Gelesener Wert ist 5
ftell= 5Keine Daten // na ja, keine Daten ist nicht Systemabsturz. Genau das hätte ich mir fast gedacht. Super!
ftell= 6Keine Daten
ftell= 7Keine Daten
ftell= 8Keine Daten
ftell= 9Keine Daten
ftell= 10Keine Daten // ab hier wird zurückgeblättert mit der - Taste
ftell= 9Keine Daten
ftell= 8Keine Daten
ftell= 7Keine Daten
ftell= 6Keine Daten
ftell= 5Keine Daten
ftell= 4 Gelesener Wert ist 5
ftell= 3 Gelesener Wert ist 4
ftell= 2 Gelesener Wert ist 3
ftell= 1 Gelesener Wert ist 2
ftell= 0 Gelesener Wert ist 1 // hier sind wir am Anschlag BOF = Dateianfang, Beginning of File
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1
ftell= 0 Gelesener Wert ist 1 // man sieht, da brennt überhaupt nichts an.


C Programmierung beschäftigt sich eigentlich zu 95 Prozent mit GRENZWERTBEDINGUNGEN. In der Mitte läuft immer alles glatt.

Wollen wir Datensätze aus einer Datei aussuchen, brauchen wir eine Speicherstruktur, die eine beliebige Anzahl von Daten aufnimmt, da scheiden indizierte Arrays aus, weil deren Größe ja vor dem Programmstart festgelegt ist. Es bleiben nur dynamische Listen ... oder?

Der Blick auf fseek/ftell ist so eine Ahnung, ich hatte da was in der Nase, als könnte man vielleicht Standard-Funktion benutzen, OHNE JEDEN PROGRAMMIERAUFWAND sich die dynamische Liste sparen.

Schauen wir mal:

Man sieht:

1.) Daß fseek eine Bereichsüberschreitung über den ersten Datensatz zurück nicht zuläßt. Egal wie weit wir zurückblättern, hat fseek 0 erreicht, BOF = Beginning of File, also den Datei-Kopf, läßt es sich nicht weiter zurückstellen. ANDERS GESAGT: wir brauchen eine Bereichsüberschreitung in den negativen Bereich nicht abzufangen, erledigt die Standard-Funktion für uns.

2.) Gehen wir in die andere Richtung über EOF hinaus, wird der Dateizeiger fortlaufend erhöht, bis er Unsinnswerte anzeigt, denen keine Daten zugrundeliegen. Er überläuft also die EOF (End of File) Bedingung ohne Fehlermeldung (die wir allerdings auch noch nicht abgefangen haben), aber so, daß auch ohne Fehlerbehandlung KEIN SYSTEMABSTURZ die Folge ist. Es passiert nichts. Es brennt nichts an (unfaßbar, oder? wink.gif )

Es sieht so aus, als würde man mit fseek() bzw. ftell() ohne jeden Programmieraufwand direkt aus der gelesenen Datei heraus eine Simulation einer dynamischen Liste "abstauben" können, daher werde ich mich wohl in diese Richtung verzweigen, wenn es darum geht, aus der Datei einen Datensatz zum editieren herauszusuchen.

Sowas würde ich als regelrechte "Entdeckung" bezeichnen. Man findet das in C sehr selten oder nie, daß eine Bereichsüberschreitung von der Mama C überhaupt mal abgefangen wird. Das habe ich bisher noch NIRGENDWO festgestellt, ABER:

Hier wird sie es anscheinend. wink.gif

Der Beitrag wurde von sharky bearbeitet: 17.02.2012, 23:05 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 17.02.2012, 23:25 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Kurz gesagt, wie fseek() funktioniert (funktionieren muß, die Beweislage ist eindeutig):

Versuch, über fseek(FILE,0,SEEK_SET) = Dateianfang hinaus weiter nach rückwärts zu lesen, werden ignoriert.

Versuch, über fseek(FILE,0,SEEK_END) = EOF = Dateiende hinaus weiter zu lesen, werden auch ignoriert. Zwar zeigt der Dateizeiger ftell() dann unsinnige Werte an, aber fseek() kann ihm unmöglich gefolgt sein, sonst würde der sich irgendwo im Nirwana des Rechenspeichers befinden und die Reaktion wäre Systemabsturz.

Der fseek()-Zeiger klemmt sich also am Dateiende fest und geht da nicht weiter, auf wenn ftell() was anderes sagt, stimmt das nicht.

fseek() geht also dankenswerterweise ( wink.gif ) weder hinten noch vorn über den Bereich hinaus, ohne daß man irgendeinen Fehler abfangen müßte (was man ja noch könnte).

Daß fsekk() am Dateiende keinen Lesezugriff mehr ermöglicht, ist klar. Es hat ja über den letzten Datensatz hinaus gelesen und befindet sich am Ende des letzten Datensatzes.

Daß man sowas in C mal erleben darf ... super.gif

Das eröffnet wie gesagt ganz neue Möglichkeiten. Wir können unter Ausnutzung dieses Sachverhalts demnächst (heute abend nicht mehr) durch eine sortierte Kopie einer Datei eine dynamische Liste simulieren ohne jeden Programmieraufwand.

Sieht so gar nicht nach K&R aus, aber sei es, wie es sei.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 18.02.2012, 16:20 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Bevor ich jetzt meine Idee weiterentwickle, anstelle einer dynamischen Liste eine Datei auf dem Massespeicher zu verwenden und mit fseek() und ftell() zu steuern, einige Ausführungen zu der Verwaltung von dynamischen Listen überhaupt.

Dynamische Listen entstehen, wenn man Speicherbereich vom HEAP anfordert (d.i.i.d.R. das wesentlich größere Speichersegment des Rechenspeichers). Ein anderes ist der STACK. Dort werden alle lokalen Variablen und Rücksprungadressen aufgerufener Funktionen etc. abgelegt. Nachteil vom STACK, außer daß er in der Größe beschränkt ist: es gilt das Prinzip der Türme von Hanoi bzw. LAST IN FIRST OUT: er muß in umgekehrter Reihenfolge wie er belegt wurde wieder abgebaut werden. Bei den Türmen von Hanoi kommt man an den zweiten Ring erst heran, wenn der oberste abgenommen wurde. Ganz so verhält es sich mit dem STACK. Er ist in Gefahr, bei unbedachter Programmierung (z.B. rekursive Funktionen) überzulaufen: STACK OVERFLOW. Was dann das PRogramm abschießt.

Der HEAP ist anders organisiert, dort werden Speicherbereiche in Blöcken freigegeben. Diese können den Gesamtspeicher fragmentieren, so daß die Anforderung von einem großen zusammenhängenden Bereich erfolglos bleiben kann, obwohl insgesamt genügend Speicher verfügbar wäre, aber eben nicht in einem zusammenhängenden Block.

Ich gehe jetzt mal davon aus, wer es besser weiß, darf mich gern korrigieren, daß auch LOKALE Variablen, die mit MALLOC() angefordert wurden, ihren Speicher vom HEAP holen, und nicht auf den Stack gepackt werden, obwohl generell lokale Variablen, die ihren Speicher automatisch zugewiesen bekommen, auf den STACK gelegt werden. Was dafür spricht, daß mit malloc() die Speicheranforderung eben NICHT AUTOMATISCH erfolgt, sondern in Eigenregie. ÜBer die Fragmentierung muß man sich auch keine Gedanken machen, wenn man immer nur kleine Datenblöcke anfordert, die passen überall rein.

Warum ich davon ausgehe: anders als in den c-Tutorials immer zu lesen (die dieses Thema alle nur anreißen, anstatt mal in die Tiefe zu gehen, das ist geradezu lächerlich, die PRogrammbeispiele alle in main() unterzubringen), will ich eine dynamische Liste mit lokalen Variablen organisieren, weil mir das nicht gefällt, daß man, um von einer anderen Funktion zugreifen zu dürfen (und aus keinem anderen, vernünftigen Grunde) ausgerechnet solche "Bodenminen" wie dynamische Listen auch noch global machen muß.

Ja, dynamische Strukturen sind gefährliche Programmbausteine. Die schießen ohne Vorwarnung, und die Fehlerbehandlung kann endlos werden. Denn oft wird das Programm nicht erkennbar instabil, und nicht an den Stellen, woher die Ursache kommt, und oft wird es auch nur "gelegentlich" instabil (da ist es manchmal besser, die dynamischen Bereiche zu löschen und neu zu schreiben anstatt Tage zu suchen).

Um solches "Dynamit" zu verwalten, ist man gut beraten, nicht einfach drauflos zu programmieren, sondern eine ganz strenge Formalisierung einzuhalten und da besonders, wo die Luft bleihaltig ist, gilt umso mehr die REGEL: VERMEIDE REDUNDANZEN.

Hier mal der Versuch, eine dynamische Liste ganz anders als immer zu lesen zu organisieren.

Wir brauchen diese Liste, wenn wir eine Datei auslesen und in den Rechenspeicher kopieren wollen, weil wir bei Dateien die Größe vorher nicht abschätzen können (die oben angedeutete Alternative folgt noch). Also erstmal dynamische Liste.

Bei dem Anlegen der dynamischen Liste haben wir zwei Fälle zu unterscheiden:

1.) Erstes Element, der Listenkopf

2.) alle weiteren Elemente, die an den Listenkopf "drangehängt" werden.

Der Unterschied ist, daß der Listenkopf nirgendwo drangehängt werden kann.

Nun möchte ich diese Elemente LOKAL deklarieren.

Was heißt das?

Es kann von außen keine Funktion drauf zugreifen.

Was ist, wenn die eine Funktion eine LOKALE dynamische Liste erstellt, eine andere Funktion aber die Ausgabe dieser Liste durchführen soll? Diese zweite Funktion hat ja keinen Zugriff auf die lokalen Variablen (genau das soll sie ja auch nicht).

Um das zu realisieren, muß die struktur der dynamischen Liste GLOBAL definiert werden. Mit der Struktur wird ja noch nichts verrraten, es fehlen ja noch die Variablen-Deklarationen, und die sind eben lokal, von außen nicht zu sehen.

//GLOBAL:
struct s_element{
struct s_element *next;
struct s_element *prev;
struct s_bsatz data;
};


Nun folgt die Deklaration der Variablen LOKAL innerhalb der Funktion:

struct s_element *dyn_liste_datei1()
{
struct s_element *listenkopf=NULL;
struct s_element *laufzeiger=NULL;

Wobei man hier schon sieht, wo der Hase lang läuft: Der Funktionskopf hat als Rückgabewert einen Pointer vom Typ s_element. Zwar kann jede Funktion in Ansi-C immer nur einen Rückgabewert haben, aber der Typ ist wahlfrei.

Diese Funktion macht eine Kopie der gesamten Datei vom Massespeicher im Rechenspeicher (HEAP), und hinterlegt die Daten in Form einer dynamischen Liste.

Nun wollen wir uns die Liste auch mal ansehen, und dazu gibt es die Funktion:

void zeige_liste(struct s_element *start)
{
struct s_element *laufzeiger=NULL;
clrSCR(DIAL);
gotoSCR(DIAL);
printf("Inhalt der dynamischen Liste \n\n\n");

Man sieht schon, worauf das hinausläuft. Die Ausgabefunktion hat ein lokales Element namens laufzeiger, dieser Laufzeiger hat aber ja noch keine Adresse. Die Adresse erhält die Ausgabefunktion als Parameter *start.

Woher hat das Programm diese Adresse? Die ist doch lokal?

Tja, es ist klar, daß die Adresse irgendwie übergeben werden muß, und das kann, wenn es nicht GLOBAL geschehen soll (soll es nicht!) nur die aufrufende Funktion von beiden leisten, das sind die Anweisungen im Hauptmenü.

Dort sieht es so aus:

void hauptmenu()
{
struct s_element *start;

....

if (taste.st==F4)
{ erzeuge_zufallsdatei(25);
zeige_zufallsdatei();
start=dyn_liste_datei1();
zeige_liste(start);
...

Die Adresse wird also von der Funktion dyn_liste_datei1() zurückgeliefert, und von hier aus können nun weitere Funktionen drauf zugreifen.

Der Sinn der Sache ist einfach der, dem Gebot der Kapselung von Daten zu genügen, so daß nicht irgendwelche Funktionen im Programm früher oder später einfach an den Daten herumfummeln können, und das wäre der Fall, würde man solche sensiblen Strukturen, das "Dynamit", als GLOBAL erklären.

Wir halten also unser Dynamit in streng nach außen abgeschirmten Sicherheitsbehältern.

Nächster Schritt: Redundanz vermeiden. Das habe ich hier mal ganz nett hingekriegt, wie ich meine:

struct s_element *dyn_liste_datei1()
{
struct s_element *listenkopf=NULL;
struct s_element *laufzeiger=NULL;
struct s_bsatz buffer;
FILE *bdp;
bdp=fopen("zufall.bin","rb");
if (bdp==NULL){meldung("Kann Datei","zufall.bin","nicht oeffnen");return;}
while (fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1)
{
if (listenkopf==NULL){listenkopf=element_anhaengen(listenkopf);laufzeiger=listenkop
f;}
else laufzeiger=element_anhaengen(laufzeiger);
laufzeiger->data=buffer;
}
fclose(bdp);
return listenkopf;
}

Das ist die gesamte Funktion, welche den Dateiinhalt in den Rechner schaufelt. While fread( ... ==1 heißt, solange die Datei noch Daten hergibt.

Die Daten werden dann in einem neuen Element der dynamischen Liste abgelegt, wie das entsteht, ist hier aber nicht zu sehen, und aus gutem Grunde: AUFGABENTEILUNG. Wenn man eine bestimmte Aufgabe zu erledigen hat, soll der Code sich auf die Aufgabe beschränken, wie hier, die Datei auszulesen und zu kopieren, und nicht nebenher und nebenbei noch andere Dinge erledigen, (REDUNDANZVERBOT), die mit der eigentlichen Aufgabe nichts zu tun haben. Das macht den Code übersichtlich, vermeidet REDUNDANZEN, weil die dynamische Liste nur an einem einzigen Ort organisiert wird (wenn Fehler, schaue da nach), und macht den Code STABIL bzw. die Fehlersuche sehr einfach.

Der Code, der uns nun die dynamischen Elemente organsiert (wie eine Pizza-Bude: Anruf genügt, Pizza wird frei Haus geliefert), ist diese Funktion:

struct s_element *element_anhaengen(struct s_element *vorgaenger)
{
struct s_element *neues;
neues=malloc(sizeof(struct s_element));
neues->next=NULL;
if (vorgaenger==NULL) neues->prev=NULL;
else {neues->prev=vorgaenger;vorgaenger->next=neues;}
return neues;
}

Das sieht einfacher aus, als es ist. Das Einfache ist eben immer das Schwierige. Es managt beide Fälle, Liste neu oder Liste anhaengen. Denn wenn die Liste neu ist, hat das ersten den Vorgaenger NULL. Dann sind Nachfolger und Vorgänger auch NULL. Ansonsten müssen die Datenzeiger anders verkettet werden.

In der Kürze liegt hier die Würze.

Dennoch müssen wir den Sonderfall des ersten Elements (leider) in der aufrufenden Funktion doch noch berücksichtigen, nämlich (code steht schon oben zu sehen):

while (fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1)
{
if (listenkopf==NULL){listenkopf=element_anhaengen(listenkopf);laufzeiger=listenkop
f;}
else laufzeiger=element_anhaengen(laufzeiger);

Läßt sich aus logischen Gründen leider nicht ausklammern.

Damit auch das Auge mal sieht, was da überhaupt abgeht, zwei Screencopies.

Es wird hier eine Zufallsdatei mit 25 Datensätzen erzeugt. Zufall deshalb, weil das Datum mit der Rand() Funktion entsteht. Wir haben dann eine Datei mit wild durcheinanderlaufenden Datums-Angaben. (Abb 1)

Diese unsortierte Datei wird nun 1:1 in die dynamische Liste kopiert (Abb. 2)

Man kann sich schon denken, wie es weitergeht: die soll natürlich sortiert werden. Wie man dahin kommt, dann im Anschluß.




Der Beitrag wurde von sharky bearbeitet: 18.02.2012, 16:32 Uhr
Angehängte Datei(en)
Angehängte Datei  dynamische_liste.jpg ( 108.76KB ) Anzahl der Downloads: 4
Angehängte Datei  dynamische_liste2.jpg ( 108.7KB ) Anzahl der Downloads: 6
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 18.02.2012, 20:11 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Jetzt wollen wir mal daran gehen, die unsortierte Kopie der Datei, welche sich im Rechenspeicher als dynamische Liste befindet, zu sortieren.

Wie man das macht, kann man überall nachlesen. Worauf ich Wert lege, ist, wie man das organisiert.

To_do_List:

Wir benötigen eine zweite dynamische Liste, exakt so groß wie die erste, wohin wir die unsortierten Daten aus Liste 1 in sortierter Reihenfolge kopieren können.

Dann wollen wir die sortierte Liste auf dem Bildschirm ausgeben (erstmal, soll ja später in eine Datei).

Alle Listen sind lokale Variablen, die per MALLOC auf dem Heap angefordert werden. Um die Funktionen miteinander zu verknüpfen, muß die Adresse des ersten Datenelements listenkopf übergeben werden.

Aufgabenverteilung:

Wenn wir in einer Funktion sortieren, die zweite Liste erzeugen und die Elemente der ersten Liste löschen oder unkenntlich machen, haben wir ein schönes klassisches Durcheinander von völlig unleserlichem und völlig instabilem Code, der für jede Art von Änderung noch labiler wird.

Solche ALL-IN-ONE Programmierungen zeugen zwar von großem programmiererischen Können, verringern aber Stabilität und Lesbarkeit, insbesondere die Stabilität ist bei solchen Konstruktionen sehr wackelig. Außerdem verstoßen sie gegen das Redundanzverbot: wir dürfen neue Elemente der Liste nur über EINE Funktion anfordern und sowas nicht nebenher noch programmieren, geschweige denn im Gemenge mit Sortierung und Bearbeitung der anderen Liste.

Wie soll ich mich nur ausdrücken?

Wir müssen nicht, auch nicht mit einem Audi Quattro, die Straßen so ausloten, daß wir auf jeder Eisfläche bis an die Leitplanke driften. Das ist es ungefähr, was ich sagen möchte.

Wir wissen, die 2. Liste muß exakt so groß sein wie die erste, also machen wir AUFGABENTEILUNG und erledigen das vorher, bevor wir sortieren.

Nun zu der Frage, ob wir die Elemente aus der ersten Liste, nachdem wir die Daten in die 2. Liste kopiert haben, löschen sollen oder nicht. Denn wir müssen ja etwas unternehmen, sonst landet der Suchlauf immer bei demselben Element der ersten Liste und wir kommen in eine Endlosschleife.

Löschen aus einer zweidimensionalen Liste hat folgende 4 Verzweiger, die alle separat behandelt werden müssen:

Element am Anfang der Liste:
Element in der Mitte der Liste
Element am Ende der Liste
Element am Anfang und am Ende der Liste = letztes Element.

Es drohen endlose if/else if Verzweiger. Dazu kommt noch, daß der Zeiger Listenkopf ständig nachgestellt werden muß, wenn es ein Element am Anfang der Liste erwischt. Aber je nachdem nicht nur um 1 Position, sondern wenn die Elemente 2,3,4,5 vorher auch schon überschreiben wurden, muß man dafür einen regelrechten Suchlauf ansetzen, wo denn der nächste sinnvolle Wert für Listenanfang sitzt. Kurz gesagt: das wird richtiger Spaghetti-Code, unleserlich, fehleranfällig und unnötig.

Zumal wir diese erste Liste später noch benötigen, um die sortierten Daten dahin zurückzukopieren.

Wir müssen die erste Liste nicht löschen. Es reicht, wenn wir nach der Kopie des betr. Elements die Daten dieses Elements so eindeutig mit einem anderen Wert überschreiben, daß dieses Element beim nächsten Suchlauf nicht mehr infrage kommt. Der Preis ist, daß wir ungefähr 50PRozent mehr Datenzugriffe haben als beim Löschen der Elemente.

In Millisekunden ausgedrückt würde ich sagen: der Preis ist 0.005 Millisekunden oder so. wink.gif

Den Preis zahlen wir gern.

Jetzt zur Organisation:

struct s_element *sortiere_liste(struct s_element *start)
{
struct s_element *listenkopf=NULL;
struct s_element *laufzeiger=NULL;
struct s_element *kleinstes=NULL;
struct s_element *listenkopf_2=NULL;
struct s_element *laufzeiger_2=NULL;
//1.: zweite dynamische Liste erzeugen
listenkopf=start; laufzeiger=start;
while (laufzeiger!=NULL)
{
if (listenkopf_2==NULL){ listenkopf_2=element_anhaengen(listenkopf_2);laufzeiger_2=listenkopf_2;}
else laufzeiger_2=element_anhaengen(laufzeiger_2);
laufzeiger=laufzeiger->next;
}

Wir sehen, da das alles wiederum lokale Variablen sind, müssen sie neu definiert werden. Zunächst wird eine zweite LEERE Liste angelegt in derselben Größe wie die erste. Die erste abzuzählen und mit Zahlenwerten zu jonglieren, wobei sich noch Fehler einschleichen a la zähler erhöhen am Schleifenkopf oder Schleifenfuß, bei 0 angefangen oder bei 1, ist unelegant und fehleranfällig. Daher nehmen wir die Liste so wie sie ist von vorn bis laufzeiger->next==NULL, Listenende.

Der rot markierte Bereich zeigt, daß die 2. Liste mit derselben Funktion wie die erste Liste um jeweils ein Element erweitert wird. Wieso geht das? Das geht, weil diese Funktion eben NICHT GLOBAL definiert ist, sondern über eine Adresse anfängt, Elemente zu stapeln. Wo diese Elemente gespeichert werden, bestimmt nicht die aufgerufene Funktion, sondern die aufrufende.

Das ist einer der vielen Vorteile, wenn man call by reference organisiert. Wir müssen dabei natürlich nicht mit dem Adreß-Operator & arbeiten, weil der übergebene Pointer selbst ja schon eine Adresse darstellt.

Nachdem die 2. leere Liste verfügbar ist, durchlaufen wir nun die 1. Liste auf der Suche nach dem Element mit dem kleinsten Datensatz und übergeben es dem ersten Element der 2. Liste. Anschließend überschreiben wir das gefundene Kleinste der Liste 1 mit maxdatum ("99.99.9999").

Und suchen erneut, übergeben das gefundene nächste Kleinste der dynamischen Liste2 für den nächsten Datensatz. Wobei wir dann den Laufzeiger der Liste 2 weiterrücken müssen.

Hier ist die gesamte Funktion, ist getestet, läuft fehlerfrei:

struct s_element *sortiere_liste(struct s_element *start)
{
struct s_element *listenkopf=NULL;
struct s_element *laufzeiger=NULL;
struct s_element *kleinstes=NULL;
struct s_element *listenkopf_2=NULL;
struct s_element *laufzeiger_2=NULL;
//1.: zweite dynamische Liste erzeugen
listenkopf=start; laufzeiger=start;
while (laufzeiger!=NULL)
{
if (listenkopf_2==NULL){ listenkopf_2=element_anhaengen(listenkopf_2);laufzeiger_2=listenkopf_2;}
else laufzeiger_2=element_anhaengen(laufzeiger_2);
laufzeiger=laufzeiger->next;
}
laufzeiger_2=listenkopf_2; // 2. Liste auf den Anfang setzen
listenkopf=start;// Adresse beziehen für die 1. Liste
char maxdatum[]="99.99.9999";
long maxindex=datindex(maxdatum);
do
{
laufzeiger=listenkopf;
kleinstes=listenkopf;
do // 1 Durchlauf durch die ganze Liste
{
if (datindex(laufzeiger->data.datum)<datindex(kleinstes->data.datum))
kleinstes=laufzeiger;
laufzeiger=laufzeiger->next;
}while (laufzeiger!=NULL); // kleinstes bleibt hängen
if (datindex(kleinstes->data.datum)<maxindex) // ist noch was da?
{
laufzeiger_2->data=kleinstes->data; // Daten des kleinsten Elements kopieren
if (laufzeiger_2->next!=NULL)laufzeiger_2=laufzeiger_2->next;// 2. Liste nachziehen
sprintf(kleinstes->data.datum,maxdatum); // Datum des Elements überschreiben
}
else break;
}while (1);
return listenkopf_2;
}

Die Frage wäre natürlich nach der Abbruchbedingung. Sie wurde für die Außenschleife nicht formuliert, sondern wenn das KLEINSTE irgendwann MAXDATUM enthält, ist die Liste abgegrast und springen mit break aus der Endlosschleife.

Endlosschleife, weil do{ ...}while(1), while (1) ist immer wahr, bricht niemals ab.

Ergebnis ist dann der Übergabewert "return listenkopf_2", also eine Adresse. Auf die man mit jedem Datensatz zugreifen kann, der die struct s_element hat, also auch mit listenkopf_1. Ja es ist hier schon etwas ziemlich abstrakt, aber alles logisch und voll im sicheren Bereich.

Mithilfe der Adresse "return listenkopf_2" können wir nun vom Hauptmenü aus die sortierte Liste sichtbar machen. Blick ins Hauptmenü:

if (taste.st==F4)
{
erzeuge_zufallsdatei(25);
zeige_zufallsdatei();
start=dyn_liste_datei1();
zeige_liste(start);
start=sortiere_liste(start);
zeige_liste(start);
}

Wir rufen (REDUNDANZVERBOT) dieselbe Funktion zeige_liste() auf wie zur Ausgabe der ersten Liste. Nur daß die Start-Adresse einmal den Listenkopf der ersten Liste enthalten hat, nun enthält er den Listenkopf der 2. Liste, also eine andere Adresse. Das ist der Funktion aber völlig wurst, sie legt eben da los, wo sie Startadresse bezieht und fertig. Würde man den ganzen Kram global organisieren, ginge das nur mit if /else if oder switch und zunehmend endloser werdenden Parameterlisten.

Kann man sich mit call by reference alles sparen. Ich finde, das ist einfach der elegantere Code.

Screencopy der sortierten Liste in der Abb.

Der Beitrag wurde von sharky bearbeitet: 18.02.2012, 20:24 Uhr
Angehängte Datei(en)
Angehängte Datei  dynamische_liste3.jpg ( 109.7KB ) Anzahl der Downloads: 6
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 19.02.2012, 19:44 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Es stehen jetzt folgende Datei- und dynamische Funktionen zur Verfügung, teilweise sind es Testfunktionen.

Wie diese Auflistung der Aufrufe aus dem Unterpunkt des Hauptmenüs zeigt:

if (taste.st==F4)
{
erzeuge_zufallsdatei("zufall.bin",200);
zeige_bdatei("zufall.bin");
start=dyn_liste_datei1("zufall.bin");
zeige_liste(start);
start=sortiere_liste(start);
zeige_liste(start);
kopiere_liste_in_datei(start, "zufall.bin");
zeige_bdatei("zufall.bin");
}

Die Zufallsdatei kann erzeugt, angezeigt, in eine dynamische Liste kopiert werden.

Die Liste kann in eine zweite dynamische Liste kopiert werden (Sortierung), und dieselbe Anzeigefunktion zeigt uns die Sortierung, weil sie auf der Adresse "start" aufbaut. Natürlich, es ist klar, das kann sie nur, wenn sie weiß, welcher Datentyp benötigt wird. Das sind hier alles Datentypen struct s_bsatz, also Buchungssätze.

Wir brauchen also EINMAL eine Kopie der Datei in der dynamischen Liste, die wird dann sortiert in die zweite dynamische Liste.

Und am Schluß wird das mit kopiere_liste_in_datei wieder zurückkopiert in die ursprüngliche Datei auf dem Massespeicher, das heißt, der unsortierte Inhalt von zufall.bin wird nun überschrieben mit demselben Inhalt in sortierter Reihenfolge.

Warum nicht gleich eine Datei in eine Datei kopieren? Warum die dynamische Liste?

Es geht natürlich um die Performance.

Nehmen wir an, die komplette Datei kann in einer Millisekunde ausgelesen werden, bei 1000 Datensätzen (Beweis: siehe unten, SPEED-TEST).

Dann brauchen wir eine Millisekunde, um sie in die Liste zu kopieren, und eine Millisekunde, um sie zurückzukopieren.

Dazwischen liegt allerdings die Sortierung, welche für jedes Element n die Liste n-mal durchläuft, bis alle Elemente gefunden und in Sortierfolge kopiert sind. Bei n=1000 (1000 Datensätze) würde die Liste

n*n = n_quadrat =1 Million

mal durchlaufen werden, 1 Million Millisekunden sind aber sage und schreibe 1000 SEKUNDEN! Macht ca. 17 Minuten, die der Rechner auf der Festplatte herumeiern würde.

Nun, "nehmen wir an" ist das eine.

Wir sehen mal genauer nach.


Performance-Vergleich von Massespeicher und Rechenspeicher


SPEED-TEST


Dafür verschandeln wir unser Hauptmenu (was ja eh noch in der Testphase ist) folgendermaßen:

if (taste.st==F4)
{
clock_t clockstart;
clock_t clockstop;
erzeuge_zufallsdatei("zufall.bin",2000);
clockstart=clock();
lies_bdatei("zufall.bin");
clockstop=clock();
clrSCR(DIAL);
gotoSCR(DIAL);
printf("Clock start = %10i\n",clockstart);
printf("Clock stop = %10i\n",clockstop);
printf("Anzahl clocks = %10i\n",clockstop-clockstart);
WAIT;

Wir definieren also mittendrin (so richtig spaghettimäßig) zwei Variablen, für die wir allerdings oben #include <time.h> einbinden müssen. Um die Prozessorclocks zu stoppen. Ein Clock ist auf meinem Notebook ungefähr 1 Millisekunde.

Um die Datei mit 1000 Datensätzen einmal zu lesen, BIldschirmausgabe:

Clock start = 1928
Clock stop = 1929
Anzahl clocks = 1 (1 Durchlauf 1000 Datensätze = 1 Millisekunde)

Das ist aber erstens zu dicht an der Meßgrenze von 1, es könnten genausogut 0,5 oder 2 sein, je nach Meßtoleranz, und es ist auch nicht das, was wir wissen wollen. Wir haben ja, um eine Datei zu lesen, mehrere Operationen, deren Zeitanteile wir nicht kennen:

1.) Suche Datei auf der Festplatte
2.) Öffne Datei
3.) Durchsuche Datei
4.) Schließe Datei

Was wir wissen wollen, ist nur der Wert für die 3. Operation, durchsuchen bei geöffneter Datei.

Wir schreiben die Funktion lies_bdatei()

void lies_bdatei(char *dname)
{
FILE *bdp;
struct s_bsatz buffer;
bdp=fopen(dname,"rb");
if (bdp==NULL){meldung("Dateifehler",dname,"");return;}
while(fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1);
fclose(bdp);
}

mal so um, daß sie bei geöffneter Datei die Daten n-mal durchsucht, sieht dann so aus:

void lies_bdatei(char *dname,int durchlaeufe)
{
FILE *bdp;
struct s_bsatz buffer;
int i;
bdp=fopen(dname,"rb");
if (bdp==NULL){meldung("Dateifehler",dname,"");return;}
for (i=0;i<durchlaeufe;i++)
{
while(fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1);
fseek(bdp,0,SEEK_SET); // auf Dateianfang zurücksetzen
}
fclose(bdp);
}

1000 Datensätze bei geöffneter Datei 1000 mal durchsuchen:

Clock start = 1910
Clock stop = 2124
Anzahl clocks = 214 (0,214 Millisekunden pro Durchlauf)

Das heißt: ist die Datei erstmal geöffnet, wird es schneller. Die Anteile für Suchen, Öffnen, Einmaloperationen im Dateikopf etc. sind relativ hoch, so daß sich das bei geöffneter Datei ziemlich schnell verbilligt (ist wie beim ALDI: großer Durchsatz = kleine Preise). wink.gif

Um quadratisch zu sortieren, müßte die Datei aber 1000*1000 = 1 Mio. Mal durchsucht werden. Bevor ich jetzt hochrechne, nähern wir uns mal an, da die Hochrechnung den "Verbilligungsfaktor" evtl. nicht korrekt berücksichtigt.

1000 Datensätze 100.000 Mal durchsuchen:

Clock start = 1765
Clock stop = 19546
Anzahl clocks = 17781
( 17,77 Sekunden, 0,177 ms pro Durchlauf)

1000 Datensätze 1 Mio mal durchsuchen:

Clock start = 2170
Clock stop = 180029
Anzahl clocks = 177859
(177 Sekunden = 3 Minuten, ALDI-Einkäufergrenzwert ist erreicht, es wird nicht mehr billiger)

Wir können also sagen, die max. Zugriffsgeschwindigkeit des Massespeichers (auf meinem Notebook) auf eine Datei mit 1000 Datensätzen liegt bei 0,178 Millisekunden im Zustand: Datei bereits geöffnet. (Im Jahre 1980 hätte der REchner dazu wahrscheinlich 3 Wochen gebraucht wink.gif )

Jetzt steigt der Gegner in den Ring:

die dynamische Liste im Rechenspeicher

Die Funktion für 1 Mio Durchläufe durch die Datei ist bereits vorhanden, es ist die bereits oben vorgestellte quadratische Sortierroutine. Bei 1000 Datensätzen in der kopierten Liste haben wir exakt 1 Mio. Zugriffe auf diese.

Hängen wir jetzt hier auch mal die Stoppuhr dran:

clrSCR(DIAL);
gotoSCR(DIAL);
printf("Sortierung beginnt\n");
clockstart=clock();
start=sortiere_liste(start);
clockstop=clock();

Ergebnis:

1000 Datensätze 1 Mio mal durchsuchen:

Clock start = 7891
Clock stop = 8236
Anzahl clocks = 345


Da sieht man, wo der Hammer hängt. :doch:

Wir haben jetzt die Performance des Massespeichers (Datei) im direkten Vergleich zum Rechenspeicher (dynamische Liste).

Sie beträgt:

177859 : 345 = 515,5333333

Das heißt, wenn wir die Daten aus der Datei in den Rechenspeicher schaufeln, sind wir 500mal schneller bei der Verarbeitung der Daten.

Im vorliegenden Fall:

Für dieselbe Operation im Speicher anstatt auf der Festplatte benötigen wir

statt 3 Minuten nur 0,3 Sekunden.

Es hat sich also nichts geändert daran, daß man zeitintensive Probleme nicht auf dem Massespeicher ausführen kann, ohne die Performance abzuwürgen.

Das ist der Hintergrund, warum dynamische Listen programmiert.

Wenn der Zusammenhang aber nicht zeitintensiv ist, wenn man also nur eine Datei auf dem Bildschirm betrachtet, kommt man auch ohne aus. Ich komme heute nicht mehr dazu, aber die Idee, statt einer dynamischen Liste mit fseek die Datei direkt auf den Bildschirm zu holen, wird funktionieren. Ich hab das schon getestet. Wie es geht, im nächsten Beitrag. wink.gif


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 20.02.2012, 19:41 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Hallo,

ich muß bzgl. meines obigen Beitrages einiges korrigieren.

Das kann man so nicht stehen lassen.

Der Fehler liegt in der Relation von Masse- und Rechenspeicher.

Ich habe die Funktionen nochmal korrigert und bin zu folgenden Ergebnissen gekommen:

Zufallsdatei mit 1 Mio. Datensätzen erzeugen 811 Clocks // Schreibgeschwindigkeit Massespeicher
Zufallsdatei mit 1 Mio. DS lesen 187 Clocks // Lesezugriff Massespeicher
Datei in dyn. Liste kopieren mit 1 Mio. DS 344 Clocks // Lesen Massespeicher und Schreiben Rechenspeicher
Dynamische Liste mit 1 Mio. DS lesen 15 Clocks // Lesen Rechenspeicher

Daran sieht man, der Lesezugriff auf dem Massespeicher ist in Relation zum Reichenspeicher nur um den Faktor 12,x langsamer. Das ist meilenweit entfernt von den 500+x wie oben behauptet.

Der Schreibzugriff im Rechenspeicher, =344- 187 Clocks für das damit verbundene Lesen im Massespeicher abgezogen, ist mit dann 157 Clocks im Vergleich zu 811 Clocks zwar auch deutlich schneller, aber als Faktor bleibt lediglich 811/157=5,16.

Das ist enttäuschend wenig und ich werde das Problem erstmal noch weiter austesten, bevor ich hier irgendwas behaupte. Wenn es in die Richtung ginge, wären Programmierlösungen, die sich allein auf den Massespeicher stützen, eine attraktive Alternative, weil man sich den ganzen Umgang mit dynamischen Listen ersparen könnte.

Die Zahlen oben betreffen ja 1 Mio. Datensätze, wie sie im realen Leben einer FIBU niemals vorkommen. 811 Clocks = 0,811 Sekunden. Auf 1000 Datensätze bezogen, gehen alle diese Werte auf 0 zurück, nicht mehr meßbar, Bruchteile von Millisekunden. Und so wäre ja die Realität.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 21.02.2012, 19:45 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Die Performance der Programmierung ist natürlich immer eine spannende Sache.

Weil man sich selbst überprüfen kann, ob die Algorithmen was taugen.

Dabei wird man regelmäßig feststellen, daß man einen Algorithmus deutlich schneller machen kann. Der Grund ist, man will erstmal das Problem lösen, und ist froh, wenn es geht, aber sobald die Funktion getestet und "stabil" läuft, will man mehr. Und geht an das Tuning.

Wann wird bei den heutigen Prozessoren eine Funktion zeitkritisch? Nun, irgendwo ist die Grenze, das erfährt man schnell, wenn man potenziert. 1000*1000 ist 1 Million, das geht immer schnell, doch 20000*20000 sind 400 Millionen, da geht es auch mit den schnellsten Rechnern schon nicht mehr schnell. Ein DOS-REchner aus dem Jahre 1980 hätten an solchen Zahlen wochenlang gearbeitet. Geschweige denn ein C64 (das war so die Vorstufe zum PC). Programmieren konnte man auch damit.

Stichwort: Sortieralgorithmen->Daten sortieren und sortiert speichern.

Zwei Algorithmen: Dynamische Liste vs. Festplatte

Dazu habe ich einige Funktionen geschrieben, die folgendes leisten:

1.) Lesen der Daten aus einer unsortierten Datei.
2.) Sortieren der Daten
3.) Zurückschreiben der Daten in sortierter Reihenfolge (in eine andere oder dieselbe Datei).

Der eine Algorithmus ist dynamisch. Es wird

1.) eine dynamische Liste als Kopie der Datei angelegt im Rechenspeicher
2.) eine zweite dynamische Liste gefüllt mit den sortierten Daten
3.) die zweite Liste auf die Festplatte als Datei zurückgeschrieben.

Der andere Algorithmus schreibt nicht in den Rechenspeicher, sondern von der Festplatte direkt wieder auf die Festplatte:

1.) Lesen der Datei und
2.) Gleichzeitiges Schreiben der sortierten Daten in eine andere Datei.

Die Sortierung erfolgt in beiden Fällen quadratisch. Das heißt, jede Datei wird sooft gelegen, wie sie Datensätze enthält (n). Bei jedem Durchgang wird 1 Element gefunden, welches das Kleinste ist, und auf den Zielort kopiert. Damit dieses Kleinste beim nächsten Durchgang nicht erneut aufgefunden wird, wird der Inhalt, nach dem indiziert wird (hier: Datum), überschrieben.

Für eine Datei mit 1000 Datensätzen ergeben sich dann 1000 Zugriffe, insgesamt werden n*n = 1000*1000 * 1 Mio. Datensätze gelesen.

Wie man dynamisch sortiert, den Code hatte ich oben schon reingesetzt. WIe man auf dem Massespeicher direkt sortiert, von einer Datei in eine Andere Datei, ohne dynamische Liste, der Code kommt im Anschluß.

Hier geht es ja um die Performance.

Erstmal aber wollen wir ja wissen, ob die Algorithmen auch funktionieren. Dazu die beiden Abb.

Sie zeigen das Ergebnis der Sortierung. Der Massespeicher-Algorithmus kopiert in die Datei sortier.bin (die im rechten Fenster zu sehen ist), der dynamische Algorithmus sortiert in die Datei listsort.bin. Die Anzeige dient der Verdeutlichung, daß die Daten auch tatsächlich in sortierter Reihenfolge vorliegen.

Im linken Fenster ist in schmuckloser ( wink.gif ) Form die Performance zu sehen. Bei 10 Datensätzen liegt die dynamische Liste mit 0 Clocks gegenüber 13 Clocks der Direktsortierung vorn. Um zu sehen, wie die Verhältnisse tatsächlich sind, müssen wir natürlich mal größere Datenmengen sortieren.

Bei einer quadratischen Sortierung ist wg. dem Potenzier-Effekt von vornherein zu erwarten, daß die Performance nach oben stark einbricht.

Hier geht es aber nur um die Relation der Algorithmen, dynamisch oder Massespeicher. Was man damit anfängt, muß man je nach den vorliegenden Datenmengen entscheiden. Dazu mehr im nächsten Beitrag.

Angehängte Datei(en)
Angehängte Datei  vergleich.jpg ( 102.27KB ) Anzahl der Downloads: 2
Angehängte Datei  vergleich2.jpg ( 103.66KB ) Anzahl der Downloads: 3
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 21.02.2012, 20:57 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Wir nehmen mal 500 Datensätze.

Für die FIBU entspräche das ungefähr dem, was bis Mitte März an Daten vorliegt. Es folgt immer die Bildschirmausgabe gem. den Abb. im obigen Beitrag.

Ergebnisse fuer 500 Datensaetze
1. Direkte Sortierung Datei-Datei = 94 Clocks
2. Dynamische Sortierung + Rueckschreiben in Datei 0 Clocks


94 Clocks sind auf meinem Rechner 94ms, also 0,094 sec. Das ist nichts, während die dynamische Sortierung noch außerhalb des Meßbereichs bleibt.

Nochmal zur Orientierung: beide Algorithmen leisten EXAKT DASSELBE, nämlich von Festplatte lesen, sortieren und auf Festplatte zurückschreiben.

Bei 1000 Datensätzen sieht es so aus:

Ergebnisse fuer 1000 Datensaetze
1. Direkte Sortierung Datei-Datei = 218 Clocks
2. Dynamische Sortierung + Rueckschreiben in Datei 16 Clocks

Die dynamische Sortierung kommt jetzt in den Meßbereich. Der Faktor ist 218/16= 13,62. Die dynamische Liste ist um diesen Faktor schneller. Den setzen wir gleich mal mit in die Bildschirmausgabe, anstelle auf dem Windows-Rechner zu tippen.

Hier übrigens ein kleiner Tipp, um DIVISION BY ZERO abzufangen:

Beide Faktoren, Clocks für Algo1 und Algo 2, können ja 0-Werte liefern, ergibt dann Fehlermeldung Division by Zero, Programmabbruch. Um einen FLOAT zu erzeugen aus 2 INTEGER, müssen wir aber ohnehin mit FLOAT multiplizieren, sonst haben wir eine Division ohne Rest, wir wollen aber eine Fließkommazahl. Wir müßten schreiben:

zeit1*1.0/zeit2*1.0, was eine implizite Typumwandlung in FLOAT ergibt. Wir könnten auch schreiben (in ANSI-C):

(float)zeit1/(float)zeit2

Da wir es aber sowieso etwas tun müssen, nehmen wir den Ausdruck 1 und machen mit einem kleinen Trick den Divisor "wasserfest", von dem wir wissen, daß er %10.1f, also eine Nachkoma anzeigen soll. Er soll bei DivByZERO dann immer noch 0.0 anzeigen, aber nicht abstürzen. Lösung:

Quotient = dividend*1.0/divisor*1.00000000000001;

Dann haben wir das Gewünschte, ohne daß die Rechnung dadurch beeinflußt wird.

Halter wir fest: bei 1000 datensätzen ist die dynamische Liste um den Faktor 13.6 vorn, allerdings sind die Absolutzeiten für beide Algorithmen noch in so kleinen Bereichen, daß beides geht.

Nächstes: 2000 Datensätze.

Ergebnisse fuer 2000 Datensaetze
1. Direkte Sortierung Datei-Datei = 749 Clocks
2. Dynamische Sortierung + Rueckschreiben in Datei 31 Clocks
Faktor Algorithmus 1 (Datei-Datei)/ Alg. 2 (dyn. Liste) ist 24.2


Machen wir mal 5000:

Ergebnisse fuer 5000 Datensaetze
1. Direkte Sortierung Datei-Datei = 4352 Clocks
2. Dynamische Sortierung + Rueckschreiben in Datei 140 Clocks
Faktor Algorithmus 1 (Datei-Datei)/ Alg. 2 (dyn. Liste) ist 31.1


Wir sehen, daß die dynamische Liste, je größer die Datenmenge, umso vorteilhafter ist. Absolut gesehen ist mit dem Algorithmus 1 mit 4,5 Sekunden die Schmerzgrenze, was der Anwender noch ertragen kann, insofern überschritten, wenn man die Routine dauernd aufruft. Wenn man die in den Programmstart oder an das Programmende verpackt, fällt es allerdings nicht auf.

Nun machen wir mal einen Sprung auf 15000 Datensätze, in der Erwartung, daß da Algorithmus 1 irgendwie nicht mehr mithalten kann. Aber was macht bei solchen Datenmengen (225 Mio. Datenzugriffe) der Algorithmus 2?

Ergebnisse fuer 15000 Datensaetze
1. Direkte Sortierung Datei-Datei = 37705 Clocks
2. Dynamische Sortierung + Rueckschreiben in Datei 1263 Clocks
Faktor Algorithmus 1 (Datei-Datei)/ Alg. 2 (dyn. Liste) ist 29.9


Absolut gesehen, ist der Algorithmus 2 mit 1,2 Sekunden auch für den Daueraufruf noch so gerade eben geeignet. Während 37,7 Sekunden jenseits von Gut und Böse sind.

Es scheint außerdem, daß der Faktor 30 so ungefähr schon das Limit ist, was man mit dynamischen Listen herausholen kann. Die hängen ja auch nicht in der Luft, sondern müssen von Festplatte lesen und zurückschreiben, die gestoppte Zeit ist ja die Zeit für den gesamten Prozeß.

Um zu überprüfen, ob wir Faktor 30 toppen können, mal 30.000 Datensätze:

Wer jetzt glaubt, 30000 sei das Doppelte von 15000, der wird feststellen, daß das irgendwie stimmt, aber nicht wirklich wink.gif

Ergebnisse fuer 30000 Datensaetze
1. Direkte Sortierung Datei-Datei = 161710 Clocks
2. Dynamische Sortierung + Rueckschreiben in Datei 5148 Clocks
Faktor Algorithmus 1 (Datei-Datei)/ Alg. 2 (dyn. Liste) ist 31.4


Mit 3 Minuten verabschiedet sich der Direktsort, und wir sehen, mehr als Faktor 30 ist mit dynamischer Liste nicht herauszuholen.

Nun ist die Sortierroutine quadratisch praktisch gut = ziemlich primitiv.

Wir haben da natürlich jede Menge Optimierungsspielraum, wenn wir den dafür erforderlichen Organisationsaufwand in Kauf nehmen wollen. Kurz gesagt: für 2000 Datensätze wie bei meiner FIBU, die aufs Jahr anfallen, wird das überhaupt nicht erforderlich sein. Aber mal theoretisch:

Optimierung von Sortieralgorithmen

Wenn wir einen Block von 10 Datensätzen quadratisch sortieren, haben wir 100 Datensätze zu lesen. Allgemein gesprochen, einen Block von n Datensätzen müssen wir dann n*n = n_quadrat mal durchsuchen.

Wenn wir den Block in 2 Hälften zerlegen (die im Idealfall auch ungefähr dieselbe Anzahl Elemente enthalten), sieht die Rechnung anders aus:

(n/2)_quadrat + (n/2)_quadrat macht dann anstatt 10*10=100 nur noch 5*5=25 + 5*5=25, also 50 Datenzugriffe anstelle von 100. Zum Zerlegen allerdings müssen wir die ursprünglichen 10 ja mindestens einmal durchlaufen, kommen 10 hinzu, plus daß wir die neuen Blöcke woanders hinkopieren müssen, kommen 2*5 hinzu, also statt ursprünglich 100 Datenzugriffen sind es dann 70 und nicht 50 (!!!).

Das Spiel läßt sich natürlich fortsetzen. Je mehr Untereinheiten wir erzeugen, umso geringer wird der Anzahl der Datenzugriffe, zerlegen wir 10 Datensätze in 10 Untereinheiten zu 1 Datensatz, haben wir anstelle von 10*10 nur noch

1_quadrat*10 =10 plus die 10 Zugriffe, um den Datensatz zu zerlegen, plus die 10 Zugriffe, um die Untermengen zu kopieren, macht dann 30 anstelle von 100.

Der Gewinn ist umso größer, je größer die Datenmenge ist und je mehr Untermengen wir bilden können. Das bringt allerdings nur dann was, wenn wir eine NORMALVERTEILUNG haben.

Das Beispiel finde ich immer wieder gut, was KEINE Normalverteilung ist: Das Vermögens des Plantagenbesitzers im 17. Jahrhundert im Vergleich zu dem Barvermögen seiner 1000 Negersklaven. Den Fall zu untersuchen, würden Untermengen gar nichts bringen. wink.gif

Eine ganz andere Überlegung ist allerdings, die Performance OHNE MEHRAUFWAND DEUTLICH ZU VERBESSERN. Und das ist, je nachdem wie der Prototyp geschrieben ist, manchmal leicht zu haben und bringt wesentlich mehr als 30/100.

So ein Punkt daraus sind FUNKTIONSAUFRUFE innerhalb ZEITKRITISCHER FUNKTIONEN = FORBIDDEN! wink.gif

Aus dem Zusammenhang. Wenn ich schreibe:

while ... ganzviele Durchläufe
{
if (datindex(mystruct1.datum)<datindex(mystruct2.datum)
...
}

wird pro Datensatz 2mal eine selbstgeschriebene Funktion datindex() aufgerufen. Die möglicherweise selbst wieder andere Funktionen aufruft oder so programmiert ist, daß sie zeitkritische Anweisungen enthält. In der Schleife könnten aber noch weitere Aufrufe von datindex() enthalten sein.

Sowas ist NO GO!

Wir müssen sowas unbedingt ausklammern. Um im Beispiel zu bleiben: die Zufallsdatei bekommt Zufallsdaten zugewiesen der Form "tt.mm.yyyy", und enthält ein Datenfeld datindex, wo man den Indexwert für diese Datumsangabe ablegen könnte, wenn nicht, müßte man dieses Feld eben hinzufügen.

Wir die Datei erzeugt, würde pro Datensatz einmal die Funktion datindex() aufgerufen. Und das Feld ist mit datindex() belegt. Die Funktion würde also außerhalb einer zeitkritischen Schleife, nämlich beim Erfassen von der Tastatur (Zufallsgenerator ist ja der Ersatz dafür zum Testen), einmal aufgerufen.

Gerät allerdings diese Datei in eine zeitkritische Schleife mit 10000 Datensätzen, welche beim Sortieren 100 Mio. Zugriffe macht, dann wird diese Funktion 200 Mio. mal aufgerufen, OBWOHL ES MIT 1000 AUFRUFEN GETAN GEWESEN WÄRE, gewesen wäre, wenn man es berücksichtigt hätte. Ist der Datindex drin, kann man anstelle von (=GROSSER MIST)

while ... ganzviele Durchläufe
{
if (datindex(mystruct1.datum)<datindex(mystruct2.datum) // NO GO!
...
}

schreiben (=VIEL VIEL BESSER):

while ... ganzviele Durchläufe
{
if (mystruct1.datindex<mystruct2.datindex) // Kein einziger Funktionsaufruf, performance + mehrere 100 Prozent
...
}

Ist eigentlich selbsterklärend, oder?

Und das ganze, ohne MEHR Organisation.

Jetzt mal noch der Code, wie man zwischen zwei Dateien ohne dynamische Liste sortiert (wie oben zu sehen, hat man es mit wenigen 1000 Datensätzen zu tun, VÖLLIG ausreichend):

void sortiere_datei_datei(char *quelldatei,char *zieldatei)
{
long n;
long bufsize=sizeof(struct s_bsatz);
long durchlaeufe;
struct s_bsatz buffer,kleinstes;
long filepos=0;
long maxindex=datindex("31.12.2099");
long schlepp;
FILE *quelle;
FILE *ziel;
quelle=fopen(quelldatei,"r+b");
if (quelle==NULL)return;
ziel =fopen(zieldatei,"wb");
if (ziel==NULL)return;
fseek(quelle,0,SEEK_END);
durchlaeufe=ftell(quelle)/sizeof(struct s_bsatz);
for (n=0;n<durchlaeufe;n++)
{
fseek(quelle,0,SEEK_SET); // Dateianfang
kleinstes.datindex=maxindex; // auf höchsten Wert
while (fread(&buffer,sizeof(struct s_bsatz),1,quelle)==1)
{
if (buffer.datindex<kleinstes.datindex)
{
kleinstes=buffer;
filepos=ftell(quelle)-bufsize;
}
} // 1 Durchlauf beendet
if (fwrite(&kleinstes,sizeof(struct s_bsatz),1,ziel)!=1){meldung("Fehler Zieldatei","","");return;} // Datensetz in 2. Datei kopieren
kleinstes.datindex=maxindex; // Element unkenntlich machen
// im Anschluß Datensatz wieder aufsuchen und ueberschreiben:
fseek(quelle,filepos,SEEK_SET);
if (fwrite(&kleinstes,sizeof(struct s_bsatz),1,quelle)!=1){meldung("Fehler Quelldatei","","");return;}

}
fclose(quelle);
fclose(ziel);

} // fu

Da sind womöglich noch unnötige Variblen drin vom Testen.

Im Prinzip funktioniert es so:

Wir stellen die Anzahl der Datensätze fest mit durchlaeufe=ftell(quelle)/sizeof(struct s_bsatz);
indem wir die BYTES von ftell durch die Buffergröße dividieren, und haben damit die Anzahl der Durchläufe. Etwas unelegant, aber man muß sich dann nicht mit den Feinheiten der Abbruchbedingung herumschlagen. Unelegant und sicher.

Dann wird die Datei exakt sooft abgesucht, wie sie Datenelemente hat, und bei jedem Durchgang das Kleinste in die andere Datei angehängt.

Wie oben zu sehen, bei 1000 Datensätzen sind das Sekundenbruchteile.

Die Regeln für die Optimierung, wie oben beschrieben für die Dynamische Liste, gelten natürlich auch hier.

In zeitkritischen Schleifen alle Funktionsaufrufe vermeiden, umorganisieren.

Kostet wenig, bringt viel. wink.gif




Der Beitrag wurde von sharky bearbeitet: 21.02.2012, 21:09 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 21.02.2012, 21:41 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Nun mal zu der Frage, die sich hier mißverständlicherweise vielleicht stellen mag:

Kann man in ANSI-C eigentlich auch schnell was programmieren, was läuft, ohne unendliche Versuche zu machen?

Die Antwort ist: ganz klar geht das, ganz einfach und ganz schnell. Meine FIBU läuft ja bereits. Programmieraufwand für die laufende Fibu würde ich so auf 30 Stunden schätzen, würde ich so ein EASY-Modell jetzt nochmal programmieren müssen, wäre sie in 3 Tagen fertig.

Das soll man nicht verwechseln mit dem, was ich hier mache in diesem Beitrag:

Ich spiele ein bißchen herum mit den Möglichkeiten.

Mit C kann man alles programmieren. CNC-Maschinen, Peterchens Mondfahrt, die Suche des Ritters im Verlies oder eine FIBU. Fibu ist da gegenüber der Suche des Ritters im Verlies fast schon langweilig. wink.gif


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 21.02.2012, 22:30 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Wenn man solche Sachen in einer Datei macht:

Suche einen bestimmten Datensatz.

Merke dir den Datensatz

---------> Dann gibt es einen sehr WICHTIGEN Unterschied zwischen dynamischen Listen und Dateien:

Die dynamische Liste arbeitet immer mit sizeof(element), die Funktion ftell() bzw. fseek() aber in Bytes.

Man kann beides verwenden. Sortiert man man nach Datensätzen, muß man ftell() durch die Größe des Datensatzes dividieren.

Dazu kommt aber, und das ist etwas heimtückisch, daß ftell() nach dem Lesezugriff in einer Datei immer HINTER dem gesuchten Datensatz steht. Die Anfangsposition des gelesenen Datensatzes ist nicht ftell(), sondern ftell()-sizeof(Datensatz). Während bei einer dynamischen Liste der Zeiger immer auf die Anfangsadresse des Elements verweist.

Da ist großes Potential für die schönsten Programmierfehler. wink.gif

Man muß bei Dateien ein wenig anders denken als bei Listen.

Der Beitrag wurde von sharky bearbeitet: 21.02.2012, 22:43 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 22.02.2012, 08:43 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Jetzt wie versprochen komme ich zurück zu der Frage, wie man sich eine Binärdatei bequem, schnell und einfach auf dem Bildschirm ansehen kann. Handelt es sich um eine Text-Datei, kann man die mit einem Editor öffnen. Bei Binärdateien ist das nicht möglich. Wir haben dann die Möglichkeit, die Datei in einer dynamischen Liste auf dem Speicher abzubilden, mein Gedanke aber war, diesen Aufwand zu unterlassen, und die Datei DIREKT MIT FSEEK/FTELL auszulesen.

Ich verrate gleich das Ergebnis: es funktioniert hervorragend.

Zur Illustration die beiden Screen-Hardcopies, mit Text, Abb1 und 2.

Zur Erinnerung die (weiter oben bereits getestete) Funktionsweise von fseek:

Definition: fseek(FILE, Offset, Origin)

Der offset ist die gewünschte Verschiebung nach beiden Seiten (Minus- oder Pluszeichen), für den Ursprung (origin), von dem aus diese Verschiebung gezählt wird, stehen die Parameter

SEEK_SET (=BOF, Beginning of File, Dateianfang)

SEEK_CUR (=current Position, da, wo der Dateizeiger aktuell steht)

SEEK_END (=EOF, End of File, Dateiende)

Jetzt müssen wir achtgeben: da der Datensatz eine gewisse Größe hat, ob der Dateizeiger am Anfang oder am Ende des Datensatzes steht. Damit wir den lesen und ausgeben können, muß er am ANFANG des Datensatzes stehen. Sonst sehen wir nämlich den nächsten Datensatz.

Um hier zum Ergebnis zu kommen, ist die Größe des Datensatzes zu berücksichtigen, sie ergibt sich mit bufsize=sizeof(Datensatz).

Um auf den Datensatz Nr. 5 vorzurücken, sagen wir: fseek(file,4*bufsize,SEEK_SET). Steht dann am ENDE (!) des 4., d.i. am Anfang des 5.

Um vom Dateiende 5 Datensätze zurückzulaufen: fseek(file,-5*bufsize,SEEK_END)

Um den Dateizeiger von der aktuellen Position um 1 Datensatz zurückzubewegen: fseek(file,-1*bufsize,SEEK_CUR). Ist man allerdings mit fwrite() an die Position gerückt, steht der Datenzeiger am Ende des Datensatzes, und die Anweisung führt uns NICHT auf den Anfang des Vorgängers zurück, sondern auf den Anfang des aktuellen Datensatzes. Wir könnten ihn dann z. B. überschreiben. Wollen wir den Vorgänger sehen, müssen wir sagen:fseek(file,-2*bufsize,SEEK_CUR)


Folgende Besonderheiten:

Liest man über den Dateianfang nach rückwärts hinaus, bleibt ftell(file) (=Pos. des Dateizeigers) immer 0, es nimmt keine negativen Werte an, muß man sich nicht drum kümmern.

Anders sieht es am Dateiende aus. Tatsächlich wird zwar über das Dateiende hinaus nicht gelesen, selbst wenn man mit fseek() über das Dateiende kommt, ohne es abzufangen, aber der Parameter ftell() nimmt sinnlose Werte an, der zählt immer weiter hoch, und "KOPPELT SICH AB", so wie ein ausgekuppeltes Zahnrad dreht er leer. Also müssen wir diesen Bereich so abfangen, daß ftell() keine sinnlosen Zahlen abgibt.

Um das zu überprüfen, benötigen wir die Anzahl der Datensätze. Da wir hier nicht sequentiell arbeiten, sondern einen random access haben, einen WAHLFREIEN ZUGRIFF, ist das alles sehr einfach.

Anzahl Datensätze = ftell(am Dateiende)/Datensatzgröße. In Code:

fseek(file,0,SEEK_END);
anz_data=ftell(file)/bufsize;

Wenn ftell(file)/bufsize größer ist als anz_data, sind wir im Nirwana (ohne allerdings daß was abstürzt) und setzen ftell() folgendermaßen zurück:

fseek(file,0,SEEK_END); ftell() folgt dann automatisch.

Wenn wir am Dateiende stehen, wir ein Lesezugriff natürlich keine Daten liefern, dahinter ist ja nichts. Wir müssen vorher soviele Datensätze zurück, wie wir sehen wollen. Auf den Bildschirm bezogen, haben wir eine Anzahl Zeilen verfügbar. Um das Dateiende zu sehen:

fseek(file,-1*anz_zeilen*bufsize,SEEK_END);

Von da an gelesen, erhalten wir die letzten Datensätze passend zu der verfügbaren Anzahl von Bildschirmzeilen.

Wie auf den Abb. erwähnt, ist es klar, wenn man nach einem bestimmten Feld sortiert, daß die Nummern der Datensätze natürlich nicht mehr fortlaufend sind. Wenn man im Dezember noch ein paar Buchungen von Mai nachträgt, kann das ja nicht anders sein. Hier ist es allerdings besonders wild, weil die Datums-Angaben mit einem Zufallsgenerator erzeugt wurden, verdeutlicht das Prinzip aber umso besser.

Wir können natürlich auch nach der Datensatz-Nr sortieren oder nach dem Betrag, nur ist die chronologische Reihenfolge die natürlichste, weil sie sich ja auch auf den Kontoauszügen, nach denen gebucht wird, findet.

Code und Erläuterungen dazu im nächsten Beitrag.

Der Beitrag wurde von sharky bearbeitet: 22.02.2012, 08:48 Uhr
Angehängte Datei(en)
Angehängte Datei  blaettern1.jpg ( 268.75KB ) Anzahl der Downloads: 5
Angehängte Datei  blaettern2.jpg ( 299.1KB ) Anzahl der Downloads: 5
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 22.02.2012, 09:12 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Wie "fühlt" sich diese Ausgabe an?

Sie ist sauschnell, ohne jede Zeitverzögerung können wir die 25 Bildschirmzeilen mit der Tastatur durchblättern, vor und zurück natürlich, und 1500 Datensätze, wie sie hier vorliegen, hat man in wenigen Sekunden durchgeblättert.

C ist eine maschinennahe Sprache und daher, wenn man richtig programmiert, IMMER sauschnell. wink.gif

Nun ist das mit geöffneten Dateien, in denen sich wichtige Daten befinden, immer so eine Sache. Stürzt der Rechner ab oder passiert sonstwas, kann die Datei beschädigt werden, und wie immer in solchen Fällen hat man natürlich keine Sicherungskopie zur Hand.

Daher blättern wir nicht in der Originaldatei, sondern fertigen uns eine Kopie vom Original. Das hat den weiteren Vorteil, daß im Untermenü weitere Sortierungskriterien berücksichtigen können, z. B. Datum von ... bis, Beträge von ... bis oder solche mit einem bestimmten Text. Der Vorteil ist die Performance: die Kopie sucht dann nach diesen Parametern nur einmal, und beim Blättern muß einfach nur ausgegeben werden. Wollte man beim Blättern sortieren, ergäbe das einen schrecklichen Code und die Performance leidet in dem Maße, wie der Organisationsaufwand steigt.

Der Kern des Programmbausteins ist dieser:

while (1)
{
clrSCR(DIAL);
gotoSCR(DIAL); // Hauptbildschirmbereich
zeile=1;
filepos=ftell(kopie);
while (fread(&buffer,bufsize,1,kopie)==1&&zeile<maxzeilen)
{
gotoxy(DIALX+1,DIALY+zeile);printf("Datensatz Nr.%6i Datum %s",buffer.ident_nr,buffer.datum);
zeile ++;
}
// Dateizeiger steht jetzt maxzeilen hinter dem ersten Datensatz daher zurücksetzen:
fseek(kopie,filepos,SEEK_SET); // steht jetzt vor dem 1. Datensatz
liestaste(&taste);

switch (taste.st) // nur Steuertasten
{

case ESCAPE:
fclose(kopie);
clrSCR(DIAL);
return;
case PFEILOBEN:
fseek(kopie,-1*bufsize,SEEK_CUR);
break;
case PFEILUNTEN:
fseek(kopie,bufsize,SEEK_CUR);
if (ftell(kopie)/bufsize>anz_datensaetze)fseek(kopie,-1*bufsize,SEEK_END); // EOF END OF FILE
break;
case POS1: fseek(kopie,0,SEEK_SET); // Dateianfang
break;
case ENDE: fseek(kopie,-1*bufsize*maxzeilen,SEEK_END); // EOF
break;
case BILDOBEN:
fseek(kopie,-1*bufsize*maxzeilen,SEEK_CUR);
break;
case BILDUNTEN:
fseek(kopie,bufsize*maxzeilen,SEEK_CUR);
if (ftell(kopie)/bufsize>anz_datensaetze-maxzeilen)fseek(kopie,-1*bufsize*maxzeilen,SEEK_END); // EOF END OF FILE
break;
}
} // while endlos

In eine Endlosschleife verpackt, gibt das Programm soviele Datensätze aus, wie auf den Bildschirmbereich passen. Und wartet dann auf eine Taste. 1 Datensatz zurück = PFEILOBEN, einen ganzen Bildschirm zurück = BILDOBEN, Dateianfang POS1 usf.

Aber Achtung: Nach dem Auslesen und Schreiben in den Bildschirm steht der Dateizeiger nicht mehr auf dem 1. Datensatz, sondern ist ja 25 Zeilen weitergerückt. Er muß also nach der Ausgabe wieder dahin zurückgesetzt werden. Das erreichen wir mit dem Schleppzeiger filepos. Dieser merkt sich vor dem Lesen der Datei die Position filepos=ftell(FILE), und kann, wenn der Bildschirm geschrieben wurde, VOR dem switch-Block den Dateizeiger wieder zurücksetzen.

Wenn wir soweit nach hinten blättern, daß der Bildschirm nicht mehr mit Datensätzen gefüllt würde,

if (ftell(kopie)/bufsize>anz_datensaetze-maxzeilen

setzen wir den Filezeiger um anzahl Zeilen zurück, dann ist der Bildschirm komplett ausgefüllt. Hat die Datei nur wenige Datensätze, passiert nichts, weil fseek() wie gesagt am Anfang der Datei immer einen ftell() von 0 ergibt.

Es würde nur helfen, das unschöne Bildschirmflackern (crlSCR) abzustellen, wenn wir wüßten, wir hätten genug Datensätze, um den Bildschirm zu füllen, können wir crlSCR weglassen, und die Darstellung beim Blättern ist für das Auge ruhiger. Wenn man will, kann man das clrSCR ja von dieser Bedinung abhängig machen.

Man sieht, das ist total easy und in wenigen Minuten fertig programmiert.

Will man weitere Parameter zur Sortierung, muß man das im Vorlauf noch einbauen.

Der Funktionskopf :

void blaettern_bdatei(char *dname)
{
FILE *bdp;
FILE *kopie;
char kopie_name[255]="kopie.bin";
struct s_bsatz buffer;
long bufsize=sizeof(struct s_bsatz);
long filepos=0;
int zeile;
int maxzeilen=DIALZ-1; // wieviele Zeilen faßt der Bildschirmbereich?
long anz_datensaetze;
struct s_taste taste;


bdp=fopen(dname,"rb"); if (bdp==NULL){meldung("Dateifehler",dname,"nicht gefunden");return;}
kopie=fopen(kopie_name,"wb"); if (kopie==NULL){meldung("Dateifehler",kopie_name,"");return;}

// kopieren der Datei entweder komplett oder mit zusätzlichen Auswahlkriterien:

while(fread(&buffer,bufsize,1,bdp)==1)
if (fwrite(&buffer,bufsize,1,kopie)!=1){meldung("Dateifehler",kopie_name,"");return;}
fclose(bdp);// originaldatei wieder schließen
fclose(kopie); // muß sein, ist im Schreibzugriff geöffnet, wir wollen aber lesen
kopie=fopen(kopie_name,"rb"); // jetzt im Lesemodus geöffnet
fseek(kopie,0,SEEK_END);
anz_datensaetze=ftell(kopie)/bufsize; // ftell() liefert diesen Wert natürlich nur, wenn es mit fseek() vorher aufs Dateiende gesetzt wurde
if (anz_datensaetze==0){meldung("Leere Datei","oder leere Auswahl","");fclose(kopie);return;}


// Aufbau des Bildschirmmenüs:
clrSCRBUF(MENU);my_puts(MENU,"Blaettern in der Datei",5,1);my_puts(MENU,dname,45,1);
clrSCRBUF(BOX2);my_puts(BOX2,"Navigation Pfeiltasten, Bildoben/unten,Pos1/Ende",5,1);
my_puts(BOX2,"Ende der Bearbeitung mit ESCAPE",5,3);
showSCRBUF(MENU,0);
showSCRBUF(BOX2,0);
clrSCR(DIAL);// bildschirm loeschen
fseek(kopie,0,SEEK_SET); // Dateizeiger auf den Anfang setzen
while (1)
{

Bei den Parametern MENU, BOX2 usf. handelt es sich um die sehr weit oben schon sattsam beschriebenen Bildschirmbereiche. Diese haben folgende Parameter:

MENUX MENUY = die Ecke oben links
MENUZ MENUS = Anzahl Spalten und Zeilen

Will man nun in das Fenster schreiben, muß man sozusagen die Nullpunktverschiebung aufschlagen (wir wollen gar nicht wissen, ob das Fenster links am Rand ist oder irgendwo in der Mitte, und müssen das auch nicht):

gotoxy(MENUX+gewünschte Position,MENUY+zeile).

Und wieviele maxzeilen wir ausgeben können, sagt uns maxzeilen=MENUZ

Für das Fenster DIAL entsprechend maxzeilen=DIALZ

Man sieht, das ist wie Bauklötzchen sortieren. Keine große Nummer.

Zusammenfassend:

Mit den Funktionen fseek und ftell kann man auf allereinfachste Weise eine Binärdatei am Bildschirm auslesen, es ist überhaupt nicht erforderlich, ein Abbild der Datei in eine dynamische Struktur zu kopieren.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 22.02.2012, 17:24 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Ein Organigramm eines Anwenderprogramms:

1. Datensätze erfassen

Neuer Datensatz--> Eingabemaske-->Datensatz bearbeiten-->speichern

2. Datensätze bearbeiten

Datensatz suchen-->Listenansicht Datei-->Datensatz auswählen-->Datensatz bearbeiten-->speichern

3. Primanota

Datei Primanota öffnen-->Auswahkriterien eingeben-->in TEXTFILE speichern=Ausdruck über Editor oder Tab-Kalk.

4. BWA

BWA für bestimmten Zeitraum errechnen-->in TEXTFILE speichern=Ausdruck s. o.

5. Kontenjournale

Auswahlmaske Konten-->Primanota nach Konto sortiert ausgeben-->Auswahl in TEXTFILE speichern=s. o.

6. Stammdatenpflege

-->Kontenrahmen
-->Betriebsdaten
-->Anlagekonten
-->Abschreibungs-Vorgaben

7. Dienstprogramme

-->Datei in Textfile umkopieren
-->Sortierung der Dateien ändern
-->Sonstiges

Ich will nicht sagen, JEDES Programm muß das leisten, dafür sind Programme im allgemeinen zu unterschiedlich, aber jedes Programm, was Daten, welche vom Anwender mit der Hand eingegeben werden, erfassen, bearbeiten, dokumentieren will, muß das, was da oben steht, MINDESTENS leisten.

Man sieht, daß die Bearbeitung von vorhandenen Datensätzen etwas komplexer ist als die Erfassung von neuen Datensätzen, denn bevor man vorhandene Datensätze bearbeitet, muß man sie ja erstmal finden.

Das Stichwort ist: LISTENANSICHT unter Punkt 2.

Wir haben ja bei der Erfassung eine Bildschirmmaske, in der sich der zu bearbeitende Datensatz über den ganzen Bildschirmbereich aufbaut, zum suchen taugt das allerdings nicht, wir müssen in die LISTENANSICHT wechseln.

So eine Listenansicht haben wir schon verfügbar, nämlich die eben vorgestellte Dateiroutine mit fseek/ftell, welche uns den Datensatz ZEILENWEISE ausgibt. Bzw. die Felder des Datensatzes, die man dafür auswählt.

Um einen Buchungssatz komplett darzustellen, braucht man DINA4QUER, 120 Zeichen in der Breite. Das muß hier noch realisiert werden durch Definition eines neuen Fensters. Am Prinzip ändert das aber nichts, wir brauchen eine zeilenweise Ausgabe.

Screencopy: Die Listenansicht der Binärdatei Buchungen bzw. hier noch Zufallsdatei etwas geändert bzw. erweitert.

Wir müssen dann irgendwie aus solch einer Liste den Datensatz aussuchen, den wir ggflls. überarbeiten wollen.

Wie?

Es würde sich anbieten, eine Zeile aus der Liste in inverser Farbdarstellung zu bringen, die man je nachdem scrollen kann. Drückt man auf Enter, hat man den Buchungssatz.

Inverse Farbdarstellung innerhalb einer Funktion sind aber immer mindestens 2 WINDOWS-Systemaufrufe. Das gefällt mir nicht, daß der Anwender mit wildem Herumspielen an der Tastatur, möglicherweise über Nacht noch den Aschenbecher draufgestellt, ein FEUERWERK VON SYSTEMAUFRUFEN provozieren kann, welche die Hardware des Computers bzw. der Grafikkarte betreffen und letztlich den Computer beschädigen könnten.

Die Eingabe der Ident-Nr. des Datensatzes ist unbequem, außerdem kann man sich vertippen.

Wir machen daher eine grafische Lösung so zwischen Touchscreen und Steinzeit angesiedelt, bleiben schön SAFE und und schreiben:

char pfeil[255]={(int)240,(int)240,(int)240,(int)240,'>'};

Schöne Definition, oder? wink.gif

Die Erklärung findet sich in Abb. 2, ASCII-Tabelle.

Die Zeichen, die dort zu sehen sind, können wir auf dem Bildschirm darstellen, indem wir den ASCII-WErt aufrufen. DUmmerweise haben wir ein Durcheinander von ASCII und ANSI-Zeichencode, was der Grund dafür ist, daß der PC im KOnsolenfenster SO OHNE WEITERES NICHT Umlaute wie ä ü ß darstellen kann, weshalb ich drauf verzichte, IM PRINZIP GEHT ES NATÜRLICH DOCH, aber mit viel Aufwand, der es nicht wert ist.

Wir wollen diesen Pfeil mal aufrufen mit der Anweisung:

if (auswahlmodus==JA)
{
gotoxy(DIALX+1,DIALY+pfeilzeile+absatz);
printf(pfeil);
gotoxy(DIALX+DIALS-1,DIALY+DIALZ-1); // Cursor hide
}

Das Cursor hide heißt, daß der Cursor nicht hinter dem PFeil erscheinen soll. Ich hab leider noch keinen guten Befehl gefunden, um den Cursor abzuschalten bzw. wieder zu aktivieren, außerdem wär das wieder eine Systemfunktion, was mir grundsätzlich bei solchen Lappalien nicht gefällt.

Der Screen sieht dann so aus wie in Abb3. Unser ASCII-Pfeil zeigt dort auf den Datensatz mit der Ident-Nr. 710.

Wir können den Pfeil nach oben oder unten scrollen, mit den Pfeiltasten, in demselben Menü, wie wir auch die Datensätze der Datei scrollen. Programmierung anschließend. Der Sinn ist es natürlich, den Datensatz, der hinter dem Pfeil steht, auszuwählen und zu editieren.

Der Beitrag wurde von sharky bearbeitet: 22.02.2012, 17:34 Uhr
Angehängte Datei(en)
Angehängte Datei  auswahlmodus2.jpg ( 164.96KB ) Anzahl der Downloads: 5
Angehängte Datei  ansii.jpg ( 319.15KB ) Anzahl der Downloads: 5
Angehängte Datei  auswahlmodus.jpg ( 164.42KB ) Anzahl der Downloads: 4
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 22.02.2012, 17:58 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Da die Tasten der Tastatur nicht beliebig vermehrbar sind, müssen wir, wenn wir zwei Scrolling-Funktionen in demselben Fenster umsetzen wollen, UMSCHALTEN.

Sowas macht man mit FLAGS.

Ein FLAG (ja ich weiß, wir leben in Deutschland, aber die Deutschen haben die Programmierung nicht erfunden, alles easy ..) ist so etwas wie ein Schalter. Den kann man ON oder OFF schalten.

Hier heißt der Schalter auswahlmodus. Ist Auswahlmodus == JA (also ON), interpretiert das Programm die Tastatureingaben anders, als wenn der auswahlmodus == NEIN (OFF) wäre. Man könnte natürlich statt JA/NEIN auch beliebig viele Zustände definieren.

Mir persönlich gefallen Funktionen, die länger als ein Bierdeckel sind, schon mal gar nicht. Diese Funktion wird leider etwas länger als ein Bierdeckel, Grund genug, sie irgendwann umzuschreiben. Um da Volumen rauszunehmen, muß man weiter abstrahieren und Befehlsgruppen bilden, mit denen man viele Zeilen zu einer Anweisung zusammenfassen kann. Das ist bei der Programmentwicklung natürlich nicht sofort möglich, folgt dann.

Schreibt man routinemäßig viele Programme, hat man die Bausteine natürlich längst parat. Ich hab aber seit 15 Jahren kein Anwender-Programm geschrieben und habe nichts parat.

Also, der Bierdeckel reicht nicht mehr ganz, weil wir nun dem Programm sagen müssen:

Ist FLAG auswahlmodus==ON dann scrolle den Pfeil, ist auswahlmodus==OFF, dann scrolle in der Datei herum. Sieht dann so aus:

while (1)
{

if (anz_datensaetze<maxzeilen)clrSCR(DIAL);
gotoSCR(DIAL); // Hauptbildschirmbereich
filepos=ftell(kopie);
zeile=1;
gotoxy(DIALX+10,DIALY+0);printf("Ident-Nr. Datum Brutto");
while (fread(&buffer,bufsize,1,kopie)==1&&zeile<maxzeilen)
{
gotoxy(DIALX+10,DIALY+zeile+absatz);printf("%6i %s %12.2f",buffer.ident_nr,buffer.datum,buffer.brutto);
zeile ++;
}

if (auswahlmodus==JA)
{
gotoxy(DIALX+1,DIALY+pfeilzeile+absatz);
printf(pfeil);
gotoxy(DIALX+DIALS-1,DIALY+DIALZ-1); // Cursor hide
}


liestaste(&taste);

fseek(kopie,filepos,SEEK_SET); // Dateizeiger auf ersten sichtbaren Datensatz

if (auswahlmodus==NEIN)
{
switch (taste.st) // nur Steuertasten
{
case F10: auswahlmodus=JA;
break;
case ESCAPE:
fclose(kopie);
clrSCR(DIAL);
return;
case PFEILOBEN:
fseek(kopie,-1*bufsize,SEEK_CUR);
break;
case PFEILUNTEN:
fseek(kopie,bufsize,SEEK_CUR);
if (ftell(kopie)/bufsize>anz_datensaetze)fseek(kopie,-1*bufsize,SEEK_END); // EOF END OF FILE
break;
case POS1: fseek(kopie,0,SEEK_SET); // Dateianfang
break;
case ENDE: fseek(kopie,-1*bufsize*maxzeilen,SEEK_END); // EOF
break;
case BILDOBEN:
fseek(kopie,-1*bufsize*maxzeilen,SEEK_CUR);
break;
case BILDUNTEN:
fseek(kopie,bufsize*maxzeilen,SEEK_CUR);
if (ftell(kopie)/bufsize>anz_datensaetze-maxzeilen)fseek(kopie,-1*bufsize*maxzeilen,SEEK_END); // EOF END OF FILE
break;
}// switch
} // kein Auswahlmodus
else if (auswahlmodus==JA)
{

gotoxy(DIALX+1,DIALY+pfeilzeile+absatz); printf(" "); // alte Pfeiltaste löschen

switch (taste.st)
{
case F5: auswahlmodus=NEIN;

break;
case ESCAPE: auswahlmodus=NEIN;

break;
case PFEILOBEN:

if (pfeilzeile>1)pfeilzeile--;
break;
case PFEILUNTEN:
if (pfeilzeile<maxzeilen-1)pfeilzeile++;
break;
case ENTER:
break;

} // switch Pfeil verschieben
} // else auswahlmodus==JA

} // while endlos



}// fu

Damit können wir mit F10 das Scrolling für den Pfeil einschalten (natürlich im Auswahlmodus NEIN, sonst liest es das Programm ja gar nicht), und mit F5 den Modus aufheben, natürlich im AUswahlmodus JA.

Nun müssen wir eine andere Taste definieren, wenn der Pfeil vor dem Datensatz xy steht, daß man den wählen kann.

Und müssen die Position des Pfeils (Zeile) mit der Positions des Datensatzes (ftell() ) verknüpfen, um diesen Datensatz anschließend zu bearbeiten.

Was wir da am Bildschirm sehen, ist eine Kopie einer Auswahl aus der Originaldatei.

Um die Änderungen durchzuführen, brauchen wir nur unsere bereits vorhandene EDIT-Funktion des Datensatzes einzulesen. Und speichern ihn dann

...

wohin?

Nun, erstmal in die Kopie natürlich. Man kann dann in die Kopie ein weiteres FLAG auf den Datensatz setzen für editiert. Ist ein solches Flag nicht vorgesehen, muß man die Datenstruktur des Datensatzes s_bsatz verändern. Und es hinzufügen.

Das würde bedeuten, daß man bereits gespeicherte Datensätze nicht mehr lesen kann, weil sich die Datenstruktur geändert hat. Oder sie sehr umständlich umwandeln muß in das neue Format.

Alles dieses spricht dafür, ein Programm VOR INBETRIEBNAHME erstmal gründlich zu testen und da noch keine WICHTIGEN DATEN einzugeben.

Dann muß man bestimmen, wann diese Kopie zurückkopiert wird in die Originaldatei, z. B. entweder sofort oder am Programmende.

Damit der geänderte Datensatz aus der Kopie an der richtigen STelle der Originaldatei landet, braucht es einen eindeutigen IDENTIFIER, als der hier die ident_nr des Datensatzes dient. Unter allen Umständen muß man vermeiden, daß Ident-Nummern doppelt vergeben werden, sonst wird es akribisch. Um die höchste vergebene Ident-Nr zu suchen, beim Programmstart, darauf hatte ich schon hingewiesen, ist es VÖLLIG FALSCH, die Ident-Nr des letzten Datensatzes zu nehmen. Der kann ja durch SOrtierung irgenwo stehen. Man muß, um die neue Ident-Nr. zu vergeben, beim Programmstart DIE GANZE DATEI DURCHSUCHEN. Alles andere ist MIST.

Aber das ist ja wohl selbsterklärend.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 23.02.2012, 18:23 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Wir verkoppeln jetzt unseren Pfeil mit dem Datensatz, den wir am Bildschirm auswählen wollen, um ihn zu bearbeiten.

Dazu wurde die Definition von "Pfeil" etwas geändert, aber dazu später.

Erstmal Screenshot, wie es funktioniert:

Wir wählen aus dem (provisorischen) Menü die Option F6, sehen die Datei mit den Buchungssätzen ab 1. Jan., scrollen dann bis in den April, aktivieren unsere Pfeilmarkierung, scrollen mit dem Pfeil bis vor den gewünschten Datensatz und drücken ENTER.

Dann erscheint hier, erstmal provisorisch, im Nachrichtenfenster unten rechts die Ident-Nr. des Datensatzes, von dem das Programm meint, daß wir ihn ausgesucht haben. (Abb. 1-6 mit Erklärungen).

Wie koppeln wir den Pfeil nun an den richtigen Datensatz?

Das ist ein bißchen kniffelig, weil wir Daten verknüpfen müssen, die logisch miteinander nichts zu tun haben, und zwar als Indizierung. Da kann man schnell danebengreifen.

Der Koppelungspunkt ist die filepos, welche ftell() liefert, also die Adresse innerhalb der Datei.

Bei der Ausgabe hat der erste Datensatz eine Adresse, die mit FILEPOS markiert ist. Mit jeder Zeile nimmt die Adresse um bufsize (Größe des Datensatzes zu):

1. Zeile Adresse=FILEPOS

2. Zeile Adresse=FILEPOS + bufsize

3. Zeile Adresse=FILEPOS + 2*bufsize

4. Zeile Adresse=FILEPOS + 3*bufsize

und so weiter, also:

n. Zeile Adresse=FILEPOS +(n-1)*bufsize

Diese Adresse wird am Anfang der Schleife fortlaufend aktualisiert mit:

pfeil.filepos= filepos+pfeilzeile*bufsize-bufsize;

pfeilzeile ist dabei der Wert für die Zeile, in der der Pfeil erscheint.

Drückt der Anwender dann Enter, geschieht folgendes:

case ENTER:
fseek(kopie,pfeil.filepos,SEEK_SET);
fread(&buffer,bufsize,1,kopie);
sprintf(info,"%8i",buffer.ident_nr);
meldung("Gewaehlter Datensatz","hat die Ident-Nr.",info);
break;

Wir suchen mit fseek() gem. der gespeicherten Adresse den Datensatz auf, lesen ihn in den Buffer buffer, und geben hier im Nachrichtenfenster unten rechts die Ident-Nr aus, um zu überprüfen, ob die Auswahl auch stimmt.

Sie stimmt.

Das Nachrichtenfenster dient hier nur zur Kontrolle. Tatsächlich müßte der Datensatz natürlich jetzt der Funktion EDIT übergeben werden, deren Definition lautet:

int bsatz_edit(struct s_bsatz *data)

also eine Funktion, die praktischerweise CALL BY REFERENCE arbeitet, erkenntlich an dem * Operator,

die Übergabe erfolgt dann mit:

if (bsatz_edit(&buffer)==OK) ... und so weiter, dann speichern.

Diese grafisch orientierte Auswahl mit dem Pfeil ist besser, als müßte man die Ident-Nr. in die Tastatur tippen. Weil man dann den Blick vom Bildschirm nehmen muß und sich evtl. vertippt oder sonstwas.

So ist es für den Anwender sehr bequem, das Scrollen hat den ergnometrischen Effekt, daß man mit den Augen am Bildschirm hängen bleiben kann und sieht, was da geschieht.

Also wir sehen, lauter Fleißkärtchen gesammelt aber es ist wirklich keine große Nummer, in 30 Minuten fix und fertig programmiert.
Angehängte Datei(en)
Angehängte Datei  pfeil1.jpg ( 125.49KB ) Anzahl der Downloads: 3
Angehängte Datei  pfeil2.jpg ( 210.35KB ) Anzahl der Downloads: 3
Angehängte Datei  pfeil3.jpg ( 208.14KB ) Anzahl der Downloads: 2
Angehängte Datei  pfeil4.jpg ( 216.15KB ) Anzahl der Downloads: 1
Angehängte Datei  pfeil5.jpg ( 220.38KB ) Anzahl der Downloads: 1
Angehängte Datei  pfeil6.jpg ( 226.24KB ) Anzahl der Downloads: 4
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 25.02.2012, 13:57 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Ich will das jetzt mal in die angedachte Verknüpfung bringen. Da Buchungs-Datensätze recht lang sind (DINA4QUER), wurde ein neues Fenster definiert von 120 Zeichen Breite. Da erscheinen nun die Datensätze aus der Datei zufall.bin.

ABB1: wir suchen einen Datensatz aus der Zufallsdatei aus.

Diese Zufallsdatei ist nicht logisch belegt, z. B. Umsatzsteuer ist frei erfunden etc., sieht man daran, daß z. B. das Umsatzsteuerkonto fehlt etc. Das macht aber gar nichts, denn nun übergeben wir diesen fehlerhaften Datensatz der Funktion

bsatz_edit(), die wie der Name sagt den Buchungssatz editieren soll. Sie gibt dann, wenn die Eingabe in Ordnung ist und der Anwender nicht mit ESC abgebrochen hat, den Funktionswert OK zurück.

Wir befinden uns noch in der Auswahl gem. Abb. 1, und nun drückt der Anwender ENTER.

(achtung Abb. 1 findet sich am Ende, da es einen Fehler beim Upload gab. Abb1=Nr. 5 und die Nr. 1 ist Abb 2, sorry).

Dann geschieht folgendes:

case ENTER:
fseek(kopie,pfeil.filepos,SEEK_SET);
if (fread(&buffer,bufsize,1,kopie)!=1)meldung("Dateifehler 107","","");
if (bsatz_edit(&buffer)!=OK){meldung("Fehler beim Editieren","","");return;}
fseek(kopie,-1*bufsize,SEEK_CUR);
if (fwrite(&buffer,bufsize,1,kopie)!=1)meldung("Dateifehler 109","","");
clrSCR(JRNL);
fseek(kopie,filepos,SEEK_SET); // zurücksetzen
break;

Die Verknüpfung von dem, was man auf dem Bildschirm sieht, und von dem, was in der Datei steht, erfolgt über FILEPOS. Diese existiert 2mal, nämlich für die Bildschirmausgabe und für die Pfeilmaske. Wie man das organisiert, ist egal, man muß nur dran denken, daß wenn man in der datei 5 Sätze vorrückt, nicht der 5. Satz gelesen wird, sondern der 6. (dazu im Anschluß ein Mini-Programm, was das sehr gut verdeutlicht. Das KLEINE 1x1 der PROGRAMMIERUNG muß man im Kopf haben, man darf da nicht nachdenken müssen. Das kann man sich mit etwas Übung so aneignen, daß man auch bei schwerwiegenden Fehlern sofort weiß, wo der Hase langläuft).

Wir suchen also den Datensatz auf, dem wir dem Pfeilzeiger (hoffentlich richtig) zugeordnet haben, lesen den Inhalt in die struct-Variable buffer, und übergeben ihn der Funktion bsatz_edit.

Wir übergeben also die Adresse der eingelesenen lokalen Variable nach außen.

bsatz_edit macht dann die schon bekannte Buchungsmaske mit den Daten des herausgefischten Buchungssatzes aus der Zufallsdatei.

ABB2: Datensatz ist in bsatz_edit() angekommen und wird angezeigt.

Aber mit den unsinnigen Zahlen des Zufallsgenerators, also falsch angezeigt. Um das zu testen, drücken wir aus der Buchungsmaske heraus mal F10 (=Eingabe Ende), und sehen uns unten rechts im Fenster an, was die Routine zu meckern hat.

Dann sind wir in ABB. 3

Die Funktion ist gnädig mit uns, sie meckert nur wg. dem Umsatzsteuer-Konto. Die logische Prüfung der Konten und Gegenkonten ist hier noch nicht implementiert und würde bei einem Zufallsgenerator auch nur unnötigen Aufwand bedeuten.

Um den Fehler mit der Umsatzsteuer in Ordnung zu bringen, brauchen wir uns nur durch die Maske mit ENTER durchzuklicken und die richtige Umsatzsteueroption zu wählen.

Danach ist der Datensatz formal in Ordnung, sieht man in ABB. 4. Damit man sieht, daß/ob hier editiert und neu gespeichert wurde, wird rein formal noch was in die Textzeile reingesetzt.

Wenn wir JETZT F10 drücken, meckert die Funktion nicht mehr, sondern gibt OK zurück. Wir befinden uns dann wieder in der aufrufenden Funktion an dieser Stelle (rot markiert):

case ENTER:
fseek(kopie,pfeil.filepos,SEEK_SET);
if (fread(&buffer,bufsize,1,kopie)!=1)meldung("Dateifehler 107","","");
if (bsatz_edit(&buffer)!=OK){meldung("Fehler beim Editieren","","");return;}
fseek(kopie,-1*bufsize,SEEK_CUR);
if (fwrite(&buffer,bufsize,1,kopie)!=1)meldung("Dateifehler 109","","");
clrSCR(JRNL);
fseek(kopie,filepos,SEEK_SET); // zurücksetzen
break;

Nachdem die Edit-Funktion also ihr OK übermittelt hat, setzen wir den FILEpointer von der Stelle, an der wir gelandet waren nach Einlesen des Datensatzes, um eben diesen einen Datensatz zurück und schreiben den geänderten Datensatz in die Datei.

Da durch unsere Manipulation der Bildschirm verschoben würde, setzen wir den FILEpointer wieder auf die Stelle zurück, die der ersten Zeile der Ausgabe entspricht. Das muß man nicht, aber sonst wird die Bearbeitung unruhig, weil die Datensätze verrutschen.

Ergebnis dann die ABB. 5.

Wir sehen unseren überarbeiteten Datensatz mit neuem Text und die Umsatzsteuer-Optionen stimmen jetzt. Wir haben jetzt als einziger in der Gruppe ein richtiges Umsatsteuer-Konto. wink.gif

Wenn man solche indizierten oder nicht indizierten Listen und Dateien über mehrere Durchlaufstationen miteinander verknüpft, sind die Fehlermöglichkeiten sehr zahlreich.

In der C-Laufzeitumgebung werden wir niemals eine Fehlermeldung sehen, wenn wir Unsinn machen. Unsinn ist erlaubt.

Hauptsächliche Fehler in C sind Bereichsüberläufe. Die hängen damit zusammen, daß der INDEX von 0..9 zählt, die resultierende Feldlänge aber 10 ist, und greifen wir auf feld[10] zu, greifen wir ins Klo.

Bei den Dateien ist das ähnlich. Sie sind schließlich auch Listen, und wenn man sich um 1 vertut, gibt das nix.

Daher statt langer Monologe mal ein sehr kurzes knappes Programm über den Umgang mit dem FILEpointer.

#include <stdio.h>
#include <stdlib.h>

char dname[]="test.bin";
struct sdata{
int nr;
char text[50];
};
long bufsize=sizeof(struct sdata);

void erzeuge_datei()
{
int i;
struct sdata data;
FILE *bdp;
bdp=fopen(dname,"wb");
for (i=0;i<20;i++)
{
data.nr=i+1;
sprintf(data.text,"Mitarbeiter Nr.");
fwrite(&data,bufsize,1,bdp);
} // for
fclose(bdp);
} // fu

void teste_datei()
{
FILE *bdp;
struct sdata buffer;
bdp=fopen(dname,"rb");
fseek(bdp,0,SEEK_SET);
fread(&buffer,bufsize,1,bdp);
printf("%s %4i\n",buffer.text,buffer.nr);
fseek(bdp,3*bufsize,SEEK_SET);
fread(&buffer,bufsize,1,bdp);
printf("%s %4i\n",buffer.text,buffer.nr);
fclose(bdp);
}

int main(int argc, char *argv[])
{
erzeuge_datei();
teste_datei();
system("PAUSE");
return 0;
}

Das Programm ist so, wie es ist, lauffähig.

Die Bildschirmausgabe:

Mitarbeiter Nr. 1
Mitarbeiter Nr. 4
Press any key to continue . . .

Der Witz, worauf ich hinweisen möchte:

fseek(bdp,3*bufsize,SEEK_SET);
fread(&buffer,bufsize,1,bdp);

Wir rücken 3 vor und befinden uns dann wo? Auf dem 4. Datensatz, nicht auf dem 3.!

Und wenn wir den Datensatz Nr. 4 gelesen haben, sind wir auf Nr. 5, und hängen nicht mehr auf Nr. 4.

Haben wir den Satz Nr. 4 gelesen, bearbeitet und wollen ihn zurückschreiben, müssen wir den Dateizeiger (FILEpointer) erstmal um 1 Datensatz zurück bewegen, um vom Ende des 4. auf Anfang des 4. zu gelangen.

Zwischen Ende n und Anfang n+1 gibt es nichts. Es ist völlig identisch. Ist die Buffergröße 5, sieht es gedanklich aus:

FALSCH:

1-> Anfang 1
2
3
4
5
6->Anfang 2
7
8
9
10

Da der Dateizeiger wie bei jedem Index auch aber nicht bei 1, sondern bei 0 beginnt zu zählen, sieht es tatsächlich nämlich so aus:

// KORREKT:

0-> Anfang 1
1
2
3
4
5->Anfang 2
6
7
8
9

Also:

Will ich Datensatz Nr. 3 lesen, muß ich 2*bufsize vorrücken und nicht 3!

Ganz schlimm wäre es, bei einer Buffergröße von 5 den Filezeiger direkt zu adressieren mit 6ff, weil er tatsächlich ab 5ff adresseirt werden müßte, weil er nicht bei 1 anfängt zu zählen sondern bei 0.

Ich hoffe, ich hab mich einigermaßen verständlich ausgedrückt.

Der Beitrag wurde von sharky bearbeitet: 25.02.2012, 14:09 Uhr
Angehängte Datei(en)
Angehängte Datei  uebergabe2.jpg ( 130.98KB ) Anzahl der Downloads: 3
Angehängte Datei  uebergabe3.jpg ( 133.27KB ) Anzahl der Downloads: 1
Angehängte Datei  uebergabe4.jpg ( 133.93KB ) Anzahl der Downloads: 3
Angehängte Datei  uebergabe5.jpg ( 274.35KB ) Anzahl der Downloads: 4
Angehängte Datei  uebergabe.jpg ( 163.02KB ) Anzahl der Downloads: 5
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 26.02.2012, 15:41 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Zur Organisation der Variablen und der Datenübergabe

In dem ganzen Programm gibt es keine einzige Globale Variable.

Was wir haben, sind globale Variablen, die als globale Konstanten verwendet werden (read only), obwohl sie nicht so deklariert sind. Dabei wäre es praktisch und auch ungefährlich, würde man etwa den sehr häufig verwendeten Buffer struct datensatz{} zumindest mit einem Datensatz global machen. Dann könnten alle Funktionen auf die Datenfelder zugreifen, die eine setzt das Datum ein, die andere den Brutto-Betrag und so weiter, selbst wenn dieser eine Datensatz beschädigt würde, würde nicht viel geschehen, weil es bei der Überprüfung vor dem Abspeichern herauskäme.

Wenn man darauf verzichtet, global zu deklarieren, hat man im Übrigen ein Problem mit der Werteübergabe. Zwar kann man auch sehr große Structures CALL BY VALUE übergeben, dann wird eine Kopie des Datensatzes oder des Datensatz-Arrays auf den Stack gelegt, aber große Datenmengen zu kopieren verschlechtert die Performance und führt möglicherweise irgendwann zum STACK-OVERFLOW.

Will man also konsequent auf Globale Variablen verzichten, bleibt nur die Werteübergabe mit einem Pointer/bzw. Adreßoperator.

Was ist an Globalen Variablen so schlimm?

Nun, ich seh das so, daß alles Globale grundsätzlich die Stabilität des Programms gefährdet. Der Grund ist nicht, daß man ein Programm nicht so schreiben könnte, daß es nicht laufen würde, sondern der Grund ist, daß es mit einem einfachen Programmgerüst beginnt, aber nicht dabei bleibt, es kommt Funktion um Funktion dazu, und die anfängliche Übersicht geht mit der Zeit zunehmend verloren. Im Prinzip ist es so, daß man über die früher geschriebenen Programmbestandteile irgendwann überhaupt keine Übersicht mehr hat. Man muß sich blind drauf verlassen können, daß diese als stabile Bausteine in spätere Funktionen problemlos eingebunden werden können. Selbst wenn es bis jetzt noch alles gut geht, kommt der Punkt der Wahrheit immer dann, wenn durchgreifende Änderungen durchgeführt werden müssen. Man wird nicht drumherumkommen. Man muß es entweder, oder man will es, um den Code kompakter zu machen.

Welches sind nun die Nachteile einer Organisation mit Globalen Variablen?

Die Liste wird lang und hat es in sich.

Die Hauptfehlerquelle in ANSI-C ist mit großem Abstand der Bereichsüberlauf. Das heißt, es werden mehr Daten geschrieben, als Platz vorgesehen ist. Das passiert z. B., wenn ein kleiner Datentyp mit einem großen überschrieben wird, beim Array-Überlauf insbesondere auch der char-arrays. Das Ergebnis ist, daß das Programm mit nur EINER EINZIGEN SOLCHEN ANWEISUNG praktisch unbrauchbar ist. Man erkennt es gelegentlich, daß bei der Ausgabe ein seltsames ASCII-Zeichen erscheint. Das ist ein WARNHINWEIS darauf, daß wir Bereichsüberlauf haben. Leider erkennt man es nicht immer, manchmal gar nicht.

Und ein Programm kann man, nach anerkannter Meinung, niemals zu 100Prozent austesten, ob es stabil läuft. Weil es keine Maschine bzw. keinen vorstellbaren Algorithmus gibt, die in einem Programm alle denkbaren Zustände provozieren und austesten kann. Es ist gerade bei C-Programmierung ein SEHR GUTER GEDANKE, alles zu vermeiden, was auch nur am Rande der Vorstellung zu Instabilitäten führen KÖNNNTE, bzw. sich stets die stabilere Alternative herauszusuchen, selbst wenn die andere "cooler" aussieht und kompakter ist.

Nun zu dem, warum Globale Variablen die Programmstabilität negativ beeinflussen.

1. Verwechslung

Es kann in C derselbe Bezeichner für mehrere Variablen verwendet werden. Wenn eine Variable foreward deklariert ist, ist sie Global für alle Funktionen, es können aber Variablen auch zwischen den Blöcken stehen, sie sind dann Global für alle nachfolgenden Funktionen, und innerhalb der Funktion. Zur Laufzeit wird diejenige Variable verwendet, die "am lokalsten" ist. Es kann dann sein, insbesondere nach Programmänderungen, daß ein anderer als der gewünschte Wert verändert wird.

2. Programmiertechnisch falsche Zuweisung/Bereichsüberlauf

Wenn der Typ der Variable verwechselt wird, kann es sein, daß man dieser einen logisch richtigen Wert zuweist vom falschen (zu großen oder unpassenden) Datentyp. Das ist sehr schnell passiert. Ein Sonderfall ist sprintf()

Die Variable int i kann mit sprintf("%s",i) mit einem Wert für ein char-Array überschrieben werden, also vom falschen Typ. Ergebnis: Bereichsüberlauf und Vernichtung des vorherigen Wertes. Bei sprintf() erhalten wir zur Laufzeit KEINE Fehlermeldung.

3. Logisch falsche Zuweisung

Es wird der Wert mit einem korrekten Datentypen zugewiesen, aber (meistens aufgrund von Programmänderungen) die Zuweisung ist logisch falsch. Da andere Funktionen auch auf diese globale Variable zugreifen, geschehen nun die tollsten Dinge. wink.gif

4. Bereichsüberlauf einer Nachbarvariable

Wenn im Speicherbereich der globalen Variable andere globale Variablen abgelegt sind (nach meiner EInschätzung relativ häufig, ich schätze, die liegen in einem Block alle zusammen), führt die Bereichsüberschreitung bei einer anderen Variable dazu, daß im globalen Block mehr Schaden auftritt, also auch andere Globale Variablen betroffen sind. Dies ist im Hinblick auf eine logische Fehlersuche sehr schwierig einzukreisen.

5. Vergessene bzw. historische Anweisungen. Diese befinden sich als Datenmüll noch als ÜBerbleibsel vorangegangener Versionen in irgendwelchen Funktionen, oftmals nur als eine Zeile oder als Operand innerhalb einer Zeile. Sowas ist je nachdem sehr schwer zu finden.

6. Bereichsüberlauf infolge Änderung des Datentypes, ist besonders bei Structures sehr häufig. Wenn man die einzelnen Datenfelder ändert, in Länge oder Reihenfolge, läuft die Wertezuweisung n die nächsten Felder über. Das ist je nachdem auch nicht einfach zu finden.

7. Bereichsüberlauf infolge Fehldefinition oder falschem Zugriff (Verwechselung INDEX mit Anzahl Elemente). Ein in C SEHR HÄUFIGER FEHLER

Beispiel:

char datum[10]="01.01.2012" ist ein Feld zu klein, kein Platz für die Stringterminierung. Die Stringterminierung läuft also über.
Beispiel für falschen Zugriff: int wochentage[7]={1,2,3,4,5,6,7}; Wer nun auf den Wochentag[7] zurückgreift, greift ins Klo.

Nun, einige dieser Fehler können bei LOKALEN VARIABLEN ebenso geschehen.

Warum wäre das weniger schlimm?

1. Speicherort Stack

Das ist schon von daher erstmal "weniger" schlimm ("weniger" schlimm ist aber auch schlimm), weil lokale Variablen erst beim Funktionsaufruf erzeugt, auf den Stack gelegt und anschließend wieder freigegeben werden. Der Stack ist ein separater Speicherbereich, und ein Bereichsüberlauf einer Variablen im Stack hat wenig Chancen, andere Variablen zu beschädigen, da LIFO-Stack, last in first out, sich hinter der letzten Variable im Stack nichts "sensibles" befindet. Das ist absolut behauptet nicht ganz richtig, aber im Prinzip ist es so. Bereichsüberlauf einer Variable im Stack endet im Niemandsland und richtet keinen (weiteren) Schaden an.

2. Bessere Eingrenzung bei der Fehlersuche

Da bei auftretenden Fehlern bei lokalen Variablen als "Täter" nur die aufrufende Funktion infrage kommt, hat man diese ziemlich schnell an den Hammelbeinen.

3. Bessere Stabilität bei Programmänderungen

Ändert man die Definitionen von Variablen (bei struct{} ist das relativ häufig), sind dadurch alle bearbeitenden Funktionen betroffen. Die müßte man nun allesamt heraussuchen und ändern. Es gilt hier das, was schon oben gesagt wurde.

Beispiel: Würde man das Namensfeld innerhalb einer Struct von char name[50] auf char name[20] verkürzen, kann natürlich sehr leicht ein Bereichsüberlauf die Folge sein, welcher in die Folgefelder hineinschwappt, oder auch über den Datensatz hinaus auf andere Variablen im Block der Globalen. Organisieren wir lokal, haben wir die Deklaration der Variablen im lokalen Block, und das Ergebnis auf dem Stack. Der Bereichsüberlauf kann also höchstens sich selbst beschädigen, und ist so leichter zu finden.

Zusammengefaßt: GLOBAL vs. LOKAL kann man sagen, daß die lokale Organisation folgende Vorteile hat:

1.) Die Schadensausbreitung bei fehlerhaftem Code wird stark begrenzt

2.) ist wg. Effekt 1) die Fehlersuche wesentlich einfacher als wenn man global organisiert.

3.) ist das Programm fehlerbehaftet, d.h., gelingt es nicht, es VOLLSTÄNDIG fehlerfrei zu machen (DAMIT MUSS MAN EIGENTLICH ALS NORMALFALL RECHNEN),

läuft ein Programm mit Fehlern, welches lokal organisiert ist, zu 99,xx Prozent dennoch fehlerfrei, während bei der globalen Organisation die berühmten

"seltsamen Effekte" auftreten, die SEHR SCHWER eingrenzbar sind.

Jetzt kommt jemand und sagt: aber ein Programm muß doch absolut fehlerfrei laufen!

Dem ist zu antworten:

Solche Programme kann es im Prinzip gar nicht geben. Jedenfalls nicht, wenn es sich um komplexe Programmierung handelt.

Und im Gegensatz zu einem Werkstück, welches sagen wir auf 0.01 Toleranz gefertigt ist, hat man bei einem Programm nicht die Möglichkeit, es zu

"vermessen", weil die Programmzustände nicht allesamt vorausberechnet werden können. Ich glaube, der Turing hat das schon vor sehr langer Zeit

vorausgesagt.

Es gilt sozusagen die alte Zimmermanssweisheit, mit der die Engländer ihre Schiffe bauten: nimm alte englische Eiche und mache es stabiler, als es muß.

---------------------------

Jetzt mal ein kleiner Blick auf die praktische Handhabung der Variablen Definition und Deklaration.

Wenn ich eine Variable deklariere, nehme ich den Typ und weise ihr Speicherplatz zu:

int i; // schafft 4 Byte Speicher in einem 32 Bit-System für die Werte von i, ca. -32000 bis + 32000

Wenn ich eine Struktur benötige, muß ich den Datentyp erst definieren:

struct adresse{
char name[50];
char ort[50];
int telefon;
}; // (1)

Ich kann ihn auch so definieren, als Datentyp:

typedef struct{
char name[50];
char ort[50];
int telefon;
} adresse; // (2)

Das sind beides nur DEFINITIONEN, keine DEKLARATIONEN, weil beides keinen Speicher anfordert, also keine Daten aufnehmen kann.

Der Unterschied ist, den Speicher bekomme ich einmal mit:

struct adresse a; // (1)

bzw.:

adresse a; // (2)

Für meinen Geschmack ist die Version (1) besser. Die 2. ist zwar kürzer, aber man sieht bei der Deklaration nicht, daß es sich hier um eine struct

handelt. Es könnte leicht eine Typvewechslung stattfinden, was bei (1) nicht zu erwarten ist.

Die Definition hat mit der Frage GLOBAL oder LOKAL nichts zu tun. Ich kann Definitionen Global machen, also an den Anfang der Datei setzen oder foreward

erklären. Da mit der Definition aber noch kein Speicher angefordert ist, kann nichts passieren. Der Compiler nimmt die Definition wahrscheinlich nicht

mal in den EXE-Code der Datei auf, weil er bemerken dürfte, wenn mit der Definition nichts passiert.

Speicher gibt es erst mit der DEKLARATION.

Lokal wäre das so:

void mache_irgendwas()
{

struct adresse a; ...

Dann liegt der Speicherplatz auf dem Stack.

Ich kann natürlich auch sagen:

struct adresse a[50000];...

Und habe damit Platz für 50.000 Mitarbeiter auf den Stack gelegt (sowas empfiehlt sich eher nicht, da Platz auf dem Stack begrenzt, und die performance

geht irgendwann in die Knie, vor allem aber deshalb nicht, weil wir so auch viel Platz verschwenden, wenn es z.B. 20000 wären).

Um den Platz aus der Funktion heraus nicht auf dem Stack, sondern auf dem Heap anzufordern, müßte es heißen:

void mache_irgendwas()
{
struct adresse *a = malloc(50000*sizeof (struct adresse));
}

Was dasselbe bringt, nur nicht als indizierte Liste, sondern man muß mit dem Pointer drüberlaufen (ist intern sowieso dasselbe).

Man sieht, daß die Definition [50] kritisch ist. 50 ist irgendwas. Man könnte die mit define etwas eleganter hinbringen:

#define MAXLEN 50

Dann hieße es:

struct adresse{
char name[MAXLEN];
char ort[MAXLEN];
int telefon;
}; // (3)

und würden wir bei der Programmänderung andere Feldlängen wünschen, müßtenn wir nur schreiben:

#define MAXLEN 20

Richtig elegant ist aber weder das eine noch das andere. Denn in dem einen Fall sind die 20 zu knapp, in der Regel aber zuviel.

Viel besser wäre es, zur Laufzeit folgende Struktur verfügbar zu haben:

struct adresse{
char *name;
char *ort;
int telefon;
}; // (4)

Also einen Pointer auf den Anfang der char-arrays zu setzen, womit wir beliebig lange aber auch beliebig kurze Felder verfügbar hätten, es würde zur

Laufzeit kein Platz verschwendet, und die Anforderung

malloc(sizeof(adresse)) würde nur noch mindestens 12 Byte anfordern, nämlich 4 für int und jeweils 4 für die beiden Pointer, anstelle, wenn man jetzt

die Adressfelder noch komplettiert, mehrere hundert Byte.

Wenn wir mit malloc() arbeiten, können wir uns die geschätzten 50000 sparen, sondern fordern den Platz an, wie wir ihn brauchen (dynamisch zur

Laufzeit).

Die gesamte Funktion:

void mache_irgendwas()
{
struct adresse *a= malloc(sizeof (struct adresse));
a->name="Mueller";
printf("%s\n",a->name);
}

Hat die Bildschirmausgabe:

Mueller
Press any key to continue . . .

Daß die Zuweisung mit dem = Operator möglich ist, hat damit zu tun, daß wir keine Zeichenkette, sondern eine Adresse zuweisen, nämlich die Adresse von

"Mueller". *a ist ja kein char-array, sondern ein Pointer auf ein char-array.

Diese Zuweisung mit Pointern (worauf die gesamte Sprache C ja ursprünglich ngelegt ist), vereinfacht auch Funktionsaufrufe, immer im Hinblick auf den

unbefriedigenden Sachverhalt, daß wir nicht wissen, wieviel Platz wir brauchen, und dafür ZUVIEL Platz vorhalten:

void mache(char info[255] ... // CALL BY VALUE

läßt sich eleganter formulieren mit

void mache(char *info ... // CALL BY REFERENCE

Das wäre alles gut und schön, müßten wir die Datensätze nicht speichern. Dann hat sich das.

Weil nämlich:

fwrite(&buffer, sizeof(...

den Sizeof Operator benutzt, und wir für unseren Pointer, der auf die Textkette zeigt, nur 4 Byte reserviert haben, würden wir die Datei zwar so

abspeichern können, aber eben mit der Adresse auf den Text, zur Laufzeit zum Zeitpunkt der Speicherung. Starten wir das Programm am nächsten Tag neu und

wollen das zurücklesen, sind die Daten natürlich verloren.

Um einen Datensatz zu speichern, kommen wir also von der Definition char text[n] nicht weg. Das läßt sich mit char *text eben nicht ersetzen, logisch.

Und zur Laufzeit mit anderen Datentypen zu handeln als die, die wir speichern wollen, nun ja. Denkbar ist vieles, nur nicht alles, was denkbar ist, ist

auch sinnvoll.

Womit wir beim letzten Punkt wären, wenn man auf GLOBALE VARIABLEN TOTAL VERZICHTEN WILL, bei der Werteübergabe.

Wir übergeben keine Werte mehr, sondern nur noch Adressen.

Damit sind wir sehr C-like, weil C legt es ja darauf an, obwohl (innerer Widerspruch) im Normalfall in C CALL BY VALUE vorgesehen ist, aber nicht

durchgängig, weil bereits der Aufruf einer Funktion:

mache(char info[255] ...

intern sowieso mit einem Pointer erfolgt. Es wird keine Textkopie übergeben, sondern die Adresse.

Man bemerkt das, wenn man

mache (char *info ...

mit dem Adreßoperator aufruft, nämlich:

mache (&text ...

Was zu einer Fehlermeldung führt, weil wir nicht die Adresse des Char-Arrays, sondern die Adresse des Pointers übergeben, welcher auf das char-array

zeigt.

Besonders Sinn macht die Werteübergabe per Adresse (CALL BY REFERENCE) nicht bei einfachen Datentypen, wie int, float, c, aber bei komplexen Datentypen

wie Strukturen. Als Folge davon brauchen die gar keinen Rückgabewert mehr, können VOID erklärt werden, es sei denn, man wollte wissen, ob das ERgebnis

OK oder NOTOK wäre.

Folgefunktion(en):

void mache_noch_mehr(struct adresse *aktuell) // Adreßoperator auf den zu bearbeitenden Datensatz
{
...
}

Aufrufende Funktion:


void mache_irgendwas()
{
struct adresse *buffer; // Lokale Deklaration buffer der Globalen Definition struct adresse

mache_noch_mehr(&buffer); // Adreßübergabe der lokalen Variable, Bei char-arrays verboten, dort ohne & Operator

Das sind die Gründe, warum das PRogramm ohne jede Globale Variable auskommt.

Ich hoffe, ich habe mich einigermaßen verständlich ausgedrückt (bin mir da selbst nicht immer so sicher). wink.gif

Der Beitrag wurde von sharky bearbeitet: 26.02.2012, 15:49 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 28.02.2012, 11:13 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Eine Standardkonstruktion für den Anwenderdialog

Bei der Vervollständigung des Programms taucht immer wieder das Problem auf, vom Anwender bestimmte (wenige) Daten abzufragen, um anschließend weitgehend automatisiert auf die Daten zuzugreifen. Als Beispiel möge hier mal der Menü-Unterpunkt "Kontenjournal" dienen.

Darunter versteht man die Auflistung aller Datensätze, die als Konto oder Gegenkonto ein bestimmtes Konto enthalten. Und das gewöhnlich in einem bestimmten Zeitraum. Wir benötigen also folgende Daten, die wir mit bereits existierenden Funktionen abrufen:

1.) Hole Konto-Nr
2.) Hole Datum von
3.) Hole Datum bis

Sind die Daten abgefragt, suchen wir in der chronologisch sortierten Datei nach den Buchungssätzen, die die Bedingungen erfüllen, und kopieren sie praktischerweise in eine temporäre Datei, um sie mit fseek() auf dem Bildschirm zum scrollen freizugeben, oder kopieren sie in eine Text-Datei, um die Daten mit Windows-Standard-Editor ausdrucken zu können.

Worauf ich hinauswill, ist die Struktur des Anwenderdialogs.

Was wir NICHT wollen ist diese elende Sequenz, wie man sie leider heute noch zu häufig antrifft:

Schleife auf

->hole Eingabe 1
->hole Eingabe 2
->hole Eingabe 3

Abfrage (J/N)

Schleife zu

Der Anwender hängt dann in einer Schlange, die er Feld für Feld abarbeiten muß, und kann erst am Ende angeben, ob die Eingabe OK ist, und wenn nicht, muß er wieder durch die ganze Schlange durchlaufen. Das ist primitiv und unzumutbar, insbesondere, wenn es sich nicht wie hier um 3 Felder handelt, sondern sagen wir mal 20 Felder.

Bei solchen Dialogen benötigen wir grundsätzlich mehrere fixe Koordinaten, für die Feldbezeichner, die Eingabefelder und evtl. Zusatzinfos (z.B. den Text zu einer gewählten Kontonummer). Diese Koordinaten dienen dazu, die Ein- und Ausgabe zu koordinieren. Handelt es sich um einen komplexen Dialog mit vielen Feldern, muß man das trennen, z. B. eine Maske schreiben, und eine Funktion, welche die Eingabefelder verwaltet, und schreibt dazu eine Struktur mit den Werten für die jew. Zeilen und Spalten, so daß beide Funktionen drauf zugreifen können und man die Anordnung der Felder auch bequem ändern kann. Hat man wie hier nur 3 Eingabefelder, kann man das in der Funktion selbst erledigen.

Wir schreiben also eine Maske, wo steht: Konto-Nr, Datum von und Datum bis und setzen dahinter die Eingabefelder.

Um den Zugriff aber wahlfrei zu machen, müssen wir dem Anwender Gelegenheit geben, zwischen den Eingabefeldern zu switchen, und geschieht mit einem ersten switch() Block, indem wir nämlich eine int nr einführen. Ist die nr==1, soll die Konto-Nr. geholt werden, ist sie 2, Datum von, bei 3 Datum bis:

int nr=1;
Schleife auf

switch (nr)
{
case 1: hole Kontonr.;
break;
case 2: hole vondatum;
break;
case 3: hole bisdatum;
break;
}

Damit der Anwender zwischen den Feldern switchen kann, muß er bestimmte Tasten drücken können, z. B. die Tasten PFEILOBEN und PFEILUNTEN. Das Problem ist nur, daß die Konsolen-Abfrage in dieser Funktion ja gar nicht stattfindet, weil wir jeweils eine Unterfunktion aufrufen, um die Daten zu holen. Würden wir nach Rückkehr aus der Unterfunktion eine Taste abfragen, z. B: Pfeil oben/unten, würde das für den Anwender bedeuten, wenn er das erste Feld erledigt hat, daß es dann nicht weitergeht, weil das Programm auf eine weitere Taste wartet. Das ist lästig und unpraktisch, weil im Normalfall der Anwender von oben nach unten durchlaufen will, ohne jedes Mal dazwischen noch eine Taste zu drücken.

Wir benötigen also, um die Eingabe zu steuern, den letzten Tastenwert aus der Unterfunktion. Dazu ist es allerdings erforderlich, daß ALLE UNTERFUNKTIONEN bei bestimmten Tasten abbrechen und zurückspringen. Wie kommt aber der Wert dann zurück? Ganz einfach, als Rückgabewert der Funktion. Wurde die Funktion "ordentlich" abgeschlossen, mit ENTER, wird im Anschluß anders verfahren als wenn mit PFEILOBEN oder ESCAPE abgeschlossen wurde.

Wie der eigentliche Wert der Eingabe zurückgeliefert wird, wurde schon oft gesagt: BY REFERENCE

Wir rufen die Unterfunktionen als auf mit:

taste=unterfunktion(&Wert <--------- Adreßoperator & , aber nicht bei char-arrays!!

Wir erhalten dann den Tastenwert, mit dem abgschlossen wurde, und wurde die EIngabe logisch und sachlich richtig abgeschlossen, liegt das Ergebnis der Eingabe bereits in wert.

Damit die Unterfunktionen diese Steuertasten auch zurückliefern, müssen sie durchgängig die Zeile enthalten:

if (taste==Steuertaste1 || taste==Steuertste 2 .... ||taste==Steuertaste n) return taste.

Drückt der Anwender dann eine dieser Tasten, bricht die Funktion sofort ab und liefert diesen Tastenwert.

Dies vorausgeschickt, können wir jetzt die DIALOGFUNKTION vervollständigen mit einem 2. switch() Block, der sich um die Tastenbehandlung kümmert:

Schleife auf

switch(feld_nr)
{
case 1: mache eingabe 1;
break;
....
case n: mache_eingabe n;
break;
}

switch(taste)
{
case ESCAPE: return;
case ENTER:
if (feld<MAXFELDER)feld++;
break;
case PFEILOBEN:
if (feld>1)feld--;
break;
case PFEILUNTEN:
if (feld<MAXFELDER)feld++;
...
alle sonstigen Tasten, die man abfragen will

}

Schleife zu

Man sieht, wie es funktioniert: Im oberen switch() Block "fällt" der Anwender in die laufende Eingabe gem. feldnr "hinein", ganz automatisch, ohne daß er eine weitere Taste drücken muß.

Und der untere Switchblock steuert den wahlfreien Zugriff.

Sagen wir, der Anwender hat sich bei der Konto-Nr. vertippt. Jetzt hängt er im Datumsfeld, will das aber vorher noch korrigieren. Drückt er dann PFEILOBEN, verläßt er das Datumsfeld, fällt in den zweiten Switchblock mit dem Tastenwert, daraufhin wird die Feldnr von 2 auf 1 geändert, und oben fällt er im ersten Switchblock wieder in das gewünschte Feld Kontonummer.

Da wir nicht sequentiell, sondern wahlfrei arbeiten, und mit dem Erreichen des letzten Eingabefeldes die Eingabe daher nicht automatisch beendet werden soll, benötigt es eine Taste, mit der der Anwender signalisiert "Eingabe ist fertig", zum Beispiel F10. Wird F10 übergeben, muß dann der 2. Switchblock eine logische Prüfung durchführen und wenn in Orndnung, die gewünschte FUnktion ausführen, z. B. wie hier das KONTENJOURNAL aufrufen mit den geprüften Werten des Anwenders.

Zusammengefaßt ist das Prinzip dieser Form des Anwenderdialogs:

1. Unterfunktionen liefern Rückgabewert der Taste und schreiben die Eingabe Call by REFERENCE in das übergebene Datenfeld:

struct taste unterfunktion(wert *vorgabe ...

wird aufgerufen mit:

taste = unterfunktion(&wert ...

2. Die durch das PRogramm durchgängig vereinbarten Sondertasten müssen in allen Unterfunktionen zum Rücksprung führen:

if (taste = Sondertaste 1 .... ) return taste;

Ist das so organisiert, kann man jeden Anwenderdialog vom Grundprinzip her so organisieren:

Funktion Dialog

Mache den Bildschirm zurecht

Schleife auf

Switch() Block 1, welcher die Eingabefelder steuert und die passenden Hilfefenster aufruft

Switch() Block 2, welcher die Rückgabewerte der Tastatur steuert

Schleife zu

Es wird dann jede Eingabe sozusagen doppelt ausgewertet.

Im Switch-Block 1 kann man dann die nötigen Anweisungen für die Umgebung unterbringen, z. B. wenn eine Konto-Nr gefragt wird, wird im rechten Bildschirmfenster der KOntenrahmen eingeblendet. Will der Anwender scrollen, drückt er F1, springt aus der Unteffunktion in den zweiten Switch() Block der Dialogfunktion, wo dann für F1 stehen muß: Scrolle_SKR03

Geht er aus dem Scrollfenster mit ESC wieder zurück, wird dieser Wert nicht mehr abgefragt, denn er fällt mit break; aus dem 2. Switch() Block raus, und landet wieder im Switch() BLock mit der Feld-Nr. Da diese sich aber nicht verändert hat durch F1, bleibt er im Eingabefeld hole_Kontonummer, nur daß er durch scrolling rechts nun die gewünschte KOnto-Nr. sehen und eingeben kann.

Ich sage ja nicht, daß dies die beste Art ist, einen Anwenderdialog zu schreiben, aber sie vom Prinzip her:

1) Sehr anwenderfreundlich, man springt völlig wahlfrei zwischen den Feldern, bekommt kontextsensitive Hilfe, kann aber auch von oben nach unten arbeiten
2.) PRogrammiertechnisch sehr übersichtlich
3.) Dient der Standardisierung aller Bedienerelemente des Programms (z. B. PFEILOBEN hat dann durchgängig im ganzen Programm dieselbe Bedeutung, daß man 1 nach oben will)
4.) Schnell und ohne großen Aufwannd programmiert
5.) Hat an bestimmte Bildschirmbereiche bereits vordefiniert als Fenster, auch optisch sehr ansprechend hinzubekommen.

Man erkennt hier übrigens auch einen weiteren Vorteil der Organisation Call by reference:

Würden wir die Funktion hole_datum mit einer Public-Variable DATUM organisieren, so würde jede Funktion, die irgendeine Datumsangabe braucht, den Wert für DATUM ständig ändern. Wird nun innerhalb einer Funktion, die den Datumswert benötigt, eine Unterfunktion aufgerufen, die ebenfalls einen Datumswert benötigt, kann das sehr leicht zu Fehlern in der aufrufenden Funktion führen, da das Datumsfeld verändert wurde. Das muß nicht, das kann aber.

Ruft man mit Adreßoperator der lokalen Variable auf, sind solche Seiteneffekte vollkommen ausgeschlossen.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 29.02.2012, 08:59 Uhr
sharky2014
sharky2014
Level 7 = Community-Professor
*******
Gruppe: Mitglied
Mitglied seit: 25.09.2008
Beiträge: 1.692

Der beschriebene zweite Switch-Block der Funktion Kontenjournal:

switch (taste.st)
{
case ESCAPE:
clrSCR(DIAL);
clrSCR(MENU);
return;
case F1:
if (nr==1)scroll(SKR3); // zum Scrollen in das Fenster Kontenrahmen
break;
case ENTER:
if (nr<3)nr++; // Eingabe mit Enter: Feldnummer erhöhen für das nächste Eingabefeld
break;
case PFEILOBEN:
if (nr>1)nr--; // Ein Eingabefeld zurückspringen
break;
case PFEILUNTEN:
if (nr<3)nr++; // ein Feld nach unten springen
break;
case F10:
// hier die Funktion ausführen, Auszüge am Bildschirm anzeigen
if (schreibe_journaldateien(konto_nr,vondat,bisdat)==NOTOK)
{ meldung("Ungueltige Auswahl","oder keine Daten vorhanden","");return ;}
meldung("Journaldateien erstellt",dn_journal_bin,dn_journal_txt);
return;

} // switch



Wenn die Eingabe formal gültig ist, geschieht alles andere von hier aus (2. Switch-Block):

case F10:
// hier die Funktion ausführen, Auszüge am Bildschirm anzeigen
if (schreibe_journaldateien(konto_nr,vondat,bisdat)==NOTOK)
{ meldung("Ungueltige Auswahl","oder keine Daten vorhanden","");return ;}
meldung("Journaldateien erstellt",dn_journal_bin,dn_journal_txt);
return;

Wir übergeben also an die Funktion schreibe_journaldateien() mit den Parametern konto_nr und Datum von-bis.

Was macht die Funktion?

Sie durchsucht die Datei mit den Buchungssätzen dn_buchungen_bin nach Datensätzen, die der Auswahl entsprechen, und kopiert sie in zwei andere Dateien, eine Binär- und eine Textdatei. Diese Funktion ist programmiertechnisch ganz interessant, weil hier fast alles vorkommt, was bisher besprochen wurde.

Die Deklaration der Dateinamen, diese sind public, aber read-only:

char *dn_buchungen_bin="primanota.bin";
char *dn_kopie="intermediate.bin";
char *dn_journal_bin="journal.bin";
char *dn_journal_txt="journal.txt";

Man beachte, daß sie NICHT char name[255] deklariert sind, sondern char *name.

Der obere Bereich der Funktion:

int schreibe_journaldateien(int konto_nr,char *vondat, char*bisdat)
{
int zaehler=0;
long von=datindex(vondat);
long bis=datindex(bisdat);
FILE *quelle; FILE *ziel;
struct s_bsatz buffer;
char tbuf[255]="";
bsatz_ausnullen(&buffer); // einmal ausnullen, um undefinierte Speicherbereiche zu überschreiben
quelle=fopen(dn_buchungen_bin,"rb");
ziel=fopen(dn_journal_bin,"wb");
if (quelle==NULL||ziel==NULL){meldung("Dateifehler 155","","");return NOTOK;}

Der Zähler dient um festzustellen, ob zu der Auswahl mehr als 0 Datensätze gefunden wurden.
KOmmen wir zu der Funktion datindex()

Sie wandelt eine char-Datumsangabe a la "tt.mm.yyyy" um in eine long-Zahl im Bereich der Tage seit Christi Geburt, also um die 750000 herum, so daß man die Datumsangaben danach sortieren kann. Diese Funktion ist aber relativ LANGSAM, weil sie die char-arrays zerlegen muß, um mit atoi() die Zahlenwerte für Tag, Monat, Jahr zu ermitteln, und anschließend muß sie diese noch zum Ergebnis multiplizieren und addieren. Um zu verhindern, daß bei jedem Zugriff auf den Datensatz diese Funktion aufgerufen werden muß, hat der Datensatz für diesen Index ein zusätzliches Feld, zusätzlich zu dem char-Datum, welches bei der Neueingabe oder nach dem Ändern belegt wird, weil es da nicht zeitkritisch ist. Durchsucht man aber einige 1000 Datensätze, IST es zeitkritisch.

Gleiches gilt für die übergebenen Daten im Funktionskopf, char *datum. Würden wir innerhalb der Leseschleife schreiben:

if (datindex(vondatum) ... datindex(bisdatum) würden wir ja die übergebenen Parameter pro Datensatz noch zweimal der Funktion übergeben. Die Umwandlung erfolgt daher nur ein einziges Mal. Anschließend müssen wir nur noch drei long Zahlen vergleichen >= wert <= und haben mit der performance Null Problem.

Die Filepointer *quelle und *ziel werden für 3 Dateien verwendet, aber nicht gleichzeitig. Zunächst wird die binärdatei ausgelesen und in eine andere Binärdatei kopiert:

while (fread(&buffer,sizeof(struct s_bsatz),1,quelle)==1)
{
if (buffer.konto_nr==konto_nr||buffer.gkonto_nr==konto_nr||buffer.ukonto_nr==konto_
nr)
if (buffer.datindex>=von&&buffer.datindex<=bis) // WErte für datindex liegen schon in der Datei fertig vor, s. o.
{
if (fwrite(&buffer,sizeof(struct s_bsatz),1,ziel)!=1) // Wenn die Auswahl paßt, datensatz kopieren
{meldung("Dateifehler 156","","");fclose(quelle);return NOTOK;}
zaehler++;
}
}//while
fclose(quelle);
fclose(ziel);

... und anschließend wieder geschlossen. Jetzt haben wir unsere Filepointer wieder frei für andere Operationen.

Wir wollen die Auswahl in eine Textdatei speichern, damit man die Daten mit einem Texteditor unter Windows ansehen und drucken kann (DIN A 4 QUER).

if (zaehler==0) return NOTOK; // keine Datensätze für diese Sortierung
// Textdatei erzeugen:
quelle=fopen(dn_journal_bin,"rb"); // BINÄR
ziel=fopen(dn_journal_txt,"w"); // TEXT !!!
if (quelle==NULL||ziel==NULL){meldung("Dateifehler 157","","");return NOTOK;}
erzeuge_titelzeile(tbuf); // KOMMENTAR s. u.
fputs(tbuf,ziel); // Schreiben in die Textdatei mit fputs. Hier wird die Spaltenüberschrift reingesetzt
fputs("\n",ziel); // und eine Leerzeile unter der Überschriftenzeile
while (fread(&buffer,sizeof(struct s_bsatz),1,quelle)==1)
{
erzeuge_textzeile(tbuf,&buffer); // Umwandlung der Struct bsatz in eine Textzeile, s. u.
fputs(tbuf,ziel); // Das Schreiben der Datensätze mit den Daten
}
fclose(quelle);
fclose(ziel);
return OK;


Zu der "hakligen" Geschichte, eine struct in eine Textzeile zu verwandeln. Und dem damit verbundenen Problem, die Spaltenüberschriften korrekt zu positionieren, ohne daß man endlos hin- und herschieben muß.

Der Aufruf erzeuge_textzeile(tbuf,&buffer);

trifft auf folgenden Funktionskopf: erzeuge_textzeile(char *ziel,struct s_bsatz *quelle)

Man beachte: im Funktionskopf sitzt ein Pointer auf dem übergebenen char-array. Dieses wird aber NICHT!!! mit dem Adreß-Operator übergeben.

Wohl aber unsere Struct buffer, nämlich mit &buffer. Da die aufgerufene Funktion den passenden Pointer bereits definiert hat mit struct s_bsatz *quelle handelt es sich hier um eine typische Werteübergabe by reference.

Die definition unseres Buchungsdatensatzes:

struct s_bsatz{
int ident_nr;
char datum[11]; // bei Dateien für Feldbezeichner feste Feldlängen, geht nicht anders
int konto_nr;
int gkonto_nr;
int ukonto_nr;
float brutto;
float netto;
float ust_betrag;
int ust_option;
float ust_proz;
char bem[41];
long datindex; // hier die long-Zahl für den Index des Datums
int loeschflag; // wenn Loeschlflag gesetzt ist, wird beim Umkopieren der Datensatz übersprungen = gelöscht
long frei2;
char frei3[8];
};

Diesen Datensatz gilt es in eine Textzeile umzuwandeln. Hier die "haklige" Umwandlung einer Auswahl dieser Felder. Man beachte, daß die Datenfelder, die mit &buffer übergeben wurden, jetzt als buffer->datenfeld, also als Felder eines Pointers angesprochen werden.

sprintf(ziel,"%6i %-12s %-6i %-6i %10.2f %10.2f %8.1f %10.2f %42s \n",
quelle->ident_nr,
quelle->datum,
quelle->konto_nr,
quelle->gkonto_nr,
quelle->brutto,
quelle->netto,
quelle->ust_proz,
quelle->ust_betrag,
quelle->bem);
}

So entsteht also eine Zeile. Nur für die ganzen Daten benötigt man Spaltenüberschriften.

Achtung die Leerzeichen zwischen den Formatierungen sind "gültig". Nimmt man die weg, wird die Zeile kürzer.

Hier mein Vorschlag (oder Tipp), wie man sowas schnell und ohne Aufwand hinbekommt. Wir setzen direkt davor oder danach unsere Funktion Titelzeile, so daß man mit Blick auf die Formatierungen von sprintf("%6i%8s ... usf. die Spaltenüberschriften mit identischen Formatierungslängen als Text zuordnen kann, wobei wir natürlich auch wieder dieselben Leerzeichen setzen. Sieht dann so aus:

void erzeuge_titelzeile(char *ziel)
{
sprintf(ziel,"%6s %-12s %-6s %-6s %10s %10s %8s %-12s %s\n",
"Nr",
"Datum",
"Konto",
"G-Kto.",
"Brutto",
"Netto",
"USt%",
"USt-Betrag",
"Bemerkungen");

Die Formatierung %-12s heißt linksbündig, ohne das - wird der Text rechtsbündig gesetzt. Auf diese Weise stehen unsere Bezeichner immer schön über der Spalte, ohne daß man groß probieren muß.

Werteübergabe auch hier wieder CALL BY REFERENCE, die Funktion hat ja keinen Rückgabewert.

Das Ergebnis ist, daß man die Auswahl aus den Buchungssätzen dann als einseh- und druckbare Textdatei vorliegen hat.

Eine Datei von 1000 Datensätzen auf so eine Auswahl zu durchsuchen und diese abzuspeichern, dauert Millisekunden.

Die Dateiverwaltung unter C ist deutlich schneller als jede Datenbank.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    



3 Besucher lesen dieses Thema (Gäste: 3)
0 Mitglieder: