Teil 16 „Tape storage“

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

Teil 16 „Tape storage“

Beitrag von bodo » 13.11.2010, 14:34

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 16

Das Kapitel 16 heißt im BASIC-Buch „Tape storage“... aber im z88dk gibt es keine fertigen Funktionen, die sichern oder laden. Daher werden wir in diesem Teil wieder einmal auf anderes eingehen.

Initialisierte Variablen

Bisher haben wir Variablen immer nur angelegt. Je nach Ort der Definition sind sie dann automatisch mit 0 (Null) initialisiert oder haben einen zufälligen Inhalt. Um einer Variablen einen bestimmten Wert zu geben, schrieben wir eine entsprechende Anweisung in den Programmtext.

Das ist auch zunächst nicht schlimm; aber es geht auch anders. Wir können gleich beim Anlegen einer Variablen ihr einen Wert zuweisen:

Code: Alles auswählen

int wert = 81;
int primzahlen[10] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 27 };
char text1[100] = { 'H', 'A', 'L', 'L', 'O', '.', '\0'};
char text2[100] = "HALLO.";
Wie ihr seht, können wir auch Arrays mit Werten initialisieren, indem wir die Werte in geschweiften Klammern durch Kommata getrennt aufführen. Die beiden letzten Zeilen zeigen das auch für Zeichenarrays (Gemeinhin als „Strings“ bezeichnet ;-)), wobei C sogar die Kurzform mit einer Zeichenkettenkonstanten erlaubt. Diese beiden letzten Zeilen sind gleichwertig, nur ist die zweite Variante viel besser lesbar.

Bei Arrays gibt es sogar noch einen bequemen Automatismus: wir brauchen die Größe nicht anzugeben, wenn wir sie über die Initialisierung festlegen. Das könnt ihr mit folgendem kleinen Programm ausprobieren:

Code: Alles auswählen

#include <stdio.h>

int werte[] = { 1, 4, 9, 16, 25, 36 };
char zeichen[] = "HALLO, ZX81.";

int main(void) {
    int anzahl;
    int index;

    anzahl = sizeof(werte) / sizeof(int);
    printf("WERTE[] ENTHAELT %d ELEMENTE:\n", anzahl);
    for (index = 0; index < anzahl; index++) {
        printf("WERTE(%d) = %d\n", index, werte[index]);
    }

    anzahl = sizeof(zeichen) / sizeof(char);
    printf("ZEICHEN[] ENTHAELT %d ELEMENTE:\n", anzahl);
    for (index = 0; index < anzahl; index++) {
        printf(" ZEICHEN(%d) = '%c' (%d)\n", index, zeichen[index], zeichen[index]);
    }

    return 0;
}
Das z88dk hatte mit dem ersten Entwurf dieses Programms Probleme:
  1. Die beiden initialisierten Arrays sollten lokal in main() liegen. Das mag das z88dk nicht, schade.
  2. Die Berechnung der Anzahl Elemente in einem Array wird professionell so berechnet: anzahl = sizeof werte / sizeof werte[0]. Wie ihr seht, behagte das dem z88dk auch nicht, daher die oben benutzte Variante.
Die Vorteile dieser Art der Initialisierung und der Verwendung von sizeof sind, dass wir die Größe des Arrays nicht selbst bestimmen müssen und der Rest des Programms automatisch damit zurechtkommt. Im Beispiel haben wir die Anzahl der Elemente ja in einer Variablen gespeichert; das ist nicht nötig, wenn wir die Berechnung an der entsprechenden Stelle direkt eintragen. Wenn der Compiler gut ist (Und tatsächlich hat der z88dk-Compiler den Vergleich mit der Konstanten „6“ erzeugt.), merkt er auch, dass die Anzahl eine Konstante ist und erzeugt entsprechend einfachen Maschinencode:

Code: Alles auswählen

#include <stdio.h>

int werte[] = { 1, 4, 9, 16, 25, 36 };

int main(void) {
    int summe;
    int index;

    summe = 0;
    for (index = 0; index < sizeof(werte) / sizeof(int); index++) {
        summe += werte[index];
    }
    printf("SUMME = %d.\n", summe);

    return 0;
}
Wann werden die initialisierten Variablen denn nun mit ihren Werten gefüllt? Das hängt davon ab, ob sie statisch oder dynamisch (siehe nächstes Kapitel) sind: statische Variablen werden direkt mit ihren Initialwerten im Maschinencode angelegt. Für dynamische Variablen erzeugt der Compiler entsprechende Anweisungen; das ist sicher der Grund, warum der Compiler des z88dk das nicht kann: es verkompliziert den Compiler.

Modifizierer und Speicherklassen

auto und static

Bisher haben wir zwei verschiedene Arten von Variablen kennengelernt: statische Variablen und dynamische Variablen. Dies hat der Teil 6 behandelt; wenn ihr euch nicht mehr erinnert, schaut es euch nochmal an.

Wenn ihr weder „auto“ noch „static“ angebt, sind globale Variablen, die außerhalb einer Funktion stehen, automatisch statisch und lokale Variablen, die innerhalb einer Funktion (vielleicht sogar innerhalb eines inneren Anweisungsblocks) stehen, automatisch dynamisch. Wie Teil 6 zeigte, könnt ihr lokalen Variablen statisch machen, indem ihr der Definition ein „static“ voranstellt.

Dagegen können globale Variablen nicht dynamisch gemacht werden, denn sie existieren sowieso während der gesamten Laufzeit des Programms. Trotzdem gibt es das Schlüsselwort „auto“ dafür, das aber an lokale Variablen geschrieben werden kann. Aufgrund der Festlegung im ISO-Standard ist es aber redundant:

Code: Alles auswählen

void function(void) {
    auto int variable;
}
register

Die Assemblerfreaks unter uns werden sicher das eine oder andere Mal in den erzeugten Maschinencode schauen und feststellen, dass der Compiler teilweise Variablen nicht im Speicher anlegt, sondern dafür Prozessorregister verwendet. Das ist natürlich toll, weil die Laufzeit oft dadurch kürzer wird. Welche Variablen nun in Prozessorregister gelegt werden, ist das Geheimnis des Compilers; meistens sind es die lokalen Variablen der innersten Blöcke und diejenigen, die häufig benutzt werden. Manchmal entscheidet sich der Compiler aber nicht so, wie wir Programmierer das möchten. Für diesen Fall können wir dem Compiler einen Tipp geben, indem wir der Variablendefinition ein „register“ voranstellen. Dies ist aber wirklich nur ein Tipp, der Compiler muss sich nicht unbedingt daran halten! Im folgenden Beispiel wird zwar tatsächlich das Registerpaar HL für „schnell“ verwendet, aber weil der erzeugte Maschinencode diesen Wert ständig im Stack ablegt und wieder hervorholt, ergibt sich kein wirklicher Vorteil gegenüber der statischen Variablen. Da merken wir wieder, dass der Compiler des z88dk nicht so richtig tollen Maschinencode erzeugt.

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    static int statisch;
    register int schnell;

    puts("START");
    for (statisch = 0; statisch < 10000; statisch++) {
        /* nix tun */
    }
    puts("DAZWISCHEN");
    for (schnell = 0; schnell < 10000; schnell++) {
        /* nix tun */
    }
    puts("ENDE");

    return 0;
}
volatile

Ein weiterer, ab und zu nötiger Modifizierer ist das Schlüsselwort „volatile“. Es kennzeichnet eine Variable als „flüchtig“, d.h. der Compiler darf lesende und schreibende Zugriffe nicht wegoptimieren. Gut optimierende Compiler stellen nämlich fest, wenn sie mehrfach nacheinander auf dieselbe Variable zugreifen und optimieren den Maschinencode dann so, dass die Werte optimal zwischengespeichert werden. Dies ist dann aber nicht unbedingt die Speicherstelle der Variablen... Uns betrifft das mit dem z88dk nicht, weil der Compiler das Schlüsselwort zwar kennt, es aber ignoriert; er generiert sogar deshalb eine Warnung.

Wann wird dieser Modifizierer benötigt? Zum Beispiel, wenn mit Interrupts oder Multithreading programmiert wird und sowohl das Hauptprogramm als auch die Interruptfunktion dieselbe Variable benutzen.

Ein Beispiel:

Code: Alles auswählen

volatile int interrupt_kam;

void interrupt(void) {
    interrupt_kam = 1;
}

int main(void) {
    interrupt_kam = 0;
    while (interrupt_kam == 0) {
    }

    return 0;
}
const

Der letzte hier vorgestellte Modifizierer ist „const“. Leider wird auch dieser vom z88dk zwar erkannt, aber (dankenswerterweise mit einer Warnung) ignoriert.

Mit „const“ werden Variablen als schreibgeschützt gekennzeichnet. Der Compiler soll dann Fehler melden, wenn auf eine solche Variable schreibend zugegriffen wird. Wenn das erzeugte Programm auf einem entsprechenden Betriebssystem wie Linux oder einem Windows der NT-Generation (nicht der Win95-Generation!) läuft, wird sogar ein Fehler zur Laufzeit ausgelöst.

Eine Variable als schreibgeschützt zu markieren ist z.B. dann sinnvoll, wenn damit eine Tabelle aufgebaut wird, die nicht verändert werden darf.

Ein Beispiel:

Code: Alles auswählen

const int primzahlen[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23 };
Hausaufgabe
  1. Zur Berechnung der Anzahl der Elemente eines Arrays im ersten Beispielprogramm: überlegt euch den Nachteil der benutzten Methode gegenüber der „professionellen“ Methode.
B0D0: Real programmers do it in hex.

Antworten