Teil 9 „More computer programming“

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

Teil 9 „More computer programming“

Beitrag von bodo » 30.10.2010, 15:36

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 9

Im Kapitel 9 erzählt uns Vickers über „More computer programming“. Dies beinhaltet so schöne Befehle wie RUN, CONT, STOP und BREAK. Diese können wir in C nicht anwenden, weil das laufende C-Programm für den Zeddy eine einzelne BASIC-Anweisung ist. So behandeln wir in diesem Teil die C-Pendants zu REM und INPUT, und noch etwas mehr...

Kommentare

Es geht nichts über ein gut kommentiertes Programm. Und im Gegensatz zu BASIC-Programmen, in denen Kommentarzeilen tatsächlich die Rechenleistung beeinflussen (wenigstens, wenn das Programm wie beim ZX81 in einem Interpreter läuft), könnt ihr eure C-Programme ohne schlechtes Gewissen ausführlich kommentieren. Denn der Kommentar wird erst garnicht an den eigentlichen Compiler durchgereicht, wie wir im letzten Teil gelernt haben.

Kommentare werden mit der Zeichenkombination „/*“ eingeleitet und gehen bis zur Zeichenkombination „*/“, auch über mehrere Zeilen. Für C++ gibt es auch die einzeiligen Kommentare, die mit der Kombination „//“ beginnen und am Zeilenende automatisch aufhören. In einem reinrassigen C-Quelltext erwarte ich aber nur C-Kommentare aus „/*“ und „*/“:

Code: Alles auswählen

  /* Dies ist ein mehrzeiliger Kommentar.
   *
   * Besonders hübsch sieht es aus, wenn am linken Rand Sternchen
   * untereinander stehen.
   *
   * Natürlich können diese Kommentare nicht ineinander geschachtelt
   * werden!
   */
Professionelle Quelltexte haben Kommentarblöcke, die per Konvention in einem Projekt immer gleich aussehen:
  • Am Dateikopf stehen u.a. Informationen, welchem Zweck der Quelltext dient, wer ihn wann geschrieben hat, und häufig auch, welche Historie er hinter sich hat. Außerdem machen sich Hinweise für besondere Umstände (wie muss compiliert werden, welche Einsatzgrenzen gibt es, ...) gut.
  • Vor jeder Funktion (BASIC-Programmierer übersetzen: „Unterprogramme“) steht ein Kommentarblock mit einer Kurzbeschreibung der Funktion, ihrer eventuellen Parameter und ihres eventuellen Rückgabewertes.
  • Kommentare im laufenden Quelltext sollen nicht diesen in Prosa wiederholen, sondern den Leser auf das hinweisen, was nicht offensichtlich, aber für das Verständnis wichtig ist.
  • Trennlinien gliedern den Quelltext hübsch, und er wird dadurch auch viel besser lesbar. Ich teile damit meine Quelltexte gerne in eine Art Kapitel.
Der Platz im ZX-Team-Magazin soll jetzt nicht durch ein Beispiel verschwendet werden. Sucht euch professionelle Quelltexte oder ebensolche Programmierrichtlinien und schaut sie euch an. Die ganze Kommentiererei ist aber pure Geschmackssache und daher auch immer Grund zur Diskussion. Lasst euch trotzdem nicht abhalten! ;-)

Quelltextzeilen umbrechen

Obwohl ihr gleich zu Anfang gelernt habt, dass C formatfrei ist und ihr deshalb die einzelnen Worte des Quelltextes beliebig umbrechen und verteilen könnt, gibt es Fälle, in denen Zeilen im Quelltext umgebrochen werden sollen, obwohl es eigentlich eine einzige Zeile sein muss.

Der wichtigste Fall ist die Präprozessoranweisung. Sie muss auf einer Zeile stehen, aber das sieht manchmal einfach nicht gut aus. Die Verbindung zweier Zeilen geschieht durch einen umgekehrten Schrägstrich genau am Zeilenende. Es darf kein weiteres Zeichen mehr kommen! Beispiel:

Code: Alles auswählen

#include \
  <stdio.h>
Ein weiterer Fall sind lange Zeichenketten; diese könnt ihr teilen, indem ihr sie an einer Stelle trennt und dort doppelte Anführungszeichen angebt:

Code: Alles auswählen

printf(
    "Hallo, "
    "Welt!"
    "\n"
);
/* ist dasselbe wie: */
printf(    "Hallo, "    "Welt!"    "\n"    );
/* und dasselbe wie: */
printf("Hallo, Welt!\n");
Präprozessoranweisungen

Außer den Kommentaren verarbeitet der Präprozessor noch seine eigenen Anweisungen. Interessanterweise hat er eine andere Syntax als C; das ist für viele Anfänger verwirrend! Dafür kann er aber auch für beliebige andere Zwecke eingesetzt werden, nicht nur für C-Quelltexte. Ich habe ihn erfolgreich und sinnvoll auch Assembler-Quelltexte filtern lassen; solche Dateien haben üblicherweise die Erweiterung „.S“ (mit großem S). Am besten merkt ihr euch, dass Präprozessor und Compiler zwei Paar Schuhe sind – sie haben eigentlich nichts miteinander zu tun, außer dass sie gern gemeinsam wahrgenommen werden...

Alle Präprozessoranweisungen beginnen mit einem Nummernzeichen „#“ am Zeilenanfang. Einrückungen durch Leerzeichen oder Tabulatore sind zulässig, aber selten. Nach dem Zeichen kommt die eigentliche Anweisung, auch sie kann durch Leerraum abgeteilt sein. Einige Anweisungen haben dann noch Argumente; aber alle Anweisungen gehen nur bis zum Zeilenende. Im Gegensatz zu C-Statements erhalten sie aber kein Semikolon am Ende!

Einfügen von Dateien: #include

Diese Anweisung kennt ihr bereits. Sie bedeutet „Füge an dieser Stelle den Quelltext der angegebenen Datei ein“. Daher benötigt die Anweisung einen Dateinamen als Parameter; die entsprechende Angabe gibt es in zwei Ausführungen:

Code: Alles auswählen

#include <stdio.h>
#include "modul.h"
Jede einzufügende Datei wird vom Präprozessor in einer Liste von Pfaden gesucht. Deshalb braucht ihr auch keine Pfade explizit anzugeben, ganz im Gegenteil, es ist fast immer schädlich. Falls ihr aber dennoch einmal einen Pfad angebt, wird als Trennzeichen zwischen den Verzeichnis- und Dateinamen ein normaler Schrägstrich „/“ verwendet, auch unter Micro$ofts Betriebssystemen!

Die Schreibweise mit spitzen Klammern lässt den Präprozessor in der Liste der ihm bekannten Systempfade suchen. Daher werden hiermit vor allem Dateien, die die Standardbibliothek betreffen, eingefügt.

Wenn der Dateinamen in doppelten Anführungszeichen steht, wird dagegen zuerst das Verzeichnis des einfügenden Quelltextes durchsucht. Solche Dateien haben also mit dem zu erzeugenden Programm zu tun.

Der Inhalt der einzufügenden Datei muss natürlich wiederum C-Quelltext sein. Was dort im Einzelnen steht, ist freigestellt: alles ist möglich. Die normale Anwendung ist die Deklaration von „Objekten“ wie Konstanten, Variablen, und Funktionen. Weil solche Deklarationen am Kopf eines Quelltextes nötig sind, damit der Compiler sie bei der späteren Verwendung kennt, heißen solche Dateien „Headerdateien“.

Textersetzungen: #define und #undef

Mit dieser Anweisung wird der Präprozessor zu einem mächtigen Werkzeug zur Bearbeitung des Quelltextes, und es wird auch viel Schindluder damit getrieben. Nach #define wird ein Name erwartet, und den Rest der Zeile merkt sich der Präprozessor unter diesem Namen.

Fangen wir mit den einfachen Anwendungen an, der Definition von symbolischen Konstanten (Sie heißen „symbolisch“, weil sie im Quelltext durch ein Symbol, also einen Namen, und nicht durch ihren Wert repräsentiert werden.). So sieht es aus:

Code: Alles auswählen

#include <stdio.h>

#define TESTZAHL 23

int main(void) {
    printf("TESTZAHL = %d\n", TESTZAHL);

    return 0;
}
Überall im Quelltext, wo jetzt „TESTZAHL“ steht, wird der Präprozessor diesen Namen durch den Text „23“ ersetzen. Das hat zwei Vorteile:
  1. Das Symbol „TESTZAHL“ ist anschaulicher als die Zahl selbst. (Vielleicht nicht in diesem Beispiel, aber generell!)
  2. Wenn dieser Wert in seiner Bedeutung mehr als einmal benutzt wird, ist es fehlerträchtig, eine korrekte Änderung durchzuführen. Zu leicht wird eine Stelle vergessen. Mit #define kann das nicht passieren, weil der konkrete Wert ja nur an einer Stelle steht!
Ihr könnt auch mehrere so definierte Konstanten miteinander verknüpfen. Der Präprozessor ersetzt die Symbole rekursiv:

Code: Alles auswählen

#define LAENGE 42
#define BREITE 23
#define FLAECHE (LAENGE * BREITE)
Weil solche zusammengesetzten Konstanten ja irgendwo im Quelltext eingefügt werden, solltet ihr sie klammern, damit das Richtige passiert. Wenn im Beispiel das Symbol „FLAECHE“ im Quelltext vorkommt, ersetzt der Präprozessor es durch „(42 * 23)“. Das Produkt im Beispiel kann durch den Compiler berechnet werden. Wenn der das aber nicht kann, geschieht es eben zur Laufzeit des Programms.

Jeder Präprozessor/Compiler nach ANSI-Standard hat vordefinierte Konstanten, die jeweils mit zwei Unterstrichen beginnen und enden; die wichtigsten gibt euch das folgende Beispielprogramm aus (mehr kennt z88dk offenbar auch nicht):

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    printf("Dateiname = %s\n", __FILE__);     /* Text */
    printf("Zeilennummer = %d\n", __LINE__);  /* Zahl */
    printf("Erstelldatum = %s\n", __DATE__);  /* Text */
    printf("Erstellzeit = %s\n", __TIME__);   /* Text */

    return 0;
}
Wie oben beschrieben, werden aufeinanderfolgende Zeichenketten ja direkt zu einer einzigen verbunden. Daher hätten wir z.B. auch schreiben können:

Code: Alles auswählen

printf("Erstelldatum: = " __DATE__ "\n");
Ein mit #define einmal angegebenes Symbol gilt bis zum Ende des Quelltexts... Oder bis ihr es explizit mit #undef wieder aus dem Gedächtnis des Präprozessors streicht.

Als ob das Ganze nicht schon genug wäre, setzen wir noch einen oben drauf: ihr könnt das Symbol mit Parametern anlegen, und diese im Ersetzungsteil referenzieren. Dies entspricht der Makroprogrammiererei anderer Gelegenheiten, daher findet ihr auch häufig den Namen „Makro“ als Bezeichnung für mit #define definierte Symbole. Ein kleines Beispiel gefällig? Bitte schön:

Code: Alles auswählen

#include <stdio.h>

#define ausgabe(wert) printf("Zahl = %d\n", wert)

int main(void) {
    int variable;

    variable = 81;
    ausgabe(23);
    ausgabe(42);
    ausgabe(variable);

    return 0;
}
Bitte achtet darauf, wann ihr wo Semikolons setzt. Ihr müsst hier genau wissen, was wann wo wie ersetzt wird, um richtig schwer zu findende Fehler zu vermeiden! Wie im letzten Teil gezeigt, ist dann die Ausgabe des Präprozessors eine gute Hilfe bei der Fehlersuche.

Wenn ihr wollt, könnt ihr mit #define auch C komplett auf deutsch umstellen; aber das ist eine der Anwendungen, die eigentlich Blödsinn ist (Wie schon einmal erwähnt, kann der Wahnsinn Methode haben: http://www.ioccc.org. Die meisten der Einsendungen arbeiten viel mit dem Präprozessor.):

Code: Alles auswählen

#define haupt main
#define gib_formatiert_aus printf
#define ganzzahl int
#define wenn if
#define sonst else
#define anfang {
#define ende }
/* ... und so weiter */
Bedingte Übersetzung: #if, #else, #endif

Beim Entwickeln stellt ihr dann und wann fest, dass ihr einen Zeilenblock testweise weglassen wollt. Oder ihr wollt alle Debuganweisungen aus dem „Produktionscode“ herauslassen. Oder ihr müsst abhängig von bestimmten Umständen unterschiedliche Zeilen compilieren lassen. Auch hier hilft der Präprozessor.

Mit #if beginnt ein Quelltextbereich, der nur dann an den Compiler weitergereicht wird, wenn die Bedingung nach dem #if wahr ist. Dieser Bereich endet bei #endif. Mit #else wird ein alternativer Bereich eingeleitet, und es gibt auch #elif als kombiniertes else-if.

Wir haben noch keine Vergleich gehabt, aber soviel sei gesagt: als Bedingung nach dem #if zählt alles als „wahr“, was nicht den Wert 0 hat. Natürlich könnt ihr nur Bedingungen abfragen, die beim Compilieren bekannt sind. Der Präprozessor lässt nicht das Programm selbst laufen!

Zusätzlich gibt es noch eine Art Funktion, die abfragt, ob ein Makro definiert ist. Früher wurden bevorzugt die speziellen Anweisungen #ifdef („if defined“) und #ifndef („if not defined“) verwendet, aber dort kann keine weitere Verknüpfung mit anderen Bedingungen stattfinden. Mit defined(Name) ist die Sache flexibler.

Ein Beispiel macht die Sache sicher deutlicher:

Code: Alles auswählen

#include <stdio.h>

#define DEUTSCH 1
#define ENGLISCH 0
/* #define DEBUG entkommentieren für die Fehlersuche */

int main(void) {
    #if DEUTSCH
    printf("Hallo Welt.\n");
    #elif ENGLISCH
    printf("Hello, world.\n");
    #else
    printf("???\n");
    #endif

    #if defined(DEBUG)
    printf(__DATE__);
    #endif

    return 0;
}
Sonstiges: #error, #pragma

Es gäbe noch viel mehr über den Präprozessor zu sagen, aber es würde diesen schon viel zu großen Teil noch mehr aufblähen. Selbst altgediente C-Veteranen kennen oft nicht alle Möglichkeiten... Daher kommen jetzt nur noch zwei einfache Anweisungen, die ihr sinnvoll einsetzen könnt.

Mit #error wird die Übersetzung abgebrochen. Der Rest der Zeile wird als Meldung ausgegeben:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    #if defined(DEUTSCH)
    printf("Hallo Welt.\n");
    #else
    #error Es ist keine Sprache definiert!
    #endif

    return 0;
}
Einzelne Compiler können durch spezielle Anweisungen in ihrem Verhalten gesteuert werden. Dazu wird die Anweisung #pragma benutzt. Wenn ein Compiler den Rest der Zeile nicht versteht, darf er ihn getrost ignorieren, auch wenn das nicht der Wunsch des Programmierers war. Beim z88dk kann z.B. mit einer #pragma-Anweisung die Adresse des HRG-Speichers eingestellt werden:

Code: Alles auswählen

#pragma output hrgpage = 36096
Das war jetzt schon ein gewaltiger Happen, es gibt auch keine Hausaufgaben! Sicher habt ihr selbst Ideen, was ihr probieren wollt. Daher kommen wir jetzt wieder zu praktischeren Dingen, nämlich dem...

Eingeben von Werten

Bisher können wir einfache Variablen anlegen, ihnen Werte zuweisen, sie miteinander verknüpfen und das Ergebnis ausgeben. Aber zu einer vollständigen Kommunikation mit dem Benutzer fehlt noch die Gegenrichtung, das Einlesen von Werten, die der Benutzer eingibt.

Die Komplementärfunktion zu printf() („print formatted“) heißt scanf() („scan formatted“). Sie ist auf den ersten Blick sehr praktisch, hat aber recht viele Tücken. Profis setzen sie nicht so gern ein, jedenfalls nicht ihre einfache Variante (mehr dazu unten).

Analog zu printf() gibt das erste Argument an, was eingelesen werden soll. Für ganze Zahlen benutzt ihr „%d“, für einzelne Zeichen „%c“, und für Zeichenketten „%s“. Die weiteren Argumente müssen Zeiger auf Zielvariablen sein – aber Zeiger bekommen wir eigentlich erst später. Fürs erste reicht es, wenn ihr nach Rezept vorgeht...

Die Implementierung unter z88dk scheint auch noch weniger perfekt zu sein, wie wir schon im Forum gelesen haben. Damit ihr überhaupt etwas seht (sprich: der SLOW-Modus aktiv ist), erweitert ihr den Aufruf des Compilers um die Option „-startup=2“.

Gehen wir mit einem Beispiel in die Vollen:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    int zahl;

    printf("Eingabe: ");
    scanf("%d", &zahl);
    printf("\nEingegeben = %d\n", zahl);

    return 0;
}
Bitte achtet darauf, dass beim Aufruf von scanf() ein Ampersand „&“ vor dem Variablennamen „zahl“ steht! Lasst es weg, und – naja, ihr werdet schon sehen...

Wenn ihr das Programm laufen lasst, stellt ihr fest, dass ihr eure Eingabe nicht sehen könnt. Ihr müsst sozusagen blind tippen. Auch nach Abschluss der Eingabe mit der Eingabetaste erscheint nichts außer der Ausgabe durch printf(). Dies ist eindeutig ein Fehler der Standardbibliothek vom z88dk.

Wenn ihr weiter damit experimentiert, merkt ihr, dass die Eingabe auch mit Zeichen außerhalb „0“ bis „9“ beendet wird. Aber nur das Leerzeichen oder die Eingabetaste führen zu dem erwarteten Ergebnis. Auch hier unterstelle ich Fehler im z88dk. Das übliche scanf() soll soviele Zeichen akzeptieren und benutzen, wie es kann. Daher erwartete ich nach dem Eintippen von „1“-“2“-“3“-“x“ auch das Einlesen von „123“, aber es erschien „0“. Schade.

Kommen wir zu zwei weiteren Ein- und Ausgabefunktionen. Zunächst gibt es puts() („put string“):

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    puts("Hallo");
    puts("Welt.");

    return 0;
}
Dies ist die supereinfache Ausgabefunktion, die eine Zeichenkette ausgibt. Keine weiteren Parameter, keine Zahlen. Ganz simpel. Und wie ihr beim Ausprobieren merken werdet, wird sogar automatisch ein Zeilenende ausgegeben, so dass die nächsten Ausgaben auf einer neuen Zeile beginnen.

Das Gegenstück dazu heißt gets() („get string“). Diese Funktion erwartet eine Zeichenkette (genauer: einen Zeiger auf eine solche) als Parameter; dort muss genügend Platz sein, die Eingabe vom Benutzer aufzunehmen. Dummerweise gibt es keine Kontrolle (Natürlich gibt es eine erweiterte Variante, die eine Längenbegrenzung bietet. Dies ist fgets(), deren genaue Beschreibung nicht in diesem Teil kommt.), so dass wir hiermit die tollsten Abstürze provozieren können, wenn wir zuviel eingeben! Die Implementierung im z88dk ist besser als bei scanf() gelungen, so dass wir unsere Eingabe sehen und sogar editieren können:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    char zeile[10];
    int zahl;

    printf("Eingabe: ");
    gets(zeile);
    sscanf(zeile, "%d", &zahl);
    printf("\nEingegeben = %d\n", zahl);

    return 0;
}
Dieses Beispiel ruft anstelle des normalen scanf() die Abwandlung sscanf() („string scan formatted“) auf, achtet auf das zusätzliche „s“ und das zusätzliche Argument am Anfang. Diese Funktion wandelt auch Zeichen in eine Zahl um, verwendet dazu aber die Zeichen in der übergebenen Zeichenkette anstelle der direkten Eingabe. Diese Zeichenkette haben wir gerade vorher durch gets() eingelesen.

Üblicherweise gibt es noch weitere Ein- und Ausgabefunktionen, die z.B. einzelne Zeichen verwenden. Sie heißen putchar() („put character“) und getchar() („get character“). Es gibt auch variablere Varianten von ihnen. Das Problem an dieser Stelle ist, dass ich (Bodo) sie nicht mit dem z88dk zum Laufen bekommen habe...

Aus den Erfahrungen mit Ein- und Ausgabe leite ich die Empfehlung ab, auf dem ZX81 C-Programme nur als Unterstützung eines BASIC-Programms zu benutzen. Wir werden sicher noch andere Einsatzmöglichkeiten kennenlernen, insbesondere bin ich auf die HRG-Fähigkeiten gespannt, die ich noch garnicht ausprobiert habe. Aber für normale Ein- und Ausgabe sind die BASIC-Befehle im Vergleich zum z88dk viel zuverlässiger – das ist aber nicht symptomatisch für C-Programme im allgemeinen...
B0D0: Real programmers do it in hex.

Antworten