594.531 aktive Mitglieder*
3.692 Besucher online*
Kostenfrei registrieren
Einloggen Registrieren

Anwenderprogrammierung in ANSI-C, Programmierung eines nicht banalen Anwenderprogramms

Beitrag 04.02.2012, 21:03 Uhr
sharky2014
Level 7 = Community-Professor
*******

So, bevor jetzt die Bayern gegen Hamburg gegen den BvB aufholen (eigentlich nicht spannend, der HSV schafft das nie, oder???)

kurzer Kommentar zu der Sortierung von Daten.

Ich hab die Zeit noch erlebt, wo der Hauptspeicher 640 KB hatte, davon das Programm so 500 KB belegte und man mit 140 KB freiem Speicher arbeiten konnte, wo eine Handvoll Datensätze hineingepaßt hat.

Daher die Denke, immer schön sparsam mit dem Speicher.

Das ist ja alles Schnee von gestern.

Eine Datei, mit einer Datensatzgröße von 128 byte, mit 10.000 Datensätzen, kann man jederzeit in den Speicher schreiben, die SPeicher haben ja heute nicht Megabyte sondern Gigabyte, da macht so eine Datei nichtmal ein Prozent aus.

Aus dem ergeben sich zwei Folgerungen:

Wir müssen nicht zwischen Festplatte (Massenspeicher) und Speicher (Rechenspeicher) Daten hin- und herschwappen, sondern wir können sie allesamt in den Arbeitsspeicher kopieren, dann sortieren und als Datei zurückspeichern.

Weil der gesamte Dateiinhalt deutlich weniger als 1 Prozent des Rechenspeichers ausmacht.

Wenn wir da Zweifel haben, können wir mit malloc() die PRobe aufs Exempel machen.

Reicht es nicht, muß man zwischen Rechenspeicher und Massenspeicher hin- und herschaufeln (die alten DOS-Zeiten mit 640 KB).

Aber der Fall wird niemals auftreten.

Vorausgesetzt wird also, wir kopieren die gesamte Datei in den Rechenspeicher.

Und sortieren sie dann anhand eines Feldes, was zur SOrtierung vorgesehen ist, z. B. Datum, oder Gehalt, oder sonstwas.

Es gibt so viele einfache wie auch sehr komplexe SOrtierungsroutinen.

Bubble-Sort etc. etc., die auch in der Standardbibliothek abrufbar sind.

BRAUCHT MAN ALLES NICHT!

Der allereinfachste Algorithmus zum Sortieren, so einfach, daß kein Mensch im Internet sich traut, das zuzugeben, ist der folgende (Tatsache, ich hab im Internet noch nirgendwo sowas gefunden, daß sich einer traut, so primitiv zu sortieren wink.gif )

1. Nimm einen Referenzwert, wenn klein, dann das Kleinste, wenn groß, dann das Größte

2. Suche die ganze datei ab, um das Kleinste/Größte zu finden

3. Kopiere das Kleinste/Größte in eine zweite Datei

4. Lösche die Daten des gefundenen Datensatzes, damit er bei dem folgenden Suchlauf nicht mehr auftaucht.

5. Wiederhole das solange, bis kein Datensatz mehr übrig ist, der kleiner als das Kleinste oder größer als das Größte ist.

Das bedeutet:

Eine Datei von 1000 Datensätzen wird zunächst 1000mal durchsucht

Dann 999 mal

Dann 989 mal

und so weiter, und jeweils das gefundene KLeinste/Größte wird kopiert in eine neue Datei.

Die dann eben sortiert ist.

Grob gerechnet, eine Datei mit 1000 Datensätzen wird 1000 mal durchsucht, macht dann 1 Mio. Datenzugriffe.

Wären unsere Computer noch aus Röhren zusammengesetzt, würde das 1 Jahr benötigen. wink.gif

Auf meinem Notebook macht das deutlich weniger als 1 Sek, vielleicht 0,1 Sek oder 0.05 Sek.

Sich angesichts der Rechenleistung noch Gedanken zu machen, wie man 0.05 Sek. auf 0.01 Sek drücken kann durch einen komplizierten Sortieralgorithmus, tja, was soll man sagen? Solange wir nicht die Echtzeitberechnung von Flugdaten eines angreifenden Kampfflugzeuges vornehmen müssen, ist das mehr als ausreichend.

Hier ist die gesamte Funktion, ist ja alles längst programmiert. Und funktioniert 1 A. Und dauert den Bruchteil einer Sekunde bei 2000 Datensätzen (die bestenfalls am Ende des Jahres überhaupt zusammenkommen dürften).

Da aber da die Bayern schon spielen, lasse ich das für heute Abend ohne Kommentar.

Nur soviel:

Die Datei wird aus dem Massenspeicher in eine dynamische Liste kopiert.

Die dynamische Liste wird nach dem beschriebenen Primitiv-Algorithmus solange durchlaufen, bis alle Elemente per

INSERT-SORT in eine Parallel-Liste kopiert sind.

Dann wird die Parallel-Liste in die ursprüngliche Datei kopiert im Modus w, die ursprüngliche Datei wird dabei also zerstört zugunsten der nunmehr sortierten Daten.

That´s all.

Gruß an alle Bayern Fans, ich bin BVB Fan, allerdings daß die Hamburger das schaffen, das wäre eine Revolution, glaub ich nicht dran. wink.gif



void sortiere_buchungsdatei_chronologisch()
{
int anz_ds;
struct datei_ds
{
strc_bsatz data;
datei_ds *next;
};

strc_bsatz buffer;
datei_ds *listenkopf_1=NULL;
datei_ds *neues=NULL;
datei_ds *laufzeiger=NULL;
datei_ds *laufzeiger2=NULL;
datei_ds *kleinstes=NULL;

datei_ds *listenkopf_2=NULL;
datei_ds *liste2_ende=NULL;
FILE *bdp;

bdp=fopen(dn_buchungen_bin,"rb");
if (bdp==NULL)return;
// fseek(bdp,(long)-1*sizeof(strc_bsatz),SEEK_END);
fseek(bdp,(long)-1*sizeof(strc_bsatz),SEEK_END);
anz_ds=ftell(bdp)/sizeof(strc_bsatz);
if (anz_ds<=1){fclose(bdp); return; } // leere Datei oder 1 DS


// Schritt 1: Datei komplett auslesen

fseek(bdp,0,SEEK_SET); // dateizeiger auf den Anfang zurücksetzen
while (fread(&buffer,sizeof(strc_bsatz),1,bdp)==1) // datei in dynamische Liste kopieren
{
neues = (datei_ds*) malloc(sizeof( datei_ds));
if (neues==NULL){fclose(bdp); meldung("Fehler dynamische Liste");return;}
if (listenkopf_1==NULL) listenkopf_1=neues; // Kopf der Liste markieren
else laufzeiger->next=neues; // an ndie Liste anhaengen

neues->next=NULL;
laufzeiger=neues;
laufzeiger->data=buffer; // daten aus Datei uebernehmen

} // while datei auslesen
fclose(bdp);

// Schritt 2: daten durch umkopieren in 2. Liste sortieren:

int ende=NEIN;
do
{
kleinstes=listenkopf_1;
laufzeiger=listenkopf_1;
while (laufzeiger->next!=0)
{
laufzeiger=laufzeiger->next;
if (datindex(laufzeiger->data.datum)<datindex(kleinstes->data.datum))kleinstes=laufzeiger;
} // kleinstes gefunden
if (strcmp(kleinstes->data.datum,"99.99.9999")==0) ende=JA; // ==0 sind identisch= JOB IS DONE
else
{
neues = (datei_ds*) malloc(sizeof( datei_ds));
if (neues==NULL){meldung("Fehler dynamische Liste 2");return;}
if (listenkopf_2==NULL) listenkopf_2=neues;
else liste2_ende->next=neues;
neues->next=NULL;
liste2_ende=neues;
neues->data=kleinstes->data; // Daten kopieren
sprintf(kleinstes->data.datum,"99.99.9999");
}
} while (ende==NEIN);

// Schritt 3: sortierte Liste in Datei zurückkopieren
laufzeiger=listenkopf_2; // auf sortierte Liste setzen
bdp=fopen(dn_buchungen_bin,"wb");
if (bdp==NULL) {meldung("Kann Datei nicht oeffnen");return;}
do
{
fwrite(&laufzeiger->data,sizeof(strc_bsatz),1,bdp);
if (laufzeiger->next!=NULL)laufzeiger=laufzeiger->next;
} while (laufzeiger->next!=NULL); // Listenende erreicht

fclose(bdp);


// Schritt 4: Speicher für die beiden Listen wieder freigeben:


do
{
laufzeiger=listenkopf_1;
laufzeiger2=listenkopf_2;
if (listenkopf_1->next!=NULL) listenkopf_1=listenkopf_1->next;
if (listenkopf_2->next!=NULL) listenkopf_2=listenkopf_2->next;
free(laufzeiger);
free(laufzeiger2);

} while (listenkopf_1->next!=NULL||listenkopf_2->next!=NULL);
clrscr();
meldung("Datei wurde chronologisch sortiert");

} // fu



Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 21:05 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 04.02.2012, 22:37 Uhr
sharky2014
Level 7 = Community-Professor
*******

Sky überträgt Bayern nicht!

Das ist unfaßbar! MEga-Sch***e.

Was ist da los???

Alles erst in alle Spiele, alle Tore um 22:30

Gut, also mach ich noch einen Kommentar zu meinem letzten Beitrag. Die Funktion ist so leicht ja auch nicht zu verstehen, trotz der eingesetzten Kommenatare.

Grundsätzlich: um eine Datei zu sortieren, reicht es, von dieser eine Kopie auf dem Massenspeicher herzustellen und diese Kopie zurückzusortieren. Dazu muß man die Datei gar nicht in den Rechenspeicher schaufeln (und unter DOS-Zeiten war das auch gar nicht möglich). Nur daß der Rechenspeicher natürlich um den Faktor einige 1000 schneller arbeitet als der Massenspeicher ist der Grund, das zu tun.

Wenn wir nicht wissen, wieviele Datensätze, scheidet die Organisation der Daten als array[n] aus. Entweder ist n zu klein (Bereichsüberlauf) oder zu groß, und zu dem Zeitpunkt, zu dem wir die Anzahl der Datensätze auslesen können, ist n schon definiert, denn n wird ja beim Compilieren definiert und nicht zur Laufzeit.

Lösung: Dynamische Liste.

Hier: eine EINFACHE dynamische Liste.

struct datei_ds
{
strc_bsatz data;
datei_ds *next;
};


Dabei ist besonders die Definition des Pointers *next interessant. Er verweist auf eine Struktur, die gerade in der Entstehung ist, nämlich datei_ds, also auf eine bis dahin gar nicht existente Struktur. Also ein Verweis auf sich selbst, ein rückbezüglicher Verweis für eine struct, die es noch gar nicht gibt.

Man duckt sich geradezu in der Erwartung, daß der Compiler jetzt eine heftige Fehlermeldung herausbringt. Dem ist aber nicht so!

Die Struktur leistet folgendes: sie nimmt Daten auf (deren näherer Inhalt hier nicht interessiert) in der strc_bsatz, welcher ebenfalls eine struct ist, bestehend aus Datenfeldern unbekanntnén Typs, und enthält einen Zeiger, der auf das nächste Element zeigt, nämlich die Adresse dieses Elements im SPeicher.

Man könnte auch einen Zeiger definieren, der auf das Vorgänger-ELement zeigt, das ist hier aber nicht nötig, daher "EINFACHE" dynamische Liste.

Es folgt dann ein Definitionsblock von verschiedenen Zeigern. Für die erste Liste brauchen wir einen, der auf den Anfang der Liste zeigt, und einen Laufzeiger, der zunächst mal auf das Ende der Liste zeigt (die entseht ja erst) bzw. im Anschluß auf der Liste entlangläuft.

datei_ds *listenkopf_1=NULL;
datei_ds *neues=NULL;
datei_ds *laufzeiger=NULL;


Dieser Zeiger (Pointer) enthalten als Inhalt lediglich die Adresse, unter denen die Daten zu finden sind.

Im Anschluß wird die Datei mit den Buchungssätzen komplett ausgelesen und kopiert:

fseek(bdp,0,SEEK_SET); // dateizeiger auf den Anfang zurücksetzen
while (fread(&buffer,sizeof(strc_bsatz),1,bdp)==1) // datei in dynamische Liste kopieren
{
neues = (datei_ds*) malloc(sizeof( datei_ds));
if (neues==NULL){fclose(bdp); meldung("Fehler dynamische Liste");return;}
if (listenkopf_1==NULL) listenkopf_1=neues; // Kopf der Liste markieren
else laufzeiger->next=neues; // an ndie Liste anhaengen

neues->next=NULL;
laufzeiger=neues;
laufzeiger->data=buffer; // daten aus Datei uebernehmen

} // while datei auslesen
fclose(bdp);




Für jeden Datensatz, den wir einlesen, müssen wir Speicher für ein neues Element der Liste anfordern, via malloc(). Das kann natürlich auch schiefgehen, ist aber wie oben gesagt eigentlich unmöglich, daher Fehlerbehandlung hier eigentlich pro forma:

neues = (datei_ds*) malloc(sizeof( datei_ds));
if (neues==NULL){fclose(bdp); meldung("Fehler dynamische Liste");return;}


Es ist klar: das neue ELement soll die Daten aufnehmen aus der Datei. Und damit man es wiederfindet, muß das Element davor die Adresse von dem neuen Element als NEXT erhalten, während das neue Element keinen Nachfolger hat, die Adresse von NEXT des neuen Elements ist natürlich NULL, also gar nix.

if (listenkopf_1==NULL) listenkopf_1=neues; // Kopf der Liste markieren
else laufzeiger->next=neues; // an ndie Liste anhaengen

neues->next=NULL;
laufzeiger=neues;
laufzeiger->data=buffer; // daten aus Datei uebernehmen


Es gibt den Sonderfall, daß das Neue das Erste ist, dann UND NUR DANN muß der Laufzeiger Listenkopf_1 darauf gesetzt werden. Ansonsten läuft es so, daß das Neue das nächste (next) des bisherigen Letzten ist und es danach die Daten aus der Datei aufnimmt.

Das geschieht solange, bis in der Datei keine Datensätze mehr vorhanden sind, EOF, aber Vorsicht, wir umschreiben EOF mit der Anweisung:

while (fread(&buffer,sizeof(strc_bsatz),1,bdp)==1)


DIe liest die Datei, wie oben schon gesagt, bis zum Ende.

Nun sind alle Datensätze in der Liste im Speicher versammelt, und wir sortieren nach einem bestimmten Feld, hier Datum. Könnte ja auch was anderes sein, wiez. B. Umsatz oder sonstwas.

Dazu durchlaufen wir die Liste bei 1000 Datensätzen exakt 1 Mio. mal, nämlich 1000*1000.

Wir suchen das kleinste Datum, und wenn wir es gefunden haben, kopieren wir es in die zweite dynamische Liste als nächsten (bzw.) ersten Datensatz.

Danach haben wir zwei Möglichkeiten:

Entweder, wir löschen das kopierte Element aus Liste 1 raus, damit es beim 2. Durchlauf ff. gar nicht mehr vorhanden ist

Oder, wir überschreiben den Inhalt so, daß es für den weiteren Durchlauf nicht mehr infrage kommt. So wird hier verfahren, das kleinste Datum wird überschrieben mit "99.99.9999".

Der Grund ist, wie gesagt, daß die Performance mehr als genug ist. Würden wir es rauslöschen, wäre die Performance natürlich besser, es würden dann nicht 1000*1000=1Mio. Durchläufe erfolgen, sondern 1000*999 + 1000*998 + 1000*997 ... 1000*1 Durchläufe, was schlicht schneller ist.

Um aber komfortabel löschen zu können, bräuchten wir entweder einen zweiten Zeiter oder eine doppelt verkettete Liste (mit der Adresse des Vorgängers), weil wir beim Löschen LINKS VOM ELEMENT stehen müssen. Das ist aber alles Quatsch mit Soße, wenn wir bei 2000 Datensätzen um 0.05 oder 0.005 Sekunden reden.

Ich bin auch immer für Performance, aber bitte da, wohin sie gehört, und nicht da, wo es wirklich keinen Sinn macht.

Müßten wir, weil der Rechenspeicher nicht reicht, zwischen Massen- und Rechenspeicher Teile der Daten sortieren und anschließend zusammenfügen, wäre der Algorithmus unendlich viel komplizierter, aber es ist eben nicht so.

Diese Sortierroutine:

finde das kleinste

kopiere es in die neue Liste

lösche das kleinste

suche das nächste kleinste

sortiere es in die neue liste als nächstes element

nennt man INSERT SORT. Wir vertauschen also nicht Elemente innerhalb einer Liste, sondern kopieren sie in eine neue Liste.

Dieser INSERT SORT hat sogar eine gewisse Berühmtheit, weil er bei kleinen Datenmengen der ABSOLUT SCHNELLSTE ist, allerdings man spricht von bis zu 20 Elementen.

Als ich das gelesen habe, habe ich mich am Kopf gekratzt und gewundert, warum das, wie ich immer schon sortiere, ohne den Namen zu kennen, so einen berühmten Namen hat. wink.gif

Also nochmal: wenn man mit dem Ruderboot über den See fährt (MS-DOS von 1980), überlegt man anders als mit einem Schnellboot, was mit 60 km/h durchgeht.

Daher eben: Insert-Sort.

Es liegen jetzt zwei Listen vor:

Die erste Liste mit dem Originalinhalt der Datei.

Die zweite Liste sortiert nach einem gewünschten Feld, hier: Datum.

Der Schritt 3 ist klar: wir schreiben die Liste 2 in die Datei zurück und zerstören damit den alten (unsortierten) Dateiinhalt.

Ergebnis: Sortierte Datei auf dem Massenspeicher.

Jetzt muß noch was kommen, nämlich Speicher-Leaks, die jedes Programm irgendwann zum Stillstand bringen.

Diese Leaks sind reservierte Speicherbereiche, die wir mit MALLOC() angefordert haben, und nachher nicht wieder freigeben.

Dann hat das PRogramm, je länger es läuft, sowas wie Arterien-Verkalkung und wird immer langsamer bis zum EXITUS. Obwohl das bei diesem Programm und 8 GB Speicher auf meinem Notebook in diesem Jahrhundert nicht mehr der Fall sein dürfte ....

dennoch ...

Der 4. Teil beschäftigt sich daher mit der Freigabe dieses reservierten Speichers:

Das geht so:

// Schritt 4: Speicher für die beiden Listen wieder freigeben:

do
{
laufzeiger=listenkopf_1;
laufzeiger2=listenkopf_2;
if (listenkopf_1->next!=NULL) listenkopf_1=listenkopf_1->next;
if (listenkopf_2->next!=NULL) listenkopf_2=listenkopf_2->next;
free(laufzeiger);
free(laufzeiger2);

} while (listenkopf_1->next!=NULL||listenkopf_2->next!=NULL);


Solange also einer der beiden Listenzeiger (ist theoretisch, beide haben ja dieselben Datenmengen, aber nur für den Fall daß ... daher die ODER Anweisung, falls der eine noch was zu erledigen hat) noch was zu tun hat, geschieht folgendes:

Der Laufzeiger wird auf den Kopfzeiger gesetzt. Kopfzeiger wird, wenn möglich, auf das nächste gesetzt. Dann wird das Element Laufzeiger mit free() gelöscht. Jetzt hängt der Laufzeiger in der Luft und wird auf den Kopfzeiger nachgezogen. Solange bis nix mehr da ist.

Entscheidend ist hier die Reihenfolge.

Würde man das Kopfzeiger Element mit free() löschen, bevor man den Zeiger auf das nächste gestellt hat, wäre das nächste ELement undefiniert und nicht mehr ansprechbar. Da der Speicher aber via MALLOC() zuvor angefordert wurde, ist der ganze Rest dann als Speicher nicht verfügbar.

Wenn man 1 Minute drüber nachdenkt, hab man es kapiert.

Zumal ich ja so schön programmiere, daß das eigentlich selbsterklärend ist. wink.gif

Jetzt bin ich mal gespannt auf ALLE SPIELE ALLE TORE, aber an die Hamburger glaub ich im Leben nicht. Ich sollte das gar nicht einschalten.

Der Beitrag wurde von sharky bearbeitet: 04.02.2012, 22:48 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 04.02.2012, 23:12 Uhr
sharky2014
Level 7 = Community-Professor
*******

Bayern spielt in Hamburg 1:1.

HÄ????


spitze.gif

Ich kann es nicht fassen!

Als alter BvB Fan da kann ich nur sagen:

UNVERHOFFT KOMMT OFT.

thumbs-up.gif thumbs-up.gif thumbs-up.gif thumbs-up.gif thumbs-up.gif thumbs-up.gif thumbs-up.gif thumbs-up.gif


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 05.02.2012, 12:41 Uhr
sharky2014
Level 7 = Community-Professor
*******

Der Datentyp static

Es geht darum, wenn zu einem nicht vorhersehbaren Augenblick ein Fenster überblendet wird, wie man den Bildschirminhalt der überblendeten Bereich wiederherstellt. Diesesr Bereich wurde ja von der Programmierung her nicht mehr direkt auf den Bildschirm geschrieben, sondern zwischengespeichert in einem Buffer für jeden Bereich extra, von daher braucht man nur den Buffer aufzurufen und der BIldschirm ist wiederhergestellt.

Das gilt aber nicht für die Fenster, in denen das Scrolling freigegeben wurde. Wenn die Überblendung erfolgt, kann man nicht wissen, was der Anwender da so zusammengescrollt hat, und dementsprechend den Bildschirm auch nicht wiederherstellen.

Was man benötigt, ist ein Datenzeiger, der auf den ersten Datzensatz in der obersten Zeile des Fensters zeigt, zu dem Augenblick, da überblendet wurde.

Die "Krücke", die mir einfiel, aber überhaupt nicht "ge"fiel, war, diese Daten als GLOBALE Daten abzulegen. Das gefällt aus verschiedenen Gründen nicht, siehe unten. Erstmal also, das PRoblem ist jetzt gelöst und sehr, sehr sauber gelöst, nämlich mittels des angesprochenen Datentyps.

Ergebnis: der Bildschirm läßt sich zu jedem Zeitpunkt überblenden und wiederherstellen, es ist sozusagen eine Softwarsimulation einer Hardcopy, bzw. Savescreen mit anschließendem Restorescreen.

Static 1 zeigt den Bildschirm beim Programmstart.

Static 2 zeigt, daß im rechten Fenster gescrollt wurde. Die Datensätze haben Nummern so um die 100 + x

Static 3 zeigt den Moment, in dem das überlappende Fenster eingeblendet und dort auch schon herumgescrollt wurde.

Static 4 zeigt den Moment, in dem das überlappende Fenster überschrieben wurde und man sieht: das Scrollfenster rechts ist unverändert, also eine exakte Wiederherstellung des Bildschirms wie er vor dem Aufruf war, aber nicht, wie er beim Programmstart war!

Static 5 ist ein Moment aus einem anderen Durchlauf, in dem das OLW erneut eingeblendet wird, und man sieht, es ist ebenfalls in dem Zustand, in dem es war, als es seinerseits überschrieben wurde.

Wie man das realisiert, dazu im nächsten Beitrag.
Angehängte Datei(en)
Angehängte Datei  static1.jpg ( 132.55KB ) Anzahl der Downloads: 7
Angehängte Datei  static2.jpg ( 138.11KB ) Anzahl der Downloads: 3
Angehängte Datei  static3.jpg ( 229.68KB ) Anzahl der Downloads: 2
Angehängte Datei  static4.jpg ( 137.47KB ) Anzahl der Downloads: 2
Angehängte Datei  static5.jpg ( 233.97KB ) Anzahl der Downloads: 4
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 05.02.2012, 13:10 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wenn wir eine Ausgabefunktion haben, die lokale Daten verwaltet, ist das PRoblem, daß nach dem Verlassen der Funktion die Daten verloren sind. Diese Funktion fängt also immer wieder von vorn an und "vergißt" den Zustand vom vorhergehenden Aufruf. Das liegt daran, daß LOKALE Variablen nach dem Verlassen der Funktion nicht mehr definiert sind.

Diese LOKALEN Variablen nun nur zu dem Zweck als GLOBAL zu deklarieren, damit man beim nächsten Aufruf der Funktion nicht bei 0 anfangen muß, gefällt aus mehreren Gründen nicht:

1.) Erstens verstößt es gegen das höchst sinnvolle Gebot, die Daten wo immer es geht in sich zu kapseln, also hermetisch von den anderen Teilen des Programms abzuschirmen.

2.) Zweitens besteht kein vernünftiger Grund, diese LOKALEN Variablen öffentlich zu machen. Vernünftiger Grund wäre, wenn man sich entscheidet (zähneknirschend), daß mehrere Programmfunktionen auf dieses Daten zugreifen dürfen (zähneknirschend, weil sauber ist das nicht). Das ist hier aber gar nicht gegeben, keine andere Funktion im Programm möchte auf diese Daten zugreifen.

Es ist bildlich gesprochen so, daß man jemandem private Unterlagen anvertraut gegen das Versprechen, daß dieser Jemand damit keinen Unsinn anstellen wird.

3.) Besteht ja nicht nur eine eingebildete, sondern eine reale Gefahr, daß diese Daten zerstört werden von anderen, evtl. fehlerhaften Programmteilen oder der Programmierer doch von anderer Stelle drauf zugreift.

Kurzum: es ist unsauberer Programmierstil, der zu Instabilitäten führen kann.

Sauberer als die Variablen Global zu machen wäre es, sie in einer Datei auf dem Massespeicher vorzuhalten. Dann ist das Gebot der Kapselung erfüllt. Man könnte dann sogar beim Neustart des Programms dort weitermachen. Dateien können aber beschädigt werden, gelöscht oder überschrieben, wirklich STABIL ist das auch nicht.

Nun bin ich heute durch Glück und Zufall über einen Datentyp gestolpert, der die Lösung für genau diese Probleme liefert. Ein phantastischer Datentyp! wink.gif

Ich will ihn mal vorstellen, es ist extrem verblüffend, wie er funktioniert.

Code:

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

void zeige()
{
int local_i=0;
local_i++;
printf("local_i = %4i\n",local_i);
}

int main(int argc, char *argv[])
{
int z;
for (z=0;z<5;z++) zeige();
system("PAUSE");
return 0;
}

Es wird 5 mal die Funktion zeige() aufgerufen, welche den Wert für local_i um 1 erhöht.

Bildschirmausgabe:
local_i = 1
local_i = 1
local_i = 1
local_i = 1
local_i = 1
Press any key to continue . . .


Kann ja nicht anders sein. Die Funktion "vergißt", was beim letzten Aufruf war, und fängt wieder bei 0 an.

Definieren wir jetzt die GLOBALE Variable GLOBAL_I und ändern den Code ein wenig:

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

int GLOBAL_I=0;

void zeige()
{
int local_i=0;
local_i++;
GLOBAL_I++;
printf("local_i = %4i GLOBAL_I = %4i\n",local_i, GLOBAL_I);
}

int main(int argc, char *argv[])
{
int z;
for (z=0;z<5;z++) zeige();
system("PAUSE");
return 0;
}


Mit der Bildschirmausgabe:

local_i = 1 GLOBAL_I = 1
local_i = 1 GLOBAL_I = 2
local_i = 1 GLOBAL_I = 3
local_i = 1 GLOBAL_I = 4
local_i = 1 GLOBAL_I = 5
Press any key to continue . . .


Das heißt, die GLOBALE Variable "erinnert" sich sehr wohl, sie macht das, was wir wollen, daß die Funktion beim nächsten Aufruf nicht bei 0 anfängt, sondern dort weitermacht, wo aufgehört wurde.

Hier eben in dem Zusammenhang, daß man den Bildschirm nach Überblendung korrekt wiederherstellen will.

Nun nehme ich die GLOBALE Variable mal wieder raus, und füge das Zauberwörtchen "static" ein. Man kann sich nur die Augen reiben, man glaubt es nicht, aber es ist Tatsache:

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

void zeige()
{
int static local_i=0;
local_i++;
printf("local_i = %4i \n",local_i);
}

int main(int argc, char *argv[])
{
int z;
for (z=0;z<5;z++) zeige();
system("PAUSE");
return 0;
}


Bildschirm:

local_i = 1
local_i = 2
local_i = 3
local_i = 4
local_i = 5
Press any key to continue . . .


Um das Neudeutsch zu sagen: geil, nicht wahr? wink.gif

Jetzt können wir die Funktionen so kapseln, daß man beim besten Willen nicht mal mit Gewalt auf diese Variablen zugreifen kann. Sie bleiben LOKAL, und damit sind sie sicher.

Mit diesem herrlichen Datentyp in der Hand können wir jetzt das Problem beenden, nur muß man dazu die Funktionen etwas umschreiben. Da diese aber ziemlich abstrakt gehalten und mit Copy und Paste die Hierarchie sehr flach ist, ist das in wenigen Minuten erledigt.

Das Ergebnis hat man dann schon bei den Screenshots gesehen.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 05.02.2012, 13:32 Uhr
sharky2014
Level 7 = Community-Professor
*******

Wie funktioniert das Wunder überhaupt?

Die STATIC Variablen erhalten vom Compiler feste Speicherplätze zugewiesen, die ich jetzt mal als "geheim" bezeichnen will, ich setze voraus, daß man vom Hauptprogramm darauf nicht zugreifen kann (ohne Sherlock Holmes zu engagieren).

Im Funktionskopf, wo ja auch die Adresse der Funktion selbst gespeichert ist, wird dann der Speicherplatz für diese STATIC Variablen eben hinzugefügt. NOchmal: die Funktion kennt den SPeicherort, das Hauptprogramm kennt den nicht.

Das ist sozusagen ein Geheimversteck, was uns nicht länger nötigt, dem Nachbarn unsere privaten Unterlagen anzuvertrauen.

Die Funktionen im Programm wurden nun folgendermaßen umorganisiert:

Die OUTPUT-Funktionen, welche den Buffer mit dem Text auf dem BIldschirm ausgeben, haben nun die Zeile des letzten Programmaufrufs stets verfügbar. Allerdings, beim Scrollen sollen sie ja die Ausgabe verändern. Wenn sie also vorher den Text ab Zeile 5 ausgegeben haben, und der Anwender die PFeiltaste unten drückt, soll die nächste Ausgabe ab Zeile 6 erfolgen (welche dann als erste Zeile auf dem Bildschirmbereich erscheint, anstelle Zeile 5).

Die Scroll-Funktion muß jetzt nur ihre Änderungswünsche mitteilen. +1 -1 bei Pfeiltaste, +-Fensterhoehe, wenn Bildoben/Bildunten gedrückt wurde.

Die OUTPUT-Funktion addiert diese Werte zum aktuellen Wert des Zeigers (den die Scroll Funktion nicht kennt, ist ja jetzt gekapselt), prüft die Bereiche und entweder geschieht was, oder nicht, z. B. nicht, wenn über die letzte oder erste Zeile des Buffers hinaus gelesen werden soll.

Sieht dann so aus:

void zeigeMTLG(int delta)
{
int static bufzeiger=0;
int z;
bufzeiger+=delta;
if (bufzeiger>MTLGBUFLEN-MTLGZ)bufzeiger=MTLGBUFLEN-MTLGZ; // überlauf nach hinten
if (bufzeiger<0) bufzeiger=0; // Überlauf nach vorn abfangen

gotoMTLG();
for (z=0;z<MTLGZ;z++)
{
gotoxy(MTLGX,MTLGY+z);
printf("%s",MTLGbuf[z+bufzeiger]); // Ausgabe des Textspeichers um den Versatz bufzeiger
}
}


bufzeiger +=delta setzt natürlich voraus, daß Vorzeichen verwendet werden. Scroll müßte also zum Zurückblättern -1 aufrufen. Das tut es auch. Scroll sieht nun so aus:

void scroll(int screen)
{
char taste;
int maxzeilen,max;
if (screen==OLW){maxzeilen=OLWZ;max=OLWBUFLEN;}
else if(screen==MTLG){maxzeilen=MTLGZ;max=MTLGBUFLEN;}
else return; // keine anderen Optionen vorgesehen
int delta=0;
do
{
if (screen==OLW) zeigeOLW(delta);
else if (screen=MTLG) zeigeMTLG(delta);
else return; // HOSENTRAEGER MIT GUERTEL :-)
taste=getch();
switch ( (int) taste)
{
case ESCAPE:
return;
break; // BREAK nach RETURN ueberfluessig, aber wenn mal editiert wird, ist das Unglueck da, daher bleibt es so
case BILDOBEN:
delta=maxzeilen*(-1); // vereinbarungsgemaess was zurueck soll, mit Vorzeichen -
break;
case BILDUNTEN:
delta=maxzeilen;
break;
case PFEILUNTEN:
delta=1;
break;
case PFEILOBEN: // Zeile zurück
delta=-1;
break;
}
}while (1);
}

Mann sieht, die ganze Progammierung hat sich wesentlich vereinfacht. Z. B. brauchen wir beim Scrollen keine Bereichsprüfung mehr, die Parameter sind weniger geworden.

Und die Stabilität des Programms ist besser geworden, weil die Daten besser gekapselt sind.

Der Beitrag wurde von sharky bearbeitet: 05.02.2012, 13:35 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 06.02.2012, 20:23 Uhr
sharky2014
Level 7 = Community-Professor
*******

Das Grundthema, was ich mit diesem Beitrag zu vermitteln versuche, ist:

Wie programmiert man auf dieser Ebene strukturiert und übersichtlich, also "aufgeräumt", daß man, wenn man 1 Jahr später in den Code geht, sofort sieht, was da los ist.

Das eigentlich zu programmierende Programm existiert ja bereits, die FIBU, nur da fehlt es an Struktur, Übersicht und kontrolliertem Bildschirmaufbau.

Der Code dieser Version ist "zu sehr" ergebnisorientiert programmiert worden, funktioniert, ist aber nicht gut strukturiert und nicht schön fürs Auge. Das soll nun nachgeholt werden. Da er aber nun läuft, hab ich die stabile (aber unschöne) vorhandene Plattform so gelassen und schreibe die neue Version nicht als Änderung, sondern von Grund auf neu. Tiefgreifende Änderungen vorhandener PRogramme sind fast sichere Programmkiller, von da her.

Der Aufbau geht jetzt umgekehrt: anstelle erstmal das logische Problem zu lösen (Datenverarbeitung), wird bei dieser, der 2. Version, erstmal der ganze FORMALE Kram programmiert, ohne eine "wirkliche" Programmzeile, die sich mit Datenverarbeitung beschäftigt.

Man kann natürlich fragen, warum ich nicht einfach ein Programm für 100 Euro kaufe, was das auch kann.

Meine Antwort: wenn man es selbst programmieren KANN, ist die Lösung immer besser, weil besser angepaßt als ein KAUFMICH-Programm, was selbstverständlich als eierlegende Wollmilchsau (KANN ALLES) ausgelegt sein muß.

Was hier herauskommt, ist eine Programmierung, die speziell auf die Bedürfnisse und Vorlieben des Anwenders ausgelegt ist (individuelle Programmierung).

Und wenn das Programmieren auch noch Spaß macht, mir macht das seit vielen Jahren schon Spaß, natürlich nicht 365 Tage im Jahr, es gibt eben so Phasen, warum nicht? wink.gif

Zum Thema Strukturierung in ANSI-C hier zwei weitere Änderungen der formalen Programmbestandteile.

Erstmal stört allmählich die immer länger werdende #define Liste sowie die banalen Funktionen, welche den wachsenden Haufen von Variablen initialisieren, also sozusagen die "Putzfrauen"- oder "Hausmeister"-Funktionen. Die sind zwar wichtig, die wollen wir aber später in dem dem Bereich, der sich mit der eigentlichen Datenverarbeitung beschäftigt, nicht immer vor Augen haben.

Deshalb wird dieser ganze ellenlange Bereich (der ja später noch länger wird) nun ausgegliedert in eine HEADER- bzw. INCLUDE-Datei.

Jeder C-Compiler hat für das Speicherformat eine Funktion.

DIe System-Dateien wie <stdio.h> liegen in dem INCLUDE Verzeichnis des Compilers, unsere Datei MYHEADER.h legen wir in das Hauptverzeichnis des PRogramms und benennen sie:

mein_header.h

Diese Include-Datei mit dem ganzen Parameter-Mist sieht dann so aus:
#define BLACK 0
#define BLUE 1
#define GREEN 2
#define CYAN 3
...
#define ENTER 13
#define ESCAPE 27
#define BACKSPACE 8
#define PFEILOBEN 72


... stark gekürzt, sie ist endlos lang, also die #define-Anweisungen für Konstanten, gefolgt von den Forward-Deklarationen der Funktionen.

Die braucht man, damit jede Funktion darauf zugreifen kann, auch wenn sie in der Reihenfolge der Programmierung an der falschen Stelle (davor) steht.

void TextColor(int fontcolor,int backgroundcolor,HANDLE SCReen);
void ResizeConsole(short x, short y);
void gotoxy ( short x, short y );
void clrSCR();


... sowie die dritte Gruppe, die nicht über #define erklärt sind, sondern als konstante Variablen beim Programmstart initialisiert werden müssen:

char MENUbuf[MENUZ][MENUS+1];
char DIALbuf[DIALZ][DIALS+1];
char MTLGbuf[MTLGBUFLEN][MTLGS+1];
char MBOXbuf[MBOXZ][MBOXS+1];
char OLWbuf[OLWBUFLEN][OLWS+1];


... und die Initialisierungsroutinen:



void clrMENU(){int z;colorMENU();for (z=0;z<MENUZ;z++){gotoxy(MENUX,z+MENUY);puts(MENUblank);}gotoMENU();}
void clrDIAL(){int z;colorDIAL();for (z=0;z<DIALZ;z++){gotoxy(DIALX,z+DIALY);puts(DIALblank);}gotoDIAL();}
void clrMTLG(){int z;colorMTLG();for (z=0;z<MTLGZ;z++){gotoxy(MTLGX,z+MTLGY);puts(MTLGblank);}gotoMTLG();}
void clrMBOX(){int z;colorMBOX();for (z=0;z<MBOXZ;z++){gotoxy(MBOXX,z+MBOXY);puts(MBOXblank);}gotoMBOX();}
void clrOLW(){int z;colorOLW(); for (z=0;z<OLWZ;z++) {gotoxy(OLWX, z+OLWY); puts(OLWblank); }gotoOLW();}



void init_formatstrings()
{
int i;
for (i=0;i<=KON_BREITE;i++)SCRblank[i]=BLANK; SCRblank[KON_BREITE+1]=EOS;

strncpy(MENUblank,SCRblank,MENUS); MENUblank[MENUS]=EOS;
strncpy(DIALblank,SCRblank,DIALS); DIALblank[DIALS]=EOS;
strncpy(MTLGblank,SCRblank,MTLGS); MTLGblank[MTLGS]=EOS;
strncpy(MBOXblank,SCRblank,MBOXS); MBOXblank[MBOXS]=EOS;
strncpy(OLWblank,SCRblank,OLWS); OLWblank[OLWS]=EOS;


}



void init()
{
resetcolor();
ResizeConsole(KON_BREITE,KON_HOEHE);
clrSCR();
init_formatstrings();

clrMENU();
clrDIAL();
clrMTLG();
clrMBOX();

}

Damit haben wir einen Riesenhaufen Code, der nur der Initialisierung dient, aus dem Programmlisting raus und halten es dadurch übersichtlicher und lesbarer.

Damit das Programm diesen COde kennt, müssen wir nur eine einzige Zeile einfügen (anstelle der 3-n DINA4-Seiten, die wir umgelagert haben):

Dazu simulieren wir nicht den Aufruf der Standard-Bibliotheken a la

# include <MEIN_HEADER.h>

sondern wir formulieren:

# include "mein_header.h" bzw, wie hier: #include "fibu2012.h"

Der Compiler sucht dann im Stammverzeichnis, findet er dort die Datei nicht, geht er in das Systemverzeichnis mit den Systeminclude-Dateien, findet er dort auch nichts meckert er bei der ersten unbekannten Variable oder Funktion.

Die Auslagerung dieser ganzen Initialisierung- und selbstgeschriebenen Funktionen hat natürlich auch den Vorteil:

Wenn man eine andere Anwendung schreibt, kann man sie mit der #include Anweisung in die nächste Anwendung einbringen und braucht die dort versammelten Funktionen nicht mehr neu zu schreiben, sondern kann direkt drauf zugreifen.

Der nächste Punkt wäre,

REDUNDANZEN

abzubauen.

Redundanzen sind gleichlautende Code-Abschnitte. Die sind entstanden durch Copy und Paste und Ersetzen, und bei der Programmentwicklung ganz praktisch, eigentlich aber keine stabilen Bausteine, weil die Änderungen an dem einen Clone nicht automatisch auf den anderen Clone übergehen.

Nach einer Zeit, wo die Funktionen so ungefähr die Form haben, wie sie benötigt wird, kann man sie zusammenlegen.

Wir haben hier 2 ursprünglich identische Code-Teile, die sich nur in der Benennung unterscheiden. Sie verwalten die Textspeicher ihrer jeweiligen Bildschirmbereiche:


int putsDIAL(char nachricht[255],int spalte, int zeile)
{
int s;
int pruef = OK;
if (strlen(nachricht)+spalte>DIALS||zeile>DIALZ) return NOTOK;
for (s=0;s<strlen(nachricht);s++) DIALbuf[zeile][spalte+s]=nachricht[s];
DIALbuf[zeile][DIALS]=EOS;
return OK;
}

int putsMTLG(char nachricht[255],int spalte, int zeile)
{
int fehler=NEIN;
int s;
int pruef = OK;
int sp=spalte; // lokale Kopie
int len=strlen(nachricht); // strlen nur einmal aufrufen und Ergebnis ablegen
if (len>MTLGS||zeile>MTLGBUFLEN) return NOTOK; // zu breit btw. Bereichsüberschreitung
if (sp+len>MTLGS)
{
sp=0;
fehler=JA;
}// zu weit rechts platziert
for (s=0;s<len;s++) MTLGbuf[zeile][sp+s]=nachricht[s];
MTLGbuf[zeile][MTLGS]=EOS; // Stringendemarkierung explizit
if (fehler==JA)
{
MTLGbuf[zeile][0]=219; // Die BEstrafung: ASCII CODE 219
return NOTOK;
} // ASCII CODE 219 zur STrafe
return OK;
}


Die ursprünglich identischen Funktionen haben sich differenziert, nämlich durch das Scrolling. Die eine kann Scrollen, die andere nicht. Insgesamt gibt es 4 Bildschirmbereiche und einen Bereich für das Überblendfenster, also 5 Funktionen, von denen bislang 2 scrollen. Stellen wir uns vor, es kommen noch 3-4 Fensterbereiche dazu, dann gibt das einen Haufen Code, der im wesentlichen identisch ist, aber bei Änderungen sehr sensibel reagiert, wir müssen bei dem einen nur ein einziges Zeichen falsch schreiben, schon spinnt das Programm herum.

Legt man diese Code-Teile zu einer einzigen Funktion zusammen, hat man 2 Nachteile:

Erstens muß die Parameter-Llste länger werden, zweitens sinkt natürlich die Performance, möglicherweise nur ein winziges bißchen, weil wir nun den ursprünglichen Funktionsaufruf verzweigen müssen, bis wir den richtigen Adressaten erkannt haben.

Der Vorteil ist aber, daß alle Änderungen aller Fensterbereiche in dieser EINEN Funktion programmiert werden können, also ÜBERSICHT + STABILITÄT.

Statt 50 Sicherungen, die im Haus verteilt an irgendwelchen Plätzen untergracht sind, haben wir nur noch EINE EINZIGE ZENTRAL-Sicherung.

So denkt ja jeder Handwerker auch, das ist eigentlich logisches Denken und keine Programmierung.

Hier werden nun diese Funktionen zusammengelegt zu einer einzigen (eierlegenden Wollmilchsau? so schlimm ist es nicht wink.gif ):

int my_puts(int screen,char text[255],int spalte, int zeile)
{
int s;
int textlen=strlen(text);
int max_sp;
int max_z;
if (screen==DIAL) { max_sp=DIALS ; max_z=DIALZ ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGZ ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUZ ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXZ ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIZ ; }
else return NOTOK;

if (textlen>max_sp || zeile>max_z || textlen+spalte>max_sp) return NOTOK ;
switch (screen)
{
case DIAL:
for (s=0;s<textlen;s++) DIALbuf[zeile][spalte+s]=text[s];
break;
case MTLG:
for (s=0;s<textlen;s++) MTLGbuf[zeile][spalte+s]=text[s];
break;
case MENU:
for (s=0;s<textlen;s++) MENUbuf[zeile][spalte+s]=text[s];
break;
case MBOX:
for (s=0;s<textlen;s++) MBOXbuf[zeile][spalte+s]=text[s];
break;
case OLWI:
for (s=0;s<textlen;s++) OLWIbuf[zeile][spalte+s]=text[s];
break;
}


}



DIe Funktion puts() ist eine Standardfunktion, der Name ist reserviert, also my_puts()

Wir haben 1 Parameter mehr als zuvor, nämlich für den Bildschirmbereich. Dafür müssen wir den Namen der Funktion nicht erinnern, für alle gilt jetzt

my_puts anstelle von putsOLW, putsDIAL usf.

Im if/else if Teil sortiert die Funktion aus, welcher Bildschirmbereich gemeint ist. Und sucht sich die passenden Werte für die Bildschirmdarstellung heraus (Breite, Höhe).

Wenn die Anforderung nicht paßt, z. B. die Zeile zu groß ist (gibt es nicht) oder das Format nicht platziert werden kann, bricht die Funktion ab mit NOTOK.

Diese Prüfung (bereichsprüfung) ist erstmal nötig, damit hier nichts anbrennt.

Leider müssen wir dann mit dem SWITCH-OPERATOR nun nochmal unterscheiden, was zu tun ist. Der Performance-Nachteil ist hier aber quasi gleich NUll, weil nur eine Handvoll Datenzugriffe mehr erfolgen.

Ruft man das so auf, erhält man erstmal nicht das gewünschte, nämlich in den Scrolling-Fenstern wird nur eine Bildschirmgröße Text abgelegt (Abb. = screenshot)

Das ist logisch.

Zunächst ist ja für alle die Bufferlänge (der gespeicherte Bildschirmbereich) als darstellbare Höhe festgelegt.

Das ist hier:

if (screen==DIAL) { max_sp=DIALS ; max_z=DIALZ ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGZ ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUZ ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXZ ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIZ ; }
else return NOTOK;


Um die Fenster jetzt scrollfähig zu machen, müssen wir mitteilen, daß zwei von denen einen längeren Textbuffer bekommen haben, nämlich so:

if (screen==DIAL) { max_sp=DIALS ; max_z=DIALZ ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGBUFLEN ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUZ ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXZ ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIBUFLEN ; }
else return NOTOK;


Man sieht, die Zusammenlegung in einer Funktion macht die Struktur auch übersichtlicher, wir sehen auf einen Blick, wo Änderungen stattgefunden haben.

Nachdem wir das nachgetragen haben, läuft das Programm wieder wie zuvor.

Der nächste Schritt wäre nun, auch die anderen Clones zusammenzführen.

Dadurch wird der PRogrammcode kürzer und übersichtlicher.

Nochmal: bisher wurde keine einzige Zeile Programmcode geschrieben, der der eigentlichen Datenverarbeitung dient.

Bisher ist das alles nur formaler Code für Gestaltung und Verwaltung.

Der Beitrag wurde von sharky bearbeitet: 06.02.2012, 20:33 Uhr
Angehängte Datei(en)
Angehängte Datei  zusammenlegen.jpg ( 99.11KB ) Anzahl der Downloads: 4
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 06.02.2012, 22:29 Uhr
sharky2014
Level 7 = Community-Professor
*******

Zusammenführung weiterer Funktionen.

Ganz examplarisch mal.

Durch Copy&Paste und Ersetzen haben wir unsere Bildschirmausgabefunktionen für die einzelnen Bildschirmbereiche geklont, es entsteht redundanter Code, was Vor- und Nachteile hat. Den Code für die Bildschirmbereiche, obwohl geklont, kann man natürlich anschließend differenzieren, so daß die Code-Bestandteile nicht mehr identisch sind. Das ist hier geschehen dadurch, daß die Scroll-Funktion nur für 2 von 5 Bildschirmbereichen eingeführt wurde.

Es ergibt sich daraus das Problem, daß diese Funktionen formal anders angesprochen werden müssen, da die Klone mit der Scroll-Funktion einen Parameter mehr benötigen als die ohne. Das ist unhandlich, daher wurde der Parameter formal in die anderen Klone übernommen mit der Namensgebung "ueberfluessig".

Spätestens an dieser Stelle wird´s natürlich Zeit, da was zu ändern. Zwei dieser ursprünglich identischen, mittlerweile aber unterschiedlichen Funktionsaufrufe, welche den Text des Bildschirmbereich-Speichers am Bildschirmbereich ausgeben, sind diese:

void zeigeDIAL(int ueberfluessig)
{
int z;
gotoDIAL();
for (z=0;z<DIALZ;z++)
{
gotoxy(DIALX,DIALY+z);
printf("%s",DIALbuf[z]);
}
}


void zeigeMTLG(int delta)
{
int static bufzeiger=0;
int z;
bufzeiger+=delta;
if (bufzeiger>MTLGBUFLEN-MTLGZ)bufzeiger=MTLGBUFLEN-MTLGZ; // überlauf verhindern
if (bufzeiger<0) bufzeiger=0; // Bereichsüberschreitung nach vorn abfangen

gotoMTLG();
for (z=0;z<MTLGZ;z++)
{
gotoxy(MTLGX,MTLGY+z);
printf("%s",MTLGbuf[z+bufzeiger]);
}
}


Will man diese (und die drei anderen bestehenden, bzw. es ist ja auch gedacht für die noch folgenden) zusammenführen, wird der Code immer abstrakter (nicht zu vermeiden), und man muß sehen, daß die Codierung übersichtlich und selbsterklärend bleibt.

Hier die neue Funktion, die beliebig viele Funktionen von dem beschriebenen Typ verwaltet.

Wir sehen hier im oberen Bereich den if/else if Block, der sortiert, für welches Fenster der Aufruf gilt. Könnte man auch als switch machen, ich lass es aber erstmal so. In diesem Block oder ansschließend erfolgen die Bereichsüberprüfungen, die ja für jeden Bildschirmbereich anders sind. Was man ausklammern kann, also für alle gilt, schreibt man außerhalb von dem Block.

Und anschließend der schon bekannte switch-Block, der die Ausgaben handhabt, NACHDEM die Bereichsüberprüfungen stattgefunden haben.

int zeige_bereich(int screen,int delta)
{
int i,z;
int static bufzeiger[LASTSCREEN]={0,0,0,0,0};

bufzeiger[screen]+=delta;
if (bufzeiger[screen]<0)bufzeiger[screen]=0; // Überlauf nach vorn für alle gleich
if (screen==DIAL) gotoDIAL();
else if (screen==MTLG) {gotoMTLG();if (bufzeiger[screen]>MTLGBUFLEN-MTLGZ)bufzeiger[screen]=MTLGBUFLEN-MTLGZ;}// Überlauf nach hinten für SCROLL
else if (screen==MENU) gotoMENU();
else if (screen==MBOX) gotoMBOX();
else if (screen==OLWI) {gotoOLWI();if (bufzeiger[screen]>OLWIBUFLEN-OLWIZ)bufzeiger[screen]=OLWIBUFLEN-OLWIZ;}
else return NOTOK;
switch (screen)
{
case DIAL:
for (z=0;z<DIALZ;z++){ gotoxy(DIALX,DIALY+z); printf("%s",DIALbuf[z]);}
break;
case MTLG:
for (z=0;z<MTLGZ;z++){ gotoxy(MTLGX,MTLGY+z); printf("%s",MTLGbuf[z+bufzeiger[screen]]);}
break;
case MENU:
for (z=0;z<MENUZ;z++){ gotoxy(MENUX,MENUY+z); printf("%s",MENUbuf[z]);}
break;
case MBOX:
for (z=0;z<MBOXZ;z++){ gotoxy(MBOXX,MBOXY+z); printf("%s",MBOXbuf[z]);}
break;
case OLWI:
for (z=0;z<OLWIZ;z++){ gotoxy(OLWIX,OLWIY+z); printf("%s",OLWIbuf[z+bufzeiger[screen]]);}
break;
} // switch

return OK;
} // fu



Das unterschiedliche Management der Fenster mit Scroll-Funktion gegenüber denen ohne Scroll-Funktion ist hier ROT dargestellt.

Es ist ersichtlich, daß wie im vorigen Fall die ÜBERSICHT über das Programm besser wird, wenn man diese ALL-IN-ONE Funktion benutzt anstelle der vielen Klone.

Jede Art von Differenzierung kann hier an dieser Stelle übersehen und gehandhabt werden.

Das ist alles gut, der Code des Programms wird bei identischer Leistung immer kürzer, die Übersicht steigt.

Nur für die static-Variablen hab ich noch keine schöne Lösung gefunden. Wir haben es ja jetzt mit allen zu tun nicht nicht mit einer, daher werden sie indiziert abgelegt. Man sieht, die müssen per ={1,1,1 ... } initialisiert werden. Initialisiert man sie in einem Array, werden sie bei jedem Programmaufruf auf 0 zurückgebeamt, das Gegenteil von dem, was gewollt ist. Diese =[1,1, ... Initialisierung heißt natürlich, händisch anpassen. Gefällt mir nicht, ist nicht stabil, fehleranfällig usf. Möglicherweise fällt mir dafür noch eine Lösung ein.

Das ganze Programm, obwohl schon sehr leistungsfähig, ist jetzt sehr klein und übersichtlich geworden.

Der formale Teil der Bildschirm-Präsentation ist damit eigentlich abgeschlossen.

Zu der Datei-Organisation hatte ich schon gesagt, daß die in c einigermaßen banal ist. Man muß nur wissen, worauf es ankommt, auch das wurde vorgestellt, wie man den Datenzugriff auf dem Massespeicher handhabt.

Man nähert sich jetzt so allmählich dem Punkt, wo man an die Datenverarbeitung geht.

In der Beitragsvorschau wurde noch der Punkt sichere Eingabe von Daten von der Tastatur erwähnt.

Die Standardeingaben mit scanf() sind nämlich nicht sicher!!! BUFFER_OVERFLOW ist der ständige Teufel, der beim C-Programmieren im nacken sitzt. Das muß man sicher ausschließen, und das kann man auch. Ist aber etwas aufwendig.

Das mach ich demnächst entweder, indem ich die vorhandenen Funktionen übernehme, oder die ganz neu schreibe.

Weiß noch nicht.

Ich danke für das Interesse an meinem Beitrag soweit.

VIelleicht kann der eine oder andere ja für sich dabei was herausziehen.

Gruß Sharky

Der Beitrag wurde von sharky bearbeitet: 06.02.2012, 22:42 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 07.02.2012, 19:20 Uhr
sharky2014
Level 7 = Community-Professor
*******

Was bis jetzt erreicht wurde:

Wir haben 4 voneinander getrennte Bildschirmbereiche, für die es eigene Speicher gibt, und ein Überblendfenster.

DIe Ansprache dieser Bereiche ist rein formal. Wir müssen uns nicht darum kümmern, wie das Fenster sein Ding hinbekommt, wo das Fenster ist oder sonstwas.

Das bedeutet, die aufrufende Funktion muß nur ganz wenige Aufrufe formulieren, damit das Ganze zum Leben erwacht.

Um beispielsweise unsere 4 Bildschirmbereiche sichtbar zu machen, sind lediglich erforderlich:

void init()
{
int screen;
ResizeConsole(KON_BREITE,KON_HOEHE);
init_formatstrings();
for (screen=0;screen<VOLL;screen++)clrSCR(screen);
}


Die Bildschirm-Bereiche sind nach der enum-Definition organisiert:

enum screens{MENU,DIAL,MTLG,MBOX,OLWI,VOLL};

int screen[VOLL+1]={MENU,DIAL,MTLG,MBOX,OLWI,VOLL};


so daß wir sie als indizierte Variable aufrufen können, nämlich sclrSRC(screen);

Im Prinzip ist das wünschenswert, daß man anstelle:

Zeige Bildbereich_DIALOG
Zeige Bildbereich_MITTEILUNGEN
Zeige Bildbereich_HINWEISE

sagt:

int bildbereiche[n], mit der Folge, daß man sagen kann:

for (i=0;i<MAXBILDBEREICHE;i++) zeige bildbereich[n],

und man kann diese Indizierung bis ganz auf den low-level Bereich treiben und hat als Folge sehr KURZEN CODE, der aber nicht unbedingt dadurch, daß er sehr kurz ist, übersichtlicher sein muß. Die Formalisierung steigt ziemlich steil an zu Lasten der Übersicht.

Wenn wir indizieren, haben wir uns die auf einer Ebene laufenden Verzweiger gespart. Statt:

switch (Bildbereich)
{
case 1: ...
break;
case 2: ...
break;
case 22:...
break
}

Müssen wir nur eine einzige Zeile schreiben (for i=0;i<ALLE_BEREICHE;i++) mach irgendwas.

Wenn wir die Bereiche mit Inhalt sehen wollen (der natürlich zuvor reingeladen werden muß), sind die Funktionsaufrufe (hier zunächst noch in der Testphase aus dem Hauptprogramm heraus, bleibt natürlich nicht so:):


int main(int argc, char *argv[])
{
init(); // Initialisierung aller Programmbereiche, auch der Bildschirmfenster
zeige_bereich(DIAL,0);
scroll(MTLG);
scroll(OLWI); // Scrollen im Überblendfenster
zeige_bereich(DIAL,0);
scroll(MTLG);
scroll(OLWI);
}

Es wird also erstmal der Bereich DIAL aufgerufen (der noch zu belebende Fensterbereich für den DIALog mit dem Anwender), dann das MTLG (Mitteilungen), dann OLWI (Überlappendes WINDOW), dann wird das zerstört und der Bildschirm wiederhergestellt, wie er vor dem Aufruf war, und wir scrollen munter weiter in MTLG bzw. nochmal in OLWI.

Natürlich nicht so hintereinander, wie hier, sondern die aufrufende Funktion muß per Menu die Bildschirmbereiche hin- und herswitchen (was bis jetzt noch keinen Sinn macht).

Es gibt ein Problem mit Fensterbereichen und der Information, die sie darstellen sollen, nämlich ob die Information zeilenweise überhaupt reinpaßt.

Auf die FIBU bezogen: wenn der Kontenrahmen SKR03 Bezeichner hat, die 40 Zeichen lang sind, der gewählte Bildschirmbereich aber nur 30 Zeichen darstellen kann, dann paßt die Info nicht rein. Das Problem stellt sich eigentlich immer.

Um die Information sichtbar zu machen, gibt es drei Möglichkeiten.

1.) Wir verbreitern den Fensterbereich

Das ist, da das Programm stark formalisiert wurde, sehr einfach zu haben. Aber, natürlich, auf Kosten des Bildbereichs der anderen Bereiche, und als das aufgeteilt wurde, hat man sich natürlich was dazu überlegt, warum das so aufgeteilt wurde.

2.) Wir verkürzen die Text-Strings, die angezeigt werden. Das ist ein schwerwiegender Eingriff, und kollidiert damit, daß diese Texte natürlich deshalb die Länge haben, weil sie auch benötigt wird. Die benötigte Länge der Textfelder an ein zufällig vorhandenes Bilschirmfenster anzupassen scheidet aus mehreren Gründen aus.

3.) Wir weisen die anzeigende Funktion an, die Texte nicht zu ignorieren, weil sie zu lang sind, sondern den Anteil zeigen, der auf den Bildschirm paßt. Bisher haben die Funktionen ja solche Überlängen einfach ignoriert.

Das machen wir jetzt mal.

Dazu dürfen aber nicht ersetzen. Die Funktion ist ja ganz allgemein gehalten und ermöglicht alles das, was man mit gotoxy(Spalte,Zeile) machen kann, also auch nachträglich in den Bereich zuvor oder danach ein Zeichen einfügen. Daher darf hier nichts gelöscht werden, was vorhanden ist. Wir müssen lediglich prüfen, ob der neue Text an der angegebenen Position reinpaßt, mehr nicht.

Das ist hier realisiert, so wie es sein soll, kurz und bündig und ohne endlose if elseif oder switch-Schalter:

if (zeile>max_z)return NOTOK; // Bereichsüberschreitung Zeile
for (s=0;s<max_sp-xpos;s++) if (s<textlen) buffer[s]=text[s]; // 2-Bedingungen Länge und verfügbarer Platz
textlen_korrigiert=strlen(buffer); // LÄnge des korrigierten Char-Arrays
buffer[textlen_korrigiert]=EOS; // Stringende-Markierung unbedingt reinsetzen nach low-level PRogrammierung


Wenn die angeforderte Zeile im Buffer gar nicht existiert, macht es keinen Sinn, return.

Jetzt müssen wir prüfen: paßt der Text an der angeforderten Position rein. Z. B. der Text ist 20 Zeichen lang, die verfügbare Breite ist 30, und die Position ist 15, ergäbe das 15+20=35, und er würde nicht reinpassen. Wir müßten den Text dann um 5 verkürzen, damit er teilweise reingeht.

Anstatt enloser IF-Verzweigungen machen wir es so, daß für die Schleife durchlaufen lassen für den verfügbaren Platz (max_sp-xpos) ist das, was verfügbar ist, der Raum zwischen angeforderter Position und Zeilenende. Und prüfen zugleich (x<textlen), damit wir nicht überschreiben, ob der Text überhaupt noch Infos enthält. Es könnte ja sein, daß es sich nur um 1 Zeichen handelt, daß nachträglich in die Zeile eingefügt werden soll (schwitz, ich weiß, daß läßt sich kaum lesen noch anstrengender ist es, das zu beschreiben, aber das PROBLEM EXISTIERT EBEN).

Wir korrigieren für die nachfolgenden Funktionen die Variable textlen, und damit das auch klar wird, nennen wir die Textlänge von dem möglicherweise abgeschnittenen Text textlen_korrigiert. Sonst könnte es nämlich passieren, daß man den zweiten Aufruf von textlen rauslöscht (weil angeblich überflüssig) und das PRogramm stürzt ab.

Am ENde setzen wir noch die EOS (END OF STRING) Markierung rein, ein Makro für '\0', da alle C-Strings, bzw. genauer, es gibt ja in C keine Strings, sondern Char-Arrays, NULLTERMINIERT sind.

Jetzt werfen wir mal einen Blick auf den Bilschirm, was das gebracht hat:

Abb1 zeigt den Startbildschirm, wo im Fenster rechts schon das Scrolling aktiv ist. Natürlich verschiebe ich die Textzeilen nach rechts, damit man überhaupt sehen kann, wie das PRogramm mit dem Problem "paßt nicht rein" umgeht.

Es nimmt also auch die Bruchteile der Zeile, wie sie eben reinpassen.

Jetzt scrollen wir, um die Grenzbedingungen zu erkennen.

Abb 2 zeigt diesen Grenzwert:

Solange noch ein Buchstabe anzeigbar ist, wird er reingenommen. Der letzte ist der erste (steht schon in der Bibel), nämlich das 'M' von Mitteilung.

Die Behandlung der Grenzwerte klappt also, alles PRogrammieren dreht sich ohnehin nur um Grenzwerte, mittendrin läuft natürlich immer alles klar.

Das heißt, die Funktion macht formal genau das, was sie soll:

Wenn in dem Bereich irgendwie was auf den Bildschirm anzuzeigen ist, zeigt sie die Krümel, die machbar sind.

Als nächstes will ich mal den Kontenrahmen SKR03 einkopieren.

Das ist jetzt, nachdem das Programm formal so schön eingerichtet ist, eine Kleinigkeit:

Wir kopieren den SKR03 Kontenrahmen einfach in irgendeinen Bilschirm-Bereich-speicher. Egal welcher. Sollten aber einen nehmen, der Scrollen kann. Bisher können scrollen das ÜBerblendfenster und das Info-Fenster rechts, was gerade seinen Test bestanden hat.

Der Beitrag wurde von sharky bearbeitet: 07.02.2012, 19:32 Uhr
Angehängte Datei(en)
Angehängte Datei  clipping1.jpg ( 135.83KB ) Anzahl der Downloads: 3
Angehängte Datei  clipping2.jpg ( 119.01KB ) Anzahl der Downloads: 4
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 07.02.2012, 20:03 Uhr
sharky2014
Level 7 = Community-Professor
*******

Um Daten verfügbar zu haben, muß man sie natürlich auf irgendeine Art erfassen.

Am besten wäre es, wenn man sich das einfach per copy und paste reinkopiert.

Um den SKR03 Kontenrahmen verfügbar zu haben, wird man nicht umhin kommen, den EINMAL mit der Hand einzutippen.

Es wäre dasselbe, als wenn man eine Datei mit den Lebensdaten berühmter Musiker verwalten wollte oder es sich um eine Teile-Liste für Ersatzteile handeln würde oder was auch immer.

Das ist aber kein Problem, das macht man einmal und erledigt.

Die Daten sind natürlich nicht sortiert. Der SKR03 ist groß, man braucht nicht alle KOnten, und nachträglich braucht man doch noch das eine oder andere. Dann sind die Konten eben nicht sortiert, siehe:

skr03[71].konto_nr=3060;
sprintf(skr03[71].konto_bez,"%-s","Lagerbedarf andere Lieferanten");

skr03[72].konto_nr=4301;
sprintf(skr03[72].konto_bez,"%-s","Nicht abziehbare Vorsteuer 7 Prozent ");

skr03[73].konto_nr=4306;
sprintf(skr03[73].konto_bez,"%-s","Nicht abziehbare Vorsteuer 19 Prozent ");

skr03[74].konto_nr=8300;
sprintf(skr03[74].konto_bez,"%-s","Einnahmen Lager 7 Proz. USt Nettobetrag");

skr03[75].konto_nr=8820;
sprintf(skr03[75].konto_bez,"%-s","Erloese Sachanlagenverk. 19 Proz. USt ");

skr03[76].konto_nr=8829;
sprintf(skr03[76].konto_bez,"%-s","Erloese Sachanlagenverk. USt frei");

skr03[77].konto_nr=4140;
sprintf(skr03[77].konto_bez,"%-s","Freiwillige soz. Aufwendungen LSt frei");

skr03[78].konto_nr=4171;
sprintf(skr03[78].konto_bez,"%-s","Vereinnahmte USt 7 Prozent Lager ");

skr03[79].konto_nr=2020;
sprintf(skr03[79].konto_bez,"%-s","Periodenfremde Aufwendungen");

skr03[80].konto_nr=1860;
sprintf(skr03[80].konto_bez,"%-s","Private Rechtsangelegenheiten");

skr03[81].konto_nr=1771;
sprintf(skr03[81].konto_bez,"%-s","Abzufuehrende USt 7 Prozent Lager");

skr03[82].konto_nr=1776;
sprintf(skr03[82].konto_bez,"%-s","Abzuf. USt 19 Proz. Anlageabgaenge");

skr03[83].konto_nr=8220;
sprintf(skr03[83].konto_bez,"%-s","Einnahmen Verrechnungstelle 2");

skr03[84].konto_nr=8310;
sprintf(skr03[84].konto_bez,"%-s","Einnahmen GeschaeftLager Ust-frei");

skr03[85].konto_nr=1790;
sprintf(skr03[85].konto_bez,"%-s","Verbindlichk. an VS 1");


Bevor das in den laufenden Programmbetrieb geht, muß also nach Konto-Nr. sortiert werden.

IN einem normalen C-Forum würde man dafür beschimpft, aber:

Um 100 DS zu sortieren, braucht man keinen Bubble- oder Quicksort, sondern die Sortierung erfolgt quadratisch, praktisch, gut = ABSOLUT FEHLERFREI, indem man sagt:

AUßenschleife:

lies alle Datensätze

Innenschleife:

vergleiche diesen Datensatz aus der Außenschleife mit allen anderen Datenstätzen
und kopirere das gefundene Element in eine Neue Liste.

Das war´s.

Bei 100 Datensätzen haben wir es mit 100*100=10.000 Datenzugrriffen zu tun, das ist NICHTS.

Millisekunden.

Zum Quicksort mal ein Zitat aus Wikipedia (für Leute die mitlachen wollen rot unterlegt):

"Quicksort (von englisch quick ‚schnell‘ und to sort ‚sortieren‘) ist ein schneller, rekursiver, nicht-stabiler Sortieralgorithmus,"


Soviel dazu, daß man in C-Foren dafür beschimpft wird, quadratisch zu sortieren. Da hängen viele rum mit Profilneurose, anders ist das nicht zu erklären. Um bei 100 Datensätzen Quicksort zu machen (NICHT_STABIL), das wäre so, als wenn man bei einem Ferrari den Kasten BIer aus dem Kofferraum nimmt, damit der besser beschleunigt. So ticken diese Typen in den C-Foren. Kann man abhaken.

Der Sortieralgorithmus (Ferrari mit Kasten Bier im Kofferraum) ist so:

void sortiere_kontenrahmen()
{
strc_konto kbuffer[MAXKONTEN];
int index=0;
int konto_nr=0;
int zaehler=0;
int i,k;
for (i=0;i<MAXKONTEN;i++)
{
konto_nr=9999;
for (k=0;k<MAXKONTEN;k++)if (skr03[k].konto_nr<konto_nr&&skr03[k].konto_nr!=0)
{
konto_nr=skr03[k].konto_nr;
index=k; // das kleinste bleibt haengen
} // innenschleife
kbuffer[zaehler]=skr03[index]; // Kopieren
skr03[index].konto_nr=0; // nach dem Kopieren ausnullen
zaehler++;
} // außenschleife
// Sortierten Inhalt zurückkopieren:

for (i=0;i<MAXKONTEN;i++)
skr03[i]=kbuffer[i];
}//fu


Mehr ist das nicht. Wir nehmen die höchstmögliche vierstellige Konten_Nr 9999 und suchen von da ausgehend das Kleinste.

Schreiben das als das Nächstkleinste in die neue Liste und wiederholen den Vorgang solange, bis alle Konten in der unsortierten Liste abgearbeitet sind.

MILLISEKUNDEN,

und der ganze Kontenrahmen steht zur Verfügung.

Natürlich, noch nicht sichtbar.

Daher kopiert man den nun in den Textbuffer irgendeines Bildschirmbereiches, um den während des Programmablaufs mit irgendeiner Taste aufrufen zu können und zu scrollen, bis man die richtige Konto-Nr. gefunden hat.

Der Beitrag wurde von sharky bearbeitet: 07.02.2012, 20:05 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 07.02.2012, 20:31 Uhr
sharky2014
Level 7 = Community-Professor
*******

Also, um diesen Sortier-Algorithmus nochmal zu erklären (ich sehe gerade, so selbsterklärend ist der gar nicht):

Wir haben eine unsortierte Liste.

Dann haben wir eine leere Liste.

Jetzt suchen wir aus der unsortierte das KLEINSTE.

Und Kopieren es auf den ERSTEN Datensatz der leeren Liste.

Dann müssen wir bei der unsortierten Liste das Element entweder löschen oder überschreiben, sagen wir mal, wir machen einen dicken Strich über dieses Element, was sich ja schon in der neuen Liste befindet, damit das nicht zum zweiten Male herausgenommen und doppelt kopiert wird.

In dem Code vielleicht versteckt, nicht gleich zu sehen:

Die zweite Liste ist kbuffer[MAXKONTEN], die uns Speicherplatz für eine Kopie der gesamten Liste bereitstellt.

Da wird nun nacheinander eine Kopie der unsortierten Liste in sortierter Reihenfolge abgelegt.

Ist das erfolgt, wird das zurückkopiert, das Sortierte in das Unsortierte, mit der Anweisung:


for (i=0;i<MAXKONTEN;i++) skr03[i]=kbuffer[i];


MIt dieser einen Zeile überschreiben wir die vorherigen Inhalte so, daß die nun sortiert sind (Schwitz, ich weiß, das ist anstrengend, aber das Problem ist nunmal so).

Nochmal:

skr03[MAXKONTEN] enthält zunächst mal alle Konten unsortiert.

DIe werden nun kopiert in aufsteigender Reihenfolge in kbuffer[MAXKONTEN], eine Struktur, die genausogroß ist wie skr03[MAXKONTEN].

Und danach überschreiben wir den ehemaligen Inhalt von skr03[MAXKONTEN] mit der sortierten Liste kbuffer[MAXKONTEN], so daß die Datensätze jetzt in

skr03[MAXKONTEN] sortiert zu finden sind, aber nur zur Laufzeit, und nur, nachdem und solange diese Routine aufgerufen wurde, die das sortiert.

Ruft man das Programm neu auf, sind sie zunächst mal nicht sortiert zu finden, aber nach dem Durchgang der Sortier-Routine eben doch (schwitz).

Da man die Sortierroutine aber fest eingebunden hat, ist der Kontenrahmen nach dem Start des Programms IMMER Sortiert zu finden.

-------------Herrgott, hilf mir, ich hoffe, ich hab das einigermaßen verständlich rübergebracht. wink.gif

Der Beitrag wurde von sharky bearbeitet: 07.02.2012, 20:38 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 08.02.2012, 06:35 Uhr
sharky2014
Level 7 = Community-Professor
*******

Nachdem der (hier: Beispielkontenrahmen) Kontenrahmen Skr03 erfaßt wurde, legen wir den ganzen Code in einer zweiten Header-Datei ab, namens skr03.h

Abb: Screencopy des Stammverzeichnisses. Die Header haben ein anderes Symbol und werden von der Entwicklungsumgebung als solche richtig interpretiert. Man soll zwar in Header keinen ausführbaren Code reinschreiben, der Übersicht halber mach ich´s trotzdem. Sofern man vom Hauptprogramm darauf zugreifen will, muß man vielleicht einige Funktionen FOREWARD erklären, also mit der Kopie des Funktionsrumpfes an den Dateieinfang stellen.

Abb: Entwicklungsumgebung mit Header-Dateien

In der Header-Datei skr03.h findet sich folgendes:

Die Auflistung des SKR03 wie oben schon gesehen.

Die Funktion, welche die Liste nach Konto-Nr sortiert

Eine zweite Funktion, welche die sortierte Liste als Textfile speichert (das benötigt man zur KOmmunikation mit Steuerberater etc, z. B. öffnen mit Notepad und ausdrucken). Machen wir das mal (Abb. 2)

Daß die Datei entwas unschön aussieht, ist Absicht. Sie soll nämlich als Schnittstelle zu Tabellenkalkulationen dienen, und dafür braucht es Feldbegrenzer (delimiter). Hier wurde das Semikolon gewählt, das Komma ist mir nicht zuverlässig, weil Texte auch Kommata enthalten können und dann gibt es Salat.

Man sieht, bei den Bestandskonten wurden pro forma einige Eröffnungswerte reingesetzt, die Anlagekonten sind aber noch nicht eingepflegt. Die Bestandskonten werden am Jahresende saldiert nach zu- und Abflüssen. Die Anlagekonten werden um die AFA korrigiert. Das soll aber der Steuerberater machen, das ist nicht Sache einer FIBU, weil da die Frage wäre, was ist zulässig, was akzeptiert das Finanzamt, und das ist nicht Sache der Programmierung.

Mit dem SKR03 Kontenrahmen haben wir zum ersten Male in den Prototyp des Programms eine SINNVOLLE Information eingebunden.

So bekommen wir das eingebunden:

#include "skr03.h"

Das ist schon alles. Damit können wir auf alle Infos und Funktionen dieses zweiten Headers zugreifen.

Damit wir den Kontenrahmen auf den Bildschirm holen, ändern wir unsere Initialisierungsroutine wie folgt:

void init()
{
int i;
int screen;
char buffer[255];
ResizeConsole(KON_BREITE,KON_HOEHE);
init_formatstrings();
for (screen=0;screen<VOLL;screen++)clrSCR(screen);
init_kontenrahmen();
sortiere_kontenrahmen();
speichere_kontenrahmen_txt();

resetBUF(MTLG); // die Bildschirmbuffer leeren
resetBUF(OLWI);
for (i=0;i<MAXKONTEN;i++)
{
if (skr03[i].konto_nr>0) // Datensatz belegt?
{
sprintf(buffer,"%4i %s",skr03[i].konto_nr,skr03[i].konto_bez);
my_puts(MTLG,buffer,0,i);
my_puts(OLWI,buffer,0,i);

}
}
}

Wir setzen den Speicherbreich für zwei Fenster MTLG und OLWI (das Überblendfenster) zurück, das heißt, überschreiben den Bereich mit Leerzeichen.

Und dann kopieren wir in beide Fenster den gesamten SKR03 Kontenrahmen ein. Dazu muß natürlich der Speicher der Bildschirmbereiche groß genug sein. Da wir diese beiden Fenster aber scrollen, ist das kein Problem, wir können diese Buffer beliebig groß machen, da ganze Bücher abspeichern.

Die Abfrage if (skr03[i].konto_nr>0 dient dazu, daß wir keine leeren Datensätze reinkopieren, sondern nur solche, wo Information drin steht.

Nachdem das erledigt ist, ist auch schon ALLES ERLEDIGT.

Das ist der Vorteil stark formalisierter Programmierung, daß man im Anschluß komplexe Aufgaben mit wenigen Programmzeilen abarbeitet.

Im Hauptprogramm stehen, um den Kontenrahmen sichtbar zu machen, nur 2 Anweisungen:

scroll(MTLG);
scroll(OLWI);


Das ist wirklich alles, ehrlich! wink.gif

Das Ergebnis ist dann, daß wir (zum mal zum Rumspielen) den Kontenrahmen in 2 Fenstern verfügbar haben und darin scrollen können (wie gesagt, das Scrolling ist jetzt in die Fenster "fest eingebaut", wir müssen uns um nichts kümmern, der Aufruf der Funktion scroll(Fensterbereich) reicht.

Abb3: Nach Aufruf des Scroll-Fensterbereich rechts.

Abb4: Zusätzlich noch ein zweites Scrollfenster aufgerufen

Abb5: Nachdem wir aus dem OLW raus sind, wird der Bildschirminhalt exakt wiederhergestellt. Wir finden also unseren rechten Bereich in dem Scrolling-Zustand wie vor der Überblendung. Der Grund sind die schon beschriebenen STATIC-Variablen.

Damit sind wir noch nicht (ganz) am Ende der formalen Vorbereitungen. Wir müssen nun noch den DIALOGBereich für den Anwender implementieren, damit die Datenerfassung erfolgen kann.

Der Beitrag wurde von sharky bearbeitet: 08.02.2012, 06:37 Uhr
Angehängte Datei(en)
Angehängte Datei  skr03_1.jpg ( 130.7KB ) Anzahl der Downloads: 6
Angehängte Datei  skr03_2.jpg ( 537.99KB ) Anzahl der Downloads: 9
Angehängte Datei  skr03_3.jpg ( 277.7KB ) Anzahl der Downloads: 5
Angehängte Datei  skr03_4.jpg ( 341.22KB ) Anzahl der Downloads: 11
Angehängte Datei  skr03_5.jpg ( 277.7KB ) Anzahl der Downloads: 6
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 08.02.2012, 21:14 Uhr
sharky2014
Level 7 = Community-Professor
*******

Implementation eines Anwender-Dialogs auf der Konsole

Der Dialog mit dem Anwender hat seine speziellen Anforderungen. Erstmal muß man, gutwillig, dem Anwender eine gute Plattform liefern, damit die benötigten Informationen so einfach wie möglich eingegeben werden können. Zweitens muß man aber verhindern, daß das Programm versehentlich oder absichtlich vom Anwender abgeschossen wird.

Die Einfallspforte bei C-Programmierung ist der Buffer-overflow. Dann werden Zeichen aus der Eingabe übernommen, für die kein Platz reserviert ist, und das Programm wird instabil. Daher scheiden sämtliche Standard-Eingabefunktionen von C aus. Kann man abhaken. Die einzige Schnittstelle zwischen Konsole und Programm ist getch(), was ein Zeichen liest. Ist es eingebettet in eine Abfrage-Schleife, kann da auch nichts anbrennen, wenn über Nacht die Brieftasche auf der Tastatur des Notebooks liegt. wink.gif

Um aus getch(), also einzelnen Zeichen, die benötigten Datentypen zu produzieren, wie Datum, Konto-Nr, Betrag, also char-array, Integer, Float (Fließkommazahl) muß man natürlich ein bißchen low-level programmieren. Das ist aber weniger schlimm als man denken mag. Wir müssen dann einfach nur die erlaubten von den unerlaubten Zeichen rausfiltern. Werde ich am Anschluß knapp umreißen, weil es eigentlich recht banal ist.

Wichtiger ist die Grundüberlegung für das DESIGN. Dieses elende Konzept, wo eine Datenerfassung von sagen wir 10 Feldern von oben nach unten erfolgt, sowas wollen wir dem Anwender nicht zumuten. Das geht ja so, ich will mal sagen: sequentielle Datenerfassung:

Gib das Datum ein

Gib die Konto Nr. ein

Gib das Gegenkonto ein

Gib den Betrag ein

...

Gib sonstwas ein.

Eingabe in Ordnung (J/N)?

Und erst hier, am Ende der langen Liste, haben wir die Gelegenheit, die ganze Schlange nochmal zu durchlaufen, VON OBEN NACH UNTEN, SEQUENTIELL, und um EIN EINZIGES DATENFELD zu ändern, was wir versehentlich falsch eingegeben haben, müssen wir den ganzen Kram nochmal neu beackern. EINE ZUMUTUNG.

SO WOLLEN WIR NICHT PROGRAMMIEREN, ,auf die Bedienerführung bezogen.

Daneben gibt es das Problem, programmiertechnisch, daß die Eingabefunktionen von NEU und EDIT, also eines neuen Datensatzes und das Überschreiben (editieren) eines vorhandenen Datensatzes eigentlich völlig identisch sind (Code-Redundanz), aber der Unterschied ist ja, daß wir es einmal mit einem leeren Datensatz zu tun haben und im anderen Fall mit einem Datensatz, der bereits Daten enthält.

Um Redundanz zu vermeiden, schreibt man EINE EINZIGE FUNKTION für das Bearbeiten des Datensatzes, nur wenn er neu ist, wird er zuvor mit leeren Daten übergeben, ist er nicht neu, übergeben wir ihn mit den vorhandenen Daten.

Man braucht ja zum Dialog eine Maske mit den Feldbezeichnern, damit der Anwender überhaupt weiß, was er eingeben soll, und die Feldbezeichner stehen natürlich nicht da, wo die Werte eingelesen werden, sondern links davon. Je mehr Kopien man von diesen bildschirmbezogenen I/O Funktionen in Betrieb hält, umso komplizierter wird es bei Programmänderungen, da rutscht dann das Eingabefeld für Betrag irrtümlich hinter Datum, oder die Spalte stimmt nicht und die Werte von Ein- und Ausgabe überschreiben sich und es gibt Mist. In jedem Fall ist das eine Menge Tippen und Suchen, daher sollte man das streng formalisieren.

Um diese Koordination zwischen Maske und Eingabefeldern in den Griff zu bekommen, empfiehlt es sich, einen übergeordneten Datentyp für die Koordinaten des Bildschirms vorzuhalten, der die Angaben enthält, an welcher Stelle auf dem Bildschirm die Daten aus- und eingegeben werden, und die beteiligten Funktionen holen sich die Daten von da. Ganz verboten ist es, solche Masken mit festen Zahlenwerten aufzubauen (siehe oben).

Ich gehe für den Prototypen erstmal, um eine Zahl zu nennen, von 10 Eingabefeldern aus. Dann müssen wir die Buchungsmaske mit 10 Feldbezeichnern einblenden, und den Cursor für die Eingaben dahinter platzieren.

Damit der Anwender zwischen den Eingabefeldern hinauf- und hinunterwechseln kann, ohne in der oben beschriebenen Schlange festzustecken, formulieren wir die Eingaberoutine ganz abstrakt, hier als Beispiel die Hauptfunktion der FIBU, Buchungen erfassen. Die nennen wir mal Buchung und aus dem vorgesagten ergibt sich ungefähr folgende Struktur (s.u.).

Vorher will ich aber noch die Bildschirmumgebung vorstellen, what you see is what you get, damit man eine grafische Orientierung dafür hat.

Das Hauptprogramm besteht aus zwei Zeilen (ja, ehrlich):

int main(int argc, char *argv[])
{

init(); // Initialisierung, Programmvorlauf

WAIT;

showSCR(SKR3,0);

WAIT

return 1;
}



Die Funktion init() initialisiert auch den Bildschirm, nämlich mit den 4 sichtbaren Bereichen:

clrSCR(MENU);
clrSCR(DIAL);
clrSCR(MTLG);
clrSCR(MBOX);

Das sind die 4 Bildschirmbereiche, die wir beim Programmstart sehen wollen. Sie sind noch ohne Inhalt. Natürlich können wir das auch anders komponieren, erstmal ist es so. Wie das aussieht, zeigt ABB 1

Die Funktion WAIT stoppt das PRogramm hier, es wird ein Tastendruck erwartet. WAIT gibt es in C nicht, da sie keine geschweiften Klammern hat wie wait(), scheint es auch keine Funktion zu sein, man könnte rätsen. Lösung: es ist ein Makro, ist als Präprozessor-Direktive abgelegt, nämlich so:

#define WAIT {char c;c=getch();}


Wenn wir dann die Taste drücken, wird die nächste Zeile des Hauptprogramms aktiv.

Dazu muß ich sagen: da man in der FIBU den SKR03-Kontenrahmen ja jederzeit verfügbar haben muß, um den nicht über die anderen Dialogfenster jedesmal einladen zu müssen, wurde ein neuer Bildschirmbereich definiert namens SKR3. Bei der Initialisierung wird der Kontenrahmen da reinkopiert:

for (i=0;i<MAXKONTEN;i++)
{
if (skr03[i].konto_nr>0) // Datensatz belegt?
{
sprintf(buffer,"%4i %s",skr03[i].konto_nr,skr03[i].konto_bez);
my_puts(SKR3,buffer,0,i);
}
}


So daß er als zusätzlicher Bildschirmbereich während des Programms jederzeit zur Verfügung steht.

Entweder als showSCR(SKR3), d. i. ohne Scrolling, oder als scroll(SKR3), dann mit Scrolling. Als Bereich wurde der rechte Fensterrand gewählt, er überblendet das MTLG-Fenster für Mitteilungen.

ABB 2 zeigt das.

Die eigentliche Buchung findet in dem Dialogfenster links statt. Damit wollen wir uns jetzt mal beschäftigen.

PROTOTYP für den Anwenderdialog BUCHUNG ERFASSEN:

void buchung()
{
int feld=0;
char c;
do
{
clrSCR(DIAL);
buchmaske();
gotoSCR(DIAL); // SCreen wählen
gotoxy(DIALX+30,DIALY+feld*2);
printf("<---");

c=getch();
switch ((int)c)
{
case ENTER:
if (feld<MAXFELDER) feld++;
break;
case PFEILOBEN:
if (feld>0) feld--;
break;
case ESCAPE:
return;
break;
case F1:
scroll(SKR3);
break;
}

} while (1);
}



Es ist also völlig abstrahiert. Anstelle zu sagen: wir gehen jetzt zu Feld 1 und machen das, danach zu Feld 2 und machen was anderes ... Wird hier nur abgefragt, wie der Anwender die Eingabe der Felder abschließt.

Fall 1: schließt er mit ENTER ab, ist er zufrieden, das nächste Feld wird aufgerufen

Fall 2: drückt er ESCAPE; will er mittendrin abbrechen

Fall 3: drückt er PFEILOBEN, will er ein Feld zurück und dort was ändern

Fall 4: drückt er die Funktionstaste F1, will er im SKR03 Kontenrahmen scrollen.

Es fehlt noch der anschließende Block, was im Falle von Feld=0, Feld=1, Feld=n überhaupt zu tun ist. Was da zu tun ist, ist aber eigentlich klar: Der Funktionsaufruf, der für das Feld vorgesehen ist. Also Feld 0 könnte sein: hole_datum(), Feld 3 könnte sein: hole_betrag usw.

Wir bleiben aber erstmal auf der formalen Schiene.

Frage:

Wenn die aufgerufene Funktion nur die Taste zurückliefert, mit der der Anwender die EIngabe abgeschlossen hat, wo ist denn dann die Eingabe, also der Wert, den er eingetippt hat, geblieben?

Wir würden ja normalerweise davon ausgehen, daß die Funktion nicht den Tastendruck der letzten Eingabe, sondern den Wert zurückliefert, den sie liefern soll.

C-Funktionen liefern immer nur 1 Wert zurück. Wir können nicht sagen: liefere den Wert der Eingabe und den Wert für die Taste.

Also? wink.gif

Nun, Hinweis auf die vielleicht etwas unfaire Frage: der Wert wird CALL BY REFERENCE übergeben, nicht als Rückgabewert der Funktion. Mehr dazu später.

Erstmal Blick auf den Bildschirm (Abb 3).

Da sind so die Anfänge einer Bildschirmmaske erkennbar, und der Cursor, hier dargestellt mit <---, ist in der Ausgangspositiong, Feld 1, Datum.

Jetzt geben wir mal 3 Felder ein und merken, wenn wir auf dem 4. Feld stehen (Abb 4), Betrag Brutto, daß wir in Feld 3 eine falsche Eingabe gemacht haben, Gegenkonto ist falsch. Was nun?

Worauf ich hinauswill: man darf dem Anwender nicht zumuten, daß er mit dem falschen Gegenkonto vor Augen alle Felder abgrast bis zum Ende und die dann alle erneut durchlaufen muß, nur um ein Feld zu ändern. Er soll und MUSS die Möglichkeit haben, dies direkt zu korrigieren. Dazu muß er ein Feld zurück.

Programmiert man von oben nach unten, geht das nicht.

Bei der sehr formalen Programmierung dieses Prototypen ist es aber kein Problem. Der Anwender drückt PFEILOBEN und befindet sich damit in dem Feld darüber.

Das ist in Abb. 5 dargestellt.

Nun wollen wir den Fall mal ausprobieren, daß der Anwender das richtige Konto nachschlagen will. Dazu muß er nach rechts rüber in das Fenster SKR3, und dazu muß er scrollen.

Diese Funktion wird bei unserem primitiven Prototypen mit F1 ausgelöst, Funktionstaste 1.

Drücken wir mal F1 und sehen, was geschieht.

Das sieht man in Abb 6. Der Bereich hat gewechselt auf das rechte Fenster, und man sieht, dort wurde gescrollt, denn der Inhalt des rechten Fensters hat sich verändert.

Die Scroll-Funktion hat als Ausgang ESCAPE, geht der Anwender mit ESCAPE raus, ist er wieder dort, wo er in dem Buchungsmenü rübergewechselt ist, und kann die Buchung beenden.

Soviel für heute abend, Fortsetzung folgt demnächst.
Angehängte Datei(en)
Angehängte Datei  buchung.jpg ( 61.56KB ) Anzahl der Downloads: 6
Angehängte Datei  buchung2.jpg ( 182.21KB ) Anzahl der Downloads: 9
Angehängte Datei  buchung3.jpg ( 194.08KB ) Anzahl der Downloads: 6
Angehängte Datei  buchung4.jpg ( 193.85KB ) Anzahl der Downloads: 8
Angehängte Datei  buchung5.jpg ( 194.69KB ) Anzahl der Downloads: 5
Angehängte Datei  buchung6.jpg ( 212.79KB ) Anzahl der Downloads: 7
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 09.02.2012, 22:43 Uhr
sharky2014
Level 7 = Community-Professor
*******

Eine Eingabemaske erstellen

Das ist so die ZU-FUSS-Programmierung. Ich liebe das.

Problem ist, daß man die Daten für einen Datensatz von der Konsole einsammeln will, und den Anwender darüber informieren muß, was er überhaupt eingeben soll. Dazu muß vor dem Eingabefeld ein Bezeichner stehen, wie z. B. Name, Vorname, Geburtsdatum etc, und rechts davon kommt die eigentliche KonsolenEINGABE, welche den gewünschten Wert holt.

Da der Anwender in der Eingabe herumswitchen (können soll), ändert sich ständig der Bildschirminhalt. Es sind alte Inhalte, die wieder mit neuen überschrieben werden, oder bei Neueingabe hängen die alten Werte noch auf dem Bildschirm herum, was unschön und verwirrend ist.

Die Lösung ist natürlch, mit der

Schleife (solange noch bearbeitet wird ...

-> Lösche Bildschirm

-> gib die Maske aus

-> gib den Datensatz aus, so wie er im MOment ist

Wem das zuviel auf dem BIldschirm flackert, der muß noch einen Schritt drunter und die Datenfelder mit Leerstrings überschreiben.

Alles machbar, alles kein PRoblem.

Nur die Koordinaten, die müssen aufeinander abgestimmt sein, und ganz unmöglich wäre es, da exakte Zahlen reinzusetzen. Das kriegt man nachher niemals mehr editiert.

Für dieses Low-Level Problem, daß man im Eingabebereich des Bildschirms die Werte aufeianander abgestimmt kriegt, muß man INDIZIEREN können, und um indizieren zu können, benötigt man einen indizierten Speicherbereich. Hier ist er:

struct BM_data{
int screen;
int fs; // Spalte u Zeile für Feldbezeichner
int fz;
int es; // spalte u Zeile für Eingabefeld
int ez;
char feldbez[255];
char feldleer[255]; // Leerstring zum überschreiben des Eingabefeldes, nicht des Bezeichners
};

struct BM_data bmdata[MAXBMFELDER];



Wir sehen hier eine Struktur, welche die x/y Positionen für die Bezeichner enthält, sowie die x/y Positionen, bei denen die Funktionen später ihre Eingabefelder öffnen sollen. Dazu den Text des Feldbezeichners und Platz für einen passenden Leerstring, um bei Bedarf die alte EIngabe am Bildschirm zu löschen.

Wenn die Daten initialisiert sind, kann die Funktion:

Maske darauf zugreifen, um die Bezeichner an die linke Seite der Zeile zu setzen,

und die Eingabefunktionen holen sich ihre Koordinaten ebenfalls aus dieser Struktur.

Man kann dann die EIngabefelder irgendwo beliebig am BIldschirm platzieren, untereinander, nebeneinander, versetzt oder was auch immer, die Funktionen erhalten immer die richtigen Koordinaten, um zu kommunizieren.

Die Initialisierung könnte so aussehen (bmdata = Daten für die BIldschirmmaske):

void init_bmdata()
{
int i;
int len;
sprintf(bmdata[0].feldbez,"%s","Datum");
sprintf(bmdata[1].feldbez,"%s","Konto -> Zugang +");
sprintf(bmdata[2].feldbez,"%s","Gegenkonto -> Abgang -");
sprintf(bmdata[3].feldbez,"%s","Betrag brutto");
sprintf(bmdata[4].feldbez,"%s","Kommentar");
sprintf(bmdata[5].feldbez,"%s","Umsatzsteuer-Option");
sprintf(bmdata[6].feldbez,"%s","Betrag netto");
sprintf(bmdata[7].feldbez,"%s","Enthaltene USt");
sprintf(bmdata[8].feldbez,"%s","Umsatzsteuer-Konto");
sprintf(bmdata[9].feldbez,"%s","Buchungssatz-Identnr.");

for (i=0;i<MAXBMFELDER;i++)
{

sprintf(bmdata[i].feldleer,"");
bmdata[i].screen=DIAL;
bmdata[i].fs=1;
bmdata[i].fz=1+2*i;
bmdata[i].es=50;
bmdata[i].ez=1+2*i;
}
}

Was man also ausklammern kann mit for i=0 bis i=für alle klammert man aus, der Rest muß händisch formuliert werden. Der Bezeichner feldleer, um die Eingaben zu überschreiben, kann noch nicht bestimmt werden, solange die Länge der Eingabefelder fehlt.

Der ganze Aufwand hier ist nicht so groß wie man denkt, alles muß ja nur einmal bei PRogrammstart erfolgen, steht anschließend kostenfrei zur Verfügung. Es ist immer komfortabler, im Anfang ganz viele Bezeichner und Daten in das Programm zu pumpen, auf die man im Betrieb mit einer einzigen Zeile zugreifen kann, als wenn man sich ständig damit herumschlägt, wild verstreute Variablen und Begriffe zusammenzusuchen, zu sortieren etc.

Grundgedanke muß sein, daß man jede Art von Änderung an einem einzigen Ort durchführt. Wenn man das will, muß man im Anfang eben stark formalisieren bis hin zum GÄHN-Effekt, das ist doch keine Programmierung, sondern Erbsenzählerei. Hier gilt: wer zuletzt lacht, lacht am besten. wink.gif

Mit dem bißchen Definition haben wir sichergestellt, daß Bezeichner und Eingabefelder anhand des Index immer harmonieren, egal, wohin wir sie anschließend verschieben wollen.

Hauptprogramm sieht jetzt so aus:

int main(int argc, char *argv[])
{
init(); // Initialisierung, Programmvorlauf
buchung();

WAIT;
return 1;
}



Und liefert uns den primitiven Prototyp für die Buchung, also die Erfassung der Datensätze, als BIldschirm-Dummy (Abb. buchung7)

Der Prototyp kann bisher die Eingabefelder von oben nach unten mit ENTER abklappern, mit PFEILOBEN wieder ein Eingabefeld zurücklaufen (oder alle), und mit F1 zwischen Eingabe und SKR03-Scrollbildschirm wechseln.

Das ist nicht besonders aufregend (GÄHN).

Der Witz liegt in der Strukturierung. Daß wir nicht von oben nach unten programmieren, wo der Anwender alles nacheinander abzuarbeiten gewzungen ist, sondern die Programmierung offen gestalten, so daß der Anwender sich nach seinen Wünschen in verfügbaren Funktionen bewegen kann, ohne daß ihm das PRogramm das vorschreibt.

Das muß im Anfang geklärt sein, nachträgliche tiefgreifende Korrekturen an falsch geschriebenen Programmen kann man abhaken, ist wie neu schreiben.

Zu der Konsolenführung mit ESCAPE

ESCAPE soll ermöglichen, daß man von JEDEM PRogrammunterabschnitt zum nächsthöheren springt und von daher bis zum PRogrammstart.

Damit man das realisieren kann, muß JEDE Funktion den Tastenwert zurückliefern, nämlich ESCAPE, und die aufrufende muß den verarbeiten, sonst geschieht ja nichts.

Folgende Reihe von Funktionen:

Hauptprogramm
Funktion Buchung
Funktion Buchung finden
Funktion BUchung editieren
Funktion bearbeite Datum
Funktion bearbeite Konto-Nr
...
Funktion bearbeite sonstwas ...

Will man ESCAPE als rote Linie im Programm etablieren, damit der Anwender, wenn er sich nur was ansehen will, nicht gezwungen ist, da eine tatsächliche Eingabe zu machen, also "leichtfüßig" durch die Menus springen können soll, nur um zu sehen, was das PRogramm da anbietet, , dann müssen alle diese Funktionen die Escape-Taste nach hinten wieder durchstecken, und die aufrufenden Funktionen müssen diesen Rückgabewert verarbeiten, sonst klemmt das.

Anders gesagt, wir benötigen 2 Rückgabewerte: das, was die Funktion eigentlich ermitteln soll, den Wert, der aus dem Dialog mit dem Anwender erfolgt, und den Tastaturwert der letzten EIngabe.

Um mehr als einen Wert zurückzuliefern, reicht der Funktions-Rückgabewert BY VALUE nicht, sondern wir müssen zusätzlich BY REFERENCE zurückliefern (oder ausschließlich, wenn die Funktion VOID ist.) In C ist ja nicht vorgeschreiben, daß BY VALUE überhaupt irgendetwas zurückgeliefert werden muß.

BY REFERENCE hat den Vorteil, daß jede Art und Zahl von Daten und Datentypen fix und fertig bearbeitet werden können. Den Nachteil, daß eine möglicherweise fehlerhafte Funktion wild in den Speicher hineinschreibt und das PRogramm instabil wird.

Nun, als C-Programmierer lebt man eigentlich nicht von Stabilitäten, sondern von INSTABILITÄTEN, sonst hätte man nichts zu tun. wink.gif

Ich halte CALL BY REFERENCE für die eigentliche, die natürliche Methode, einen Wert zurückzuliefern. Insbesondere da man da ja auch kapselt, also keine Daten GLOBAL machen muß. Alles geht im geheimen vor sich, und was die Funktion da bearbeitet, ist eigentlich klar, weil wir dafür eine Adresse übergeben müssen.

Entweder setzen wir einen Pointer drauf und übergeben den oder wir liefern den Adreßwert für die Speichervariable bzw. -Struktur.

Das heißt, wir wissen, was wir tun.

Also nur keine Panik.

Wenn wir sagen:

int macheirgendetwas( int vorgabe ...
int wert;
...
return wert;

Haben wir als Rückgabe eine Ganzzahl.

Wenn wir die letzte Eingabe des Anwenders als int haben wollen, und die Funktion sagen wir mal einen ganzen Adreßsatz erfassen und zurückliefern soll, sagen wir:

int erfasse_komplette_adresse (&adresssatz ... // & Adreß-Operator, damit die Adreßdaten direkt in den Datensatz geschrieben werden können
char taste

...
return (int) taste;

Dann kann die Funktion die ganze Adresse überschreiben, und liefert die Taste zurück.

Genau das will ich.

Der Zugriff auf einen Datensatz Adresse ist ja nicht kritisch. Steht da nach der Rückgabe Mist drin, kann man ihn ja ausnullen oder zur Weiterbearbeitung nochmal zurückliefern.

Jedenfalls, also,

solche grundsätzlichen Fragen, nach dem DESIGN und der STRUKTURIERUNG des PRogramms müssen am Anfang geklärt werden. BEDIENERFÜHRUNG ist ein wesentliches Merkmal, was man sich während der PRogrammierung nicht dauernd anders überlegen kann, daher der Hinweis auf die ESCAPE-Taste.

Man kann da besser am Anfang etwas herumspielen und testen und ausprobieren, als an einem späteren Programmabschnitt nachträglich was ändern.

Der Beitrag wurde von sharky bearbeitet: 09.02.2012, 22:53 Uhr
Angehängte Datei(en)
Angehängte Datei  buchung7.jpg ( 244.71KB ) Anzahl der Downloads: 8
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 10.02.2012, 16:23 Uhr
sharky2014
Level 7 = Community-Professor
*******

Horizontale vs. vertikale Strukturierung

Erstmal Screencopy: der Bildschirm belebt sich ein wenig.

Abb 1: Menufenster in Betrieb, Im Dialogfenster steht der Cursor auf Konto-Nr, und von wollen wir rechts in den Bereich mit dem Kontenrahmen.

Menu zeigt, das geht mit F1, also machen wir das mal.

Abb2

Jetzt befinden wir uns im Scroll-Modus im rechten Bereich, erkennbar daran, daß unten rechts die MBOX aktiv wird, die uns mitteilt, wie wir uns im SKR03 zu bewegen haben und wie wir abschließen, mit ESCAPE, um wieder in den Dialog zurückzukommen.

Wenn wir das tun Abb3

sehen wir, daß die MBOX unten rechts geleert wurde, der Cursor steht wieder auf dem Eingabefeld für Konto-Nr.. Und man sieht, rechts sieht das anders aus als zuvor, es wurde gescrollt. Diesen Scrollzustand behält das Fenster während des ganzen Programmlaufs wegen der STATIC-Variablen für den ersten Datensatz bis zum nächsten Aufruf bei. Das ist wichtig, damit man bei Überblendungen den original-Bildschirmzustand vor der Überblendung jederzeit wiederherstellen kann.

Soweit an der Oberfläche nicht viel neues, aber im Inneren wurde das Programm einer Roßkur unterzogen. Thema vertikale und horizontale Strukturierung. Dieses Flickenteppich-Muster, alle Definitionen auf einer Ebene, läßt sich schnell realisieren und wenn es gewollt ist, kann man Funktionen durch Copy/PAste/Ersettzen klonen, was einen schnellen Erfolg bringt. Das Programm läuft. Wenn wir so strukturieren, ist die Folge, daß wir bei übergeordneten Funktionen ständig und zunehmend mit if/elsif und switch verzweigen müssen, nach dem Schema:

Übergeordnete Funktion, Dialog mit dem Anwender:

// Block von if/if else bzw. switch Verzweigern
// kann sehr lang sein
// viel Code

--> erfasse die Eingabe

// ZWeiter Block von Verzweigern, damit nach der Datenbearbeitung das geschieht, was verlangt wurde
// kann sehr lang sein
// viel code
// vieles davon ist schlicht redundant.

Ein Beispiel dafür sehen wir hier. Der Code arbeitet völlig korrekt, es läuft alles bestens:

Beispiel: Code vor der Roßkur:

int my_puts(int screen,char text[255],int xpos, int zeile)
{

char buffer[255]="";
int s;
int max_sp;
int max_z;
int textlen_korrigiert;
int textlen=strlen(text);

if (screen==DIAL) { max_sp=DIALS ; max_z=DIALBUFLEN ; }
else if (screen==MTLG) { max_sp=MTLGS ; max_z=MTLGBUFLEN ; }
else if (screen==MENU) { max_sp=MENUS ; max_z=MENUBUFLEN ; }
else if (screen==MBOX) { max_sp=MBOXS ; max_z=MBOXBUFLEN ; }
else if (screen==SKR3) { max_sp=SKR3S ; max_z=SKR3BUFLEN ; }
else if (screen==OLWI) { max_sp=OLWIS ; max_z=OLWIBUFLEN ; }
else return NOTOK;

if (zeile>max_z)return NOTOK; // Bereichsüberschreitung Zeile
for (s=0;s<max_sp-xpos;s++) if (s<textlen) buffer[s]=text[s]; // 2-Bedingungen Länge und verfügbarer Platz
textlen_korrigiert=strlen(buffer); // LÄnge des korrigierten Char-Arrays
buffer[textlen_korrigiert]=EOS; // Stringende-Markierung unbedingt reinsetzen nach low-level PRogrammierung

switch (screen)
{
case DIAL:
for (s=0;s<textlen_korrigiert;s++) DIALbuf[zeile][xpos+s]=buffer[s];DIALbuf[zeile][max_sp]=EOS;
break;
case MTLG:
for (s=0;s<textlen_korrigiert;s++) MTLGbuf[zeile][xpos+s]=buffer[s];MTLGbuf[zeile][max_sp]=EOS;
break;
case SKR3:
for (s=0;s<textlen_korrigiert;s++) SKR3buf[zeile][xpos+s]=buffer[s];SKR3buf[zeile][max_sp]=EOS;
break;
case MENU:
for (s=0;s<textlen_korrigiert;s++) MENUbuf[zeile][xpos+s]=buffer[s];MENUbuf[zeile][max_sp]=EOS;
break;
case MBOX:
for (s=0;s<textlen_korrigiert;s++) MBOXbuf[zeile][xpos+s]=buffer[s];MBOXbuf[zeile][max_sp]=EOS;
break;
case OLWI:
for (s=0;s<textlen_korrigiert;s++) OLWIbuf[zeile][xpos+s]=buffer[s];OLWIbuf[zeile][max_sp]=EOS;
break;
}
return OK;
}


Hier mal das Beispiel DERSELBE CODE, leistet EXAKT DASSELBE, nach der ROSSKUR:

int my_puts(int screen_nr,char text[255],int xpos, int zeile)
{
char buffer[255]="";
int s, max_sp,max_z,textlen_korrigiert;
int textlen=strlen(text);
max_z=screen[screen_nr].bufzeilen;
max_sp=screen[screen_nr].breite;
if (zeile>screen[screen_nr].bufzeilen) return NOTOK; // Bereichsüberschreitung Buffer-LEn
for (s=0;s<screen[screen_nr].breite;s++) if (s<textlen) buffer[s]=text[s]; // 2-Bedingungen Länge und verfügbarer Platz

textlen_korrigiert=strlen(buffer); // LÄnge des korrigierten Char-Arrays , der buffer ist jetzt "passend"
buffer[textlen_korrigiert]=EOS; // Stringende-Markierung unbedingt reinsetzen nach low-level PRogrammierung

for (s=0;s<textlen_korrigiert;s++) screen[screen_nr].screenbuffer[zeile][xpos+s]=buffer[s];// buffer reinkopieren
screen[screen_nr].screenbuffer[zeile][max_sp]=EOS;// und auch dort Hosenträger mit Gürtel
return OK;
}



Man möge das mal auf sich einwirken lassen!

Was noch hinzuzufügen wäre: Würde man die Bildschirmbereiche von 5-6 auf 25 erhöhen, würde sich die erste Version im Code vervierfachen, während die zweite Version nicht eine Code-Zeile länger würde.

Und das gilt für alle Funktionen (bisher ein rundes Dutzend, die sich mit der Datenstruktur befassen.

Durch die ROSSKUR wurde der Code schlicht auf ca. 1/3 der ursprünglichen Größe komprimiert.

Der Beitrag wurde von sharky bearbeitet: 10.02.2012, 16:30 Uhr
Angehängte Datei(en)
Angehängte Datei  strukturierung1.jpg ( 215.93KB ) Anzahl der Downloads: 8
Angehängte Datei  strukturierung2.jpg ( 236.95KB ) Anzahl der Downloads: 4
Angehängte Datei  strukturierung3.jpg ( 218.2KB ) Anzahl der Downloads: 5
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 10.02.2012, 16:45 Uhr
sharky2014
Level 7 = Community-Professor
*******

Der Grund für das Wunder der Komprimierung auf 1/3 (man könnte das noch weiter treiben) ist die Strukturierung aller Daten in vertikalen Strukturen, also übergeordneten Datensätzen, welche die Menge der Daten als Felder enthalten und über einen Index angesprochen werden.

Aus:

Name1, Name2, Name3, Name 4... Name n

mit der Folge: // Verzweigerblock am Kopf der FUnktione

switch(person)
case Name1:
...
break;
case Name 2:
...
break;

...

case Name n
...
break;

bearbeite die Daten und verzweige für die Bearbeitung nochmal

// VErzweigerblock am Fuß der Funktion

wird schlicht und einfach:

erfasse (Name[i]);

bearbeite die Daten für (Name[i]),


Man erreicht das, diese Kürze des Codes, der nebenbei auch viel übersichtlicher und stabiler ist und leichter zu korrigieren, wenn man alle Daten, die sich irgendwie logisch unter einer Struktur zusammenfassen lassen, im Programmvorlauf als Elemente einer übergeordneten Struktur definiert und initialisiert. Das ist dann zwar im Anfang ein bißchen Aufwand, aber während des PRogramms zahlt sich das 100fach aus. Die Deklaration etwa zu den Bildschirmbereichen, weshalb nun nicht länger endlos verzweigt werden muß, sieht einmal und damit hat es sich dann aber so aus:

struct s_screen{
int bez;
int olx;
int oly;
int breite;
int hoehe;
char screenbuffer[MAXBUFZEILEN][MAXBUFBREITE];
int bufzeilen;
int scroll;
int textfarbe;
int hintergrundfarbe;
char leerstring[KON_BREITE];
};

enum screens{MENU, DIAL, D120, MTLG, MBOX, SKR3, OLWI, LASTSCREEN};

struct s_screen screen[LASTSCREEN+1];

void init_screens()
{
screen[0].bez=MENU;
screen[0].olx=MENUX;
screen[0].oly=MENUY;
screen[0].breite=MENUS;
screen[0].hoehe=MENUZ;
screen[0].bufzeilen=MENUZ;
screen[0].scroll=NEIN;
screen[0].textfarbe=WHITE;
screen[0].hintergrundfarbe=DARKGREY;
strncpy(screen[0].leerstring,leer255,screen[0].breite); screen[0].leerstring[screen[0].breite]=EOS;


screen[1].bez=DIAL;
screen[1].olx=DIALX;
screen[1].oly=DIALY;
screen[1].breite=DIALS;
screen[1].hoehe=DIALZ;
screen[1].bufzeilen=DIALZ;
screen[1].scroll=NEIN;
screen[1].textfarbe=BLACK;
screen[1].hintergrundfarbe=WHITE;
strncpy(screen[1].leerstring,leer255,screen[1].breite); screen[1].leerstring[screen[1].breite]=EOS;

screen[2].bez=D120;
screen[2].olx=D120X;
screen[2].oly=D120Y;


Die logische Struktur screens = die verschienden Bildschirmbereiche also mit allen nötigen Daten zusammengefaßt und initialisiert.

Der Index ist im enum-Typen vorbereitett:

enum screens{MENU, DIAL, D120, MTLG, MBOX, SKR3, OLWI, LASTSCREEN};


und mit der Definition des Indextypen screen

struct s_screen screen[LASTSCREEN+1];


Haben wir alle screens, egal wieviele, egal ob 5 oder 6 oder 250, in dieser Indexvariablen summiert.

Die Funktionen werden dann formaler, aber die Länge ändert sich nicht mehr.

Während wir, wenn wir mit Verzweigungen arbeiten, den Code ständig aufblähen.

Wenn wir 5 Bezeichner zu unterscheiden haben, müssen wir fünfmal if schreiben, Mit dem Indizierten Typ macht es immer nur eine Zeile, auch wenn der Indexwert auf 20 oder 50 zunimmt.

Die Notation wird allerdings ziemlich komplex, bzw. etwas unhandlich zu tippen, weil wir Felder von Indizierten Typen wieder als Indices ansprechen müssen, aber ich denke, das Codebeispiel im vorherigen Beitrag spricht für sich.

So forste ich das PRogramm also vor der Weiterentwicklung auf solche Strukturverbesserungen hin durch. Damit man vermeidet, mit rein ergebnisorientierter Programmierung ein strukturelles Chaos anzurichten.


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 10.02.2012, 17:28 Uhr
sharky2014
Level 7 = Community-Professor
*******

Mit der Bearbeitung der Bildschirmbereiche (die ja virtuell ist), befassen sich gut ein Dutzend Funktionen. ShowSCR, clrSCR, SCROLL, und so weiter, wir müssen ja die Textspeicher ebenso bearbeiten wie das, was sichtbar am BIldschirm wird.

Und bei allen muß bei horizontaler Namensgebung immer wieder verzweigt werden, entweder einmal, oder am Fuß- und am Kopfende. Und je mehr Bildschirmbereiche, umso aufgeblähter wird das.

Hier mal ein anderes Beispiel, was vielelcht noch besser klar macht, was man mit guter Strukturierung erreichen kann. Die Funktion clsSCR(screen) löscht den ausgewählten Bildschirmbereich, indem sie Leerzeichen drüberschreibt. Sieht so aus:

void clrSCR(int screen)
{
if (screen<0||screen>LASTSCREEN) return;
SCRfarbe(screen);
int z;
if (screen==LASTSCREEN) system("cls");
else if(screen==MENU){for (z=0;z<MENUZ;z++){gotoxy(MENUX,z+MENUY);puts(MENUblank);}}
else if(screen==DIAL){for (z=0;z<DIALZ;z++){gotoxy(DIALX,z+DIALY);puts(DIALblank);}}
else if(screen==MTLG){for (z=0;z<MTLGZ;z++){gotoxy(MTLGX,z+MTLGY);puts(MTLGblank);}}
else if(screen==SKR3){for (z=0;z<SKR3Z;z++){gotoxy(SKR3X,z+SKR3Y);puts(SKR3blank);}}
else if(screen==MBOX){for (z=0;z<MBOXZ;z++){gotoxy(MBOXX,z+MBOXY);puts(MBOXblank);}}
else if(screen==OLWI){for (z=0;z<OLWIZ;z++){gotoxy(OLWIX,z+OLWIY);puts(OLWIblank);}}
gotoSCR(screen); // Cursor nach oben links
}



Jetzt dieselbe Funktion nach der Umstrukturierung und Indizierung der Bildschirmbereiche screens:

void clrSCR(int screen_nr)
{
if (screen_nr<0||screen_nr>LASTSCREEN) return;
SCRfarbe(screen_nr);
int z;
if (screen_nr==LASTSCREEN) {system("cls");return;}
for (z=0;z<screen[screen_nr].hoehe;z++){gotoxy(screen[screen_nr].olx,screen[screen_nr].o
ly + z);puts(screen[screen_nr].leerstring); }
gotoSCR(screen_nr); // Cursor nach oben links
}


Im ersten Falle haben wir im Anweisungsblock 7 if/else if Verzweiger, im zweiten KEIN EINZIGES!

Hätten wir 25 Bildschirmbereiche, würden es 25 Verzweiger werden, im zweiten Falle würde kein einziges hinzukommen.

Das ist der Vorteil der Indizierung.

Oder der hier, der ist auch gut:

vorher:

void gotoSCR(int screen)
{
if (screen<0||screen>LASTSCREEN-1) return;
SCRfarbe(screen);
if (screen==MENU) gotoxy(MENUX,MENUY);
else if (screen==DIAL)gotoxy(DIALX,DIALY);
else if (screen==MTLG)gotoxy(MTLGX,MTLGY);
else if (screen==MBOX)gotoxy(MBOXX,MBOXY);
else if (screen==SKR3)gotoxy(SKR3X,SKR3Y);
else if (screen==OLWI)gotoxy(OLWIX,OLWIY);
}
nachher:

void gotoSCR(int screen_nr)
{
if (screen_nr<0||screen_nr>LASTSCREEN-1) return;
SCRfarbe(screen_nr);
gotoxy(screen[screen_nr].olx,screen[screen_nr].oly);
}

Hier wurde der gesamte Anweisungsblock für 6 Fensterbereiche (bei 20 Fensterbereichen wären es 20 !)

if (screen==MENU) gotoxy(MENUX,MENUY);
else if (screen==DIAL)gotoxy(DIALX,DIALY);
else if (screen==MTLG)gotoxy(MTLGX,MTLGY);
else if (screen==MBOX)gotoxy(MBOXX,MBOXY);
else if (screen==SKR3)gotoxy(SKR3X,SKR3Y);
else if (screen==OLWI)gotoxy(OLWIX,OLWIY);


durch eine einzige Zeile ersetzt, die so wie sie ist eine unbegrenzte Zahl von Fensterbereichen verwalten könnte:

gotoxy(screen[screen_nr].olx,screen[screen_nr].oly);



Mehr muß man wohl dazu nicht sagen. wink.gif

Der Beitrag wurde von sharky bearbeitet: 10.02.2012, 17:31 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 10.02.2012, 18:34 Uhr
sharky2014
Level 7 = Community-Professor
*******

Die Konsolen-Schnittstelle mit dem Anwender ist getch(), niemals eine Standardfunktion aus dem string.h Baukasten !

Um mal zu sehen, was scanf() für einen Unsinn anstellt:

int main(int argc, char *argv[])
{
char text[8];
do
{
scanf("%8s",&text); printf("%s",text);
}while (1);
system("PAUSE");
return 0;
}




Wir haben einen Buffer, mit 8 Zeichen für Texteingabe, und bemühen das Programm, daß es uns 8 Zeichen Text einliest. Das sieht dann in der Realität so aus:

sdafffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffffffffffffffffffffffffsssssssssssssssssssssssssssssssssssssssss
sssssssss44444444444444444444444444444444444444444444444444444444444444444444444
4444444444444444444444444444444


Der Buffer hat 8 Zeichen Speicherplatz, wo bleiben die übrigen Zeichen, die der Anwender da reingepinnt hat? wink.gif

Steht da nicht "%8s"? Tja, das steht da. Nur das bringt NIX.

Man beachte den Operator &

Das ist der Adreßoperator!

Die Funktion schreibt entweder direkt oder nach Überlauf des Tastaturpuffers in den Speicher, Programmabschuß. wink.gif

Um so einen Mist zu vermeiden, bleibt nur eine einzige Funktion, die sicher ist: getch().

Liest ein Zeichen von der Tastatur.

Nun hat die Tastatur ja nicht nur Tasten für druckbare Zeichen, sondern auch Sondertasten. Wie liest man das aus?

Das Programm:

#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
int main(int argc, char *argv[])
{
char c,c2;
do
{
c=32;
c2=(int) 0;
c=getch();
if ((int)c<=0)c2=getch();
printf("Zeichen = %c Ascii-Code 1 = %4i Ascii-Code 2= %4i \n",c,c,c2);
if ((int)c==27) break;
}while (1);
system("PAUSE");
return 0;
}


Ich gebe jetzt mal die Buchstaben a,b,c und so weiter ein. Mit der Ausgabe:

Zeichen = a Ascii-Code 1 = 97 Ascii-Code 2= 0
Zeichen = b Ascii-Code 1 = 98 Ascii-Code 2= 0
Zeichen = c Ascii-Code 1 = 99 Ascii-Code 2= 0
Zeichen = d Ascii-Code 1 = 100 Ascii-Code 2= 0
Zeichen = e Ascii-Code 1 = 101 Ascii-Code 2= 0
Zeichen = f Ascii-Code 1 = 102 Ascii-Code 2= 0
Zeichen = g Ascii-Code 1 = 103 Ascii-Code 2= 0
Zeichen = h Ascii-Code 1 = 104 Ascii-Code 2= 0
Zeichen = i Ascii-Code 1 = 105 Ascii-Code 2= 0


Wie sieht das aus mit Sondertasten?

Ich gebe jetzt die Funktionstasten F1-F5 ein, dann die Funktionstasten F11 und F12:

Zeichen = Ascii-Code 1 = 0 Ascii-Code 2= 59
Zeichen = Ascii-Code 1 = 0 Ascii-Code 2= 60
Zeichen = Ascii-Code 1 = 0 Ascii-Code 2= 61
Zeichen = Ascii-Code 1 = 0 Ascii-Code 2= 62
Zeichen = Ascii-Code 1 = 0 Ascii-Code 2= 63
Zeichen = Ó Ascii-Code 1 = -32 Ascii-Code 2= -123
Zeichen = Ó Ascii-Code 1 = -32 Ascii-Code 2= -122


Man bemerkt, c=getch() geht dann ins leere, und die zweite Anweisung:

if ((int)c<=0)c2=getch() wird erklärlich.

Es folgen:

Strg+C
Strg+V
Der Buchstabe 'ß'
ENTER

mit der Ausgabe:

Zeichen = ♥ Ascii-Code 1 = 3 Ascii-Code 2= 0
Zeichen = ▬ Ascii-Code 1 = 22 Ascii-Code 2= 0
Zeichen = ß Ascii-Code 1 = -31 Ascii-Code 2= -31
Ascii-Code 1 = 13 Ascii-Code 2= 0


Daß bei Sondertasten beide Werte gefüllt werden, liegt daran, daß die Tastatur bei SOndertasten zwei Werte übermittelt.

Verbiegen wir aber das Programm folgendermaßen:

if ((int)c<=0)c=getch();


ERhalten wir als Ausgabe für F1,F2,F3,F4,F5 und F11, F12 folgendes:

Zeichen = ; Ascii-Code 1 = 59 Ascii-Code 2= 0
Zeichen = < Ascii-Code 1 = 60 Ascii-Code 2= 0
Zeichen = = Ascii-Code 1 = 61 Ascii-Code 2= 0
Zeichen = > Ascii-Code 1 = 62 Ascii-Code 2= 0
Zeichen = ? Ascii-Code 1 = 63 Ascii-Code 2= 0
Zeichen = à Ascii-Code 1 = -123 Ascii-Code 2= 0
Zeichen = å Ascii-Code 1 = -122 Ascii-Code 2= 0


Also alles in Butter und klaro? NIcht wirklich.

Biegen wir das Programm mal wieder zurück auf

if ((int)c<=0)c2=getch();


und drücken zwei Tasten, nämlich die Funktionstaste F1 und das Semikolon.

Mit der Ausgabe:

Zeichen = Ascii-Code 1 = 0 Ascii-Code 2= 59
Zeichen = ; Ascii-Code 1 = 59 Ascii-Code 2= 0


Das ist nicht identisch, oder? wink.gif

Es kommt drauf an, was man braucht.

Wenn man es genau wissen will, muß man sich zu getch() eine kleine Funktion schreiben.

Der Beitrag wurde von sharky bearbeitet: 10.02.2012, 18:42 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.02.2012, 14:51 Uhr
sharky2014
Level 7 = Community-Professor
*******

Die Implementation der Dialogfunktionen

Der Bildschirm belebt sich zunehmend (Abb.1)

Es gibt ein weiteres Fenster (bzw. einen Bildschirmbereich), welcher die Navigationsdaten des aktuellen Menus erklärt. Das sieht oben im Menü-Feld etwas wild aus. Das kleine gelbe Fenster MBOX bleibt reserviert für Kurzmitteilungen und Abfragen a la Eingabe beenden (J/N)?

Wie schon gesagt, befinden wir uns bei der Dateneingabe nicht in einer Schlange, die von vorn bis hinten bzw. von oben nach unten durchlaufen werden muß, sondern mit den Pfeiltasten hat man wahlfreien Zugang zu dem gewünschten Datenfeld, ohne dass bis dahin eine Dateneingabe erfolgen MUSS, man kann einfach durchhuschen. Hat man das gewünschte Datenfeld erreicht, klinkt man sich mit ENTER ein und erfaßt die Daten.

Bislang ist nur verfügbar die Wahl des Buchungskontos.

Das Programm ist ja von vorn bis hinten so durchkonstruiert, daß bei der Tpyendefinition niemals eine Zahl vorkommt. Wir arbeiten mit Bezeichnern, die mit #define vorgegeben werden, welches keine Konstanten sind (!!!!!)

Der Unterschied ist folgender:

Bezeichne ich einen Wert mit

#define BIGSTRINGLEN 80
und definiere:

char info[BIGSTRINGLEN], so ist das gleichbedeutend mit char info[80]
Ändere ich dann #defeine BIGSTRINGLEN 60 im Wert, so habe ich den Datentyp

char info[BIGSTRINGLEN], was jetzt gleichbedeutend ist mit char info [60]

Es werden also keine Werte geändert, sondern wir erhalten dann durchgängig, da alle Datentypen so definiert sind, neue Datentypen, ohne daß wir eine Zeile Code am Programm ändern müßten. Man kann ähnliches irgendwie auch mit Konstanten bewerkstelligen, dieser Weg ist aber mühsam, fehleranfällig und hat noch andere Nachteile.

Betrachten wir jetzt mal die erste Dialog-Funktion, den Prototypen hole_kontonr()

Wenn wir die Konsole zeichenweise auslesen, ergibt sich ein typischer Funktionsaufbau, nämlich:

Endlosschleife

lies ein Zeichen

verarbeite das Zeichen

Lies die Steuertasten aus

lies das nächste Zeichen, beende die Funktion oder brich sie ab

Endlosschleife Ende


Wir haben also im Kopfteil der Funktion c=getch(), anschließend die Verzweiger, was damit zu tun ist.

Der Prototyp sieht dann so aus:


QUOTE
int hole_kontonr(int *konto_nr,int spalte, int zeile,int maxlen)
{
int zahl;
char c;
int pos=0;
char buffer[255]="0000";
do
{
gotoxy(spalte,zeile);printf("%4s",buffer);
gotoxy(spalte+pos,zeile);
c=getch();
{
buffer[pos]=c;
if (pos<maxlen-1)pos++;
}
switch ((int)c)
{
case BACKSPACE:
buffer[pos]='0';
if (pos>0)pos--;
buffer[pos]='0';
break;
case ENTER:
zahl=atoi(buffer);
*konto_nr=zahl;
return (int) c;
break;
case ESCAPE:
return (int) c;
break;
}//switch
}while (1);

return (int) c;
}


Wenn mir hier jemand sagen könnte, wie man den Editor dazu bringt, daß der nicht alles linksbündig setzt (sieht ja furchtbar aus), wäre ich sehr dankbar. Dieses Linksbündige zerstört ja den ganzen Code bis zur Unleserlichkeit. danke.gif

Ich erklär mal das wesentliche:

Wir wollen eine 4stellige Zahl für die KOnto-Nr. Die holen wir uns aber nicht als Zahleneingabe, sondern wir sammeln die Zeichen ein und sortieren sie in eine 4stellige Zeichenkette. Damit wir wissen, an welcher Stelle wir sind, setzen wir den Int pos (=Position), bei jedem Zeichen nimmt Position solange zu, bis die Länge von 4 erreicht ist. Wir können mit Backspace wieder nach vorn zurück und überschreiben.

Daß wir nur Ziffern-Zeichen erhalten, bewirkt die Anweisung
if (c>='0'&&c<='9') , die eben nur Ziffern passieren läßt.

Anschließend prüfen wir auf Steuertasten und verzweigen entsprechend.

Um aus der Zeichenkette eine integer-Zahl zu machen, dient die Funktion atoi(buffer), welche einen Integer zurückliefert.

Der Rückgabewert ist aber nicht dieser Integer, sondern das (int) c, also die Taste, mit der der Anwender geendet hat.

Wie der eigentliche Wert für die Konto-Nr dann zurückkommt, mach ich gleich anschließend nochmal ausführlich.

Hier ein Hinweis auf den ständigen Pferdefuß in C, dem man selbst immer wieder zum Opfer fällt:

Für den 4stelligen Buffer sind definiert buffer[255], also 255 Zeichen Breite.

Warum nicht buffer[4]? Passen doch auch 4 Zeichen rein, reicht doch!

Ich kann nur warnen, das Kleid zu eng zu schneidern!

Es gibt ja auch 6-stellige Konto-Nrn, z.B. bei den Anlagekonten sind die in der Regel 6stellig. Die kleinste Programmänderung, den Buffer übersehen, und peng, Buffer overflow.

Aber auch ohne Programmänderung kann das sehr leicht ins Auge gehen:

Nehmen wir Pos1=1, dann ist Pos4= 4, und buffer[4] ist nicht mehr definiert! Das 4. Zeichen ist nämlich Buffer[3]. Sagen wir buffer[4] haben wir buffer-overflow.

Jetzt sind wir brav und nehmen Pos1=0, also 0,1,2,3, dann kann es trotzdem geschehen. Das Eingabefeld soll 4 Zeichen lang sein, wie der Parameter len im Funktionskopf sagt. Und so sagen wir:

if (pos<len) pos++ und haben schon wieder buffer-overflow! Deshalb steht da auch if (pos<len-1), diese -1 ist der Unterschied zwischen Länge und INdex.

Lokale Varible werden nach Verlassen der Funktion wieder freigegeben, belegen keinen Speicher!

Aus all diesen beschriebenen Gründen gibt es keinen vernünftiges Argument, das Kleid so eng zu schneidern.


Angehängte Datei(en)
Angehängte Datei  dialog.jpg ( 270.29KB ) Anzahl der Downloads: 3
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 11.02.2012, 15:34 Uhr
sharky2014
Level 7 = Community-Professor
*******

Werteübergabe CALL BY REFERENCE und das ARBEITEN MIT MEHREREN DATEIEN

Der Aufruf der Funktion:

rueckgabe=hole_kontonr(&bsatz.konto_nr,bmdata[feld].es,bmdata[feld].ez,4);

enthält, wie man sieht, den Adressoperator &

Das ist klarer und deutlicher Code, wir sehen mit einem Blick, aha, die aufgerufene Funktion wird ganz sicher den Rückgabewert hier ablegen, insbesondere weil der Bezeichner bsatz.konto_nr auch eindeutig ist.

Was ist dann rueckgabe? Nun, das wollen wir für den Tastenwert reservieren, durchgängig im ganzen Programm. Wir rufen also alle Funktionen CALL BY REFERENCE auf.

Die wilde Variante (von der dringend abzuraten ist !!! ) wäre, den Übergabewert überhaupt zu verschweigen. Wir brauchen den ja gar nicht, wenn z. B. die Struktur bsatz GLOBAL definiert ist. Der Funktionsaufruf könnnte dann so ausfallen:

rueckgabe=hole_kontonr(bmdata[feld].es,bmdata[feld].ez,4);

und wenn man sich den Code ansieht, kann man nur mutmaßen, was da los ist. Es ist eine unklare, schwammige Formulierung, und die aufgerufene Funktion schreibt irgendwo im Speicher herum (wie gesagt, das ist kein guter Code).

Woher weiß nun die aufgerufene Funktion, daß sie CALL BY REFERENCE agieren soll? Das sieht man hier:

int hole_kontonr(int konto_nr,int spalte, int zeile,int maxlen)

Diese Funktion wird NICHT BY Reference aufgerufen, und der Compiler schmeißt eine Fehlermeldung raus.

Völlig zu Recht, denn die richtige Formulierung ist so:

int hole_kontonr(int *konto_nr,int spalte, int zeile,int maxlen)

Der Unterschied ist der Operator *, welcher darauf hinweist, daß jetzt ein Pointer draufsitzt.

Dadurch sieht man also auch in der aufgerufenen Funktion, was man programmieren muß, damit es stimmt.

Wie gibt die Funktion nun den gefundenen Wert zurück?

Die Zuweisung konto_nr=wert löst eine Fehlermeldung aus, da hier ein Integer zu einem Pointer zugewiesen wird, was unzulässig ist.

Wir benötigen den sogenannten DEREFERNZIERUNGSOPERATOR, ebenfalls ein *, und so ist es richtig:

*konto_nr=wert;

Das bedeutet, daß wir nicht versuchen, dem Pointer einen Wert zuzuweisen (der könnte ohnehin nur eine Adresse sein), sondern daß wir auf dem Speicherplatz, auf den der Pointer zeigt, einen Wert kopieren. Genau das wollen wir ja.

Es gibt noch mehr Gründe, die für Call by reference sprechen.

Es ist sogar so, daß die die Struktur bsatz, welche einen Haufen Elemente enthält, bislang noch GLOBAL definiert ist. Was spricht dafür, dann trotzdem mit

&bsatz.konto_nr zu arbeiten?

Der Grund ist, daß die Funktion zwar eine Konto-Nr holt, aber ja gar nicht wissen kann, wofür man die benötigt. Es gibt ja eine Reihe von Konto-Nummmern, die unterschiedliche Speicherplätze finden, z. B. im Kontenrahmen, beim Buchen als Konto, Gegenkonto oder Umsatzsteuerkonto oder sonstwas.

Wollten wir das implementieren, müßten wir eine Parameterliste einfügen, z. B. 1=Konto_nr, 2=Gegenkto_nr, 3=Kontenrahmen, 4=sonstwas, was sehr unhandlich ist, und fehleranfällig und pflegeintensiv, wenn sich was ändert, und den weiteren Nachteil hat, daß die aufgerufene Funktion

einen Haufen if/else if/switch VErzweiger aufgebrummt bekommt.

Lange parameterlisten haben NUR NACHTEILE, ich kann da überhaupt keinen Vorteil erkennen. Wenn man das nicht will und nicht by reference aufrufen will, wird es noch krimineller, dann muß man für jeden Speicherort eine Kopie der Funktion anlegen und den Funktionskopf der Kopie umbenennen. Herrgott, bewahre! wink.gif

Was geschieht nun, wenn die Funktion Mist baut? Also den Zielbereich überschreibt mit buffer-overflow?

Das läßt sich infolge der Struturierung &bsatz.konto_nr auf einen Blick sehen und abstellen. Wir sehen sofort, woher der Fehler kommt.

Kommen wir zu dem, was die Funktion leisten muß, abgesehen davon, daß sie 4 Ziffern einfangen soll:

Es sind ja nicht alle Ziffernfolgen gültige Konto-Nummern. Die Funktion sollte keinen Unsinn zurückliefern, z. B. die Konto-Nr 4711, wenn es die nicht gibt. Sie muß also irgendwie feststellen, ob der Wert zulässig ist, und das kann sie nach formalen Bedingungen nicht, sondern sie muß den Wert mit den Datensätzen der zugehörigen Datei KONTENRAHMEN vergleichen.

Wir arbeiten mit zwei Dateien, nämlich einmal der Datei BUCHUNGSSÄTZE, zum anderen mit der Datei KONTENRAHMEN.

Unabhängig davon, ob die sich auf dem Massenspeicher oder im Rechenspeicher befinden, die Bearbeitung ist gleich. Natürlich packt man die 2. oder 3. oder n. Datei in den Rechenspeicher, wo immer das geht, weil die Performance so natürlich 1000mal besser ist.

ALLE DATENBANKSYSTEME früherer Jahre haben daran gekrankt, daß der Rechenspeicher nicht groß genug war, und sie ständig von der Festplatte kleine Blöcke in den Speicher und zurückschwappen mußten. Wie das heute ist, weiß ich nicht, ich benutze keine Datenbanken.

Die 2., 3. oder n. Datei hat je nachdem eine unbekannte Größe. Ist die Größe einigermaßen absehbar, läßt man sie als indiziertes Array laufen. Ist das nicht der Fall, packt man sie in eine oder mehrere dynamische Listen, deren Speicherbedarf mit malloc() angefordert werden muß. Ich hatte dazu weiter oben schon ein Beispiel gebracht, wie man das macht.

Den Zugriff auf die 2. Datei, also den Kontenrahmen, sehen wir in dieser Funktion (Ausgabe Buchungssatz) schön deutlich implementiert:

QUOTE
void zeige_bsatz()
{
int k,kind,gind,uind;
kind=-1; if (bsatz.konto_nr>0)for (k=0;k<MAXKONTEN;k++)if (skr03[k].konto_nr==bsatz.konto_nr)kind=k;
gind=-1; if (bsatz.gkonto_nr>0)for (k=0;k<MAXKONTEN;k++)if (skr03[k].konto_nr==bsatz.gkonto_nr)gind=k;
uind=-1; if (bsatz.ustkonto_nr>0)for (k=0;k<MAXKONTEN;k++)if (skr03[k].konto_nr==bsatz.ustkonto_nr)uind=k;

gotoSCR(DIAL);
char loesch[255];
int i;
int lb=screen[DIAL].breite-bmdata[0].es;
strncpy(loesch,leer255,lb);loesch[lb]=EOS; // Alte Werte erstmal überschreiben
for (i=0;i<MAXBMFELDER;i++){gotoxy(bmdata[0].es,bmdata[0].ez);puts(loesch);}

gotoxy(bmdata[0].es,bmdata[0].ez);printf("%12s",bsatz.datum);
gotoxy(bmdata[1].es,bmdata[1].ez);printf("%12i",bsatz.konto_nr);
if (kind<0)printf(" Konto nicht bekannt");else printf(" %s",skr03[kind].konto_bez);

gotoxy(bmdata[2].es,bmdata[2].ez);printf("%12i",bsatz.gkonto_nr);
if (gind<0)printf(" Konto nicht bekannt");else printf(" %s",skr03[gind].konto_bez);

gotoxy(bmdata[3].es,bmdata[3].ez);printf("%12.2f",bsatz.brutto);
gotoxy(bmdata[4].es,bmdata[4].ez);printf("%40s",bsatz.kommentar);
gotoxy(bmdata[5].es,bmdata[5].ez);printf("%12i",bsatz.ust_option);
gotoxy(bmdata[6].es,bmdata[6].ez);printf("%12.1f",bsatz.ust_proz);
gotoxy(bmdata[7].es,bmdata[7].ez);printf("%12.2f",bsatz.ust_betrag);
gotoxy(bmdata[8].es,bmdata[8].ez);printf("%12.2f",bsatz.netto);
gotoxy(bmdata[9].es,bmdata[9].ez);printf("%12i",bsatz.ustkonto_nr);
if (uind<0)printf(" Konto nicht bekannt");else printf(" %s",skr03[uind].konto_bez);

gotoxy(bmdata[10].es,bmdata[10].ez);printf("%12i",bsatz.ident_nr);
}


Wir haben im Funktionskopf 3 Durchgänge wie diesen:

kind=-1; if (bsatz.konto_nr>0)for (k=0;k<MAXKONTEN;k++)if (skr03[k].konto_nr==bsatz.konto_nr)kind=k;

Das bedeutet, daß die Funktion die Datei skr03 (=den Kontenrahmen) durchsucht, und wenn sie für die konto_nr des Datentypen bsatz eine Übereinstimmung findet, diesen Index der Variable kind übergibt.

Wir wissen dann, erstens, daß diese Konto-Nr. existiert, und wir finden (dazu dient das hier) den Text zu der Konto-Nr. (z. B. Bank oder Telefon oder sonstwas) unter dem index skr03[kind]

Da der Buchungssatz mehrere Konto-Nummern führt, natürlich Gegenkonto und evtl. ein- oder mehrere Umsatzsteuerkonten, wiederholt man das für gkind (Index für Gegenkonto) und uind (Index für Umsatzsteuer-Konto).

Und dann geht es an die Ausgabe.

Da die 3 Indizes zuvor auf -1 gesetzt wurden, heißt das: wenn der Wert immer noch -1 ist, wurde kein passendes Konto gefunden (das darf natürlich nicht sein, die Fehlerroutine fehlt hier aber noch). Man sieht, was die Funktion dann macht:


if (kind<0)printf(" Konto nicht bekannt");else printf(" %s",skr03[kind].konto_bez);

Auf dem Bildschirm sieht das derzeit so aus (Abb):

Der Gründe, warum man mit 2 oder mehreren Dateien so zusammenarbeitet und nicht den Text des Kontobezeichners in den Buchungssatz reinschreibt (dann bräuchte man ja bei der Ausgabe nicht mehr zu suchen), sind viele.

Erstens hätte man dann die historische Kopie eines vielleicht längst geänderten Textes (Konto wurde umbenannt), zweitens würde sich die Datei für die Buchungssätze aufblähen auf das Vielfache der benötigten Größe (der Datensatz hat ca. 120 Byte, nimmt man für 3 Kontenbezeichner den TExt dazu, hätte er über 250 Byte), und er wäre als Textdatei nicht mehr darstellbar, weil die Zeilenlänge DINA4QUER = 120 Zeichen nunmal im Bereich BÜRO das Maximum ist, was man drucken kann (mit einem DIN4 Laserdrucker).

Das fehlende Konto könnte natürlich trotz Fehlerroutine auftreten, wenn aus der Datei das Konto gelöscht wurde.

Nun, logisch:

Das Löschen von Konten, die schon bebucht wurden, ist nicht nur aus programmiertechnischen Gründen streng verboten. wink.gif
Angehängte Datei(en)
Angehängte Datei  dialog2.jpg ( 269.11KB ) Anzahl der Downloads: 5
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 12.02.2012, 16:07 Uhr
sharky2014
Level 7 = Community-Professor
*******

Was man einmal erledigen muß, sind die Subroutinen zum EInfangen und Formatieren der Zeichen, bezogen auf Datentypen.

Einmal, weil wenn man die Bausteine hat, kann man sie in jedes Programm einbauen als fertige Bausteine, die z. B. folgendes erledigen:

lies_integerzahl

lies_floatzahl

lies_datum

und so weiter, weil sich ja nie was dran ändert. Ich hab bestimmt 15 Jahre lang kein Anwenderprogramm mehr geschrieben und verfüge über nichts davon, daher muß ich die Steinchen jetzt erzeugen, um sie ins Brett zu setzen.

Wenn wir als Schnittstelle zur Konsole getch() benutzen, gestaltet sich das immer gleich: wir geben einen Zeichenstring vor und setzen die Zeichen passend ein, anschließend formatieren wir den gewünscshten Datentyp daraus.

Integerzahlen einlesen ist wenig aufwendig, wir lassen nur Ziffern von 0 bis 9 zu und bestimmen die Länge des Eingabefeldes.

Bei Float-Zahlen ist es aufwendiger, weil wir das Dezimaltrennzeichen behandeln müssen. Dafür gibt es im Grunde 3 Möglichkeiten:

Festkomma (ist für den Anwender sehr unbequem)

Gleitkomma(ist besser einzugeben, programmiertechnisch etwas mehr Aufwand)

oder eine Mischform aus beiden, Gleitkomma, wobei die Eingabe nach dem Komma auf das Festkomma hinüberrutscht (ist auch gut einzugeben).

Dazu kann man dann das Trennzeichen . oder , wahlweise zulassen oder aus dem , einen Punkt machen, um das einheitlich zu handhaben.

Datumsangaben der Form "tt.mm.yyyy" sind recht einfach zu implementieren. Es bleibt die Wahl, ob der Anwender erstmal Unsinn eingeben darf ("89.01.2012") und dann eine Fehlermeldung erscheint oder ob man jedes Zeichen während der Eingabe überprüfen will.

Alle diese FUnktionen sind in wenigen Stunden erledigt. Das ist keine großartige Programmierung.

Zur Behandlung des Datums noch zwei Hinweise bzgl. eines gültigen Datums und eines Index, um Datumsangaben sortieren zu können.

Zur Ermittlung gültiger Daten benötigen wir (hypothetisch) den Gregorianischen Kalender vom ollen Papst Gregor. Dieser legt fest:

Schaltjahr ist jedes Jahr, das durch 4 ohne Rest teilbar ist.

Mit Ausnahme der Säkularjahre, welche ohne Rest durch 100 teilbar sind. Diese sind keine Schaltjahre.

Allerdings die Säkularjahre, die ohne Rest durch 400 teilbar sind, sind dann doch wieder Schaltjahre. wink.gif

Der Algorithmus ist also der:

int jahr;

if (jahr % 400==0 || jahr%4==0&&jahr%100!=0) schaltjahr = JA;

Der Modulo-Operator liefert den Rest einer Ganzzahl-Division. Geht das glatt auf, ist er 0.

Zum Index von Datumsangaben:

Seit Christi Geburt haben wir ungefähr 365*2012 Tage zu verzeichnen. Liegt so in der Größenordnung von ca 734.500 Tagen.

Weniger als 1Mio, aber deutlich mehr als eine Integer-Zahl speichern kann (max ca. 64000), wir brauchen dafür den Datentyp long.

Ein Algorithmus für einen Index, der Datumsangaben zuverlässig nach ihrer Reihenfolge sortiert, muß zwei Bedingungen erfüllen:

Bedingung 1: Innerhalb des laufenden Jahres dem 1. des Folgemonats einen höheren Wert zuweisen als dem 31. des Vormonats

Bedingung 2: Innerhalb des Jahreswechsels dem 1. des Folgejahres einen höheren Wert zuweisen als dem 31.Dezember des Vorjahres

Da die Zahlenwerte für die Tage (31) wesentlich größer sind als die der Monate (12), und die Jahreswerte beim Jahreswechsel nur um (1) zunehmen, müssen wir Multiplikatoren verwenden.

Der kleinste ganzzahilge Multiplikator, um die Bedingung 1 zu erfüllen, ist die 31. Wir müssen die Monate mit 31 multiplizieren. Dann ist:

31.01.=31+31=62
und
01.02=62+1=63

Der kleinste ganzzahlige Multiplikator, um die Bedingung 2 zu erfüllen, ist 372. Der entsteht als Produkt von 12*31

Index für den 31.12.2000 ist somit 31+12*31+2000*372=744403
Index für den 01.01.2001 ist somit 1+31+2001*372=744404

Daß wir die Tage seit Christi Geburt in dem Index etwas aufblähen, liegt daran, daß wir jeden Monat großzügigerweise mit 31 Tagen zählen. Trotzdem ist dieser Index so etwas wie ein "natürlicher" Index, weil der die kleinstmöglichen ganzzahligen Multiplikatoren verwendet.

Wir legen den Index des Datums in einem Feld long.datindex der Datei Buchungssätze ab und können so sehr schön chronologisch sortieren.

Man sieht, das ist eigentlich nix dolles, worüber sich schreiben ließe, man muß das einfach programmieren und fertig.

Daher mal ein kleines Schmankerl, siehe Abbildung 1:

Wir haben da eine Abfrage, unten rechts, in dem gelben Fenster, Eingabe fortsetzen (J/N)?

Und wenn wir Ja sagen, bricht die Funktion ab und kehrt zum Hauptmenü zurück.

Wir sagen aber jetzt NEIN, nicht abbrechen, und erhalten als Resultat die Abb. 2, welche einen gelben Balken in unser Fenster zeichnet.

Woher kommt der? Der kommt natürlich von daher, daß wir im gelben Mitteilungsfenster eine andere Hintergrundfarbe haben und beim Rücksprung diese Farbe mitgenommen wird. Unsere Funktion sagt aber:

do
{
gotoSCR(DIAL); // SCreen wählen
zeige_bsatz();
gotoxy(bmdata[feld].es,bmdata[feld].ez);
c=getch();

Wobei gotoSCR(BEREICH) die Farbwerte desjenigen Fensterabschnitts jedesmal wiederherstellt, wenn zum Schleifenanfang zurückgesprungen wird. Wieso ist der Balken dann trotzdem gelb?

Es liegt daran, daß wir folgende Strutur haben, nämlich Endlosschleife und innerhalb der Schleife mehrere Switch-Blöcke:

Endlosschleife

Switch-Block 1 für die Sondertasten

Switch-Block 2 für die gewählte Option, was zu tun ist (Datum, Konto-Nr usw.)

Switch-Block 3 für die Rückgabewerte, mit denen die Funktionen abgeschlossen werden

Das hat zur Folge, wenn der vorherige Switch-Block mit break; verlassen wird, daß das Programm dann in die nachfolgenden Switch-Blöcke hineinfällt. Normalerweise macht das nichts, nur hier macht es was, nämlich es will wieder zurück in das Feld, in dem es gestanden hat, als ESCAPE aufgerufen wurde. Und ruft die rot markierte Löschzeile auf. Was ja auch richtig ist, um den alten Wert vom Bildschirm zu nehmen.

case ESCAPE:
option=abfrage("Eingabe fortsetzen?");
if (option=='N')
{
clrSCR(DIAL);
return (int) c;
}
break;
case F1:
feldalt=feld;
scroll(SKR3);
break;
} // switch
rueckgabe=0;
switch (feld)
{
case 0:
gotoxy(bmdata[feld].es,bmdata[feld].ez);printf(loeschzeile);
rueckgabe=hole_datum(&bsatz.datum,bmdata[feld].es,bmdata[feld].ez);
break;
case 1:

Es macht dies aber, ohne zuvor zum Schleifenanfang oben zurückgekehrt zu sein, wo ja die Anweisung steht, die Textfarbe vom aktuellen Bildschirmbereich zu wählen.

Wir haben es sozuagen damit zu tun, daß einer, der eigentlich nach oben in die Chefetage will, erstmal durch alle Etagen nach unten durchrutscht und erst von da den Aufzug nach ganz oben (vorn in der Schleife) nehmen kann.

Selbstverständlich kann man solche Dinge strukturiert lösen. Je mehr Dinge aber, umso mehr if else if Verzweiger braucht es, um die Switch-Blöcke zu isolieren. Das macht bei den vielen Switch-Blöcken auch keinen schönen Code, wenn die noch mit if else if geschachtelt werden müssen.

Es gibt im Standard-Sprachumfang von C dazu zwei Anweisungen, welche das PRoblem lösen könnten.

Nämlich continue

und die verfemte Anweisung, noch aus Zeiten der BASIC-Programmierung goto

(Letztere ist wirklich der Teufel in Person wink.gif )

Was macht continue? Es sprint direkt zum Schleifenanfang zurück. Das heißt, wir fallen nicht durch die nachfolgenden Switchblöcke durch, sondern springen sofort zum Schleifenanfang (was wir ja wollen).

Aaaaaaaaaaaber: ist ein Switch-Block nicht auch eine Schleife? Wir müssen switch ja mit break; verlassen wie jede do-while Schleife auch.

Antwort: es funktioniert. Die switch-Blöcke werden von continue mißachtet.

Um direkt dahin zu springen, müssen wir also formulieren:

case ESCAPE:
option=abfrage("Eingabe fortsetzen?");
if (option=='N')
{
clrSCR(DIAL);
return (int) c;
}
continue;
break;
case F1:
feldalt=feld;
scroll(SKR3);
break;
} // switch
rueckgabe=0;
switch (feld)
{
case 0:
gotoxy(bmdata[feld].es,bmdata[feld].ez);printf(loeschzeile);
rueckgabe=hole_datum(&bsatz.datum,bmdata[feld].es,bmdata[feld].ez);
break;

Das rettet uns davor, unten in die loeschzeile zu fallen. Die wird dann natürlich auch ausgeführt, aber korrekt, vom Schleifenanfang aus.

Jetzt die teuflische wink.gif Variante:


case ESCAPE:
option=abfrage("Eingabe fortsetzen?");
if (option=='N')
{
clrSCR(DIAL);
return (int) c;
}
goto SCHLEIFENANFANG;
break;
case F1:
feldalt=feld;
scroll(SKR3);
break;
} // switch
rueckgabe=0;
switch (feld)
{
case 0:
gotoxy(bmdata[feld].es,bmdata[feld].ez);printf(loeschzeile);
rueckgabe=hole_datum(&bsatz.datum,bmdata[feld].es,bmdata[feld].ez);
break;

die funktioniert natürlich nur (ja, die guten alten BASIC-Zeiten), wenn wir die Sprungmarke dort auch eingefügt haben:

do
{
SCHLEIFENANFANG:
gotoSCR(DIAL); // SCreen wählen
zeige_bsatz();
gotoxy(bmdata[feld].es,bmdata[feld].ez);
c=getch();

Auch das rettet uns vor der gelben Zeile.

Wirklich teuflisch nicht wahr? sauer.gif

Der Beitrag wurde von sharky bearbeitet: 12.02.2012, 16:16 Uhr
Angehängte Datei(en)
Angehängte Datei  continue.jpg ( 266.29KB ) Anzahl der Downloads: 6
Angehängte Datei  continue2.jpg ( 266.16KB ) Anzahl der Downloads: 5
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 14.02.2012, 20:29 Uhr
sharky2014
Level 7 = Community-Professor
*******

Die Programmierung einer Anwender-Schnittstelle ist mühsam.

Man muß den Spagat schaffen zwischen dem möglichen Unsinn, den der Anwender in die Tastatur eingeben könnte, dem Abfangen von logisch falschen Eingaben und zugleich eine komfortable Anwenderführung realisieren, so daß dieser sich leicht und ohne in irgendwelchen Abfrageschlangen "gefangen" zu sein, durch das Programm bewegen kann.

Es gibt ja keine allgemeinen Regeln, wie man sowas programmiert. Es ist auch Geschmacksache, was ein Anwender als "angenehm" empfindet. Manche möchten ja geradezu als "Knechte" des Programms durch die Abfragen geprügelt werden, damit sie bloß keinen Fehler machen. Andere möchten so leicht über das Programm "hinüberschweben".

Ich löse das für mich so, daß ich tagelang und nächtelang verschiedene Versionen daraufhin checke, ob sie mir "gefallen". Damit bin ich unter anderem gerade beschäftigt. Das ist das "Design" der Programmierung, was man eben nicht in Regeln fassen kann.

Ich möchte nochmal auf die Strukturierung des Programms in Bezug auf die sichtbaren Bildschirmabschnitte zu sprechen kommen. Der Bildschirm von derzeit 120 Spalten und 40 Zeilen (das ist DINA4QUER) wurde unterteilt in Bildschirmbereiche, die bestimmten Zwecken dienen. Diese Bereiche, ich nenne sie mal "Fenster", können in beliebiger Zahl, in beliebiger Anordnung und Größe definiert werden. Sie können sich überlappen, können ersetzt werden durch Fenster gleicher Größe und Orientierung, nur eben mit anderen Inhalten, können konstante Inhalte enthalten oder mit anderen überschrieben werden.

Diese Fensterbereiche sind definiert

1.) Durch den Nullpunkt oben links mit den absoluten Bildschirmkoordinaten

2.) Durch Länge (Anzahl der Zeilen) und Breite (Anzahl der Spalten), das sind dann die relativen Koordinaten

3.) Durch Bildschirmfarben

4.) Durch einen Textspeicherbereich, der mindestens ZEILEN*SPALTEN entspricht, aber auch mehr Zeilen enthalten kann, wenn man Scrolling aktivieren will.

Für alle diese Bereiche wurde festgelegt, daß niemals direkt auf den Bildschirm geschrieben wird, sondern nur in den Textspeicherbereich. Soll der sichtbar gemacht werden, wird mit showSCRBUF(screen) der Inhalt ausgegeben.

Das dient dazu, daß man zu JEDEM ZEITPUNKT im Programm den Bildschirminhalt wiederherstellen kann. Es ist sozusagen eine permanente HARDCOPY aller Bildschirmbereiche.

Das Beschreiben der Textspeicher für die Bereiche wurde so organisiert, daß man dort jede Art von Text nach Spalte und Zeile koordiniert eingeben kann, z. B. my_puts("ich sage mal was",30,5) setzt den Text in Spalte 30, Zeile 5. Steht da schon was anderes, wird es überschrieben, wenn nicht, kommt neues hinzu.

Die Funktion my_puts() simuliert exakt das, was geschehen würde, wenn man mit gotoxy(Spalte,Zeile);printf("irgendwas"); arbeiten würde, nur daß eben die Eingaben zwischengespeichert werden und nicht sofort sichtbar sind, aber jederzeit sichtbar gemacht werden können mit der Funktion showSCRBUF(screen), zeige Bildschirmspeicher von dem Bereich.

Wie die Organisation ist, sieht man hier sehr schön. Abb1: der Primitivtyp des kommendes Hauptmenüs. Drückt man nun F1, um die Auswahl 1 zu machen, ergibt sich Abb. 2.

Da scheint einiges durcheinandergeraten, denn der Bildschirminhalt des linken Hauptfensters hat sich über den vorherigen Inhalt überlagert. Genau so arbeitet die Funktion my_puts(). Sie löscht nichts weg, sondern platziert alle neuen Zeichen dazu, wie auf einem Bildschirm, auf dem man was dazu schreibt.

Will man die alten Inhalte aus dem Speicher löschen, damit es aussieht wie in Abb 3 (alte Eingaben gelöscht), muß man

// resetSCRBUF(MENU);resetSCRBUF(DIAL);resetSCRBUF(BOX2);

die hier zur Demo außer Kraft gesetzt wurden, wieder in Betrieb nehmen. Das überschreibt alles mit Leerzeichen, um dann mit my_puts() die neuen Inhalte einzufügen. Es ist eine perfekte Simulation von clrscr(), gotoxy(x,y) und prinft(), es ist völlig identisch, nur daß es nicht am Bildschirm stattfindet, sondern in dem zugewiesenen Bufferbereich, und damit nicht mehr flüchtig ist, sondern jederzeit wiederherstellbar.

Der geänderte Inhalt wird dann mit

showSCRBUF(Bildschirmbereich) ausgegeben.

Alternativ haben wir noch

clrSCR(Bildschirmbereich), was aber nur die historische Ausgabe auf dem Bildschirm löscht und nichts mit dem Buffer (=Speicher für Bildschirmbereich) zu tun hat. Da der Buffer mit resetSCRBUF() sowieso alles mit Leerzeichen ersetzt, was nicht mehr gebraucht wird, braucht man hier, um Abb. 3 zu erreichen, die Anweisung clrsSCR(Bildschirmbereich) nicht.

Den Sinn dieser Bildschirm-Organisation sieht man hier im MINI-FORMAT bei der Funktion Abfrage sehr schön. Abfrage hängt unten im rechten Fenster und fragt den Anwender irgendwas, liefert dann JA oder NEIN zurück:

int abfrage(char nachricht[255])
{
char c;
int result=-2; // Vorgabe = unsinniger Wert
resetSCRBUF(MBOX); // Der Bildschirmbuffer wird mit Leerzeichen überschrieben
my_puts(MBOX,nachricht,1,1); // Eingabe der Abfrage in den SPeicher zu dem Fesnter
my_puts(MBOX,"( J / N ) " ,1,3); // und noch den Standard anfueben
showSCRBUF(MBOX,0); // Dann ausgeben, was am Bildschirm in dem Bereich zu sehen war, wird überschrieben
do
{
c=getch();
if (c=='j'||c=='J') result=JA; // JA ist JA, ob groß oder klein
if (c=='n'||c=='N') result=NEIN; // dito
if ((int)c==ESCAPE) result=ESCAPE; // Ob die aufrufende Routine mit ESC was anfangen kann? Egal
}
while (result==-2); // solange also der Unsinnswert nicht überschrieben wurde, eigentlich Endlos-Schleife
clrSCR(MBOX); // JETZT das FENSTER LÖSCHEN, damit der Text wegkommt, der hat sich ja erledigt!
return result;
}

clrSCR sieht man hier ebenso in Aktion wie die anderen beschriebenen Funktionen. clrSCR dient also dazu, den Bildschirm wieder freizumachen, hat mit dem eigentlichen Bildschirmbuffer nichts zu tun. Die Info steht dann da noch genauso drin wie beim Aufruf, ist nur nicht mehr zu sehen.
---------------------------------------------------------------------------------------------------
Jetzt mal weiter mit C-Programmierung, CALL BY REFERENCE

Es hat so seine Tücken.

während: mache_irgendwas(&zahl,char sonstwas) mit dem Funktionskopf void mache_irgendwas(int *zahl,char sonstwas) oder

mache_irgendwas(&fzahl,char sonstwas) mit dem Funktionskopf void mache_irgendwas(float *zahl,char sonstwas)


bestens funktioniert,

haben wir, wenn wir Glück haben, eine Compiler-Fehlermeldung, wenn wir folgendes versuchen:

mache_irgendwas(&textzeile,char sonstwas) mit dem Funktionskopf void mache_irgendwas(char *textzeile,char sonstwas)

Das gibt Systemabsturz, sobald wir etwa versuchen, mit sprintf(buffer,*textzeile) drauf zuzugreifen.

Der Grund ist, daß char-arrays in C IMMER mit einem Pointer übergeben werden. Setzt man einen Adreßoperator auf ein char-array, übergibt man damit nicht die Adresse des char-arrays, sondern die Adresse des darauf sitzenden Pointern, Ergebnis Systemabsturz.

FEHLER MIT POINTERN MACHEN DAS PROGRAMM IMMER INSTABIL, c ist eine hardwarenahe Sprache die fast assembler-gleich auf den Speicher zugreift. Es gibt aber nicht immer einen Systemabsturz, sondern schwieriger sind die "seltsamen Effekte" zu entdecken.

Wir können also CALL BY REFERENCE für char-arrays ganz einfach so formulieren:

char info[255];

mache_was(info,1,1); ---> anstelle von mache_was(&info,1,1);

mit Funktionskopf void mache_was(char *info,int spalte, int zeile)

und die Rückgabe des Wertes erfolgt über:

sprintf(info,"was immer der Anwender eingetippt hat");

anstelle von: sprintf(*info ... was falsch wäre, weil man damit in die Adresse des Pointers schreibt!


Ich weiß, das ist alles anstrengend zu lesen. Aber so ist C nun mal. Man kann das schnell verstehen, aber bei den Feinheiten ist C immer für eine Überraschung gut. wink.gif
Angehängte Datei(en)
Angehängte Datei  scrbuf.jpg ( 82.81KB ) Anzahl der Downloads: 3
Angehängte Datei  scrbuf2.jpg ( 279.85KB ) Anzahl der Downloads: 2
Angehängte Datei  scrbuf3.jpg ( 263.03KB ) Anzahl der Downloads: 2
 


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 14.02.2012, 21:07 Uhr
sharky2014
Level 7 = Community-Professor
*******

Ein grundsätzliches Problem der Strukturierung eines Programms ist das Ausklammern von Redunzanzen in den Funktionen, also von gleichlautendem Code.

Gleichlautender Code entsteht i.d.R. durch copy&paste und Ersetzen und ist erstmal sehr bequem, weil man das Programm sehr schnell mit einer Anzahl leistungsfähiger FUnktionen bevölkert.

Redundanz ist aber nicht mehr bequem, wenn man den Prototyp der Funktion ändert, und das geschieht ja eigentlich immer. Jetzt muß man Sorge tragen, daß alle mit c&p gezogenen Clones ebenfalls entsprechend geändert werden, und dann artet das Ganze im Mindesten zu wüster Tipparbeit aus, in der Regel aber geht die ÜBersicht irgendwann völlig verloren.

Das Ziel einer guten Strukturierung muß sein, daß AN KEINER STELLE DES PROGRAMMS REDUNDANTER CODE ZU FINDEN IST.

Die Vorteile sind klar: jede Änderung wird an einer Stelle vorgenommen, alle Clones greifen darauf zu und alles ist in Butter.

Das ist aber programmiertechnisch gar nicht so einfach herzustellen.

Wenn wir eine Funktion klonen, z. B.

Funktion 1

Funktion Clone1 von 1

Funktion Clone2 von 1

Funktion Clone3 von 1

etc. pp.

Dann enthalten diese Funktionen IMMER zwei Code-Abschnitte:

-gemeinsamer Code (aus copy&paste)
-spezieller Code (aus Ersetzen)

Das sieht dann so aus:

Funktion1
gemeinsamer Code
spezieller Code

Funktion Clone1 von 1
gemeinsamer Code
spezieller Code

und so weiter.

Was man dann machen muß, ist praktisch ausklammern.

Wir nehmen den gemeinsamen Code raus, in eine neue Funktion:

Funktion neue Funktion

Funktion 1
Funktion neue Funktion
spezieller Code

Funktion Clone1 von 1
Funktion neue Funktion
spezieller Code

Dann ist das Problem der Redundanz gelöst.

Dann stellen sich aber viele weitere Fragen, angefangen bei der Werteübergabe (Parameter) , gefolgt von der Art der Rückgabewerte (VALUE/REFERENCE) und auch alles, was am Bildschirm stattfinden soll.

ES IST NICHT EINFACH.

Man muß sich zu sehr abstraktem Denken zwingen.

Ein Beispiel, ganz abstrakt, was bei jeder Programmierung eines Anwenderprogramms auftauchen wird, mit dem überhaupt Anwenderdaten gesammelt werden:

Funktion Neu

Funktion Edit (Edit heißt, bereit vorhandene Werte überarbeiten)

Um einen Datensatz zu bearbeiten, benötigt man einen Haufen Code, allein nur, um logische Fehler abzufangen oder die Frage zu klären, ob der gespeichert werden soll oder nicht.

Die Funktionen enthalten folgende Code-Bestandteile:

Funktion Neu
Nulle-Datensatz
Hole die Daten
Speichere den Datensatz, indem du den an die Datei hinten dranhängst

Die Funktion Edit:
Suche den Datensatz, der geändert werden soll
Editiere die Daten
Speichere den Datensatz, indem du den in die Datei hineinschreibst an die Stelle, wo er vorher gestanden hat

Die Bestandteile Hole die Daten und Editiere die Daten sind identisch. Ich schreib das mal um:

Funktion Neu
Nulle-Datensatz
Editiere die Daten
Speichere

Funktionn Edit
Suche die Daten
Editiere die Daten
Speichere

Wir müssen daraus ausklammern die immer sehr aufwendige Funktion Editiere die Daten, weil da 99 Prozent stattfindet, der ganze Dialog, die Bildschirmanordnung, die logischen Überprüfungen, die Hinweise und so fort.

Speichern ist eine Zeile, Suchen eine separate Funktion, trotzdem diese beiden Funktionen nur 1 Zeile gemeinsamen Code haben, macht diese 1 Zeile aber über 90 Prozent des Codes aus.

Daher muß das ausgeklammert werden. 90 Prozent Redundanz sind indiskutabel.

Macht man das nicht, bleibt man bei Copy&Paste und alles doppelt und sechsfach, ist der Untergang des Programms eine Frage der Zeit.

Ich bin gerade dabei, auch die kleinste Redundanz rauszukopieren, solange noch Zeit bleibt.

Die beste Zeit ist im Anfang.

Man verzögert dann zwar die Ergebnisse, das Programm läuft dann noch nicht, aber die STruktur stimmt.

Das ist so meine Meinung zu diesen DIngen.

Lieber 3 Monate an der STruktur feilen als 6 Monate später einzusehen, daß das PRogramm IRREPARABEL ist und man selbst nicht mehr versteht, was man da reingeschrieben hat.

Ist ja Hobby.

Bei meinem Hobby bin ich eben zu 99 Prozent nicht zu Kompromissen geneigt.

Kann man sich im wirklichen Leben, womit man das Geld verdient, nicht leisten.

Im Hobby aber schon. wink.gif

Der Beitrag wurde von sharky bearbeitet: 14.02.2012, 21:16 Uhr


--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 15.02.2012, 17:14 Uhr
sharky2014
Level 7 = Community-Professor
*******

Performance Test

Man wundert sich ja manchmal, ich besonders, wenn eine mächtige Software unter Windows über dutzende von Sekunden regelrecht aussteigt, um so eine völlig banale Angelegenheit wie die Löschung eines Datensatzes zu erledigen, wobei dann je nachdem noch einige Dateien abgeglichen werden müssen, aber sei´s drum. Man wird den Verdacht nicht los, daß solche Windows-Anwenderprogramme nicht wirklich programmiert, sondern aus eierlegenden Bausteinen zusammengestöpselt wurden, die dann eine Performance jenseits von Gut und Böse abliefern.

C ist eine maschinennahe Sprache und die Performance darf daher kein anderes Prädikat als "sehr gut" ertragen. Solange das Operationen im Rechenspeicher betrifft, muß man da auch gar nicht wirklich hinsehen (siehe Sort-Algorithmus einer dynamischen Liste wie oben "Kasten Bier im Ferrrari" erwähnt). Aber wie sieht es mit dem Massespeicher aus? Dateizugriffe sind ganz sicher um den Faktor 1000 oder mehr langsamer als Zugriffe im Rechenspeicher, nur um welche Dimensionen handelt es sich da? Wenn es da klemmen sollte, muß man das bei der Strukturierung des Programms entsprechend berücksichtigen. Nur klemmt es da überhaupt?

Ich werd mal vorstellen, wie man sowas testet, und was dabei herausgekommen ist.

Wir bauen uns einen Datentypen:

struct s_test{
int nr;
float betrag;
char text[128];
char c;
};

struct s_test buffer;

Dann füllen wir den Datentypen mittels der Funktion rand() mit Zufallswerten auf. Dazu setzen wir den Zufalls"Generator" erstmal zurück, weil der sonst bei jedem Programmdurchlauf immer exakt dasselbe ausspucken würde:

srand(time(NULL));

Jetzt werden die Zufallswerte in den Buffer geschrieben:

for (l=0;l<anzahl;l++)
{
buffer.nr=rand()%400+1;
buffer.betrag=(float)(rand()% RAND_MAX)*1.42;
sprintf(buffer.text,"Test");
buffer.c='#';

RAND_MAX ist eine Systemkonstante, die den oberen Wert der Zufallszahlen angibt. Sollte so im Bereich ca. 32000 liegen, für signed int.

Die Anweisung: buffer.nr=rand()%400+1 erzeugt Zufallszahlen zwischen 1 und 400.

Nun, der buffer ist nicht indiziert, was tut der da in der Schleife?

Antwort:

Der wird gleich in die Datei gepackt, nämlich:

int schreibe_datei(long anzahl)
{
FILE *bdp;
long l;
srand(time(NULL));
bdp=fopen(dn,"wb");
if (bdp==NULL)return NOTOK;
for (l=0;l<anzahl;l++)
{
buffer.nr=rand()%400+1;
buffer.betrag=(float)(rand()% RAND_MAX)*1.42;
sprintf(buffer.text,"Test");
buffer.c='#';
if (fwrite(&buffer,sizeof(struct s_test),1,bdp)!=1)
{fclose(bdp);return NOTOK;}

}
fclose(bdp);
return OK;
}

Wir öffnen eine neue Datei im Binär-Modus und schreiben dann mit fwrite(&buffer ... diese Datensätze hinein.

Es wäre nicht nötig, die Datensätze mir irgendwelchen Inhalten zu belegen, da die Datei natürlich als Länge für den Datensatz mit sizeof() operiert, und nicht etwa mit strlen(). Würde sie letzteres tun, wäre ja ein Lesezugriff eine Katastrophe. Egal also ob der Datensatz Daten enthält oder nicht, er belegt auf dem Massespeicher exakt denselben Platz.

Anschließend wollen wir die Datei wieder auslesen, mit fread(). Das sieht dann so aus:

long lies_datei()
{
FILE *bdp;
long l=0;
bdp=fopen(dn,"rb");
if (bdp==NULL)return NOTOK;
while (fread(&buffer,sizeof(struct s_test),1,bdp)==1)l++;
fclose(bdp);
return l;
}

Das Dateiende ist erreicht, wenn fread() einen anderen als den angeforderten Wert übergibt. Angefordert ist 1 Datensatz der Größe sizeof(struct s_test), und wenn der nicht 1 ist, konnte er nicht gelesen werden =EOF bzw. feof(FILE).

Nun wollen wir wissen:

Wie lange braucht das System, um die Datensätze in die Datei zu schreiben bzw. die Datei wieder auszulesen?

Dazu können wir time nicht gebrauchen, weil die kürzeste Einheit Sekunden darstellt. Wir müssen, für Kurzzeitmessungen, die Prozessor-Clocks auslesen.

Auf meinem Notebook entspricht 1 Prozessorclock 1/1000 Sekunde, 1 Millisekunde. Das kann aber von System zu System variieren, daher gibt es die Variable CLOCKS_PER_SEC, die wir abrufen oder zum dividieren benutzen können, um einen Sekundenwert zu erhalten.

Um die Performance zu testen, schreiben wir ein bißchen Code fürs Schreiben:

start=clock();
result=schreibe_datei(anz_ds);
if (result==NOTOK)printf("ERROR");
end=clock();
diff=end-start;
printf("Clocks beim Schreiben der Datei %10i\n",diff);
printf("Zeit in Sekunden %12.5f\n\n\n",(double)diff/(double) CLOCKS_PER_SEC);

und fürs Lesen:

start=clock();
gelesen=lies_datei();
if (gelesen==0)printf("ERROR");
end=clock();
diff=end-start;

printf("%10i Datensaetze gelesen. Clocks %10i\n",gelesen,diff);
printf("Zeit in Sekunden %12.5f\n\n\n",(double)diff/(double) CLOCKS_PER_SEC);

Die FIBU für meinen Selbstgebrauch wird am Jahresende vielleicht auf 2000 Datensätze kommen. Je nachdem, wie man die eigentlich Buchung organisiert, z. B. für jeden Umsatzsteuer-Anteil eigener Buchungssatz (ist nicht nötig) oder auch immer, sind es vielleicht 4000 Datensätze, jedenfalls sind deutlich unter 10000 zu erwarten.

Wir fangen mal an mit 5000 Datensätzen und erhalten folgende Bildschirmausgabe:

Clocks beim Schreiben der Datei 0
Zeit in Sekunden 0.00000

5000 Datensaetze gelesen. Clocks 0
Zeit in Sekunden 0.00000

Press any key to continue . . .


Huch?

Programmfehler?

Mitnichten! wink.gif

Erhöhen wir mal auf 100000 Datensätze, dann sieht man es:

Clocks beim Schreiben der Datei 47
Zeit in Sekunden 0.04700

100000 Datensaetze gelesen. Clocks 16
Zeit in Sekunden 0.01600

Press any key to continue . . .

Daß er zum Lesen weniger Zeit benötigt, ist logisch, er muß ja am Massespeicher nichts ändern. Zudem kommt beim Schreiben die Initialisierung mit der rand() Funktion irreführenderweise hinzu.

Nehmen wir die interessehalber mal raus, initialisieren wir den Buffer nur einmal außerhalb der Schleife, ergibt sich folgendes:


Clocks beim Schreiben der Datei 16
Zeit in Sekunden 0.01600

100000 Datensaetze gelesen. Clocks 16
Zeit in Sekunden 0.01600

Press any key to continue . . .

AHA.

Da wir es mit 2000 Datensätzen zu tun haben, können wir aber unseren Profiler gleich wieder einpacken.

Zwar lieber einmal zuviel als zuwenig nachgesehen, aber hier gibt es nun wirklich nichts zu "performieren".

Der Kasten Bier kann im Kofferraum stehen bleiben. wink.gif




--------------------
A programmer is just a tool which converts caffeine into code
TOP    
Beitrag 16.02.2012, 12:21 Uhr
sharky2014
Level 7 = Community-Professor
*******

Eine Funktion für das exakte Auslesen der Tastatur

Bisher ging das ganz gut mit getch(), und dabei bleibt es auch, aber wir müssen allmählich trennen zwischen druckbaren Zeichen und Steuertasten.

Folgende Bildschirmausgabe verdeutlicht das Problem:

QUOTE
Tastaturwerte fuer folgende Eingaben:

1. Pfeil oben
2. Buchstabe H
3. Pfeil links
4. Buchstabe K
5. ENTER
6. ESCAPE
7. Funktionstaste F1
8. Semikolon ;
1. -32 72
2. 72
3. -32 75
4. 75
5. 13
6. 27
7. 0 59
8. 59


Bei der einfachen Abfrage mit

char c

c=getch()

erhält man identische Werte für Buchstabe H und PFEILTASTE OBEN, F1 und Semikolon usf.

Das kommt daher, daß manche Taten doppelt codiert sind, so daß dem dem ersten noch ein zweiter Wert hinterhergeschickt wird. Wird der nicht gesondert abgefragt, wird der erste Wert überschrieben. Z. B. für eine Steuertaste wie Pfeil oben kommt erst die Ziffer 0 an, die wird dann überschrieben von der Ziffer 72. Bei dem Buchstaben H kommt gleich die 72 an, nur am Ende kann man nicht unterscheiden, ob das der erste oder der zweite Wert gewesen ist. Folge, daß dann, wenn man eine Texteingabe macht, bei groß H das Programm herausspringt, wenn für PFEILOBEN eine Sonderfunktion definiert ist.

Man benötigt dafür eine Funktion.

Nun liegt die Sache noch etwas vertrackter, weil ein Teil der Steuertasten zwei Werte übergibt, von denen der erste immer 0 ist, andere aber wie ENTER, ESCAPE etc. übergeben nur einen Wert, genau wie die druckbaren Zeichen.

Wir haben also 3 Gruppen:

1. Solche mit 2 Werten, davon der erste eine 0, nicht alphanumerisch

2. Alphanumerische Zeichen mit 1 Wert

3. Steuertasten mit 1 Wert

Die beiden letzteren kann man trennen anhand der ASCII Tabelle. Alphanumerische Zeichen beginnen mit dem ASCII-Wert 32, d.i. das Leerzeichen. Werte mit ASCII<32 sind Steuerzeichen.

Unsere Funktion müßte in jedem Falle mehr als einen Wert zurückliefern, das geht aber nicht, da alle Funktionen in c nur einen Wert zurückliefern.

Um das Problem zu umgehen, greift man auf eine STRUCT zurück:

struct s_taste{
int lo; // alphanumerische + Blank
int hi;
int st; // Steuertasten allgemein
};

Der erste Tastaturwert wird in taste.lo abgelegt (low, niederer Wert), WENN dieser Wert 0 ist, liest man den zweiten Wert aus, indem man getch() nochmals aufruft, und legt den in taste.hi ab. Die Steuerzeichen mit ASCII<32 kommen in das Feld taste.st.

Da es bei der Verarbeitung lästig wäre, taste.st parallel zu taste.hi abzufragen, legen wir eine Kopie von taste.hi in das Feld taste.st.

Das heißt, jedes irgendwie geartetete Steuerzeichen ist in taste.st abgelegt und kann dort abgerufen werden.

Nun findet sich der Wert 72 für den Großbuchstaben H in taste.lo, der Wert 72 für die PFeiltaste oben in taste.hi und taste.st, so daß fortan keine Verwechselung mehr stattfinden kann.

Hier ist der Code der zugehörigen Funktion:

QUOTE
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;
}
else
{
d=getch(); // zweiten Tastaturwert holen
taste->hi=(int)d;
taste->st=(int)d; // als Kopie von hi zu den Steuerzeichen packen
}
}



Man sieht, daß die Funktion BY REFERENCE auf die übergebene Adresse für Taste zugreift. Einen Rückgabewert hat sie nicht und braucht sie nicht.

Will man die Tastatur nun abfragen, kann man eine globale Variable Taste verwenden, wovon dringend abzuraten ist, oder eine lokale Definition:

struct s_taste taste;

Die Abfrage erfolgt dann mit:

liestaste(&taste);

Um den Wert von taste.lo wieder als char verfügbar zu haben, muß man casten:

char c;

c=(char) taste.lo;

weil taste.lo ja als int deklariert ist.

Die Abfrage der Steuertasten gestaltet sich nun einheitlich mit:

switch(taste.st)
{
case ENTER:
...
case F1:
...
case ESCAPE:
....

Will man die Tastaturwerte an die aufrufende Funktion zurückgeben, a la

hole_kontonummer() (aufgerufene Funktion)
hole_datum() (aufgerufene Funktion)

editiere_buchungssatz() (aufrufende Funktion)

sieht der Deklarationsteil der aufgerufenen Funktionen so aus:

int editiere_buchungssatz() Gibt keine Taste zurück, sondern int, z. B. OK oder NOTOK

struct s_taste hole_kontonummer(int *kontonr) mit dem Rückgabewert return taste;
struct s_taste hole_datum(char *datum)

Der Aufruf erfolgt mit:

taste=hole_kontonummer(&bsatz.kontonummer); // also mit dem Adreßoperator, weil hier die Werterückgabe erfolgt.
taste=hole_datum(&bsatz.datum); // dito

Warum ist es überhaupt sinnvoll, einen Tastaturwert zurückzugeben?

Der Grund findet sich darin, daß man aus einer laufenden Funktion herausspringen will, z. B. mit F1 für Hilfe, mit ESCAPE für Abbruch, oder PFEILOBEN, um abzubrechen und auf das nächsthöhere Eingabefeld zurückzuspringen. Die aufrufende Funktion kann diesen Wunsch nur erfüllen, wenn sie von der Tastatureingabe in Kenntnis gesetzt wird. Und da jede Funktion nur einen Wert zurückliefern kann, hatten wir ja bereits sämtliche Funktionen CALL BY REFERENCE umgeschrieben.

Diese Subfunktionen, aus denen man zurückspringen können will, müssen natürlich die Sondertasten auch "durchreichen". Das geschieht etwa in dieser Form:

do
{
gotoSCR(DIAL);
gotoxy(spalte,zeile);
printf(buffer);
gotoxy(spalte+pos[p],zeile);
liestaste(&taste);
c=(char)taste.lo;
if (c>='0'&&c<='9')
{
buffer[pos[p]]=c;
if (p<3)p++;
}
if (taste.st==PFEILOBEN||taste.st==PFEILUNTEN||taste.st==ESCAPE||taste.st==F1||tast
e.taste.st==F10)return taste;
switch (taste.st)
{
case BACKSPACE:
buffer[pos[p]]='0';
if (p>0)p--;
buffer[pos[p]]='0';
break;
case PFEILLINKS:
if (p>0)p--;
break;
case PFEILRECHTS:

Der entsprechende Codeabschnitt ist hier rot gekennzeichnet.

Er bewirkt, daß die Funktion mit der Eingabe augenblicklich abbricht und an die aufrufende Funktion zurückverzweigt. Die Eingabe kommt dann nicht zum Abschluß, daher wird das zu bearbeitende Feld auch noch nicht überschrieben, es bleibt wie es war.

Man kann sich so durchgängig aus einer beliebigen Verschachtelungstiefe heraus.


Funktion 1 ruft F2
--------------------F2 ruft F3
----------------------------------F3 ruft F4
----------------------------------------------F4 ruft F5 und so weiter...

Man kann sich so durchgängig aus einer beliebigen Verschachtelungstiefe heraus wieder bis zum Programmanfang zurückverzweigen, z. B. mit der Taste ESCAPE oder sonstwas, was vorgesehen ist.

Das ist bei tastaturgesteuerten Programmen immer sehr hilfreich, z. B. dann, wenn man sich nur mal die Menüverzweigungen ansehen will, ohne gleich in einer Eingabe "hängen" zu bleiben.



Der Beitrag wurde von sharky bearbeitet: 16.02.2012, 12:30 Uhr


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



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