Menu

Arrays in C: deklarieren, initialisieren und durchlaufen

Wie man Arrays in C deklariert und initialisiert, sie ab null indiziert, die Länge mit sizeof berechnet, über die Elemente iteriert - und warum das Lesen hinter dem Ende undefiniertes Verhalten ist statt einer Fehlermeldung.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein Array ist ein Block fester Größe aus Elementen desselben Typs, die hintereinander im Speicher liegen. Diese zusammenhängende Anordnung ist die ganze Geschichte: Sie ist der Grund, warum das Indizieren sofort geht, warum Arrays und Zeiger so eng verwandt sind und warum C dir die Adresse von Element null geben und den Rest per Arithmetik finden lassen kann.

Ein Array deklarieren

Elementtyp, Name und eine Größe in eckigen Klammern:

int    scores[5];       // 5 ints
double prices[100];     // 100 doubles
char   initials[3];     // 3 chars

Die Größe ist die Anzahl der Elemente, und sie muss dort bekannt sein, wo das Array deklariert wird - üblicherweise ein Literal oder eine per #define festgelegte Konstante. Der Speicher wird sofort reserviert; später wird nichts mehr belegt.

Ein frisch deklariertes lokales Array enthält Müll, keine Nullen:

Die zweite Zeile kann Nullen ausgeben, riesige Zahlen ausgeben und sich zwischen Läufen oder zwischen einem Debug- und einem Release-Build unterscheiden. Diese Werte zu lesen ist undefiniertes Verhalten; initialisiere, bevor du liest.

Initialisieren

Mehrere Formen, alle nützlich:

Das {0}-Idiom ist das, was man sich merken sollte: alle Elemente, die du nicht aufführst, werden mit null initialisiert, eine einzelne Null nullt also das gesamte Array. Benannte Initialisierer ([4] = 99) erlauben dir, bestimmte Positionen zu setzen und den Rest bei null zu lassen, was für Nachschlagetabellen praktisch ist, die überwiegend leer sind.

Eines geht nicht: einem Array nachträglich etwas zuweisen:

int a[3] = {1, 2, 3};
int b[3];
b = a;                    // Fehler: einem Array kann nicht zugewiesen werden
memcpy(b, a, sizeof a);   // so kopiert man eines

Die Indizierung beginnt bei null

Das erste Element ist arr[0] und das letzte arr[n-1]. Ein arr[n] gibt es nicht.

Führe die letzte Schleife aus und sieh dir die Adressen an: Jede liegt genau sizeof(int) Bytes hinter der vorherigen. Die Elemente liegen tatsächlich Ende an Ende:

 Index      0         1         2         3         4
         +--------+--------+--------+--------+--------+
 Wert    |   88   |   95   |   75   |   60   |  100   |
         +--------+--------+--------+--------+--------+
 Versatz  +0       +4       +8       +12      +16      Bytes
          ^
          &scores[0], wozu auch `scores` zerfällt

Die nullbasierte Indizierung ist keine willkürliche Wahl - sie macht den Index zu einem Versatz vom Anfang, und genau das braucht die Adressarithmetik. scores[i] ist definiert als *(scores + i); siehe Zeiger und Arrays.

Die Länge ermitteln

C speichert die Länge eines Arrays zur Laufzeit nirgends, aber der Compiler kennt sie, und sizeof verschafft dir Zugriff:

sizeof(arr) / sizeof(arr[0]) ist das Idiom. Durch arr[0] zu teilen statt durch ein fest eingetragenes sizeof(double) bedeutet, dass die Zeile weiter funktioniert, wenn du den Elementtyp änderst.

Die Falle: Das funktioniert nur dort, wo das Array deklariert wurde. Übergib das Array an eine Funktion, und der Parameter ist ein Zeiger, also misst sizeof stattdessen den Zeiger - typischerweise 8 Bytes, was eine „Länge" von 1 oder 2 ergibt. Deshalb nimmt jede array-verarbeitende Funktion in C auch eine Anzahl entgegen:

Über ein Array iterieren

Die Standardform ist eine for-Schleife von 0, solange i < n:

Beachte i < n, nicht i <= n. Bei n Elementen sind die gültigen Indizes 0 bis n - 1, <= führt also einen zusätzlichen Durchlauf aus und liest hinter das Ende. hottest und coldest aus temps[0] statt aus 0 zu initialisieren ist die andere Gewohnheit, die man übernehmen sollte - ein Maximum bei null zu beginnen geht bei ausschließlich negativen Daten still kaputt.

Außerhalb der Grenzen ist undefiniertes Verhalten

Das ist der Teil von C, der Leute aus anderen Sprachen überrascht. Es gibt keine Bereichsprüfung. Keine zur Übersetzungszeit, keine zur Laufzeit. arr[10] bei einem Array mit fünf Elementen ist kein Fehler - es ist eine Adressberechnung, die der Compiler kommentarlos durchführt.

int arr[5] = {1, 2, 3, 4, 5};
arr[7] = 99;     // schreibt 8 Bytes hinter das Ende des Arrays
int x = arr[-1]; // liest vor dem Anfang

Was danach passiert, ist undefiniertes Verhalten, und seine Symptome sind ärgerlich vielfältig:

  • es scheint zu funktionieren, weil der getroffene Speicher unbenutzt war;
  • eine andere Variable ändert sich ohne sichtbaren Grund;
  • das Programm stürzt mit einem Segmentierungsfehler ab, womöglich viel später;
  • das Verhalten unterscheidet sich zwischen -O0 und -O2.

„Scheint zu funktionieren" ist der gefährliche Fall, denn er bedeutet, dass der Bug ausgeliefert wird. Drei Verteidigungslinien:

  1. Iteriere mit i < n. Die meisten Überschreitungen sind Off-by-one-Fehler in einer Schleifenbedingung.
  2. Prüfe Indizes, die aus Eingaben stammen.
  1. Baue während der Entwicklung mit einem Sanitizer. gcc -fsanitize=address -g prog.c verwandelt die meisten Überschreitungen in einen sofortigen Bericht mit Datei, Zeile und dem beteiligten Array.

Arrays anderer Typen

Dieselbe Syntax funktioniert für jeden Elementtyp, auch für Strukturen:

Die Zeile char word[6] = "hallo"; ist eine Pause wert: Eine C-Zeichenkette ist nur ein char-Array, dessen letztes bedeutsames Byte '\0' ist. Dieser Terminator ist der Grund, warum das Array für fünf Buchstaben sechs Plätze braucht.

Arrays variabler Länge, kurz gefasst

C99 erlaubt ein Array, dessen Größe ein Laufzeitwert ist:

int n = get_count();
int buffer[n];        // ein Array variabler Länge

Zwei Warnungen. Die Größe steht fest, sobald das Array erzeugt wurde, ein VLA ist also keine wachsende Liste. Und der Speicher kommt vom Stack, ein großes oder von einem Angreifer gesteuertes n kann ihn also überlaufen lassen und das Programm zum Absturz bringen - weshalb viele Projekte, darunter der Linux-Kernel, VLAs komplett verbieten. Für C11-Implementierungen ist ihre Unterstützung zudem optional.

Wenn die Größe wirklich erst zur Laufzeit bekannt ist, belege stattdessen Speicher:

Beachte, dass sich arr[i] identisch liest, egal ob arr ein Array oder ein Zeiger auf belegten Speicher ist - die Indizierungssyntax kümmert das nicht. Alle Einzelheiten unter dynamischer Speicher.

Häufig gestellte Fragen

Wie deklariert man ein Array in C?

Gib den Elementtyp, einen Namen und eine Größe in eckigen Klammern an: int scores[5]; reserviert Platz für fünf ints. Die Größe muss dort, wo das Array deklariert wird, eine bekannte Konstante sein (abgesehen von Arrays variabler Länge), und die Elemente sind zunächst uninitialisiert, sofern du keine Werte angibst.

Wie ermittelt man die Länge eines Arrays in C?

sizeof(arr) / sizeof(arr[0]) - die Gesamtgröße in Bytes geteilt durch die Größe eines Elements. Das funktioniert nur in dem Gültigkeitsbereich, in dem das Array deklariert wurde: Sobald das Array an eine Funktion übergeben wurde, ist es zu einem Zeiger zerfallen und sizeof misst stattdessen den Zeiger.

Was passiert, wenn man in C über das Ende eines Arrays hinausgeht?

Nichts hält dich auf. C führt keine Bereichsprüfung durch, also liest oder schreibt arr[10] bei einem Array mit 5 Elementen den Speicher, der dort zufällig liegt. Das ist undefiniertes Verhalten: Es kann Müll ausgeben, eine andere Variable beschädigen oder abstürzen - und es kann scheinbar funktionieren, bis es das nicht mehr tut.

Wie initialisiert man in C alle Elemente eines Arrays mit null?

int arr[100] = {0}; - alle Elemente, die du nicht aufführst, werden mit null initialisiert, eine einzige Null setzt also das gesamte Array. int arr[100] = {}; funktioniert ebenfalls in C23. Ohne jeden Initialisierer ist der Inhalt eines lokalen Arrays unbestimmter Müll.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S