Lektion 3 - Arrays und Strings

Arrays

In Programmen wollen wir öfter mal Daten gruppieren, die irgendwie zusammengehören, z.B. in einer Liste. Der einfachste Weg, dies in C zu tun, ist die Verwendung eines Arrays. Ich gehe davon aus, dass die meisten von euch schon mit Arrays gearbeitet haben, in der einen oder anderen Weise. In dieser Übung werden wir uns auf die Auswirkungen von Arrays auf den Speicher konzentrieren. Lass uns ein Integer-Array der Größe 10 deklarieren:

int a[10];

Das Verhalten auf dem Stack ähnelt dem, das wir in der letzten Lektion gesehen haben. Das Programm weist einfach Platz für eine Reihe von 10 Integer zu, beginnend an der nächsten freien Stack-Adresse. Diese Adresse, d.h. die Adresse des Array-Anfangs, wird in a gespeichert. Wie sich herausstellt, ist a kaum mehr als ein Pointer!

Auf einzelne Elemente eines Arrays können wir mit dem Operator [ ] zugreifen. Lass uns dem ersten Element des Arrays einen Wert zuweisen.

a[0] = 5;

Das erste Element enthält nun die Zahl 5. Da die Indizierung bei 0 beginnt, ist der letzte Index eines Arrays mit n Elementen der Index n-1 (im Fall des Arrays a wäre das also der Index 9). Es gibt jedoch keinen C-Mechanismus, der verhindert, dass man über den letzten Index hinausgeht. Die Anweisung

a[10] = 5;

ist valide C Syntax. Der Grund dafür ist, dass a[i] einfach syntaktischer Zucker ist. Was wir wirklich tun, wenn wir mit a[i] auf einen Index i eines Arrays zugreifen, ist Folgendes: Wir nehmen den Wert von a (eine Adresse), schauen i Integer voraus, und nehmen dann den Wert, der an der Adresse gespeichert ist, an der wir gelandet sind. Die obige Zeile ist also eigentlich nichts weiter als der Ausdruck:

*(a+10) = 5;

Man sollte deshalb darauf achten, dass man nur auf Speicher zugreift, der tatsächlich für das Array reserviert wurde. Außerdem ist zu beachten, dass eine Array-Deklaration den Inhalt des Arrays nicht initialisiert und man nicht davon ausgehen kann, dass es Nullen enthält. (Dasselbe gilt für jede Variablendeklaration in C. Das Überschreiben von Speicher, der nicht mehr verwendet wird, wird als zu ineffizient angesehen.)

Auf Arrays wird oft in Kombination mit for-Schleifen zugegriffen, zum Beispiel wie folgt:

int len = 20;
int array[len];
for(int i = 0; i < len; i++) {
  array[i] = 0;
}

Mit diesem Pattern können wir jeden gültigen Index eines Arrays besuchen und jedes Risiko von Speicherfehlern vermeiden.

Als praktische Übung: schreibe ein Programm, das ein Integer-Array beliebiger Länge als Eingabe nimmt und ein neues Array erstellt, das nur die geraden Zahlen aus dem Eingabe-Array enthält. Verwende gerne diese sehr minimale Vorgabe:

vorgabe3_1.c

Strings

Programme arbeiten oft mit Daten, die dafür gedacht sind, für Menschen lesbar zu sein. Viele Programmiersprachen haben einen eigenen Typ hierfür: Strings. In C gibt es keinen Datentyp für Strings. Tatsächlich könntest du selbst entscheiden, wie du sie implementieren möchtest. Es gibt jedoch eine Konvention, die festlegt, wie Strings zu implementieren sind. Da jede andere Software dieser Konvention folgt, sollten wir das auch tun.

Zunächst einmal sind Strings ein Array von Charakteren, sodass die Deklaration eines Strings sehr einfach ist:

char str[12];

Eine Funktion wie printf iteriert einfach über ein char-Array und gibt die Zeichen aus. Aber woher weiß printf (oder ein Programm, was wir selbst implementieren), wann es aufhören soll? Eine Möglichkeit wäre gewesen, einfach immer die Größe der Zeichenkette in einer separaten Variablen zu speichern. Diese Lösung wurde als zu ineffizient angesehen. Stattdessen wird ein spezieller Wert hinter den Text gesetzt, um dessen Ende zu markieren: die Null (viele halten diese Designentscheidung für einen Fehler. Das bedeutet, dass wir, wenn wir n Zeichen für einen String reservieren, nur die ersten n-1 verwenden können, da das letzte Zeichen benötigt wird, um die abschließende Null zu speichern. Wenn wir also das Folgende tun:

str[11] = 0;
// or str[11] = '\0', it's the same

jedes andere Stück Code, das str verarbeitet, weiß, wo es endet, und wird nicht in Speicherprobleme geraten. Wir müssen jedoch beachten, dass diese sogenannte terminierende Null tatsächlich vorhanden ist, da unsere Programme sonst so lange Zeichen bis erst ganz zufällig eine Null auftaucht, oder das Programm abstürzt. Füllen wir nun den String mit etwas Inhalt.

str[0] = 'H';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = ' ';
str[6] = 'W';
str[7] = 'o';
str[8] = 'r';
str[9] = 'l';
str[10] = 'd';
printf("%s\n", str);

Implementiere es selbst, oder vertraue mir einfach: Die printf-Anweisung wird “Hello World” ausgeben.

In C gibt es auch das Konzept des “String-Literal”. Betrachte den folgenden Code:

char* a = "Hello World";
char a[] = "Hello World";

Diese erzeugen ebenfalls einen mit Null terminierten String. Sie unterscheiden sich von den vorher betrachteten Strings vor allem dadurch, dass sie nicht verändert werden sollten.

Als praktische Übung: erstelle ein Programm, das ein Array aus beliebig vielen Strings beliebiger Länge zu einem einzigen String verkettet. Verwende dafür gerne diese sehr minimale Vorgabe:

vorgabe3_2.c