Programmieren in C mit dem z88dk

Software-Entwicklung, Compiler, Interpreter, ...
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

Programmieren in C mit dem z88dk

Beitrag von bbock »

1. Vorwort
Das z88dk, also das Z88 development kit, ist ein Entwicklungspaket für C und Assembler, mit dem man Programme für über 100 Z80-Systeme erstellen kann. Es ist also schon lange nicht mehr nur für die ursprüngliche Zielplattform Cambridge Z88, sondern u.a. auch für die Amstrad PCW-Computer verwendbar. Da die Programme auf einem PC entwickelt werden und nicht direkt auf dem Zielsystem, spricht man von einem cross compiler.

2. Setup
Als Standardausrüstung nehmen wir einen Windows-PC. Auch ältere Systeme sollten vollauf genügen. Um mit dem z88dk in C für die Joyce programmieren zu können, brauchen wir noch drei Dinge: Das neueste z88dk-Paket ist der nightly build, also nehmen wir den Link hinter "Nightly": http://nightly.z88dk.org/ und dort die ZIP-Datei "z88dk-win32-latest.zip - Latest win32 build". Die ZIP-Datei entpacken wir nach C:\, dort erhalten wir ein Verzeichnis namens z88dk. Natürlich kann man die Datei auch an anderer Stelle entpacken - die folgenden, vom z88dk-Verzeichnis abhängigen Pfade müssen dann entsprechend angepasst werden. Der Einfachheit halber beziehe ich mich ab jetzt immer auf C:\z88dk.

Jetzt müssen wir nur noch eine Umgebungsvariable setzen und eine weitere anpassen. Dazu rufen wir die Systemeigenschaften auf. Das geht so:
Startmenü > Einstellungen > Suchfeld: Umgebungsvariablen > Systemumgebungsvariablen bearbeiten.
Oder so:
Windows 10: Startmenü > Einstellungen > Info > Erweiterte Systemeinstellungen
Windows 11: Startmenü > Einstellungen > System > Info > Erweiterte Systemeinstellungen
Oder so:
Windows-Taste + R > sysdm.cpl > Registerkarte "Erweitert"
In allen drei Fällen erscheint der Dialog "Systemeigenschaften". Im Register "Erweitert" gibt es unten die Schaltfläche "Umgebungsvariablen". Ein Klick darauf führt zu einem zweigeteilten Fenster: oben sind die Benutzervariablen, unten die Systemvariablen.

Wir fügen eine neue Benutzervariable durch Klick auf die obere "Neu..."-Schaltfläche hinzu. Bei "Name der Variablen" tragen wir ZCCCFG ein, bei "Wert der Variablen" den Pfad C:\z88dk\lib\config (oder <mein z88dk-Verzeichnis>\lib\config, falls ihr das z88dk woanders entpackt habt).

"Path" findet man bei den Benutzervariablen und bei den Systemvariablen. Wir ändern die Benutzervariable per Doppelklick. Über die Schaltfläche "Neu" fügen wir einen Eintrag mit dem Pfad zum bin-Verzeichnis des z88dk hinzu, also C:\z88dk\bin.

So, das war's auch schon mit der z88dk-Installation. Entpacken, zwei Umgebungsvariablen und fertig. Wir testen mal, ob's auch funktioniert: Dazu geben wir im Suchfeld neben dem Startmenü cmd gefolgt von der Return-Taste ein; das startet die gute alte Windows-Eingabeaufforderung. Wenn man jetzt zcc eintippt (und wieder Return), dann sollte eine längere Liste von Compiler-Optionen ausgegeben werden.

Den Editor und den Emulator muss man nur herunterladen und wie jedes gewöhnliche Windows-Programm installieren. Das bekommt ihr hin, denke ich. Falls nicht: einfach fragen.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

3. Unser erstes Programm

Beitrag von bbock »

Wir brauchen ein Verzeichnis, in dem wir die Dateien unseres ersten Projekts unterbringen wollen. Wie wäre es mit C:\dev\hello? Wir legen das Verzeichnis an, starten unseren Texteditor und schreiben folgenden Code (Copy-Paste geht natürlich auch):

Code: Alles auswählen

#include <stdio.h>

void main() {
    printf("Hallo, Welt!\n");
    return 0;
}
Das speichern wir dann als "hello.c" im Verzeichnis hello ab - die erste Datei in unserem neuen Projektverzeichnis. Nun müssen wir noch kompilieren und binden (neudeutsch: linken); beides erledigt das Kommando zcc. Wir starten die Eingabeaufforderung (cmd), falls sie nicht schon offen ist, und tippen folgenden Befehl, gefolgt von RETURN, ein:

Code: Alles auswählen

zcc +cpm -subtype=pcw80 hello.c -o hello.com
Wenn ihr jetzt im Projektverzeichnis hello nachseht, z.B. per dir-Kommando, dann werdet ihr eine neue Datei namens hello.com vorfinden. Das ist die ausführbare COM-Datei, die wir im Emulator testen können - oder auf der Joyce selbst.

Wir starten die CP/M Box. Unter Options > Hardware stellen wir im Register PCW die 8512 als "Quick model" ein. Dann brauchen wir noch ein Verzeichnis, das die RAM-Disk simuliert: Wir erstellen zunächst ein Verzeichnis, z.B. C:\dev\ramdisk. Dann in der CP/M Box: Options > Emulation > Register Mapping (ganz rechts). Dort aktivieren wir "Enabled", stellen das eben erstellte ramdisk-Verzeichnis unter "Directory" ein und belassen das "Filler byte" auf 0x001A. Dann klicken wir auf "Accept".

Jetzt müssen wir noch eine Boot-Diskette in Laufwerk A: einlegen. Das geht mit File > Drive A > Insert oder mit der Taste F4. Die Boot-Diskette mit CP/M braucht ihr natürlich, und zwar als DSK-Datei. Die wählt ihr aus und das System sollte starten. Wenn nicht, dann schaut mal, ob die Emulation pausiert ist. Ihr müsst dann nur auf die "Play"-Schaltfläche unter dem File-Menü klicken.

Um das Programm zu starten, kopieren wir die COM-Datei in die simulierte RAM-Disk, also ins Verzeichnis C:\dev\ramdisk. In der CP/M Box wechseln wir zum Laufwerk M: (die RAM-Disk), eben indem wir M: eingeben. Dann starten wir die COM-Datei durch Eingabe von hello, gefolgt von RETURN. Der Text Hallo, Welt! erscheint - wir haben soeben einen Schritt in eine größere Welt getan!
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

4. Unser erstes Grafik-Programm

Beitrag von bbock »

Um den Appetit auf C und das z88dk noch etwas anzuregen, erstellen wir ein erstes kleines Grafik-Programm, bevor wir uns dem Aufbau von C-Programmen widmen.

In unserem Verzeichnis C:\dev erstellen wir ein neues Projektverzeichnis namens graphic_hello. Die Länge des Verzeichnisnamens ist kein Problem; insbesondere bei der ausführbaren COM-Datei sollten wir uns aber an das 8.3-Schema halten, also max. 8 Zeichen für den Dateinamen und max. 3 Zeichen für die Extension.

Im Editor schreiben wir den Quellcode unseres Programms (s.u.) und speichern ihn unter dem Namen hellogfx.c im Verzeichnis graphic_hello.

Code: Alles auswählen

#include <stdio.h>
#include <graphics.h>

int main(void)
{
    clg();
    printf("%cH", 27); // ESC H = VT100 cursor home
    printf("\n\n  graphics demo");
    
    plot(360, 128);
    
    draw(10, 10, 710, 10);
    drawto(710, 250);
    drawto(10, 250);
    drawto(10, 10);
    
    circle(360, 128, 80, 10);

    getchar();
    clg();
    printf("%c1", 27); // ESC 1 = switch on status bar
    printf("%ce", 27); // ESC e = show cursor
    
    return 0;
}
In der Eingabeaufforderung wechseln wir in das Projektverzeichnis graphic_hello und erzeugen das ausführbare Programm mit der Befehlszeile

Code: Alles auswählen

zcc +cpm -subtype=pcw80 -create-app hellogfx.c -o hellogfx.com
Das erzeugte ausführbare Programm hat den Dateinamen HELLOGFX.COM. Wir kopieren die Datei in das ramdisk-Verzeichnis, wechseln zur CP/M Box und starten das Programm durch Eingabe von hellogfx.

hellogfx.png
hellogfx.png (5.26 KiB) 4467 mal betrachtet

Was passiert in diesem Programm?
  • Zuerst wird der Grafik-Bildschirm mit clg() gelöscht.
  • Der Cursor wird in die linke obere Ecke gesetzt und es wird eine Überschrift ausgegeben (graphics demo).
  • Es wird ein Punkt in der Mitte des Bildschirms gezeichnet (plot).
  • Ein Rahmen wird mit vier Linien gezeichnet (draw, drawto).
  • Ein Kreis wird in die Mitte gezeichnet.
  • Das Programm wartet auf einen Tastendruck (getch).
  • Die Statuszeile, also die unterste Zeile, wird eingeschaltet (falls sie ausgeschaltet war).
  • Der Cursor wir angezeigt (falls er ausgeschaltet war).
  • Der Return Code 0 wird an das Betriebssystem zurückgegeben um zu signalisieren, dass kein Fehler aufgetreten ist.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

5. Die Struktur von C-Programmen

Beitrag von bbock »

C-Programme bestehen aus Dateien mit den Endungen .c (C-Quellcode) und .h (Header-Dateien). Die Header-Dateien enthalten die Schnittstellen um Funktionen und weitere Definitionen aus anderen C-Dateien verfügbar zu machen. Dies geschieht per #include-Anweisung. Die Definition der Programme, also die eigentliche Logik, befindet sich aber in den C-Dateien selbst.

Alle C-Programme bestehen aus Funktionen. Prozeduren gibt es nicht; stattdessen gibt es Funktionen, die keinen Wert zurückliefern. Diese haben dann den Typ void. Eine besondere Funktion hat einen festgelegten Namen und stellt den Einsprungpunkt für ein C-Programm dar: die Funktion main. Sie muss in jedem C-Programm vorhanden sein und hat folgende Signatur:

Code: Alles auswählen

int main(int argc, char *argv[])
Die Parameter argc und argv sind für den Zugriff auf die Kommandozeilenparameter des Programms zuständig; dazu später mehr. Werden keine Kommandozeilenparameter benötigt, dann kann man die main-Funktion auch so schreiben:

Code: Alles auswählen

int main(void)
Weitere Funktionen können in derselben oder in einer weiteren C-Datei geschrieben werden. Alle Funktionen haben den generellen Aufbau

Code: Alles auswählen

Rückgabetyp Funktionsname(Parameterliste) {
  ...
}
Innerhalb der geschweiften Klammern stehen die Anweisungen, die die Funktion ausmachen. Die folgende Funktion wandelt z.B. eine Temperatur von Grad Fahrenheit in Grad Celsius um:

Code: Alles auswählen

float fahrenheitToCelsius(float fahrenheit) {
  return (fahrenheit - 32.0) / 1.8;
}
Hier wird als Parameter eine Zahl vom Typ float, also eine Fließkommazahl, übergeben. Auch der Rückgabewert ist vom Typ float; er wird berechnet und mit dem Befehl return zurückgegeben.

Ein Beispielprogramm, dass die Funktion verwendet könnte wie folgt aussehen. Wir speichern den Programmtext in einem neuen Projektverzeichnis namens fahrenheit unter dem Dateinamen temperat.c:

Code: Alles auswählen

#include <stdio.h>

float fahrenheitToCelsius(float fahrenheit) {
  return (fahrenheit - 32.0) / 1.8;
}

int main(void) {
    float f, c;

    f = 72.0;
    c = fahrenheitToCelsius(f);
    
    printf("%f Grad Fahrenheit sind %f Grad Celsius\n", f, c);
    
    return 0;
}
Dann erzeugen wir das ausführbare Programm mit

Code: Alles auswählen

zcc +cpm -subtype=pcw80 -lm temperat.c -o temperat.com
Anschließend kopieren wir temperat.com in das ramdisk-Verzeichnis der CP/M Box und starten es. Die Ausgabe lautet
72.000000 Grad Fahrenheit sind 22.222222 Grad Celsius.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

6. Variablen und Datentypen

Beitrag von bbock »

C kennt folgende elementare Datentypen:

char: ein einzelnes Zeichen
short: ein kleiner, ganzzahliger Wert
int: ein ganzzahliger Wert
long: ein großer, ganzzahliger Wert
float: ein Gleitkomma-Wert
double: ein Gleitkomma-Wert mit doppelter Genauigkeit

Die ganzzahligen Datentypen können mit Vorzeichen oder (mit dem Schlüsselwort unsigned) ohne Vorzeichen verwendet werden. Die Größen der elementaren Datentypen sind in C - anderes als etwa in Java - maschinenabhängig. Für unsere Joyce gelten folgende Angaben:

char: 1 Byte, -128 bis 127
unsigned char: 1 Byte, 0 bis 255
short: 2 Bytes, -32768 bis 32767
unsigned short: 2 Bytes, 0 bis 65535
int: 2 Bytes, -32768 bis 32767
unsigned int: 2 Bytes, 0 bis 65535
long: 2 Bytes, -32768 bis 32767
unsigned long: 2 Bytes, 0 bis 65535
float: 6 Bytes, 1.000E-38 bis 9.995E+37 (6 Dezimalstellen)
double: 6 Bytes, 1.000E-38 bis 9.995E+37 (6 Dezimalstellen)

Die Angaben können je nach verwendeter math library abweichen. Das z88dk kennt mehrere davon; im Beispielprogramm temperat.c haben wir die Standard-Mathematik-Bibliothek mit -lm eingebunden. Sie verwendet ein 6-Byte-Format mit 40 Bit Mantisse und 8 Bit Exponent. Man sieht insbesondere, dass sich die Datentypen short, int und long nicht unterscheiden, genausowenig wie float und double.

Neben den elementaren Datentypen gibt es noch:
  • void (die große Leere, also gar nichts)
  • Aufzählungstypen (enumerations, C-Schlüsselwort enum)
  • Datenfelder (arrays)
  • Strukturen (C-Schlüsselwort struct)
  • Unions (C-Schlüsselwort union)
  • Zeigertypen (pointer)
  • Funktionstypen
Es gibt in C keinen Datentyp "String", dennoch gibt es natürlich strings und auch eine ganze library mit Funktionen dafür. Allerdings sind C-Strings einfach nur Datenfelder von Zeichen, bei denen das letzte Zeichen das Nullzeichen '\0' ist. Wie man damit umgeht, folgt in einem späteren Kapitel.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

7. Ein- und Ausgabe

Beitrag von bbock »

Ein zentrales Thema bei der Verarbeitung von Computerprogrammen ist die Ein- und Ausgabe von Daten. Wir legen ein neues Projektverzeichnis mit dem Namen einaus an, in dem wir folgenden Quellcode als einaus.c speichern:

Code: Alles auswählen

#include <stdio.h>

#define PI 3.141592654

int main(void) {
    float radius, flaeche;
    
    printf("Kreisflaeche berechnen\n\nRadius: ");
    scanf("%f", &radius);
    flaeche = PI * radius * radius;
    printf("\nDie Flaeche betraegt: %f\n", flaeche);
    
    return 0;
}
Wir erzeugen das ausführbare Programm mit

Code: Alles auswählen

zcc +cpm -subtype=pcw80 -lm einaus.c -o einaus.com
Es ist an der Zeit ein paar Worte über das Kompilieren und Binden zu verlieren. zcc ist nicht der Compiler, sondern das Compiler Frontend, das seinerseits einen C-Compiler aufruft. Das z88dk hat gleich zwei zur Auswahl: den klassischen Compiler sccz80 und den moderneren Compiler zsdcc. Über die zcc-Option -compiler=sdcc können wir zsdcc auswählen; mit -compiler=sccz80 wird der klassische Compiler verwendet, der auch der Standard ist, wenn wir die Option -compiler weglassen.

Die Option +cpm wählt die Plattform aus, für die der Code erzeugt werden soll (CP/M). Dazu geben wir den Untertyp pcw80 über die Option -subtype an. pcw40 wäre genausogut möglich; der Unterschied ist nur das Diskettenformat der bei Verwendung der zusätzlichen Option -create-app erzeugten Image-Datei, die automatisch die Endung .dsk bekommt. Beide pcw-Optionen erzeugen Code für die Amstrad PCW-Rechner.

zcc kompiliert nicht nur, sondern kann auch gleich den Linker aufrufen um eine ausführbare COM-Datei zu erstellen, was wir ja bereits fleißig nutzen. Mit -l kann man zusätzliche Bibliotheken (libraries) angeben, die zum Programm dazugebunden werden. Hier wird mit -lm die Standard-Mathematik-Library dazugebunden, die wir für die float-Verarbeitung brauchen.

Dann folgt die zu kompilierende C-Datei, hier einaus.c. An dieser Stelle kann auch eine Liste von mehreren C-Dateien stehen, von denen aber nur eine eine main-Funktion enthalten darf. Über die Option -o können wir schließlich den Namen der Ausgabedatei angeben, hier einaus.com.

Nun zum Programm einaus:

Der #include-Befehl ist eine sog. Präprozessor-Direktive, wie alle Befehle, die am Zeilenanfang ein Hash-Symbol (#) haben. Wichtig: das # muss ganz am Anfang der Zeile stehen! Der Präprozessor verarbeitet alle Quellcode-Dateien, bevor der Compiler zum Zuge kommt. Er liest nur die Zeilen, die mit # führt dann bestimmte Textersetzungsoperationen durch. Bei #include fügt er genau an dieser Stelle den Inhalt der Datei an, die hinter der #include-Direktive aufgeführt ist. Steht der Dateiname in spitzen Klammern wie hier, dann wird die Datei im include-Verzeichnis des Compilers gesucht. Steht er in Gänsefüßchen, dann wird sie im selben Verzeichnis gesucht, in dem sich auch die Datei befindet, die die #include-Direktive enthält.

Die #define-Direktive wird oft für die Definition von Konstanten verwendet, hier für die Kreiszahl Pi. Der Präprozessor sucht in der Folge alle Textstellen, in denen "PI" vorkommt und ersetzt sie durch "3.141592654". Das funktioniert wie beim Suchen / Ersetzen im Editor.

Es folgt die Funktion main, also der Einsprungpunkt in das Programm. In unserem Beispiel werden keine Parameter von der Kommandozeile gebraucht, also steht zwischen den Klammern das Schlüsselwort void, was so viel wie "nichts" bedeutet.

Dann folgt einen geöffnete geschweifte Klammer. Alle Anweisungen bis zur zugehörigen schließenden geschweiften Klammer bilden den Funktionsblock. Jede Anweisung wird in C mit einem Semikolon beendet. Das ist anders als in Pascal, wo das Semikolon zwischen Anweisungen steht.

Zu Beginn des Funktionsblocks werden zwei Variablen vom Typ float deklariert: radius und flaeche. Dann geben wir Text über die Funktion printf aus. printf ist eine Abkürzung für "print formatted", weil sie die Ausgabe von formatiertem Text unterstützt. Der Text besteht aus der Überschrift "Kreisflaeche berechnen", dann folgen zwei Zeilenumbrüche (\n) und die Eingabeaufforderung "Radius: ". Die Ausgabe erscheint auf dem Bildschirm.

Wir wollen eine Gleitkommazahl eingeben; dazu dient die Funktion scanf ("scan formatted"). scanf kann mehrere Parameter haben. Der erste ist immer der sog. Format-String: "%f" steht hier für "float" - wir wollen also einen Gleitkomma-Wert von der Tastatur "scannen". Die folgenden Parameter geben für jede mit dem Prozentzeichen eingeleitete Formatangabe jeweils eine Variable an, in die die eingegebenen Daten gespeichert werden sollen. Anders als bei printf werden die Daten aber nicht aus einer Variablen gelesen, sondern sie sollen in eine Variable geschrieben werden. Dazu muß scanf die Adresse der Variablen kennen. Als zweiten Parameter haben wir daher nicht radius, sondern &radius. Der &-Operator ermittelt die Adresse der Variablen im Speicher des Rechners.

Die flaeche wird mit der bekannten Kreisformel berechnet. Ich möchte noch darauf hinweisen dass der Präprozessor aus dem "flaeche = PI * radius * radius;" bereits ein "flaeche = 3.141592654 * radius * radius;" gemacht hat. Das ist der Code, den der Compiler "sieht".

Die Fläche wird mit printf ausgegeben, wobei auch hier eine Formatangabe "%f" verwendet wird, um eine Gleitkommazahl auszugeben. An Stelle des "%f" wird der Wert der Variablen flaeche eingesetzt, der printf als zweiter Parameter übergeben wurde.

Schließlich geben wir via return eine 0 an das Betriebssystem CP/M zurück und signalisieren damit, dass kein Fehler aufgetreten ist.

Wenn wir das Programm einaus.com starten, dann erscheint die Aufforderung zur Eingabe des Radius. Geben wir z.B. 12.5 ein, dann erscheint das Ergebnis: "Die Flaeche betraegt: 490.873852".
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

8. Kontrollstrukturen

Beitrag von bbock »

8.1 Anweisungen und Blöcke

Einzelne Anweisungen werden in C immer mit einem Semikolon beendet, z.B.

Code: Alles auswählen

x = 0;
Eine oder mehrere Anweisungen werden mit geschweiften Klammern zu Blöcken zusammengefasst. Der Programmcode von Funktionen ist etwa immer in einem Block zusammengefasst, aber auch Befehlssequenzen von Kontrollanweisungen wie if, else, while oder for.

8.2 if-Anweisungen

Die allgemeine Syntax von if-Anweisungen ist:

Code: Alles auswählen

if (Ausdruck)
    Anweisung1
else
    Anweisung2
Anweisung1 und Anweisung2 können dabei einzelne Statements oder Blöcke sein. Das else ist optional. Beispiele:

Code: Alles auswählen

if (a > b)
    z = a;
else
    z = b;

Code: Alles auswählen

if (r > 10) {
    printf("r darf max. 10 sein.\n");
    r = 10;
}
8.3 else-if Ketten

Code: Alles auswählen

if (Ausdruck1)
    Anweisung1
else if (Ausdruck2)
    Anweisung2
else if (Ausdruck3)
    Anweisung3
else
    Anweisung4
Dies ist eine häufig anzutreffende Konstruktion. Hier wird ein Ausdruck nach dem anderen ausgewertet. Ist ein Ausdruck true, dann wird die zugehörige Anweisung ausgeführt. Trifft keine der Bedingungen zu, d.h. alle Ausdrücke ergeben false, dann wird der else-Zweig ausgeführt.

8.4 switch

Mit switch kann bequem eine von mehreren Alternativen ausgewählt werden, wobei mit Konstanten verglichen wird. Beispiel einer Menüauswahl:

Code: Alles auswählen

char c;
c = getMenuSelection();

switch (c) {
case 'x':
case 'X':
    cleanup();
    exitApp = true;
    break;
case 'd':
case 'D':
    drawPicture();
    break;
default:
    printf("ungültige Option\n");
}
Hier wird die Benutzerauswahl im Menü als Zeichen zurückgeliefert. Bei kleinem oder großem X soll die Anwendung beendet werden, bei kleinem oder großem D wird ein Bild gezeichnet. Andernfalls erscheint eine Fehlermeldung.

Wichtig sind die break-Befehle, denn ohne sie würde der Code einfach zum nächsten case "durchfallen". So passiert bei der Eingabe eines kleinen x zunächst nichts und der Code des nächsten case (mit dem großen X) wird ausgeführt. Dessen Code endet aber mit break, so dass anschließend hinter der geschweiften Klammer zu fortgesetzt wird.

Der default-Fall wird ausgeführt, wenn keine der case-Anweisungen einen Treffer liefert oder wenn im darüberstehenden case nicht mit break abgeschlossen wurde.

8.5 Schleifen - do, while und for

C kennt drei Konstrukte zur wiederholten Ausführung von Anweisungen. Die do-Schleife wird immer mindestens einmal durchlaufen; die Prüfung der Abbruchbedingung erfolgt in der while-Anweisung am Ende. Bei der while-Schleife ist es genau umgekehrt: die Abbruchbedingung wird gleich zu Beginn geprüft. Dadurch kann es sein, dass der Schleifenblock überhaupt nicht durchlaufen wird. Die for-Schleife schließlich ist ideal für Zählvorgänge, bei der eine Schleifenvariable hoch- oder heruntergezählt wird.

Beispiel do-Schleife:

Code: Alles auswählen

do {
    c = getMenuSelection();
    performFunction(c);
} while (c != 'x' && c != 'X');
Die Menüauswahl wird ermittelt und eine dazu passende Funktion via performFunction ausgeführt. Das geschieht so oft, bis ein X eingegeben wurde (unabhängig von der Groß-/Kleinschreibung). performFunction wird auch einmal mit X aufgerufen und muss das berücksichtigen.

Beispiel while-Schleife:

Code: Alles auswählen

c = '.';
i = 0;
while (c != '\0') {
    c = s[i++];
    if (c != '\0')
        putchar(c);
}
Alle Zeichen im Array s werden nacheinander ausgegeben, bis ein Null-Zeichen erreicht wird. Das entspricht der Ausgabe eines Strings, wie er ähnlich auch in der Funktion puts aus der C-Standard-Ein-Ausgabe-Bibliothek zu finden sein dürfte. Unschön ist hier allerdings die doppelte Auswertung der Bedingung c != '\0'.

Beispiel for-Schleife:

Code: Alles auswählen

for (i = 0; i < 10; i++) {
    printf("%d^2 = %d\n", i, i * i);
}
Eine Liste der Zahlen 1 bis 9 und deren Quadratzahlen wird ausgegeben:
0^2 = 0
1^2 = 1
2^2 = 4
...
9^2 = 81

Schleifen können durch die Anweisung break vorzeitig abgebrochen werden. Die Anweisung sorgt dafür, dass die innerste Schleife (oder eine switch-Anweisung) sofort verlassen wird.

Beispiel String-Ausgabe:

Code: Alles auswählen

i = 0;
while (true) {
    c = s[i++];
    if (c == '\0')
        break;
    putchar(c);
}
Mit der Anweisung continue leitet sofort die nächste Wiederholung der Schleife ein ohne den darauffolgenden Code auszuführen.

Im folgenden Beispiel werden nur die positiven Elemente eines Arrays bearbeitet:

Code: Alles auswählen

for (i = 0; i < N; i++) {
    if (a[i] < 0)
        continue; /* negative Elemente überspringen */
    ...           /* positive Elemente bearbeiten */
}
Die Anweisung goto sollte möglichst nicht verwendet werden, da sie leicht zu unübersichtlichem "Spaghetti-Code" führt und den Grundsätzen der strukturierten Programmierung widerspricht. Dennoch gibt es einen Fall, in dem goto nützlich sein kann: das Beenden einer tiefer verschaltelten Schleife. Die break-Anweisung beendet nur die innerste Schleife. Das Verlassen einer z.B. doppelt verschachtelten Schleife kann man mit goto zu einer Sprungmarke bewerkstelligen:

Code: Alles auswählen

for (...) {
    for (...) {
        ...
        if (Katastrophe)
            goto error;
    }
    ...
}
error:
    ...
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

8.5 Schleifen (Fortsetzung)

Beitrag von bbock »

Wir schreiben ein Programm zur Berechnung einer Primzahlenliste:

Code: Alles auswählen

#include <stdio.h>

int main(void)
{
    int z, d;
    char prim;
    int grenze;
    int count;

    printf("Bis zu welcher Zahl sollen die Primzahlen ermittelt werden? ");
    scanf("%d", &grenze);
    printf("\nPrimzahlen bis %d:\n", grenze);
    count = 0;
    
    printf("  2 ");
    
    for (z = 3; z < grenze; z += 2) {
        prim = 'Y';
        
        for (d = 3; d < z / 2; d += 2) {
            if (z % d == 0) {
                prim = 'N';
                break;
            }
        }
        
        if (prim == 'Y') {
            printf("%4d ", z);
        }
    }
    
    return 0;
}
Wir kompilieren und binden mit:

Code: Alles auswählen

zcc +cpm -subtype=pcw80 prim.c -o prim.com
In der Funktion main werden zunächst die benötigten Variablen deklariert. Dann erfolgt die Eingabe der Obergrenze, bis zu der Primzahlen ausgegeben werden sollen. Das geschieht wieder mit der C-Funktion scanf; zur Erinnerung: wir müssen hier die Adresse der Variablen grenze übergeben, also &grenze.

Die erste Primzahl - die 2 - geben wir immer aus (was zu einer kleinen Unstimmigkeit führt, wenn wir als Obergrenze 0 oder 1 eingeben). Dann durchlaufen wir mit der Variablen z alle Zahlen bis zur Obergrenze in Zweierschritten. Wir prüfen bis z / 2, ob die Zahl durch d teilbar ist. d beginnt mit 3 und wird ebenfalls in Zweierschritten hochgezählt. Dann prüfen wir, ob die Division von z durch d einen Rest ergibt (Modulo-Division); falls nein, dann ist die Zahl z keine Primzahl und wir beenden die innere Schleife mit break.

Wurde die innere for-Schleife durchlaufen, ohne dass wir einen Teiler gefunden haben, dann ist die Zahl eine Primzahl und wir geben sie aus.

Die for-Schleifen haben drei Ausdrücke innerhalb der Klammern, die den Ablauf bestimmen. Wir sehen uns das am Beispiel der z-Schleife an:

for (z = 3; z < grenze; z += 2) {

Der erste Ausdruck ist die Initialisierung. Sie wird einmalig zu Beginn der Schleife durchgeführt. Hier wird die Variable z mit 3 initialisiert. Im ersten Schleifendurchlauf hat z also den Wert 3.

Der zweite Ausdruck ist die Schleifenfortsetzungsbedingung, also das Gegenteil der Abbruchbedingung. Solange die Bedingung erfüllt ist, läuft die Schleife.

Der dritte Ausdruck wird am Ende jedes Schleifendurchlaufs ausgeführt. Oft wird hier die Schleifenvariable hoch- oder heruntergezählt.

Jeder der drei Ausdrücke ist optional. Es können auch alle drei Ausdrücke weggelassen werden; in dem Fall erhält man eine Endlosschleife (die natürlich innerhalb des Schleifenkörpers mit break oder return verlassen werden kann). Die Semikoli müssen aber bleiben.

Da wir drei Varianten für die Formulierung von Schleifen in C kennen, gibt es auch drei Möglichkeiten eine Endlosschleife zu schreiben:

1. for (;;) {...}
2. while (true) {...
3. do {...} while (true);
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

9 Operatoren

Beitrag von bbock »

9.1 Arithmetische Operatoren

Im Programm prim wurden Operatoren verwendet, die noch erläutert werden müssen. Die Grundrechenarten sind + (plus), - (minus), * (mal) und / (geteilt durch). Es gibt sie sowohl für Gleitkommazahlen wie auch für Ganzzahlen. Bei der Ganzzahldivision werden die Nachkommastellen abgeschnitten; es wird dabei nicht gerundet. Den Rest einer Ganzzahldivision kann man mit dem Modulo-Operator % ermitteln. In C gilt "Punkt- vor Strichrechnung", d.h. *, / und % haben Vorrang vor + und -. So ist 3 + 2 * 5 nicht 25, sondern 13, weil 2 * 5 zuerst berechnet wird. Möchte man eine andere Berechnungsreihenfolge haben, dann muss man Klammern setzen. (3 + 2) * 5 ergibt folglich 25.

9.2 Vergleiche und logische Verknüpfungen

Die Vergleichsoperatoren sind > (größer als), >= (größer oder gleich), < (kleiner als), <= (kleiner oder gleich) und == (gleich). Vorsicht: ein häufiger Programmierfehler ist das Verwechseln des Zuweisungsoperators = mit dem Vergleichsoperator ==.

Für logische Verknüpfungen werden die Operatoren && (und) und || (oder) verwendet. Dies sind die logischen Operatoren, die man nicht mit den bitweise Verknüpfungen & und | verwechseln sollte.

Beispiel: if (a > 5 && b <= 7) printf("passt!");

9.3 Bit-Manipulationen

In C kann man ganzzahlige Variablen bitweise manipulieren. Folgende Operatoren stehen zur Verfügung:

& - und-Verknüpfung von Bits
| - oder-Verknüpfung von Bits
^ - exklusive oder-Verknüpfung von Bits (XOR)
<< - Bit-Verschiebung nach links
>> - Bit-Verschiebung nach rechts
~ - Bit-Komplement (Einer-Komplement)

Beispiele:
  • c = n & 0177; löscht alle Bits in n bis auf die letzten sieben (0177 ist eine oktale Zahlkonstante und entspricht der Bitfolge 01111111)
  • x = x | MASK; setzt in x genau die Bits, die in MASK auf 1 gesetzt sind
9.4 Inkrement und Dekrement

Ganzzahl- und char-Variablen können inkrementiert und dekrementiert, d.h. um eins erhöht oder erniedrigt werden. Außerdem gibt es in C abkürzende Schreibweisen für bestimmte Operationen:

Code: Alles auswählen

#include <stdio.h>

int main(void)
{
    int i;

    puts("i ist zu Beginn immer gleich 5");
    
    i = 5;
    ++i;
    printf("++i -> %d\n", i); // ergibt 6

    i = 5;
    --i;
    printf("--i -> %d\n", i); // ergibt 4

    i = 5;
    i += 2; // entspricht i = i + 2;
    printf("i += 2 -> %d\n", i); // ergibt 7

    i = 5;
    i -= 2; // entspricht i = i - 2;
    printf("i -= 2 -> %d\n", i); // ergibt 3

    i = 5;
    i *= 2; // entspricht i = i * 2;
    printf("i *= 2 -> %d\n", i); // ergibt 10

    i = 5;
    i /= 2; // entspricht i = i / 2;
    printf("i /= 2 -> %d\n", i); // ergibt 2 (eigentlich 2.5, aber bei Ganzzahlen werden die Nachkommastellen abgeschnitten)

    i = 5;
    i %= 2; // entspricht i = i % 2;
    printf("i %%= 2 -> %d\n", i); // ergibt 1
    
    return 0;
}
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

10. Pointer, Array, String

Beitrag von bbock »

10.1 Pointer

Alle Daten, die ein Computer verarbeitet, werden in Speicherzellen abgelegt, auf die der Mikroprozessor über deren Adressen zugreifen kann. Man kann sich das wie eine lange Straße vorstellen, bei der die aneinandergereihten Häuser die Speicherzellen darstellen; die Adressen entsprechen dann den Hausnummern. Bei Byte-Maschinen - und zu denen zählt die Joyce - besteht eine Speicherzelle aus acht Bit oder einem Byte. Der Z80-Prozessor kann 64 kByte, also 65.535 einzelne Adressen verwalten. Das sind 2 hoch 16 Adressen; daraus folgt, dass eine Adresse 16 Bit lang ist.

char ist ein Datentyp, der aus einem einzelnen Byte besteht. Eine Variable, die als char ch deklariert wurde, kann also ein Byte speichern. Greift man im Programmcode auf ch zu, dann ist der Inhalt dieses Bytes gemeint, von dem der C-Compiler weiß, wo im Speicher es sich befindet. Wenn wir wissen müssen, wo im Speicher sich ch befindet, z.B. um ch mit der C-Funktion scanf zu füllen, dann müssen wir die Adresse von ch ermitteln. Wir haben das bereits bei der Verwendung von scanf erfahren: der &-Operator ermittelt die Adresse einer Variablen, d.h. &ch liefert die 16-Bit-Adresse der Variablen ch.

Speichert man diese Adresse wiederum in einer Variablen ab, dann nennt man diese Variable einen Zeiger oder englisch Pointer. Pointer-Variablen werden mit einem Stern und dem Typ der Ziel-Variablen gekennzeichnet. Um einen Pointer für ch zu deklarieren, schreiben wir z.B. char *p;. Damit p auf ch zeigt, weisen wir p die Adresse von ch zu: p = &ch;. Um nun auf das Zeichen in ch über den Pointer zuzugreifen, müssen wir den Pointer "dereferenzieren". Das geschieht über den *-Operator, z.B. putchar(*p);.

Ein kleines Beispielprogramm soll das veranschaulichen:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    char ch;
    char *p;
    
    ch = 'X';    // ch enthaelt jetzt das Zeichen 'X'
    putchar(ch); // gibt X auf der Konsole aus
    p = &ch;     // p erhaelt die Adresse von ch, d.h. p zeigt auf ch
    *p = 'Y';    // das Zeichen, auf das p zeigt, also ch, wird auf 'Y' gesetzt
    putchar(*p); // gibt Y auf der Konsole aus
    putchar(ch); // gibt ebenfalls Y auf der Konsole aus, da ch ueber p veraendert wurde
    
    return 0;
}
10.2 Array

Arrays sind Speicherstrukturen mit mehreren Elementen gleichen Typs; auf die Elemente kann man über einen Index zugreifen. Es gibt eindimensionale Arrays, die auch als Vektoren bezeichnet werden, und mehrdimensionale Arrays. Die Anzahl der Dimensionen bestimmt, wieviele Indizes man benötigt, um auf ein einzelnes Element zuzugreifen.

Einen Array mit fünf int-Elementen deklariert man etwa so: int a[5];. Damit erhält man eine Array-Variable a, deren Elemente man über die Indizes 0 bis 4 erreicht: a[0], a[1], ..., a[4]. Der kleinste Index ist in C-Arrays immer 0, der größte ist die Anzahl der Elemente minus eins. Da die Indizes auch über Variablen bereitgestellt werden können, hat man damit eine sehr bequeme Speicherstruktur für den Zugriff auf gleichartige Elemente.

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    int a[5];
    int i;
    
    // Array mit Vielfachen von 3 belegen
    for (i = 0; i < 5; i++) {
        a[i] = i * 3;
    }
    
    // Array ausgeben
    for (i = 0; i < 5; i++) {
        printf("Zahl an Index %d: %d\n", i, a[i]);
    }

    return 0;
}
Das Programm gibt folgendes aus:

Zahl an Index 0: 0
Zahl an Index 1: 3
Zahl an Index 2: 6
Zahl an Index 3: 9
Zahl an Index 4: 12

In C sind Arrays gleichzeitig Pointer-Strukturen. a ist also in unserem Beispiel ein int-Pointer. Um das erste Element, also das Element mit dem Index 0, auf den Wert 17 zu setzen, schreiben wir normalerweise a[0] = 17;. Genausogut können wir aber auch *a = 17; schreiben, da a eben auch ein Pointer ist. Um das vierte Element auf 21 zu setzen, schreiben wir a[3] = 21; oder *(a + 3) = 21;.

Die Pointer-Schreibweise ist noch erklärungsbedürftig. a ist ein Pointer; die Variable enthält also die Adresse des Arrays bzw. die Adresse des Elements mit dem Index 0. (a + 3) ist die Adresse des Elements mit dem Index 3, wobei aber nicht 3, sondern 6 zur Adresse des Arrays addiert wird, denn a ist vom Typ int und ein int beansprucht 2 Bytes Speicherplatz. Die Multiplikation mit der Typgröße macht der Compiler automatisch. Bei Zeigerarithmetik muss man immer beachten, von welchem Typ die Pointer sind. Die Pointer-Variablen selbst belegen dagegen immer 2 Bytes, weil jede Adresse 16 Bits = 2 Bytes lang ist, egal, ob der Pointer auf einen char, einen int oder sonst etwas zeigt.
Antworten