Programmieren in C mit dem z88dk

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

10.3 String

Beitrag von bbock »

Wie bereits erwähnt, gibt es in C keinen Datentyp "String". Wenn man von C-Strings spricht, dann meint man Arrays von Zeichen, die mit einem Null-Zeichen '\0' abgeschlossen ("terminiert") werden. Diese Art von Zeichenketten ist sehr verbreitet; in C gibt es eine ganze Bibliothek von String-Funktionen, die man #include <string.h> einbinden kann.

Da Strings eigentlich Arrays von char-Werten sind, gelten für sie auch die bereits genannten Regeln für Pointer und Arrays. Besonders häufig trifft man String-Literale an, also String-Konstanten, die in Gänsefüßchen geschrieben werden, z.B.:

"Dies ist ein String"

Wie wird ein String im Speicher abgelegt? Nehmen wir beispielsweisen den String "hallo": Er besteht aus fünf Zeichen, hat also eine Länge von 5. Dazu kommt aber noch das Abschlusszeichen, so dass im Speicher tatsächlich sechs Bytes belegt werden. Ohne das Abschlusszeichen wüsste eine Funktion wie printf oder puts nicht, wann sie mit dem Ausgeben von Zeichen aufhören soll. Diese Funktionen bekommen die Adresse des Strings übergeben und geben Zeichen für Zeichen aus, wobei sie die Adresse immer weiter hochzählen, bis sie schließlich auf eine 0 stoßen. Dann ist der String zuende und die Ausgabe hört auf.

Code: Alles auswählen

Adresse    3000 3001 3002 3003 3004 3005
ASCII-Wert  104   97  108  108  111    0
Zeichen       h    a    l    l    o   \0
Beginnen wir mit einem Beispielprogramm. Hier wird ein String als Array angelegt:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    char s[6];
    
    s[0] = 'h';
    s[1] = 'a';
    s[2] = 'l';
    s[3] = 'l';
    s[4] = 'o';
    s[5] = '\0'; // Terminierung
    
    puts(s);     // String ausgeben
}
Hier weisen wir einem char-Pointer eine String-Konstante zu - auch das ist ein String:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    char *s;
    
    s = "hallo"; // Terminierung implizit enthalten
    puts(s);     // String ausgeben
}
Nun wollen wir aus dem "hallo" ein "halli" machen; hier die Array-Variante:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    char s[6];
    
    s[0] = 'h';
    s[1] = 'a';
    s[2] = 'l';
    s[3] = 'l';
    s[4] = 'o';
    s[5] = '\0'; // Terminierung
    
    puts(s);     // String ausgeben
    
    s[4] = 'i';  // String aendern
    puts(s);     // geaenderten String ausgeben
}
Nun die Variante mit der String-Konstanten:

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

int main(void) {
    char *s1;
    char s2[6];
    char *cp;
    
    s1 = "hallo";   // String-Konstante
    puts(s1);       // String ausgeben
    strcpy(s2, s1); // String von s1 nach s2 kopieren
    cp = s2;        // cp zeigt auf s2[0]
    cp += 4;        // cp zeigt auf s2[4]
    *cp = 'i';      // String aendern
    puts(s2);       // geaenderten String ausgeben
}
Hier ändern wir nicht den String in der String-Konstanten - das sollten wir niemals tun! - sondern wir kopieren den String in eine lokale Variable mit genügend Speicher und ändern die String-Kopie. Diese ändern wir etwas umständlich über einen Hilfs-Pointer. Man hätte natürlich auch genausogut s[4] = 'i'; schreiben können; das wäre drei Zeilen kürzer gewesen.

Das folgende Programm zeigt einige Beispiele zur Verwendung von Funktionen aus string.h:

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[255];
    char *cp;
    int len;
    int i;

    // copy a string
    strcpy(s, "12345678");
    puts(s);
    
    // concatenate strings
    strcat(s, "9ABCDEF");
    puts(s);
    
    // string length
    len = strlen(s);
    printf("L%cnge: %d\n", 123, len);
    
    // compare strings
    i = strcmp("abc", "bcd");
    printf("strcmp Ergebnis: %d\n", i);
    if (i < 0) {
        puts("abc < bcd");
    }
    else if (i > 0) {
        puts("abc > bcd");
    }
    else { // i == 0
        puts("abc == bcd");
    }
    
    // find character in string
    cp = strchr(s, 'A');
    printf("A gefunden; String ab dem A: %s\n", cp);
    
    // find substring in string
    cp = strstr(s, "567");
    printf("567 gefunden; String ab 567: %s\n", cp);
    
    getchar();
    
    return 0;
}
Bei der Ausgabe von "Länge" nach strlen wurde ein Trick angewandt um den Umlaut korrekt auszugeben: Da die Zeichensätze auf dem PC und der Joyce voneinander abweichen, wurde im printf an der Stelle, wo das 'ä' erscheinen soll, ein %c geschrieben, d.h. an dieser Stelle wird ein char eingefügt, das als dem printf in der Parameterliste mitgegeben werden muss. Daher die 123, denn das ist der Code für ein 'ä'.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

Übung

Beitrag von bbock »

Das bis hierher Gelernte wollen wir in einer kleinen Übung vertiefen: wir programmieren einen einfachen Taschenrechner. Damit das Ganze überschaubar bleibt, sollen nur die Grundrechenarten unterstützt werden - ohne Punkt- vor Strichrechnung.

Wir beginnen mit der main-Funktion mit einigen Variablendeklarationen und der Ausgabe einer Überschrift. Endlich bekommt die Funktion auch einen Kommentar:

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

/* Main.

   Returns an error code (== 0: ok, != 0: error).
*/
int main(void)
{
    float fInput, fAkku;
    char op, currentOp;
    char continueLoop;
    char formatted[20];
    
    printf("Mini-Taschenrechner\n\n");
Die Variable fInput verwenden wir für die Benutzereingabe, mit fAkku wird gerechnet. Dann brauchen wir noch zwei Variablen für den Operator, eine Hilfsvariable für die Schleife und einen String (d.h. einen char-Array) für die formatierte Ausgabe von Zahlen.

Dann folgt ein wenig Initialisierung:

Code: Alles auswählen

    op = '\0';
    fAkku = 0;
    continueLoop = 1;
Und schließlich die Schleife und der Rest der main-Funktion:

Code: Alles auswählen

    do {
        printf("Zahl     : ");
        scanf("%f", &fInput);
        
        printf("Operation: ");
        currentOp = getchar();
        currentOp = getchar();
        
        switch (op) {
            case '\0':
                fAkku = fInput;
                break;
            case '+':
                fAkku = fAkku + fInput;
                break;
            case '-':
                fAkku = fAkku - fInput;
                break;
            case '*':
                fAkku = fAkku * fInput;
                break;
            case '/':
                fAkku = fAkku / fInput;
                break;
            default:
                continueLoop = 0;
        }
        
        formatNumber(fAkku, formatted);
        printf("    %s\n", formatted);
        op = currentOp;
    } while (currentOp != '=' && continueLoop);
    
    return 0;
}
In der Schleife gibt der Benutzer eine Zahl ein, die via scanf als float-Wert eingelesen wird (daher der Format-String "%f"). Der Wert landet in der Variablen fInput, deren Adresse wir scanf als zweites Argument übergeben.

Dann fordern wir den Benutzer auf einen Operator einzugeben. Den lesen wir mit getchar ein, dann muss der Operator nicht mit der Enter-Taste bestätigt werden. Es sieht hier so aus, als würden wir den Operator zweimal einlesen, aber der erste getchar-Aufruf räumt nur etwas "Müll" aus dem Tastaturpuffer, den scanf zurückgelassen hat. Der zweite getchar-Aufruf ist der, der uns den Operator liefert.

In der switch-Anweisung sieht man, welche Operatoren erlaubt sind: +, -, *, / und =. Der case-Zweig mit dem '\0' wird nur beim ersten Durchlauf erreicht, weil op mit '\0' initialisiert wurde. Im ersten Durchlauf gibt es auch noch nichts zu rechnen; wir speichern nur die erste Zahl in fAkku.

In jedem weiteren Durchlauf wird fAkku abhängig vom Operator mit dem nächsten Eingabewert verknüpft. Wird ein ungültiger Operator eingegeben, dann bricht die Schleife ab, indem continueLoop auf 0 gesetzt wird (continueLoop wurde mit 1 initialisiert). Aber continueLoop ist doch eine char-Variable, wie kann sie dann auf eine Zahl wie 0 oder 1 gesetzt werden? Der C-Compiler ist hier recht tolerant; wenn man einer char-Variablen einen Zahlwert zuweist, dann nimmt er das klaglos als ASCII-Code an, solange der Wertebereich nicht überschritten wird. Für vorzeichenbehaftete char ist das -128 bis +127, für unsigned char 0 bis 255. Wir verwenden hier den char als verkappte Boolean-Variable.

Die while-Bedingung gibt an, wann die Schleife weiterlaufen soll. Ist der aktuelle Operand ein '=', dann wird die Schleife regulär beendet.

Eine Sache fehlt noch: Die Ausgabe des jeweiligen Zwischenergebnisses erfolgt nicht direkt durch printf("%f", fAkku);, sondern es wird die Funktion formatNumber aufgerufen. Die macht die Zahl etwas hübscher, indem sie sie in einen String umwandelt.

Schauen wir uns die Funktion mal genauer an. Sie muss vor der main-Funktion, aber nach den #include-Anweisungen eingefügt werden:

Code: Alles auswählen

/* Formats a float by cutting off the zeroes at the end.
   If there are only zeroes after the decimal point, then
   the decimal point is cut off, too.
   
   Parameters: f          the float value to format
               formatted  a character buffer to hold the float as string
*/
void formatNumber(float f, char *formatted) {
    char *s;
    
    sprintf(formatted, "%f", f);
    s = formatted + strlen(formatted);
    --s;
    
    while (s != formatted) {
        if (*s != '0') {
            if (*s != '.') {
                ++s;
            }
            *s = '\0';
            break;
        }
        --s;
    }
}
Wir übergeben ihr den zu formatierenden float-Wert und einen Pointer (die Adresse) auf eine Zeichenkette, in die die formatierte Zahl abgelegt werden soll. Das ist unser char-Array formatted.

Zu Beginn wird die Zahl f in eine Zeichenkette umgewandelt. Dies geschieht mit der Funktion sprintf, die genauso funktioniert wie printf, nur landet die Ausgabe nicht auf dem Bildschirm, sondern in der String-Variablen, die als erstes Argument übergeben wird. Danach ist alles wie bei printf: der Format-String, dann die Variablen, die die Format-Platzhalter mit Werten füllen.

Die folgende Schleife schneidet alle unnötigen Nullen am Ende der Zahl, nach dem Dezimalpunkt ab. Der String wird von rechts beginnend Zeichen für Zeichen durchlaufen, bis ein Zeichen ungleich '0' gefunden wird. Das folgende Zeichen wird mit dem String-Abschlusszeichen '\0' überschrieben, wodurch der String verkürzt wird. Eine Sonderbehandlung gibt es noch für den Dezimalpunkt: Damit eine Zahl wie 2.000000 nicht zu 2. resultiert, wird hier nicht das folgende Zeichen, sondern der Dezimalpunkt selbst mit dem Abschlusszeichen überschrieben. Das Ergebnis ist dann einfach eine 2 ohne den verwaisten Dezimalpunkt.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

11. Aufzählungstypen

Beitrag von bbock »

In C gibt es den keinen Boolean-Datentyp, obwohl man ihn gut gebrauchen könnte. In Bedingungsausdrücken gilt jeder Wert ungleich 0 als true, die 0 als false. Daher kann man z.B. eine char-Variable anstelle einer Boolean-Variablen verwenden und ihren Wert auf 1 (bzw. '\1') für true oder 0 (bzw. '\0') für false setzen. In der Taschenrechner-Übung haben wir das bereits bei der Variablen continueLoop getan.

Man kann aber eine Boolean-Variable in C nachbilden, indem man den Datentyp Boolean als eine Aufzählung der beiden Werte true und false auffasst. Das wird unser erster Aufzählungstyp (englisch enumeration type):

Code: Alles auswählen

enum bool {false, true};
Mit dieser Deklaration können wir Variablen vom Typ bool anlegen, denen man ausschließlich die beiden Werte true und false zuweisen kann:

Code: Alles auswählen

enum bool adult;
...
if (age >= 18)
    adult = true;
else
    adult = false;
In der enum haben wir nicht zufällig zuerst false und dann true notiert: intern werden enums als Zahlwerte repräsentiert, mit 0 beginnend. Dadurch entspricht false der 0 und true der 1. Weil C die 0-Werte als false und Werte ungleich 0 als true interpretiert, passt das.

Aufzählungstypen kann man für alle Variablen verwenden, die nur bestimmte, festgelegte Werte annehmen sollen. Eine Variablentyp für die Planeten unseres Sonnensystems könnte z.B. so deklariert werden:

Code: Alles auswählen

enum planet {Merkur, Venus, Erde, Mars, Jupiter, Saturn, Uranus, Neptun};
Ein kleines Beispielprogramm dazu:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    enum planet {Merkur, Venus, Erde, Mars, Jupiter, Saturn, Uranus, Neptun};
    enum planet myPlanet;
    char *planetName;
    float density;
    
    for (myPlanet = Merkur; myPlanet <= Neptun; ++myPlanet) {
        switch (myPlanet) {
            case Merkur:
                planetName = "Merkur";
                density = 5.427;
                break;
            case Venus:
                planetName = "Venus";
                density = 5.243;
                break;
            case Erde:
                planetName = "Erde";
                density = 5.514;
                break;
            case Mars:
                planetName = "Mars";
                density = 3.933;
                break;
            case Jupiter:
                planetName = "Jupiter";
                density = 1.326;
                break;
            case Saturn:
                planetName = "Saturn";
                density = 0.687;
                break;
            case Uranus:
                planetName = "Uranus";
                density = 1.271;
                break;
            case Neptun:
                planetName = "Neptun";
                density = 1.638;
                break;
        }
        
        printf("Die mittlere Dichte des Planeten %s betraegt %5.3f g/cm^3\n", planetName, density);
    }
    
    return 0;
}
Man beachte, dass die symbolischen Namen aus der enum-Deklaration nicht direkt mit printf als Text ausgegeben werden können. Wenn der Compiler den Code erzeugt hat, dann existieren diese Namen nicht mehr, sondern nur noch die zugehörigen Zahlwerte. Daher weisen wir der Variablen planetName die passenden String-Konstanten zu.

Die Formatangaben in printf bedeuten:
%s gibt einen String aus; die korrespondierende Variable muss ein char-Pointer sein.
%5.3f gibt eine Fließkommazahl mit insgesamt fünf Zeichen (inkl. Dezimalpunkt) und drei Nachkommastellen aus.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

12. Strukturen

Beitrag von bbock »

Strukturen sind Zusammenfassungen von mehreren Variablen, die in der Regel - im Gegensatz zu Arrays - von verschiedenem Datentyp sind. Sie beschreiben oft Objekte mit ihren interessierenden Eigenschaften. So könnte man z.B. ein Buch über Titel, Autor, Thema und die Buch-ID definieren:

Code: Alles auswählen

struct Book {
    char title[50];
    char author[50];
    char subject[100];
    int  book_id;
};
Wir wollen zwei Bücher initialisieren und ausgeben. Zunächst die Variablendeklarationen:

Code: Alles auswählen

struct Book book1;
struct Book book2;
Dann befüllen wir die zur Struktur gehörigen Variablen (Member-Variablen) der beiden Bücher. Schließlich geben wir sie auf dem Bildschirm aus; dafür verwenden wir eine eigene Funktion. Alles zusammen könnte etwa so aussehen:

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

// A book.
struct Book {
    char title[50];
    char author[50];
    char subject[100];
    int  book_id;
};

/* Print a book.
   Parameter: book  pointer to the book to print
*/
void printBook(struct Book *book) {
    printf( "title   : %s\n", book->title);
    printf( "author  : %s\n", book->author);
    printf( "subject : %s\n", book->subject);
    printf( "book_id : %d\n", book->book_id);
}

/* Main.
*/
int main(void) {
    struct Book book1;
    struct Book book2;

    /* book 1 specification */
    strcpy(book1.title, "Programmieren in C");
    strcpy(book1.author, "Kernighan & Ritchie"); 
    strcpy(book1.subject, "Programmierlehrbuch");
    book1.book_id = 5407;

    /* book 2 specification */
    strcpy(book2.title, "Telecom Billing");
    strcpy(book2.author, "Zara Ali");
    strcpy(book2.subject, "Telecom Billing Tutorial");
    book2.book_id = 5700;
    
    puts("*** Book 1 ***");
    printBook(&book1);
    
    puts("\n*** Book 2 ***");
    printBook(&book2);
    
    return 0;
}
Wie wir sehen, greift man auf die Member-Variablen eines struct mit Hilfe des Punkt-Operators zu, beispielsweise book1.title. Wollen wir aber über einen Pointer auf einen struct auf Member-Variablen zugreifen, dann ist der Pfeil-Operator dafür zuständig, z.B. book->title. Man könnte auch (*book).title schreiben, aber das ist umständlicher. Der Pfeil erinnert sofort an "Zeiger", daher ist diese Schreibweise intuitiver.

Beim Arbeiten mit Strukturen werden wir häufig Pointer verwenden, insbesondere, wenn wir struct-Variablen als Argument einer Funktion übergeben oder als Funktionsergebnis zurückliefern wollen.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

13. Typ-Definitionen

Beitrag von bbock »

Die einfachste Form der Typ-Definition in C ist die Umbenennung eines bestehenden Typs, wie z.B.

Code: Alles auswählen

typedef int LENGTH;
Das kann die Lesbarkeit fördern, indem man z.B. Variablen wie folgt definiert:

Code: Alles auswählen

LENGTH len, maxlen;
LENGTH *lengths[];
Oder man verwendet Abkürzungen wie

Code: Alles auswählen

typedef unsigned int UINT;
Auf unserem 8-Bitter hätte eine Variable vom Typ UINT dann einen Wertebereich von 0 bis 65535 - genau wie ein unsigned int, nur ist es mit dem typedef halt kürzer. Manch einer mag argumentieren, dass man damit nur die Sprache verwässert und Verwirrung stiftet. Es gibt natürlich auch sinnvollere Anwendungen von typedef, wie folgende Typvereinbarung für einen Knoten in einem binären Baum zeigt:

Code: Alles auswählen

typedef struct tnode {
    char *word;
    int count;
    struct tnode *left;
    struct tnode *right;
} TREENODE, *TREEPTR;
Hier wurden gleich zwei Namen festgelegt, TREENODE für die Struktur und TREEPRT für einen Zeiger auf einen TREENODE. Eine Knoten-Variable legt man z.B. folgendermaßen fest:

Code: Alles auswählen

TREENODE t;
Man hätte die Variable auch ohne typedef als struct tnode t; anlegen können, aber es ist durchaus angenehm, dass das Schlüsselwort struct bei Verwendung von typedef entfällt. Schauen wir uns an, wie das Programm aus Kapitel 12 bei Verwendung von typedef aussieht - wir speichern es als struct2.c:

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

// A book.
typedef struct {
    char title[50];
    char author[50];
    char subject[100];
    int  book_id;
} Book, *BookPtr;

/* Print a book.
   Parameter: book  pointer to the book to print
*/
void printBook(BookPtr book) {
    printf( "title   : %s\n", book->title);
    printf( "author  : %s\n", book->author);
    printf( "subject : %s\n", book->subject);
    printf( "book_id : %d\n", book->book_id);
}

/* Main.
*/
int main(void) {
    Book book1;
    Book book2;

    /* book 1 specification */
    strcpy(book1.title, "Programmieren in C");
    strcpy(book1.author, "Kernighan & Ritchie"); 
    strcpy(book1.subject, "Programmierlehrbuch");
    book1.book_id = 5407;

    /* book 2 specification */
    strcpy(book2.title, "Telecom Billing");
    strcpy(book2.author, "Zara Ali");
    strcpy(book2.subject, "Telecom Billing Tutorial");
    book2.book_id = 5700;
    
    puts("*** Book 1 ***");
    printBook(&book1);
    
    puts("\n*** Book 2 ***");
    printBook(&book2);
    
    return 0;
}
Hier wurde auch gleich ein Pointer auf ein Book definiert, so dass die Funktion für die Ausgabe eines Buchs mit void printBook(BookPtr book) beginnt. Ohne den Pointer-Typ hätte man auch void printBook(Book *book) schreiben können.

Das Programm kompilieren wir zur Abwechslung mit

Code: Alles auswählen

zcc +cpm -subtype=pcw80 -lndos -create-app struct2.c -o struct2.com
Wir kennen bereits das Compiler-Frontend zcc mit den Optionen +cpm, -subtype=pcw80 und -create-app für die Schneider Joyce. Die Option -lndos sorgt für eine kleinere ausführbare com-Datei, weil der Code für Dateizugriffsfunktionen weggelassen wird. Das können wir hier tun, weil wir keine Dateioperationen verwenden.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

14. Arbeiten mit Dateien

Beitrag von bbock »

Der Zugriff auf Dateien erfolgt in C immer nach dem Muster Öffnen - Dateioperationen - Schließen. Die Dateioperationen werden im z88dk standardmäßig dazugebunden; braucht man sie nicht, dann kann man sie mit der zcc-Option -lndos weglassen, wodurch die ausführbare COM-Datei dann etwas kleiner wird. Möchte man die Dateifunktionen verwenden, dann bindet man sie in der C-Datei mit #include <stdio.h> ein.

Sehen wir uns die drei Schritte des Dateizugriffs anhand der zugehörigen Funktionen an:
  1. Öffnen
    Eine Datei öffnet man mit der Funktion fopen. Sie erwartet zwei Parameter: den Dateinamen und den Modus. Letzterer gibt u.a. an, ob man die Datei zum Lesen oder Schreiben öffnen möchte. fopen liefert einen Pointer auf eine Variable vom Typ FILE zurück. Diesen Pointer braucht man für alle weiteren Dateioperationen.
  2. Dateioperationen
    Es gibt mehrere Funktionen, um etwas mit Dateien zu machen. So kann man Daten lesen mit fgetc, fgets, fscanf oder fread. Oder man kann Daten schreiben mit fputc, fputs, fprintf oder fwrite. Welche Funktion man verwendet, hängt vom jeweiligen Anwendungsfall ab.
  3. Schließen
    Nach erfolgter Bearbeitung muss man die Datei schließen. Das ist unbedingt erforderlich, sonst hat man ein Speicherleck und man kann sogar Daten verlieren. Das Schließen bewerkstelligt die Funktion fclose, die nur den FILE-Pointer als Parameter verlangt.
Erzeugen wir mal eine kleine Textdatei. Dazu speichern wir folgendes Programm als file1.c:

Code: Alles auswählen

#include <stdio.h>

main() {
   FILE *fp;

   fp = fopen("hello.txt", "w");
   fputs("Kommt ein Pferd in eine Bar...", fp);
   fclose(fp);
}
Wir erzeugen eine COM-Datei mit folgender Befehlszeile:

Code: Alles auswählen

zcc +cpm -subtype=pcw80 file1.c -o file1.com
Wenn wir nun file1.com starten, dann wird die Datei hello.txt erzeugt. Deren Inhalt können wir uns mit type hello.txt anzeigen lassen.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

15. Arbeiten mit Dateien 2

Beitrag von bbock »

Diesmal wollen wir eine Textdatei einlesen, verarbeiten und das Ergebnis in eine zweite Textdatei ausgeben. Die Eingabedatei ist eine CSV-Datei, allerdings mit Semikolon als Trennzeichen statt Komma. Die Datei enthält Informationen über die Hauptstädte der Erde; die Daten stammen aus Wikipedia. Zur Vermeidung unnötiger Probleme habe ich sie um Sonderzeichen bereinigt.

Die Datei hat den Namen hauptst.csv und enthält die Felder Staat, Hauptstadt, Einwohner, Stand und Quelle. Wir wollen daraus eine Datei erstellen, die zu jeder Hauptstadt die Anzahl ihrer Einwohner im Format <Hauptstadt>: <Anzahl Einwohner> ausgibt.

Dazu öffnen wir zunächst die Eingabe- und die Ausgabedatei. Dann lesen wir in einer Schleife Zeile für Zeile ein, verarbeiten die Zeile und schreiben das Ergebnis in die Ausgabedatei. Zuletzt schließen wir beide Dateien.

Beginnen wir mit der Funktion main(), wo wir zuerst die benötigten Variablen deklarieren:

Code: Alles auswählen

/* Main
 */
int main(void) {
    char filename[13];
    char filenameOut[13];
    FILE *fp;
    FILE *fpOut;
    char buf[100];
    t_line line;
Die beiden Arrays für die Dateinamen sind 13 Zeichen lang; das genügt für 8.3-Dateinamen plus das String-Ende-Zeichen '\0'. Wenn man noch eine Laufwerksangabe etc. unterbringen möchte, dann muss man die Arrays natürlich entsprechend größer dimensionieren. Es folgen zwei file pointer, ein Puffer für die aktuell eingelesene Zeile und eine Variable für das Ergebnis der verarbeiteten Zeile mit den einzelnen Feldern. Letztere definieren wir folgendermaßen mit typedef:

Code: Alles auswählen

typedef struct {
    char staat[60];
    char hauptstadt[50];
    char einwohner[12];
    char stand[10];
    char quelle[40];
} t_line;
Alle Felder sind als char-Arrays definiert, weil eine Textdatei nun einmal Textzeichen enthält. Nun könnte man argumentieren, dass die Anzahl der Einwohner doch eigentlich eine Zahl ist und man besser einen Zahlentyp verwenden sollte. Aber das brauchen wir nur dann, wenn wir damit Berechnungen anstellen wollen, wie z.B. die Summe der Einwohner aller Hauptstädte zu ermitteln. Für unseren Anwendungsfall brauchen wir das nicht, also verzichten wir auf eine Typumwandlung.

Wir öffnen die Eingabedatei:

Code: Alles auswählen

    strcpy(filename, "hauptst.csv");
    fp = fopen(filename, "r");
    if (fp == NULL) {
        printf("Fehler: Eingabedatei %s nicht gefunden!\n", filename);
        return -1;
    }
Wir kopieren die String-Konstante "hauptst.csv" in die Variable filename, weil wir sie weiter unten nochmal benötigen und wir den Dateinamen nicht doppelt in den Quellcode schreiben wollen. Dafür gibt es auch noch andere Lösungen. Die Datei wird wieder mit fopen geöffnet, hier im Modus "r" (für "read"). Der resultierende file pointer fp kann in der Folge nur für Leseoperationen verwendet werden. Liefert fopen einen NULL-Pointer zurück, dann ist das Öffnen fehlgeschlagen, was wir mit einer Fehlermeldung und dem Rücksprung ins Betriebssystem mit dem Fehlercode -1 quittieren.

Die Ausgabedatei wird ganz ähnlich geöffnet:

Code: Alles auswählen

    strcpy(filenameOut, "extrakt.csv");
    fpOut = fopen(filenameOut, "w");
    if (fpOut == NULL) {
        printf("Fehler: Konnte Ausgabedatei %s nicht oeffnen!\n", filenameOut);
        fclose(fp);
        return -1;
    }
Hier verwenden wir den Modus "w" (für "write") und öffnen die Datei damit zum Schreiben. Ergo sind mit fpOut nur Schreiboperationen möglich. Die Fehlerbehandlung ist ähnlich der bei der Eingabedatei, allerdings dürfen wir hier nicht vergessen die Eingabedatei zu schließen, bevor wir zum Betriebssystem zurückkehren, denn diese ist zu diesem Zeitpunkt ja bereits geöffnet.

Die Schleife für das Lesen der Zeilen sieht folgendermaßen aus:

Code: Alles auswählen

    // Zeilen lesen
    while (fgets(buf, sizeof buf, fp) != NULL) {
        printf(".");

        // Zeile in Felder aufteilen
        parseLine(buf, &line);

        // Hauptstadt und Einwohner schreiben
        fprintf(fpOut, "%s: %s\r", line.hauptstadt, line.einwohner);
    }

    putchar('\n');
Die Funktion fgets liest eine ganze Zeile ein. Die maximale Länge der Zeile wird dabei durch die Länge des Puffers buf begrenzt, die wir mit sizeof buf ermitteln. Der Compiler-Operator sizeof ermittelt die Größe einer Variablen zur Compile-Zeit. Hier liefert er den Wert 100. fgets liefert einfach den ersten Parameter, also buf, zurück, es sei denn, wir sind am Dateiende angekommen: dann liefert fgets NULL zurück, was wir als Schleifenende-Kriterium verwenden.

Die Ausgabe eines Punkts pro Zeile soll nur etwas visuelles Feedback für das Lesen der Zeilen geben. Die Funktion parseLine soll dann die Zeile in die Felder Staat, Hauptstadt, Einwohner, Stand und Quelle aufteilen und das Ergebnis in die Struktur line schreiben. Der typedef für die Struktur sieht so aus:

Code: Alles auswählen

typedef struct {
    char staat[60];
    char hauptstadt[50];
    char einwohner[12];
    char stand[10];
    char quelle[40];
} t_line;
Schließlich werden die Felder Hauptstadt und Einwohner via fprintf in die Ausgabedatei geschrieben. Das putchar soll nur einen Zeilenumbruch nach den vielen Punkten ausgeben, damit der Cursor nach Beendigung des Programms wieder am Zeilenanfang steht, wo dann der Prompt des Betriebssystems erscheint.

Am Ende von main() müssen wir noch die Dateien schließen:

Code: Alles auswählen

    fclose(fp);
    fclose(fpOut);

    return 0;
}
Jetzt fehlt noch die Funktion parseLine zum Aufteilen der eingelesenen Zeilen:

Code: Alles auswählen

/* Zerlegt eine Zeile in Felder.
 */
char *parseLine(char *buf, t_line *line) {
    int i;
    int len;

    initLine(line);

    buf = parseToken(buf, line->staat, ';');
    buf = parseToken(buf, line->hauptstadt, ';');
    buf = parseToken(buf, line->einwohner, ';');
    buf = parseToken(buf, line->stand, ';');
    buf = parseToken(buf, line->quelle, ';');
}
Zunächst wird die Struktur line initialisiert. Beim Aufruf von parseLine hatten wir die Adresse der Struktur mit &line als zweiten Parameter übergeben. Wir haben jetzt also einen Pointer auf die Struktur. Den übergeben wir an initLine, um die Struktur zu initialisieren. initLine setzt nur das erste Zeichen aller char-Arrays auf '\0' und erzeugt damit leere Strings. Nota bene: das erste Zeichen eines Arrays ist in C immer das Zeichen mit dem Index 0!

Code: Alles auswählen

void initLine(t_line *line) {
    line->staat[0] = '\0';
    line->hauptstadt[0] = '\0';
    line->einwohner[0] = '\0';
    line->stand[0] = '\0';
    line->quelle[0] = '\0';
}
So, die Felder sind leer, also können wir sie jetzt mit den Daten der aktuellen Zeile füllen. Das erledigt die Funktion parseToken, die den char-Pointer buf sukzessive weiterschaltet, bis alle fünf Felder eingelesen sind. buf zeigt also immer auf den Anfang des nächsten Feldes im Puffer. Man beachte, dass die Parameter-Variable buf eine eigene lokale Variable der Funktion parseLine ist. Wenn wir sie verändern, dann wird die gleichnamige Variable buf der Funktion main() nicht verändert!

Code: Alles auswählen

/* Liest ein Feld bis zum Trennzeichen.
 * Gibt einen Zeiger auf das naechste Feld zurueck.
 */
char *parseToken(char *start, char *field, char delimiter) {
    while (*start != delimiter && *start != '\0') {
        *field++ = *start++;
    }
    *field = '\0';

    return ++start;
}
In der while-Schleife wird Zeichen für Zeichen aus dem Puffer in die jeweilige Feld-Variable aus der Struktur line kopiert, bis wir das Trennzeichen oder das String-Ende finden. Das Kopieren könnten wir mit der Anweisung *field = *start durchführen; anschließend müssten wir noch die beiden Pointer weiterschalten. Die hier gezeigte Variante führt die Zuweisung durch und schaltet anschließend die Pointer weiter - alles auf einmal. Nach der Schleife zeigt start auf das Trennzeichen, daher das Inkrementieren vor dem return.

Einige Dinge hätte man auch anders machen können. So wäre es z.B. möglich ohne Kopieren der Strings auszukommen, indem man '\0'-Zeichen in den Puffer schreibt, wo die Trennzeichen sind. Oder man hätte mit strtok arbeiten können - eine String-Funktion, die Tokens anhand von Trennzeichen ermittelt. Diese hat allerdings den Nachteil, dass sie aufeinanderfolgende Trennzeichen wie eines behandelt. Das taugt nichts, wenn Felder auch leer sein können.

Auch die Struktur wäre nicht unbedingt nötig. Man könnte stattdessen mit separaten Array-Variablen arbeiten, die nicht zu einer Struktur zusammengefasst sind. Dann hätte man allerdings auch alle Felder einzeln als Parameter an die Unterfunktionen übermitteln müssen - oder man hätte die Arrays gleich global gemacht. Letzteres ist aber wegen des "information hiding"-Prinzips in der Software-Entwicklung nicht wünschenswert.

Hier ist noch das fertige C-Programm und die Eingabedatei zum Download:

file_io_2.zip
file2.c und hauptst.csv
(4.85 KiB) 222-mal heruntergeladen

Das ganze wird kompiliert mit

Code: Alles auswählen

zcc +cpm -subtype=pcw80 file2.c -o file2.com
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

16. Arbeiten mit Dateien 3

Beitrag von bbock »

Diesmal wollen wir eine C-Quelldatei bearbeiten: die Zeilenkommentare mit dem doppelten Schrägstrich sollen in gewöhnliche C-Kommentare mit Schrägstrich-Stern am Anfang und Stern-Schrägstrich am Ende des Kommentars umgewandelt werden, denn nicht alle C-Compiler unterstützen die Zeilenkommentare. Wie können wir dabei vorgehen?

Wir benötigen zwei Dateien: die Originaldatei mit den Zeilenkommentaren und die umgewandelte, die nur noch normale Kommentaren enthält. Die Originaldatei öffnen wir zum Lesen, die Ausgabedatei zum Schreiben. Wir lesen die Originaldatei Zeile für Zeile ein. Dann suchen wir nach "//". Finden wir die beiden Schrägstriche, dann ersetzen wir die beiden Zeichen durch "/*"; danach suchen wir das Zeilenende und fügen davor ein "*/" ein. Finden wir die beiden Schrägstriche nicht, dann schreiben wir die Zeile unverändert in die Ausgabedatei.

Wir beginnen mit ein paar Variablen in der Funktion main():

Code: Alles auswählen

#include <stdio.h>
#include <string.h>

#define FILENAMELENGTH  20
#define BUFSIZE        512

/* Main
 */
int main(void) {
    char filename[FILENAMELENGTH];
    char filenameOut[FILENAMELENGTH];
    FILE *fp;
    FILE *fpOut;
    char buf[BUFSIZE];
    char *cp;
    size_t len;
Wir brauchen die beiden char-Arrays für die Dateinamen, die wir während der Programmausführung über die Tastatur eingeben werden. Wir sehen eine Länge von 20 Zeichen vor, was für CP/M-Dateinamen ausreicht. Dann brauchen wir noch zwei FILE pointer und einen Puffer für eine eingelesene Zeile. Der Puffer ist mit 512 Zeichen ausreichend groß bemessen, dass man gängige C-Programme damit bearbeiten können sollte. Natürlich ist C nicht formatgebunden, d.h. man könnte sogar ein komplettes C-Programm in eine einzige Zeile schreiben. Für solche Fälle ist unser Programm nicht ausgelegt; in der Praxis sollten Zeilen mit mehr als 512 Zeichen eher nicht vorkommen. Schließlich haben wir noch einen char pointer und eine Längen-Variable vom Typ size_t. Die Typdeklaration von size_t finden wir im include-Verzeichnis des Compilers unter sys/types.h: size_t entspricht hier einem unsigned int. Variablen dieses Typs können also Werte im Bereich von 0 bis 65535 annehmen.

Es folgt die Eingabe der Dateinamen und das Öffnen der Dateien:

Code: Alles auswählen

    printf("Zu konvertierende Datei: ");
    scanf("%s", filename);
    
    fp = fopen(filename, "r");
    if (fp == NULL) {
        printf("Fehler: Datei %s nicht gefunden!\n", filename);
        return -1;
    }

    printf("Ausgabedatei: ");
    scanf("%s", filenameOut);
    
    fpOut = fopen(filenameOut, "w");
    if (fpOut == NULL) {
        printf("Fehler: Konnte Ausgabedatei %s nicht oeffnen!\n", filenameOut);
        fclose(fp);
        return -1;
    }
Wenn eine der Dateien nicht geöffnet werden kann, z.B. weil wir einen ungültigen Dateinamen eingegeben haben, dann liefert fopen() den Wert NULL zurück und wir geben eine Fehlermeldung aus und beenden das Programm mit dem Fehlercode -1.

Wir lesen der Zeilen in einer while-Schleife ein:

Code: Alles auswählen

    // Zeilen lesen
    while (fgets(buf, sizeof buf, fp) != NULL) {
Die bereits bekannte Funktion fgets() liest die Zeile in den Puffer buf ein und begrenzt das Einlesen sicherheitshalber auf die Größe des Puffers. Solange die Funktion einen Wert ungleich NULL zurückliefert, haben wir eine weitere Zeile einlesen können, die wir anschließend verarbeiten.

Zunächst ermitteln wir die Länge der Zeile:

Code: Alles auswählen

        len = strlen(buf);
Dann suchen wir den doppelten Schrägstrich mit der String-Funktion strstr(). Diese sucht in einem String nach dem Vorkommen eines Such-Strings. Wenn der Suchstring gefunden wird, dann liefert strstr() einen Pointer auf das erste gefundene Zeichen zurück, also den ersten Schrägstrich. Den wollen wir nicht verändern und inkrementieren den Pointer, damit er auf den zweiten Schrägstrich zeigt. Mit *cp = '*'; überschreiben wir den Schrägstrich mit einem Stern.

Code: Alles auswählen

        // doppelten Schraegstrich suchen
        cp = strstr(buf, "//");
        if (cp != NULL) {
            ++cp; // zeigt jetzt auf den zweiten Schraegstrich
            *cp = '*'; // "//" in "/*" umwandeln
Jetzt müssen wir noch das Zeilenende finden um dort die Kommentarende-Kennung */ einzufügen.

Code: Alles auswählen

            // Zeilenende suchen
            cp = strchr(cp, '\n');
            if (cp == NULL) {
                printf("Oh - das sollte nicht passieren: Zeilenende nicht gefunden!\n");
                return -1;
            }
            
            // Zeilenende mit " */" ueberschreiben
            *cp++ = ' ';
            *cp++ = '*';
            *cp   = '/';
Wir suchen das Newline-Zeichen mit der String-Funktion strchr(), da wir diesmal nur nach einem einzelnen Zeichen suchen wollen. Mit strstr() hätte man das auch machen können, allerdings wäre das weniger effizient.

Finden wir kein Newline-Zeichen, dann stimmt etwas ganz und gar nicht und wir steigen mit einer Fehlermeldung aus. Andernfalls überschreiben wir das Newline-Zeichen und die beiden folgenden Zeichen mit Leerzeichen, Stern und Schrägstricht, also " */". Damit zerstören wir das Zeilenende, das wir gleich aber wieder anhängen. Das tun wir in jedem Fall, also auch dann, wenn wir keinen doppelten Schrägstrich gefunden haben. Das ist nötig, weil fgets() nur das Newline-Zeichen einliest, nicht aber das Carriage-Return-Zeichen.

Vorher passen wir aber noch die Zeilenlänge an, die ja jetzt um drei eingefügte Zeichen angewachsen ist:

Code: Alles auswählen

            // Laenge anpassen: wir haben jetzt 3 Zeichen mehr
            len += 3;
        }
Jetzt schreiben wir das Zeilenende und das String-Abschlusszeichen in den Puffer und schreiben den Puffer mit der evtl. geänderte Zeile in die Ausgabedatei. Dazu verwenden wir die Funktion fwrite():

Code: Alles auswählen

        // CR, LF und String-Abschluss
        buf[--len] = '\r';
        buf[++len] = '\n';
        buf[++len] = '\0';
        
        // Zeile schreiben
        fwrite(buf, 1, len, fpOut);
Am Ende, also nach der Schleife, müssen wir noch die beiden Dateien schließen und liefern den Fehlercode 0 an das Betriebssystem zurück.

Code: Alles auswählen

    }

    fclose(fp);
    fclose(fpOut);

    return 0;
}
In file_io_3.zip befindet sich das komplette Programm file3.c und eine Quelldatei mit Zeilenkommentaren zum Ausprobieren (pointer1.c).

file_io_3.zip
file3.c und pointer1.c
(1.22 KiB) 219-mal heruntergeladen

Es sei noch anzumerken, dass das Programm in dieser Form noch nicht perfekt ist. Die Limitierung auf eine Zeilenlänge von max. 512 Zeichen (eigentlich noch etwas weniger wegen der Zeilenende- und Stringende-Zeichen) wurde bereits erwähnt. Außerdem erkennt das Programm nicht, wenn sich die doppelten Schrägstriche in einem String-Literal befinden, d.h. wenn im Code so etwas wie s = "abc // xyz"; vorkommt, dann würde hier der doppelte Schrägstrich fälschlicherweise als Kommentar erkannt. Selbiges gilt für doppelte Schrägstriche in Präprozessor-Direktiven. Natürlich kann man das Programm erweitern, damit es solche Fälle erkennt, aber das würde hier den Rahmen sprengen.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

17. Mehrdimensionale Arrays

Beitrag von bbock »

Wir sind bereits in Kapitel 10.2 mit Arrays in Berührung gekommen. Die dort behandelten Beispiele waren eindimensionale Arrays, auch Vektoren genannt. Deren Elemente werden durch einen Index adressiert. Es gibt in C auch mehrdimensionale Arrays, deren Elemente mit zwei oder mehr Indizes adressiert werden; dementsprechend spricht man von zwei-, drei- oder mehrdimensionalen Arrays.
Ein zweidimensionaler Array mit zwei Zeilen und vier Spalten könnte z.B. so deklariert werden:

Code: Alles auswählen

int zweidim[2][4];
Möchte man ihn bei der Deklaration bereits initialisieren, dann geht das wie folgt:

Code: Alles auswählen

int zweidim[2][4] = {
                      {1, 2, 3, 4},
                      {5, 6, 7, 8}
                    };
Das veranschaulicht die zweidimensionale Struktur. Die Initialisierung ist aber auch folgendermaßen möglich:

Code: Alles auswählen

int zweidim[2][4] = {1, 2, 3, 4, 5, 6, 7, 8};
Werden nicht für alle Elemente Werte bei der Initialisierung angegeben, dann werden die übrigen Elemente automatisch mit 0 initialisiert. Fehlt die Initialisierung, dann sind die Elemente mit zufälligen Werten belegt, es sei denn, es handelt sich bei dem Array um eine globale oder um eine static-Variable: dann werden alle Elemente mit 0 initialisiert.

Wie man oben sieht, ist die Anzahl der Elemente eines mehrdimensionalen Arrays gleich dem Produkt der Dimensionen, in obigem Beispiel also 2 * 4 = 8 Elemente. Man beachte, dass mehrdimensionale Arrays schnell sehr groß werden können. So hat der vierdimensionale Array int vierdim[14][14][14][14] bereits 14*14*14*14 = 38.416 Elemente und belegt auf einem 8-Bit-System (int = 2 Bytes) 76.832 Bytes. Das sprengt bereits das 64k-Limit.

Als praktisches Beispiel wollen wir eine Quartalsabrechnung erstellen, ähnlich wie in einer Tabellenkalkulation:

Code: Alles auswählen

      Artikel1   Artikel2   Artikel3   Gesamt
Q1       15         98         35        148
Q2       23         47         52        122
Q3       48         32         14         94
Q4       47         56         76        179
Gesamt  133        233        177        543
Dazu verwenden wir einen zweidimensionalen Array:

Code: Alles auswählen

    int matrix[5][4] = {
        // Zeile: Artikel 1, Artikel 2, Artikel 3, Gesamt
        {15, 98, 35, 0}, // Q1
        {23, 47, 52, 0}, // Q2
        {48, 32, 14, 0}, // Q3
        {47, 56, 76, 0}, // Q4
        { 0,  0,  0, 0}  // Gesamt
    };
Die Zeilen- und Spaltensummen sind mit 0 initialisiert; wir werden die Werte im Programm berechnen.

Code: Alles auswählen

#include <stdio.h>

int main(void)
{
    int matrix[5][4] = {
        // Zeile: Artikel 1, Artikel 2, Artikel 3, Gesamt
        {15, 98, 35, 0}, // Q1
        {23, 47, 52, 0}, // Q2
        {48, 32, 14, 0}, // Q3
        {47, 56, 76, 0}, // Q4
        { 0,  0,  0, 0}  // Gesamt
    };
    unsigned char zeile, spalte;
    int sum;

    // Spaltensummen
    for (spalte = 0; spalte < 3; spalte++) {
        sum = 0;
        for (zeile = 0; zeile < 4; zeile++) {
            sum += matrix[zeile][spalte];
        }
        matrix[4][spalte] = sum;
    }

    // Zeilensummen
    for (zeile = 0; zeile < 5; zeile++) {
        sum = 0;
        for (spalte = 0; spalte < 3; spalte++) {
            sum += matrix[zeile][spalte];
        }
        matrix[zeile][3] = sum;
    }
    
    // Ausgabe
    printf("\tArt1\tArt2\tArt3\tGesamt\n");
    
    for (zeile = 0; zeile < 5; zeile++) {
        if (zeile < 4) {
            printf("Q%d\t", zeile + 1);
        }
        else {
            printf("Gesamt\t");
        }
        
        for (spalte = 0; spalte < 4; spalte++) {
            printf("%d\t", matrix[zeile][spalte]);
        }
        printf("\n");
    }
    
    return 0;
}
Hier ist das ganze Programm in der main-Funktion realisiert, was nicht besonders schön ist. Besser wäre es, das Programm in meherere Funktionen aufzuteilen; dabei lernen wir, wie man ein Array an eine Funktion übergibt:

Code: Alles auswählen

#include <stdio.h>

/* Calculates the sum for each column of the matrix. */
void calcColumnSums(int m[5][4]) {
    unsigned char zeile, spalte;
    int sum;

    for (spalte = 0; spalte < 3; spalte++) {
        sum = 0;
        for (zeile = 0; zeile < 4; zeile++) {
            sum += m[zeile][spalte];
        }
        m[4][spalte] = sum;
    }
}

/* Calculates the sum for each row of the matrix. */
void calcRowSums(int m[5][4]) {
    unsigned char zeile, spalte;
    int sum;

    for (zeile = 0; zeile < 5; zeile++) {
        sum = 0;
        for (spalte = 0; spalte < 3; spalte++) {
            sum += m[zeile][spalte];
        }
        m[zeile][3] = sum;
    }
}

/* Prints the quarterly statement. */
void printQuarterlyStatement(int m[5][4]) {
    unsigned char zeile, spalte;
    
    printf("\tArt1\tArt2\tArt3\tGesamt\n");
    
    for (zeile = 0; zeile < 5; zeile++) {
        if (zeile < 4) {
            printf("Q%d\t", zeile + 1);
        }
        else {
            printf("Gesamt\t");
        }
        
        for (spalte = 0; spalte < 4; spalte++) {
            printf("%d\t", m[zeile][spalte]);
        }
        printf("\n");
    }
}

/* The main function. */
int main(void)
{
    int matrix[5][4] = {
        // Zeile: Artikel 1, Artikel 2, Artikel 3, Gesamt
        {15, 98, 35, 0}, // Q1
        {23, 47, 52, 0}, // Q2
        {48, 32, 14, 0}, // Q3
        {47, 56, 76, 0}, // Q4
        { 0,  0,  0, 0}  // Gesamt
    };

    calcColumnSums(matrix);
    calcRowSums(matrix);
    printQuarterlyStatement(matrix);
    
    return 0;
}
Normalerweise verwenden alle Funktionsaufrufe call-by-value bei der Parameterübergabe. Die Ausnahme sind Arrays: hier wird nicht etwa eine Kopie des gesamten Arrays an die Funktion übergeben, sondern lediglich die Adresse des ersten Elements des Arrays. So kann eine Funktion wie calcColumnSums() die Inhalte der Variablen matrix in main() verändern ohne dabei besonders mit Pointern arbeiten zu müssen. Wir erinnern uns: Der Name eines Arrays ist ein Zeiger auf das erste Element.
Benutzeravatar
bbock
Beiträge: 242
Registriert: 08.02.2015, 15:31

18. Absolute Adressen

Beitrag von bbock »

Manchmal ist es erforderlich auf bestimmte Speicheradressen zuzugreifen. Hierfür bietet das z88dk verschiedene Möglichkeiten. Eine Möglichkeit ist die Verwendung von Pointer-Variablen. So kann man z.B. den Wert 23 folgendermaßen in die Speicherzelle FF40 schreiben:

Code: Alles auswählen

unsigned char *p;
p = (unsigned char *) 0xFF40;
*p = 23;
Um den Wert wieder auszulesen, dereferenzieren wir den Pointer:

Code: Alles auswählen

printf("%d\n", *p);
Das komplette Testprogramm könnte so aussehen:

Code: Alles auswählen

#include <stdio.h>

int main(void) {
    unsigned char *p;
    p = (unsigned char *) 0xF400;
    *p = 23;

    printf("%d\n", *p);

    return 0;
}
Wir kompilieren es mit

Code: Alles auswählen

zcc +cpm -subtype=pcw80 -compiler=sccz80 -pragma-define:REGISTER_SP=0xF3FF -create-app testprg1.c -o testprg1.com
Die pragma-define-Option stellt sicher, dass der Stackpointer bei F3FF beginnt. Damit bleibt der Speicherbereich ab F400 sicher vor Überschreibung durch den Stack.

Es gibt noch einen einfacheren Weg um auf Werte an absoluten Adressen zuzugreifen: Variablen können so deklariert werden, dass sie an einer bestimmten Adresse im Speicher liegen.

Code: Alles auswählen

unsigned char ch @ 0xF400;
Das obige Testprogramm kann also wie folgt geändert werden:

Code: Alles auswählen

#include <stdio.h>

unsigned char ch @ 0xF400;

int main(void) {
    ch = 23;
    printf("%d\n", ch);
    printf("%d\n", *((unsigned char *) 0xF400));

    return 0;
}
Das zweite printf dient zur Überprüfung, dass der Wert tatsächlich in der Speicherzelle F400 angekommen ist.

Hat man Maschinencode an einer bestimmten Speicheradresse abgelegt, dann kann man die Einsprungstellen als C-Funktionen zugänglich machen. Die Syntax ist wie folgt:

Code: Alles auswählen

void *gx_scrAcc(void) @ 0xF494;
Ruft man im C-Programm die Funktion mit gx_scrAcc() auf, dann erzeugt der Compiler einen CALL-Befehl zur Speicheradresse F494. Für weitere Details zur Zusammenarbeit von C mit Maschinencode sei auf die z88dk-Dokumentation unter "Mixing C and Z80 Assembler" verwiesen. Dort werden Details zur Aufrufkonvention, die Parameter-Übergabe, das Erstellen von Bibliotheken und die Verwendung von Inline Assembler erläutert.
Antworten