Programmieren mit C


Inhalt


Willkommen

Installation

Das Terminal

Exkurs: Scanf und Sonderzeichen

Exkurs: continue & break

Statische Arrays

Exkurs: sizeof

Dynamische Arrays

Exkurs: Speicherbereiche

Funktionen

Exkurs: Funktionen & Arrays

Kommandozeilenparameter

Structs

Dateien

Anwendung: Textdatei einlesen

Hintergrund: Unicode

Stringbibliothek

Ausblick

Dark Mode

Funktionen und Arrays

In der Vorlesung haben wir uns mit der Auslagerung von Codesegmenten in Funktionen beschäftigt. Ein häufiges Problem ist hier die Nutzung von Arrays, besonders statischen. Hier werden wir einen näheren Blick auf häufige Probleme und Besonderheiten werfen.

Nehmen wir an, wir wollen eine Funktion schreiben, die alle geraden Zahlen von 1 bis zu einer Variable N errechnet und zurück gibt. Ein erster Versuch könnte so aussehen.

#include <stdio.h>

int* gerade_zahlen(int N) {
    int ret[N/2];

    int j = 0;
    for(int i=2; i<N; i += 2) {
        ret[j] = i;
        ++j;
    }

    return ret;
}

int main() {
    int N = 20;

    int arr[N/2];
    arr = gerade_zahlen(N);

    printf("%d\n", arr[0]);
}

Der Compiler gibt folgende Fehler aus.

test.c: In function ‘gerade_zahlen’:
test.c:12:12: warning: function returns address of local variable [-Wreturn-local-addr]
   12 |     return ret;
      |            ^~~
test.c: In function ‘main’:
test.c:19:9: error: assignment to expression with array type
   19 |     arr = gerade_zahlen(N);
      |         ^

Kümmern wir uns zuerst um den Fehler. In der Vorlesung zu statischen Arrays haben wir gelernt, dass eine Zuweisung an ein ganzes Array nur beim Anlegen stattfinden darf. Danach kann nur auf die einzelnen Elemente zugegriffen werden. Versuchen wir es also mit einer Zuweisung beim anlegen.

int arr[N/2] = gerade_zahlen(N);

Hier bekommen wir einen anderen Fehler.

test.c:18:5: error: variable-sized object may not be initialized
18 |     int arr[N/2] = gerade_zahlen(N);
   |     ^~~

Als Rückgabewert unserer Funktion haben wir int* definiert. Hier kann C natürlich nicht überprüfen, ob ein Array oder ein einfacher Pointer zurückgegeben wird. Wir erinnern uns: für ein Array wird nur ein Pointer zum ersten Objekt in diesem angelegt.

Definieren wir also in der main() auch nur einen int* und weisen diesem den Rückgabewert zu.

int main() {
    int N = 20;

    int *arr = gerade_zahlen(N);

    printf("%d\n", arr[0]);
}

So verkürzt sich die Ausgabe des Compilers zu folgendem.

test.c: In function ‘gerade_zahlen’:
test.c:12:12: warning: function returns address of local variable [-Wreturn-local-addr]
   12 |     return ret;
      |            ^~~

Wir bekommen noch eine Warnung, das Kompilieren war aber erfolgreich. Führen wir das Programm jetzt aus, sieht das Ergebnis so aus.

Speicherzugriffsfehler Der Speicherzugriff in der printf Funktion - arr[0] war ungültig. Lesen wir also nochmals die Warnung des Compilers. Es wird davor gewarnt eine local variable, in unserem Fall das statische Array ret aus der Funktion zurückzugeben. Wir erinnern uns: statische Arrays werden auf dem Stack und sind damit an ihren aktuellen Scope gebunden. Sobald dieser endet, wird der Speicherplatz wieder freigegeben. Da das Array in der Funktion gerade_zahlen angelegt ist, wird der Speicherplatz freigegeben, sobald diese endet. Zurückgegeben wir dann ein Pointer auf nichts - ein NULL-Pointer. Versuchen wir diesen zu dereferenzieren, so kommt es zu einem Speicherzugriffsfehler.

Hier gibt es zwei Lösungen. Entweder wir legen das statische Array in der main() Funktion an und geben es unserer Funktion als Parameter mit, oder wir nutzen ein dynamisches Array.

Eine Lösung mit statischen Arrays könnte so aussehen.

void gerade_zahlen(int *arr, int N, int *j) {
    *j = 0;
    for(int i=2; i<=N; i += 2) {
        arr[*j] = i;
        *j += 1;
    }
}

int main() {
    int N = 20;
    int j = 0;

    int arr[N/2];
    gerade_zahlen(arr, N, &j);

    for(int i=0; i<j; ++i) {
        printf("%d\n", arr[i]);
    }
}

Wir geben der Funktion unser Array, die größte zu berechnende Zahl N und die Zählvariable j mit. Letztere wird als Pointer übergeben, um den Inhalt ändern zu können. Da wir nicht vorher wissen, wie weit das Array befüllt sein wird (nehmen wir es zumindest an), wollen wir mit j tracken, wo der letzte Eintrag ist.

Bei dieser Variante gibt es noch eine Besonderheit zu beachten. Da wir das Array lediglich als Pointer an die Funktion übergeben können, funktioniert die Bestimmung der Größe mit sizeof() auch nicht mehr. Benötigen wir die Länge des Arrays, so müssen wir diese als Argument an die Funktion übergeben.

Nutzen wir dynamische Arrays, können wir die Arbeit des Nutzers der Funktion noch verkürzen.

int* gerade_zahlen(int N, int *j) {
    int *arr = malloc(N/2 * sizeof(int));

    *j=0;
    for(int i=2; i<=N; i += 2) {
        arr[*j] = i;
        *j += 1;
    }

    return arr;
}

int main() {
    int N = 20;
    int j = 0;

    int *arr = gerade_zahlen(N, &j);

    for(int i=0; i<j; ++i) {
        printf("%d\n", arr[i]);
    }

    free(arr);
}

Um die Länge des Arrays nicht separat übergeben zu müssen, können wir diese auch am Anfang des Arrays speichern.

int* gerade_zahlen(int N) {
    int *arr = malloc((N/2+1) * sizeof(int));

    arr[0] = 1;
    for(int i=2; i<=N; i += 2) {
        arr[arr[0]] = i;
        arr[0] += 1;
    }

    return arr;
}

int main() {
    int N = 20;

    int *arr = gerade_zahlen(N);

    for(int i=1; i<arr[0]; ++i) {
        printf("%d\n", arr[i]);
    }

    free(arr);
}

Wollen wir außerdem die Länge des Arrays immer passend wählen, können wir mit realloc nur bei Bedarf zusätzlichen Speicherbereich reservieren und mit einer Größe von 1 starten.

int* gerade_zahlen(int N) {
    int *arr = malloc(1 * sizeof(int));

    arr[0] = 1;
    for(int i=2; i<=N; i += 2) {
        arr[0] += 1;
        arr = realloc(arr, (arr[0])*sizeof(int));
        arr[arr[0]-1] = i;
    }

    return arr;
}

So haben wir aus den eingebauten Arrays eine Datenstruktur erstellt, die ihre eigene Länge speichert und bei Bedarf einfach vergrößert werden kann. Diese Datenstruktur könnten wir nun wiederum mit eigenen Funktionen verwalten, um die Verwendung zu vereinfachen.