C Lernen mit cc65 und C64

Abend 3: Viele Typen und eine Schleife

Unser heutiges Testprogramm

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);
    }
}

dreierei.c

Ü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

Mehr Ganzzahltypen

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.

Während es auf neueren Prozessoren eine gute Wahl ist, für Allerweltsvariablen den Typ int zu benutzen, solltet Ihr mit dem cc65 und allen anderen 8-Bit-Compilern so oft wie möglich einen der char-Typen benutzen. long solltet Ihr nur einsetzen, wenn Ihr tatsächlich den Wertebereich benötigt (z.B. beim Start einer Ariane 5).

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.

Ein Tip: Beim cc65 sind die unsigned-Typen etwas schneller als die signed-Typen.

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.

Und mehr numerische Konstanten

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...

Automatische Umwandlung von Ganzzahltypen

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.

Diese Beispiele zeigen, dass Ihr beim Mischen unterschiedlicher Ganzzahltypen genau wissen solltet, was Ihr tut. Grundsätzlich solltet Ihr das entweder ganz vermeiden, oder die in Frage kommenden Wertebereiche, Typen und Umwandlungsregeln genau kennen.

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.

Regeln für die implizite Integer-Umwandlungen

Die implizite Umwandlung (implicit conversion) von Integer-Typen sieht 1) so aus:

  1. Wenn der Zieltyp den Wert des Quelltyps abbilden kann, bleibt der Wert unverändert
  2. Ansonsten, wenn Quelltyp und Zieltyp gleich groß sind, wird das Bitmuster übertragen
  3. Ansonsten, wenn das Ziel einen kleinere Länge hat, werden die höheren Bits abgeschnitten
  4. Ansonsten, wenn das Ziel vorzeichenlos ist und die Quelle negativ, werden die höherwertingen Bits mit 1 aufgefüllt

Regeln für die Integer-Erweiterung

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:

  1. Ist der Typ schmaler als int, wird er zu int konvertiert
  2. Ist der Typ genauso breit wie int und hat ein Vorzeichen, wird er zu int konvertiert
  3. Ist der Typ genauso breit wie ein int und hat kein Vorzeichen, wird er zu unsigned int konvertiert.
  4. Typen, die breiter sind als ein int bleiben unverändert.

Beispiele:

  1. Ein unsigned char wird bei der Integer-Erweiterung automatisch in ein int umgewandelt.
  2. Ist ein short so groß wie ein int, z.B. beide 16 Bit, wird short in ein int umgewandelt.
  3. Ist ein unsigned short so groß wie ein unsigned int, z.B. beide 16 Bit, wird unsigned short in ein unsigned int umgewandelt.
  4. Andere Typen werden nicht umgewandelt, z.B. long bleibt long.

Und jetzt merken wir uns schonmal, dass diese Erweiterung für die Argumente von printf vorgenommen wird.

Unsere ersten Operatoren

Wir haben sie schon benutzt, nun schauen wir mal etwas genauer auf unsere ersten Operatoren.

Die einfache Zuweisung "="

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.

Aufgabe 1: Übersetze mit dem cc65 ein Programm, das die Anweisung 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.

Addition "+", Subtraktion "-"

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).

Multiplikation "*", Division "/", Modulo "%"

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 :-)

Aufgabe 2: Untersuche, was auf dem C64 mit cc65-Code passiert, wenn eine Division durch 0 versucht wird.

Vergleichsoperatoren "<", ">", "<=", ">=", "==", "!="

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.

Unsere erste Schleife mit "for"

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

  • Am Anfang wird der Variablen i eine Null zugewiesen:
    i = 0
  • Vor jedem Durchlauf wird überprüft, ob i noch kleiner oder gleich 10 ist:
    i <= 10
  • Nach jedem Durchlauf wird i um zwei erhöht:
    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

Es gibt einen wichtigen Unterschied zwischen FOR in BASIC und for in C: Bei C wird die Bedingung vor einem Durchlauf der Schleife geprüft (kopfgesteuert), bei BASIC danach (fußgesteuert).

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.

Dieser Programmteil tut nicht das gleiche wie der oben:

{
    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!

Nochmal ein Blick auf unser Programm

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);
    }
}

dreierei.c

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.

Aufgabe 3: Erhöhe die Grenze im Testprogram von 40000 auf 60000 und starte es mit dem cc65. Nanu, was passiert da? Erkläre den Effekt und ändere das Programm so, dass es das Richtige(tm) tut. Poste den vollständigen Quelltext im unten verlinkten Thread im Forum64.

Aufgabe 4: Schreibe ein Programm, das das 1×1 von 1 mal 1 bis 9 mal 9 ausgibt. Störe Dich nicht dran, das die Ergebnisse oben raus-scrollen. Poste den vollständigen Quelltext im unten verlinkten Thread im Forum64.

Fragen zu dieser Lektion

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

1) auf Rechnern, die mit dem Zweierkomplement arbeiten, also praktisch alle
2) locate value, obwohl die Abkürzung ursprünglich eine andere Bedeutung hatte
3) Natürlich wird bei einer Ganzzahloperation nicht wirklich gerundet. Der Begriff wird hier zum leichteren Verständnis benutzt.
ckurs/03-abend3.txt · Zuletzt geändert: 11/05/2009 12:59 (Externe Bearbeitung)
www.chimeric.de Creative Commons License Driven by DokuWiki Recent changes RSS feed