Teil 17 „Printing with frills“

Der C-Kurs
Antworten
Benutzeravatar
bodo
User
Beiträge: 319
Registriert: 14.02.2007, 17:21
Kontaktdaten:

Teil 17 „Printing with frills“

Beitrag von bodo » 13.11.2010, 15:13

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 17

In diesem Teil kommen wir wieder zu praktischen Dingen, nachdem der letzte Teil mehr theoretischer Natur war. Bei Vickers heißt das Kapitel „Printing with frills“, und um so etwas geht es auch: Ausgeben mit allem Drum und Dran.

Beim Ausgeben müssen wir zwei Dinge auseinanderhalten:
  • Auf welche Weise mit printf() Ausdrücke in Zeichenketten umgewandelt werden. Hauptsächlich darum geht es in diesem Teil.
  • Wie und wo die ausgegebenen Zeichen auf dem Bildschirm landen. Darum geht es zwar bei Vickers, aber darauf gehen wir hier nur rudimentär ein; so richtig tief wird uns das im Teil 20 beschäftigen, versprochen!
Positionierungen auf dem Bildschirm

Neue Zeile: Wenn ihr den PRINT-Befehl von BASIC verwendet, wird automatisch am Ende ein Zeilenvorschub ausgeführt, es sei denn, ihr gebt ein Semikolon als letztes Zeichen an. Bei C gibt es diesen Automatismus nicht, wenn wir von puts() absehen, das tatsächlich von selbst ein '\n' anhängt. Das ist aber die einzige Ausnahme! Stattdessen müssen wir dort, wo wir ein Zeilenende haben wollen, explizit ein '\n' angeben.

Anfang der Zeile: Eigentlich sollte '\r' nur einen Wagenrücklauf auslösen, also den Cursor an den Anfang der aktuellen Zeile setzen. Aber die z88dk-Funktionen erzeugen damit auch eine neue Zeile wie mit '\n'.

Scrollen: Die Ausgaberoutinen vom z88dk scrollen auch automatisch den Schirm, sobald die letzte Zeile voll ist oder dort ein Zeilenendezeichen ausgegeben wird. Das ist ganz praktisch, vor allem bildet es das übliche Verhalten von Terminals nach. Es gibt im Textmodus auch keine Funktion oder kein Steuerzeichen, um den BASIC-Befehl SCROLL nachzubilden. Übrigens verwendet das z88dk alle 24 Zeilen!

Bildschirm löschen: Aber es gibt ein Steuerzeichen, das dem BASIC-Befehl CLS entspricht: '\f'. Wir hatten alle Sonderzeichen bereits im Teil 11, dort war das Zeichen bereits mit „form feed“, also Seitenvorschub, erklärt. Normalerweise wird dadurch ein Drucker aufgefordert, die aktuelle Seite auszuwerfen und eine neue zu beginnen. Und ein Bildschirm macht das dann eben auch – und löscht seinen Inhalt. ;-)

So, das Ganze könnt ihr mit diesem kleinen Testprogramm nachvollziehen:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    int schirm;

    for (schirm = 1; schirm <= 5; schirm++) {
        int zeile;

        printf("\fSCHIRM %d\n", schirm);
        for (zeile = 1; zeile <= 40; zeile++) {
            printf("\tZEILE %d\n", zeile);
        }
    }

    return 0;
}
Freies Positionieren: Im Textmodus haben wir keine Möglichkeit, die BASIC-Funktion AT nachzubilden.

Tabulator: Auch etwas wie TAB gibt es leider nicht, wobei das nur ein Versäumnis der z88dk-Macher ist. Denn es gibt das Steuerzeichen '\t', das genau dafür da ist...

Formatierung

Bisher haben wir nur einfache printf()-Ausgabeformate verwendet. Aber die Funktion ist sehr mächtig, mit allen Features kann sie sehr groß werden, ich habe schon von über 10 KB Maschinencode gehört. Das z88dk hat drei verschiedene Varianten:
  • Die Miniversion kann nur die einfachen Formate für ganze Zahlen und Zeichenketten, ohne weitere Kinkerlitzchen.
  • Die komplexe Version beherrscht einige Formatierungsoptionen, die in diesem Kapitel vorgestellt werden, wie Breite und Ausrichtung. Der Dokumentation nach soll sie aber keine breiten Ganzzahlen (long) ausgeben können; die Versuche zeigen glücklicherweise etwas anderes.
  • Und dann gibt es noch die Version für Fließpunktzahlen. Leider scheint diese Version nicht für den ZX81 zu existieren...
Der Compiler entscheidet anhand des Quelltextes, welche Version einzusetzen ist. Hoffen wir, dass ihm das immer richtig gelingt!

Hier nun die Übersicht, was für Datentypen ausgegeben werden können, einige davon kennt ihr schon:

Formatzeichen = Datentyp
%c = int, als unsigned char; wird als Zeichen interpretiert
%d = signed int; wird dezimal ausgegeben
%u = unsigned int; wird dezimal ausgegeben
%x = unsigned int; wird hexadezimal ausgegeben
%ld = Wie %d, nur signed long
%lu = Wie %u, nur unsigned long
%lx = Wie %x, nur unsigned long
%f = float; geht beim z88dk nicht
%e = float; geht beim z88dk nicht
%s = char-Array
%p = Adresse, Zeiger; geht beim z88dk nicht
%% = Ein Prozentzeichen wird ausgegeben

Zwischen dem Prozentzeichen und dem eigentlichen Formatzeichen können nun noch mehrere weitere Angaben stehen. Diese dienen der feineren Steuerung, die folgende Liste zeigt die üblichen von printf() verstandenen Möglichkeiten:
  • Ein Pluszeichen '+' fordert die Ausgabe eines Vorzeichens auch bei positiven Werten. Das kann das printf() vom z88dk aber nicht...
  • Ein Minuszeichen '-' sorgt für eine linksbündige Ausgabe; siehe auch den nächsten Punkt.
  • Eine Zahl (auch mehrstellig) ohne führende Null gibt die Anzahl der Zeichen an, die mindestens benutzt werden. Die Ausgabe erfolgt normalerweise rechtsbündig, links stehen dann Leerzeichen.
  • Eine Null (vor der Breitenangabe) fordert, dass zum Auffüllen anstelle der Leerzeichen Nullen verwendet werden. Das ist praktisch für tabellarische Ausgaben.
  • Eine Zahl nach einem Dezimalpunkt hat je nach Datentyp verschiedene Bedeutungen. Bei Fließpunktzahlen wird damit die Anzahl der Nachkommastellen bestimmt. Bei Zeichenketten wird dadurch dagegen die maximale Anzahl Zeichen angegeben.
Das nächste Beispielprogramm zeigt einige Beispiele, und führt auch vor, wie ihr ausprobieren könnt, was welche Formatangabe macht. Natürlich ist das kein erschöpfendes Beispiel, hier ist jede Menge Raum zum Experimentieren. Auch die Profis probieren aus, wenn sie bestimmte Vorstellungen haben...

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    printf("c von %d = \"%c\"\n", 65, 65);

    printf("d von %d = %d\n", +81, +81);
    printf("u von %d = %u\n", +81, +81);
    printf("x von %d = %x\n", +81, +81);
    printf("d von %d = %d\n", -81, -81);
    printf("u von %d = %u\n", -81, -81);
    printf("x von %d = %x\n", -81, -81);

    printf("ld von %ld = %ld\n", +123456, +123456);
    printf("lu von %ld = %lu\n", +123456, +123456);
    printf("lx von %ld = %lx\n", +123456, +123456);
    printf("ld von %ld = %ld\n", -123456, -123456);
    printf("lu von %ld = %lu\n", -123456, -123456);
    printf("lx von %ld = %lx\n", -123456, -123456);

    printf("4d von %d = <%4d>\n", +81, +81);
    printf("04d von %d = <%04d>\n", +81, +81);
    printf("+4d von %d = <%+4d>\n", +81, +81);
    printf("-4d von %d = <%-4d>\n", +81, +81);
    printf("4d von %d = <%4d>\n", -81, -81);
    printf("04d von %d = <%04d>\n", -81, -81);
    printf("+4d von %d = <%+4d>\n", -81, -81);
    printf("-4d von %d = <%-4d>\n", -81, -81);

    return 0;
}
Mögliche Ausgaben von Zeichenketten zeigt das nächste Beispiel. Für den vollen Genuss empfehlen wir das parallele Betrachten von Quelltext und tatsächlicher Ausgabe!

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    printf("s von \"%s\" = \"%s\"\n", "ZX", "ZX");
    printf("s von \"%s\" = \"%s\"\n", "HALLO", "HALLO");
    printf("s von \"%s\" = \"%s\"\n", "LANGESWORT", "LANGESWORT");

    printf("4.6s von \"%s\" = \"%4.6s\"\n", "ZX", "ZX");
    printf("4.6s von \"%s\" = \"%4.6s\"\n", "HALLO", "HALLO");
    printf("4.6s von \"%s\" = \"%4.6s\"\n", "LANGESWORT", "LANGESWORT");

    printf("6s von \"%s\" = \"%6s\"\n", "ZX", "ZX");
    printf("6s von \"%s\" = \"%6s\"\n", "HALLO", "HALLO");
    printf("6s von \"%s\" = \"%6s\"\n", "LANGESWORT", "LANGESWORT");

    printf("-6s von \"%s\" = \"%-6s\"\n", "ZX", "ZX");
    printf("-6s von \"%s\" = \"%-6s\"\n", "HALLO", "HALLO");
    printf("-6s von \"%s\" = \"%-6s\"\n", "LANGESWORT", "LANGESWORT");

    printf("06s von \"%s\" = \"%06s\"\n", "ZX", "ZX");
    printf("06s von \"%s\" = \"%06s\"\n", "HALLO", "HALLO");
    printf("06s von \"%s\" = \"%06s\"\n", "LANGESWORT", "LANGESWORT");

    return 0;
}
Refaktorieren

Das letzte Beispiel kann auch gut vereinfacht werden. Der Profi nennt das „Refaktorieren“, also „Neubauen“. Ein sehr gutes Prinzip in der Entwicklung ist zum Beispiel das DRY-Prinzip: Don't Repeat Yourself! Die mehrfache Angabe von sehr ähnlichen Zeichenketten und gleichen Parametern schreit geradezu danach, eingespart zu werden. Weil das z88dk leider keine mehrdimensionalen Arrays beherrscht, habe ich hier verschiedene Unterprogramme eingesetzt:

Code: Alles auswählen

#include <stdio.h>

void gib_aus(char *art, char *text) {
    char format[30];

    sprintf(format, "%s von \"%%s\" = \"%%%s\"\n", art, art);
    printf(format, text, text);
}

void teste_texte(char *art) {
    gib_aus(art, "ZX");
    gib_aus(art, "HALLO");
    gib_aus(art, "LANGESWORT");
}

int main(void) {
    printf("%-31s\n", "MIT UNTERPROGRAMM"); /* <-- siehe Text */

    teste_texte("s");
    teste_texte("4.6s");
    teste_texte("6s");
    teste_texte("-6s");
    teste_texte("06s");

    return 0;
}
Verschiedene Punkte sind an dem Programm interessant und ihr solltet sie euch genauer ansehen:
  • Damit überhaupt die richtige printf()-Version benutzt wird, musste die markierte Ausgabe eingefügt werden.
  • Es gibt nicht nur die Funktion printf(), die auf den Bildschirm (Eigentlich schreibt printf() in den sogenannten Standardausgabekanal. Das ist per Default der Bildschirm... Außerdem gibt es noch die Funktion fprintf(), die in einen „Datenstrom“ wie z.B. eine Datei schreiben kann.) schreibt, sondern auch eine Funktion sprintf(), die in eine Zeichenkette schreibt. Diese wird als erster Parameter noch vor dem Formatstring angegeben.
  • Der für die Ausgabe auf dem Bildschirm benutzte Formatstring wird erst zur Laufzeit des Programms erzeugt. Dafür wird die eben erwähnte sprintf()-Funktion verwendet.
  • printf() und Genossen müssen nicht unbedingt eine Zeichenkettenkonstante als Formatangabe bekommen. Genauso gut können wir auch den Inhalt einer Variablen verwenden.
  • Die Größen des ursprünglichen Programms und des verbesserten Programms unterscheiden sich kaum. Daraus können wir schließen, dass die konstanten Zeichenketten nur einmal im Binärcode vorkommen. Und ein Blick in diesen bestätigt die Vermutung, der Compiler hat das schön optimiert!
Schauen wir uns die Sache mit sprintf() noch einmal im Detail an:

Code: Alles auswählen

sprintf(format, "%s von \"%%s\" = \"%%%s\"\n", art, art);
Der erste Parameter ist das Ziel der Ausgabe, die Variable heißt nur deshalb „format“, weil sie der Formatstring für den nächsten Aufruf von printf() wird.

Der zweite Parameter ist der Formatstring für den Aufruf von sprintf(), er enthält außer konstantem Text drei Formatangaben, eigentlich sogar vier! Die erste („%s“) nimmt sich den dritten Parameter („art“) und gibt ihn als Zeichenkette aus. Die zweite („%%s“) erzeugt einfach nur „%s“; ein doppeltes Prozentzeichen ergibt ja in der Ausgabe ein einfaches Prozentzeichen. Die dritte („%%%s“) erzeugt zunächst ein Prozentzeichen und nimmt sich dann den vierten Parameter (wieder „art“) und gibt ihn als Zeichenkette aus.

Wenn wir beispielsweise als „art“ die Zeichenkette „4.6s“ übergeben, wird der Formatstring „4.6s von \"%s\" = \"%4.6s\"\n“ erzeugt. Falls euch das noch nicht ganz klar wird, ergänzt das Programm ruhig um alle nötigen Debuganweisungen, siehe Teil 15.

Grafikzeichen des ZX81

Wie versprochen, kommen wir auf die speziellen Zeichen des ZX81 zurück. Natürlich gibt es keine Entsprechungen im ASCII, daher müssen wir zur Ausgabe den Zeichensatzkonverter abschalten (siehe Teil 13). Die einzelnen Zeichen schreiben wir dann z.B. mit \x... in die Zeichenkette, wie im Teil 11 beschrieben. Das nächste Programm gibt die 16 möglichen Low-Resolution-Zeichen aus, die „11“ erzeugt Anführungszeichen für eine hübsche Darstellung:

Code: Alles auswählen

#include <stdio.h>
#include <zx81.h>

int main(void) {
    zx_asciimode(0);
    printf("%c%c\x01\x02\x03\x04\x05\x06\x07%c\n", 11, 0x00, 11);
    printf("%c\x80\x81\x82\x83\x84\x85\x86\x87%c\n", 11, 11);

    return 0;
}
Fließpunktzahlen gehen manchmal doch?!

Je mehr wir mit dem z88dk herumspielen, desto mehr entdecken wir: ein bisschen scheinen die Fließpunktzahlen doch zu funktionieren. Leider ist wie gesagt die dazu passende printf()-Funktion nicht vorhanden, deshalb hat das folgende Beispiel seine selbstgebaute Ausgabefunktion. Damit das Programm auch ohne Fehlermeldung erzeugt wird, müsst ihr noch die Option „-lm81“ in der Kommandozeile angeben, damit die Fließpunktbibliothek mitgelinkt wird. Dann könnt ihr die Datentypen „float“ (mit etwa 9 Stellen) und „double“ (leider kein Unterschied zu „float“) verwenden.

Code: Alles auswählen

#include <math.h>
#include <stdio.h>

void print_float(char *format, float wert) {
    int vorkomma;
    int nachkomma;

    if (wert < 0.0) {
        printf("-");
        wert = -wert;
    }
    vorkomma = (int)wert;
    nachkomma = (int)((wert - vorkomma) * 10000.0);
    printf(format, vorkomma, nachkomma);
}

int main(void) {
    float winkel;

    printf("PI IST %d.%04d\n", 3, 1416); /* zur printf()-Auswahl */
    for (winkel = 0.0; winkel <= 2.0 * 3.141593; winkel += 0.31415926) {
        print_float("sin(%d.%04d) = ", winkel);
        print_float("%d.%04d\n", sin(winkel));
    }

    return 0;
}
Zu dumm nur, dass die ganze Sache ziemlich instabil läuft. Das Programm stürzt gleich bei den ersten ausgegebenen Zeilen ab. Schade! Aber eventuell funktionieren ja andere Programme, die ihr mit Fließpunktzahlen schreibt.

Hausaufgaben
  1. Warum können wir das ZX81-Zeichen mit dem Kode 0 (Null) nicht direkt als „...\x00\x01\x02...“ in die Zeichenkette schreiben?
  2. Schreibt ein Programm, das eine Tabelle erzeugt, die in der ersten Spalte rechtsbündig die Zahlen von 1 bis 20 ausgibt und in der zweiten Spalte linksbündig deren Quadrate.
B0D0: Real programmers do it in hex.

Antworten