- C-Kurs
- C-Kurs: Exkurse
Nachdem die letzte Folge ja ziemlich gespickt mit Theorie war, wollen wir heute mal langsam in Richtung spannendere Sachen gehen. Dazu gibt's - wie immer - erstmal ein kleines Programm. Danach sehen wir uns mal an, wie man Funktionen selbst umsetzt. Eine praktische Kontrollstruktur gibt's dann noch und zum Schluss schauen wir dem Compiler auf die Finger.
Wir berechnen mit einem nicht-so-tollen Algorithmus die Primzahlen zwischen 3 und 999. Auf den Algorithmus kommt es hier aber nicht so an.
Wie sieht das denn in beststrukturiertem BASIC aus:
100 FOR ZAHL = 3 TO 1000 STEP 2 110 : GOSUB 200 120 : IF (PRIM) THEN PRINT "PRIMZAHL: "; ZAHL 130 NEXT 140 END 150 : 200 IF (ZAHL = 3) THEN PRIM = -1 : RETURN 210 FOR DIVISR = 3 TO (ZAHL / 2) STEP 2 220 : MOD = (ZAHL - INT(ZAHL / DIVISR) * DIVISR) 230 : IF (MOD = 0) THEN PRIM = 0 : RETURN 240 NEXT 250 PRIM = -1 : RETURN
Die letzte berechnete Primzahl erscheint nach etwa 350 Sekunden. Mhhh, das kann BASIC auch schneller, wenn man ein bisschen optimiert (gleicher Algorithmus!):
1 FORZ=3TO1000STEP2 2 H=Z/2:IFZ=3GOTO7 4 FORD=3TOHSTEP2 5 IFINT(Z-INT(Z/D)*D)=0THEN8 6 NEXTD 7 PRINT"PRIMZAHL: ";Z 8 NEXTZ
Mit diesem Kunstwerk schaffen wir schon etwa 290 Sekunden.
Und jetzt exakt der gleiche Algorithmus (gestreckt mit richtig viel Kommentar) in C:
#include <stdio.h> /******************************************************************************/ /* * Ueberpruefe, ob die uebergebene Zahl eine Primzahl ist. * * Parameter: * n Zahl * Return: * 0 => Zahl ist keine Primzahl * sonst => Zahl ist Primzahl */ unsigned char istPrimzahl(unsigned int n) { unsigned int divisor; unsigned int testEnde = n / 2; /* Alle potentiellen Teiler bis zur Mitte testen */ for (divisor = 3; divisor < testEnde; divisor += 2) { /* Mit Rest 0 teilbar? */ if (n % divisor == 0) { /* Ueberprüfung abbrechen, keine Primzahl */ return 0; } } /* Kein Test durchgefallen, ist eine Primzahl */ return 1; } /******************************************************************************/ /* * Hauptprogramm */ int main(void) { unsigned int zahl; /* Von 3 beginnend jede zweite Zahl testen, bis unter 1000 */ for (zahl = 3; zahl < 1000; zahl += 2) { if (istPrimzahl(zahl)) { printf("Primzahl: %u\n", zahl); } } }
Diesmal ist die Berechnung schon nach etwa 33 Sekunden abgeschlossen. Also ca. zehn mal so schnell wie der gleiche Algorithmus in BASIC.
In Assembler kann man mit dem gleichen Algorithmus vermutlich eine Laufzeit von weniger als 10 Sekunden erreichen, aber ich habe gerade keine Lust, dass zu implementieren
Die optimalsten Ergebnisse bekommt man zwar in Assembler; aber der Zeitaufwand ist im Mittel wesentlich größer (falls man beide Sprachen gleich gut beherrscht).
Was wäre Programmieren ohne if-Abfrage? Nüschts richtiges. Also packen wir's an:
if (Bedingung) Anweisung
oder:
if (Bedingung) Anweisung else Anweisung
Wie bei for wird auch hier abgeprüft, ob die Bedingung ungleich 0 ist. In dem Fall wird die Anweisung hinter if ausgeführt. Ist die Bedingung aber gleich 0, wird der else-Zweig ausgeführt. Wenn es einen gibt, versteht sich.
Die Anweisung kann hier auch wieder ein Block in geschweiften Klammern sein, der aus mehreren Anweisungen besteht.
In C werdet ihr oft über solche Konstrukte stolpern:
if (i)
was inhaltlich das gleiche ist wie:
if (i != 0)
Wir erinnern uns: Der Operator != bedeutet ungleich. Wenn beide Seiten ungleich sind, gibt dieser Operator eine „1“ zurück.
Ohne Optimierung kann ein Compiler unter Umständen für die zweite Variante längeren und langsameren Code erzeugen. Ein optimierender Compiler erzeugt normalerweise in beiden Fällen den gleichen Code. Wie es mit dem cc65 aussieht, sehen wir uns heute noch an.
Und hier die böse Falle des Tages:
if (a) if (b) { /* block 1 */ } else { /* block 2 */ }
Auch wenn es hier bösartigerweise anders eingerückt ist: Das else gehört immer zum letzten if. Mit geklammerten Blöcken wäre uns das nicht passiert:
if (a) { if (b) { /* block 1 */ } else { /* block 2 */ } }
Oder, falls doch die andere Bedeutung gemeint ist:
if (a) { if (b) { /* block 1 */ } } else { /* block 2 */ }
In unserem Beispiel ist er Euch vielleicht schon aufgefallen, der Operator +=.
Ist eigentlich einfach:
a += 3;
macht das gleiche wie:
a = a + 3;
Diese Schreibweise könnt ihr auch für andere uns schon bekannte Operatoren anwenden. Sie heißen verbundene Zuweisung (compound assignment):
| Verbundene Zuweisung | Einfache Zuweisung und Operator |
|---|---|
| a += 3 | a = a + 3 |
| a -= 3 | a = a - 3 |
| a *= 3 | a = a * 3 |
| a /= 3 | a = a / 3 |
| a %= 3 | a = a % 3 |
Der einzige Unterschied ist, dass der Compiler den Ausdruck a bei der verbundenen Zuweisung nur einmal auswerten muss, was bei bestimmten Konstrukten und Compilern zu kürzerem und schnellerem Code führen kann.
Am Ende dieses Abends könnt ihr ja mal den cc65 daraufhin untersuchen.
Wir haben schon die Bibliotheks-Funktionen puts und printf aufgerufen. Wir haben auch schon eine selbst erstellt: main. Unser heutiges Testprogramm definiert noch eine weitere: istPrimzahl.
Am Abend1 haben wir uns schon angesehen, wie so eine Funktion aussieht:
Rueckgabetyp Name(Parameterliste) { }
Im Funktionskopf sehen wir, welchen Name die Funktion hat, mit welchen Argumenttypen wir sie aufrufen müssen und welchen Rückgabetyp sie hat. Der Funktionsrumpf (Block) umfasst den Code, der beim Aufruf der Funktion ausgeführt werden soll.
Wir wissen auch schon, dass bei einem Rückgabetyp void die Funktion nichts zurückgibt. Eine leere Parameterliste - in c durch (void) ausgedrückt - hingegen zeigt, dass die Funktion keine Argumente erwartet.
Unsere Funktion istPrimzahl hat einen Parameter vom Typ unsigned int. Das ist die Zahl, die sie prüfen soll. Sie gibt den Typ unsigned char zurück.
/* * Ueberpruefe, ob die uebergebene Zahl eine Primzahl ist. * * Parameter: * n Zahl * Return: * 0 => Zahl ist keine Primzahl * sonst => Zahl ist Primzahl */ unsigned char istPrimzahl(unsigned int n) { ... /* Ueberprüfung abbrechen, keine Primzahl */ return 0; ... /* Kein Test durchgefallen, ist eine Primzahl */ return 1; }
Mit der Anweisung return wird die Funktion beendet. Der Ausdruck hinter return gibt den Rückgabewert an. Die Ausführung geht beim Aufrufer weiter, der diesen Wert erhält und diesen nach belieben weiterverwursten kann.
Jede Funktion, die einen „richtigen“ Rückgabetyp hat, muss auch einen passenden Wert mit return zurückgeben.
Seit C99 gibt es aber für die main-Funktion eine Ausnahme: Dort darf man das return weglassen, der Compiler gibt dann beim Beenden des Programms 0 zurück.
Eine Funktion mit dem Typ void kann mit einer return-Anweisung beendet werden. Muss aber nicht. Wenn die Funktion zuende ist, also bei der letzten schließenden geschweiften Klammer, wird sie auch beendet.
Speichern wir uns mal diese Funktion in eine c-Datei:
int main(void) { unsigned char i; i = 0; if (i != 0) { i = 2; } }
Hiermit können wir cc65 veranlassen, den übersetzten Quelltext als Assembler-Listing auszugeben, ohne dass weitere Schritte zur Programmerstellung unternommen werden:
cc65 -T -o if.s if.c
Und mit folgender Zeile können wir Compiler-Optimierungen einschalten:
cc65 -O -T -o if.s if.c
Den Schalter -O könnt ihr auch beim cl65 zum Übersetzen Eurer Programme angeben.
Jetzt lasst uns doch mal den oben besprochenen Unterschied zwischen if (a != 0) und if (a) ansehen. Erstmal ohne Optimierung:
Wenn wir die Optimierung einschalten, sehen beide Übersetzungen so aus:
... ; if (i != 0) ; lda (sp),y jeq incsp1 ; ; i = 2; ; lda #$02 sta (sp),y ...
Wir sehen also, dass der optimierte Code im Vergleich zum nicht optimierten viel besser ausschaut. (Assemblerbefürworter würden vermutlich eher sagen: weniger albern.) Was das Optimierungsverfahren von cc65 kann und was nicht, und warum das so ist, schauen wir uns in einer späteren Folge an.
Wir haben ja schon gesehen, dass wir mit Hilfe von Anführungszeichen (“) Strings in unsere Programme einbauen können. Wenn wir aber nur einzelne Zeichen benötigen, müssen diese in einfache Hochkommas (') eingeschlossen werden.
'a' 'b' 'c'
Interessanterweise sind auch diese Konstanten vom Typ int. Ob wir also 'a' oder 65 schreiben, ist egal. Und außerdem lässt sich mit ints gut rechnen:
'a' + 2 // das gleiche wie 'c' '0' + 3 // das gleiche wie '3'
Bestimmte Zeichen lassen sich über sogenannte Escape-Sequenzen erreichen, z.B.:
'\'' // Hochkomma '\n' // New line (Zeilenumbruch) '\r' // Carriage return (Wagenrücklauf)
Es gibt noch weitere Schreibweisen für Zeichenkonstanten, die wir momentan noch nicht brauchen.
Die Zeichen im Quelltext des Entwicklungs-Rechners sollten beim cc65 in ISO 8859-1 kodiert sein. Das Zielsystem C64 benutzt PETSCII. Der Compiler muss also ggfs. eine Konvertierung der Zeichenkodes vornehmen. Für portierbare Programme solltet ihr am besten nur Zeichen aus dem ASCII-Bereich benutzen. Aber selbst aus diesem Bereich gibt es nicht für jedes Zeichen eine Entsprechung im C64-Zeichensatz, z.B. für '~' und '\'.
Es wird Zeit für eine neue Funktion:
int putchar(int c);
Dieser Funktion übergibt man das auszugebene Zeichen. Sie macht also fast das gleiche wie:
PRINT CHR$(C)
Wer's ganz genau wissen möchte, kann dort nachschauen:
#include <stdio.h> /******************************************************************************/ /* * Gib die uebergebene Zahl gefolgt von einem Zeilenumbruch aus. * Die Ausgabe funktioniert mindestens im Bereich 0..99, einstellige Zahlen * werden mit einem Leerzeichen eingerueckt. * * Parameter: * z Zahl * Return: * - */ void gibZahlAus(unsigned char z) { printf("%2d\n", z); } /******************************************************************************/ /* * Hauptprogramm */ int main(void) { unsigned char i; for (i = 0; i < 20; i += 1) { gibZahlAus(i); } }
Übersetzt und startet das Programm einmal. Man man man, mehr als 2600 Bytes trotz Optimierung. Wir wissen ja schon, dass der größte Batzen die in unser Programm eingebundene Funktion printf ist.
Hinweise: Es reicht, wenn die Ausgabe zwischen 0 und 99 korrekt funktioniert. Die Aufgabe lässt sich mit if, Zeichenkonstanten, und ein paar einfachen Operatoren lösen. Die Ausführbare Datei wird mit Optimierung weniger als halb so groß werden. Beachtet auch, dass eine Funktion nur einmal in's Programm eingebunden wird. Auch wenn sie im Programm mehrfach verwendet wird.
In diesem Thread im Forum64 können Fragen zu dieser Lektion gestellt werden:
http://www.forum64.de/wbb3/index.php?page=Thread&threadID=27985
In diesem Thread diskutieren Experten, was hier nicht stimmt und wie der Kurs verbessert werden kann:
http://www.forum64.de/wbb3/index.php?page=Thread&threadID=27986
Hier sind ein paar interessante Fragen aus dem oben genannten Forum:
(Noch keine)
So, das war's schon für heute, weiter geht's mit 05-abend5