- C-Kurs
- C-Kurs: Exkurse
Heute haben wir mal ein Programm, bei dem auch ein bisschen was passiert. Das ist noch ungefähr so langweilig wie ein Stapel Ziegelsteine; aber ohne diese Ziegelsteine kann man nun mal kein Haus bauen. Man bin ich heute metaphorisch drauf. Es gibt heute sehr viel zu lesen - aber wer sich hier durchknabbert, ist wieder einen großen Schritt näher an C.
#include <stdio.h> /******************************************************************************/ /* * Hauptprogramm */ int main(void) { unsigned int zahl; /* Mit 1 anfangen, dann immer verdreifachen bis unter 40000 */ for (zahl = 1U; zahl < 40000U; zahl = zahl * 3U) { printf("%5u\n", zahl); } }
Übersetzt es mit
cl65 -o dreierei.prg dreierei.c
und nach dem Starten sollte das erscheinen:
1
3
9
27
81
243
729
2187
6561
19683
Gestern haben wir uns schon den Typ int angesehen. Dieser ist mindestens 16 Bit groß. Aber manchmal ist es sinnvoll, nur 8 Bit zu verwenden - besonders auf einem 8-Bit-Prozessor. Und manchmal braucht man auch breitere Variablen.
Deshalb kennt C noch mehr numerische Typen, die wir uns heute mal ansehen.
Der kleinste unter ihnen ist char. Dieser ist mindestens 8 bit breit. Obwohl char in den meisten Implementierungen genau 8 Bit breit ist, gibt es tatsächlich Ausnahmen.
Der Standard legt nicht fest, ob char vorzeichenbehaftet (signed) ist oder nicht.
Oft spielt dieses Detail eine Rolle, z.B. bei Berechnungen. Deshalb gibt es auch die Typen signed char und unsigned char. Der erste ist vorzeichenbehaftet und hat einen Wertebereich von mindestens -128 bis 127, der zweite hat kein Vorzeichen und bildet deshalb 0 bis 255 ab.
Übrigens habe ich vor ein paar Monaten auf einem Rechner den Exomizer nicht zum Laufen bekommen, weil im Code an einer Stelle nicht zwischen signed char und unsigned char unterschieden wurde, sondern einfach char enthielt. Der Autor hat das inzwischen berichtigt. Also: Aufpassen!
Der nächstgrößere Typ ist short. Dieser ist mindestens 16 Bit groß und hat immer ein Vorzeichen. Es gibt auch einen vorzeichenlosen Typ der gleichen Breite, der konsequenterweise unsigned short heißt.
Der Typ int ist, wie wir bereits wissen, mindestens 16 Bit groß und hat immer ein Vorzeichen. Der Typ unsigned int ist genauso breit, hat aber kein Vorzeichen.
Außerdem gibt es long und unsigned long mit einer Breite von mindestens 32 Bit.
Bei der Betrachtung dieser Typen fragt ihr Euch sicher, warum es short und int gibt, wo doch beide mindestens 16 Bit breit sind. Den Grund findet man in der Definition der Typen. int stellt den Typ dar, der mindestens 16 Bit breit ist und mit dem der Prozessor am besten umgehen kann. Deshalb ist int üblicherweise so breit wie die Register der CPU, z.B. 32 Bit auf x86. Im Gegensatz dazu ist short ein Typ mit mindestens 16 Bit ist, der auf den meisten Systemen auch tatsächlich genau 16 Bit breit ist.
C99 definiert auch noch breitere Typen, die uns aber auf älteren Compilern und auch auf dem cc65 nicht zur Verfügung stehen.
Hier eine Übersicht der genannten Typen:
| Typ | Alternative Bezeichnungen | Mindestbreite | minimaler Wertebereich |
|---|---|---|---|
| char | 8 | -128..127 oder 0..255 | |
| short | signed short, short int, signed short int | 16 | -32768..32767 |
| unsigned short | unsigned short int | 16 | 0..65535 |
| int | signed, signed int | 16 | -32768..32767 |
| unsigned int | unsigned | 16 | 0..65535 |
| long | long int, signed long int | 32 | -2147483648..2147483647 |
| unsigned long | unsigned long int | 32 | 0..4294967295 |
Der Standard definiert die Mindestbereiche übrigens symetrisch, z.B. -127 bis 127. Weil die meisten Rechner das sogenannte Zweierkomplement benutzen, entstehen Bereiche wie -128 bis 127.
Der cc65 implementiert genau die angegebene Mindestbreite.
Die nicht festen Breiten dieser Datentypen waren in der Vergangenheit oft Fehlerquellen in oberflächlich implementiertem Code. Deshalb bringt uns C99 ein paar Hilfestellungen für die Verwendung von Datentypen mit fester Breite. Die würden aber den Rahmen für heute sprengen, heben wir uns für später auf.
Gestern haben wir schon numerische Konstanten kennengelernt. Zur Erinnerung: 123 ist dezimal, 0x5b ist hexadezimal und 09 ist oktal.
Jetzt kennen wir jede Menge Typen - aber welchen Typ haben denn unsere numerischen Konstanten? Auch dafür gibt es Regeln.
Eine Dezimalkonstante ist ein int. Wenn der Wert nicht in ein int passt, ist es ein long. Wenn der Wert auch mit long nicht darstellbar ist, wird es ein unsigned long.
Beispiel: Weil mit einem 16-Bit-int keine 40000 dargestellt werden kann, würde der cc65 die Zahl 40000 als long werten. Wenn wir versuchen, 40000 einem int zuzuweisen, warnt uns der Compiler mit Warning: Constant is long.
Um numerischen Konstanten einen bestimmten Typ aufzudrängeln, gibt es in C die Endungen L wie long und U wie unsigned. Die Zusammenhänge findet ihr in folgender Tabelle:
| Schreibweise | Interpretation | Beispiele und die Interpretation auf cc65 |
|---|---|---|
| dezimal | int / long / unsigned long | 40000 ist long |
| hexadezimal oder oktal | int / unsigned int / long / unsigned long | 0xaffe ist unsigned int |
| mit Endung U | unsigned int / unsigned long | 0xaffe3U ist unsigned long |
| mit Endung L | long / unsigned long | 4L ist long |
| mit Endung U und L | unsigned long | 0xdeadbeefUL ist unsigned long |
Wie gesagt wird jeweils die erste Interpretation gewählt, mit der die Zahl darstellbar ist. Die Endungen U und L können auch klein geschrieben werden. Ein kleines l sieht einer 1 aber ziemlich ähnlich...
Jetzt kennen wir verschiedene Ganzzahltypen. Nun müssen wir uns die Frage stellen, was passiert, wenn wir diese mischen. Die Frage klingt zwar nicht sehr spannend, aber ihre Antwort kann Euch später viel Fehlersuche ersparen!
Startet mal dieses kleine Testprogramm:
#include <stdio.h> int main(void) { unsigned char uc; int i; uc = 7; i = uc; printf("int ist %d\n", i); i = 300; uc = i; printf("unsigned char ist %d\n", uc); i = -1; uc = i; printf("unsigned char ist %d\n", uc); }
Beim Start auf einem C64 erhalten wir diese Ausgabe:
int ist 7 unsigned char ist 44 unsigned char ist 255
Nanu? Was'n hier los? Schauen wir uns das Programm einmal an. Wir nehmen bei dieser Betrachtung die Integer-Größen an, die auf dem cc65 verwendet werden. Außerdem gehen wir davon aus, das der Rechner negative Zahlen intern als Zweierkomplement darstellt, was bei praktisch jedem gebräuchlichen Rechner so ist.
uc = 7; i = uc; printf("int ist %d\n", i);
Einem unsigned char uc (auf cc65 8 Bit) wird der Wert 7 zugewiesen. Dieser Wert lässt sich mit unsigned char abbilden und entspricht dem Bitmuster 00000111. Als nächstes wird der Wert aus uc nach int i übertragen (auf cc65 16 Bit). Das klappt problemlos, weil in C die unbenutzten Bits auf der linken Seite bei dieser Zuweisung mit 0 gefüllt werden. Das Ergebnis in i sieht also binär so aus: 0000000000000111. Dieser Wert wird mit printf als int ausgegeben und entspricht 7.
i = 300; uc = i; printf("unsigned char ist %d\n", uc);
In diesem Beispiel wird int i der Wert 300 zugewiesen. Das klappt auch problemlos; wir erhalten binär 0000000100101100. In der nächsten Zeile wird der größere Typ int in einen kleineren Typ unsigned char übertragen. In C werden bei dieser Zuweisung die höherwertigen Bits abgeschnitten. Wir erhalten dadurch binär 00101100, also den Wert 44. Beim Aufruf von printf wird dieser dann wieder als int ausgegeben, also auf binär 0000000000101100 erweitert. Was immer noch einer 44 entspricht.
i = -1; uc = i; printf("unsigned char ist %d\n", uc);
In diesem Beispiel wird int i der Wert -1 zugewiesen. Das Zweierkomplement führt zu einer binären Darstellung 1111111111111111. Bei der Zuweisung des int i auf den unsigned char uc muss Information verloren gehen, weil int breiter ist als char. Die Bits auf der linken Seite werden einfach weggelassen, in uc steht jetzt 11111111. Für die Ausgabe wird der unsigned char uc wieder auf int erweitert, was zum Wert 0000000011111111 führt, der einer 255 entspricht.
In den Beispielen haben wir automatische Umwandlungen bei Zuweisungen gesehen. Außerdem wurden Werte vor der Ausgabe mit printf einer Integer-Erweiterung unterzogen. Der Grund für letzteres liegt übrigens daran, dass wir uc an die variable Parameterliste von printf übergeben haben.
Damit Ihr wisst, womit ihr Rechnen könnt (oh, wie doppeldeutig), sind im Folgenden die Umwandlungsregeln kurz zusammengefasst. Wann genau welche dieser Umwandlungen vorgenommen werden, werden wir bald sehen.
Die implizite Umwandlung (implicit conversion) von Integer-Typen sieht 1) so aus:
Die Integer-Erweiterung (integer promotion) sorgt u.A. dafür, dass Typen, die kleiner sind als int, an Stellen verwendet werden können, wo ein int erwartet wird. Diese Umwandlung erfolg automatisch an bestimmten Stellen, auf die wir dann aufmerksam machen.
Dafür gibt es ein paar einfache Regeln:
Beispiele:
Und jetzt merken wir uns schonmal, dass diese Erweiterung für die Argumente von printf vorgenommen wird.
Wir haben sie schon benutzt, nun schauen wir mal etwas genauer auf unsere ersten Operatoren.
Mit dem Operator = kann eine einfache Zuweisung vorgenommen werden, was wir bereits gesehen haben.
Auf der rechten Seite muss ein Ausdruck (expression) stehen, auf der linken Seite ein modifizierbarer L-Wert (lvalue, locate value 2)). Ein L-Wert ist ein Ausdruck, der ein Objekt im Speicher benennt, üblicherweise ein Variablenname.
Der Wert des Ausdrucks auf der rechten Seite wird in den Typ des L-Wert auf der linken Seite konvertiert und dann dort abgelegt. Sollte C keine Regel für so eine Umwandlung kennen, wird beim Übersetzen des Programms ein Fehler ausgegeben. Wir haben z.B. vorhin die Regeln für die Umwandlung von Ganzzahlen kennengelernt.
4 = 7; enthält. Lies die ausgegebene Fehlermeldung und verstehe, warum wir hier im Kurs auch solche merkwürdigen Begriffe wie L-Wert erwähnen.
Die gesamte Zuweisung hat übrigens wieder den Wert, der zugewiesen wurde. Was das heißt, zeigt folgendes Beispiel:
a = b = 3;
Um die linke Zuweisung durchführen zu können, muss der Compiler die rechte Seite auswerten. Diese ist b = 3. Um diese auswerten zu können, muss er den Wert von 3 rausfinden. Der ist 3. Dieser wird dann in den Typ von b konvertiert und b zugewiesen. Das Wert der Zuweisung ist 3, wird in den Typ von a konvertiert und a zugewiesen. Beide Variablen haben dann den Wert 3. Die Konvertierung spielt hier nur eine Rolle, wenn man mal auf die Idee kommt, hier Typen zu mischen.
Wenn man diese Operatoren auf zwei arithmetische Operanden loslässt, tun sie genau dass, was man von BASIC kennt.
Bevor die Berechnung vorgenommen wird, wird die Integer-Erweiterung auf beide Operanden angewandt. Sind danach unsigned und signed gleicher Breite gemischt, wird mit unsigned gerechnet. Hat eine Seite einen breiteren Typ, wird mit dem breiteren Typ gerechnet. Diese Umwandlungen gelten auch für viele andere Operatoren, sie heißen übliche Umwandlungen (usual arithmetic conversions).
Auch bei diesen Operatoren werden die üblichen Umwandlungen durchgeführt.
Bei der Division im Ganzzahlbereich wird der ganzzahlige Teil berechnet. Wenn beide Operanden positiv sind, ist das Ergebnis immer „abgerundet“3). Ist einer der Operanden negativ, ist nicht definiert, ob „aufgerundet“ oder „abgerundet“ wird.
Der Modulo-Operator berechnet den Rest einer Ganzzahldivision.
Ist der zweite Operand bei “/“ oder bei “%“ 0, ist das Ergebnis nicht definiert. Mein Rechner mit x86-Linux beendet so ein Programm mit einer Fehlermeldung zur Laufzeit: „Floating point exception“ - und das bei einer Integer-Division
Die Vergleichsoperatoren tun das, was ihr Name sagt. Auch bei diesen Operatoren werden die üblichen Umwandlungen durchgeführt. Ein paar der Operatoren kennen wir von BASIC.
| Operator | Bedeutung |
|---|---|
< | Kleiner als |
> | Größer als |
<= | Kleiner oder gleich |
>= | Größer oder gleich |
== | Test auf Gleichheit |
!= | Test auf Ungleichheit |
Diese Vergleichsoperatoren haben eine interessante Eigenschaft: Sie erzeugen einen int-Wert. Ist der Vergleich falsch, hat der Vergleich den Wert 0. Ist er wahr, hat er den Wert 1.
Deshalb kann man in C schreiben:
unsigned char istKleiner; istKleiner = 4 < 7;
Da 4 tatsächlich kleiner ist als 7, hat der Vergleich den Wert 1. Dieser wird dann durch den Zuweisungsoperator = in ein unsigned char umgewandelt und in istKleiner gespeichert.
Jedes längere Program braucht eine Anweisung, um bestimmte Befehle zu wiederholen. In BASIC gibts ja dafür den FOR-Befehl. Und wie es der Zufall will, kennt auch C eine for-Anweisung, die wie alle anderen Schlüsselwörter auch kleingeschrieben wird.
Aber schauen wir uns doch erstmal an, wie FOR in BASIC arbeitet:
10 FOR I = 0 TO 10 STEP 2 20 : PRINT I 30 NEXT I
Die Variable I wird von 0 beginnend um jeweils 2 hochgezählt, bis 10 erreicht ist. Und bei jedem Durchlauf wird der Inhalt von I ausgegeben.
Nun dasselbe in C:
#include <stdio.h> int main(void) { int i; for (i = 0; i <= 10; i = i + 2) { printf("%i\n", i); } }
Auch wenn das auf den ersten Blick etwas kompliziert aussieht, erkennen wir doch einige Elemente aus unserem BASIC-Programm wieder
i = 0
i <= 10
i = i + 2
Im Gegensatz zu BASIC müssen wir uns also in C um viele Dinge selbst kümmern. Aber dadurch ist C umso flexibler, was wir im Verlauf dieses Kurses noch oft sehen werden.
In allgemeiner Form sieht eine for-Anweisung so aus:
for (Initialisierung; Bedingung; Reinitialisierung) Anweisung
Sowohl die Initialisierung, Bedingung als auch die Reinitialisierung sind jeweils optional. Wir können sie also weglassen, wenn wir wollen. Nur die zwei Semikola müssen immer stehenbleiben. Wenn man eine Endlosschleife braucht, kann man die so implementieren:
for (;;) Anweisung
Damit lässt sich das ultimative Killerprogramm schreiben:
#include <stdio.h> int main(void) { for (;;); { puts("Der C64 ist mein Leben!"); } }
So, schnell mal übersetzen, starten und ... Was haben wir denn jetzt wieder falsch gemacht?!?
Schauen wir uns mal das Ganze genauer an. Am Ende der for-Zeile steht ein Semikolon, das vorher nicht da war. Und das ist hier tatsächlich der Übeltäter.
In C steht so ein “;“ nämlich für eine leere Anweisung. Die Schleife läuft dadurch endlos, ohne jemals die Ausgabe in der nächsten Zeile zu erreichen. Solche kleinen Flüchtigkeitsfehler führen oft dazu, dass sich ein kompiliertes Programm anders verhält, als wir beabsichtigt haben.
Also korrigieren wir das Superprogramm, indem wir das besagte Semikolon löschen; und erfreuen uns an dem gewünschten Ergebnis.
Wie bewegen wir aber eine for-Schleife dazu, mehr als eine Anweisung auszuführen? In BASIC genügt es ja, vor NEXT weitere Zeilen einschieben. In C erreichen wir das durch eine sogenannte zusammengesetzte Anweisung (compound statement) oder einfach Block.
Sehen wir uns das doch wieder an einem Beispiel an:
#include <stdio.h> int main(void) { int i; int summe = 0; for (i = 1; i <= 10; i = i + 1) { summe = summe + i; printf("Summe: %d\n", summe); } }
Wir erkennen, dass ein Block durch geschweifte Klammern “{“ und “}“ eingefasst wird. Jede einzelne Anweisung lässt sich in einem C-Programm durch so einen Block ersetzen.
{ for (i = 1; i <= 10; i = i + 1) summe = summe + i; printf("Summe: %d\n", summe); }
Deshalb solltet ihr immer Blöcke benutzen, selbst wenn sie nur eine Anweisung enthalten. Dann könnt ihr später bei Erweiterungen nichts übersehen. Und richtige Einrückung ist auch Pflicht!
Wer sich jetzt das Programm noch einmal ansieht, sollte eigentlich jedes einzelne Zeichen in der Funktion main verstehen können. Oder haben wir was vergessen?
#include <stdio.h> /******************************************************************************/ /* * Hauptprogramm */ int main(void) { unsigned int zahl; /* Mit 1 anfangen, dann immer verdreifachen bis unter 40000 */ for (zahl = 1U; zahl < 40000U; zahl = zahl * 3U) { printf("%5u\n", zahl); } }
Eine Kleinigkeit noch: Das %u im printf ist ein Platzhalter für ein unsigned int. Außerdem gibt es noch das Flag l, mit dem man long und unsigned long mit printf ausgeben kann, also z.B. %ld und %lu. Genaueres steht dort:
http://www.rt.com/man/printf.3.html.
In diesem Thread im Forum64 können Fragen zu dieser Lektion gestellt werden:
http://www.forum64.de/wbb3/index.php?page=Thread&threadID=27823
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=27824
Hier sind ein paar interessante Fragen aus dem oben genannten Forum:
(Noch keine)
So, das war's für heute, weiter geht's mit 04-abend4