Wir sind die Erben der Vergangenheit und sollten uns dessen stets bewusst sein. Lass uns die Chance nutzen, um Fehler nicht zu wiederholen und errungenes Wissen anwenden. Wir brauchen nicht das Rad nicht neu erfinden, doch seine Entstehung zu kennen schadet auf keinen Fall.

Warum dieser Appell? Zum einen gilt seine Aussage allgemein und übergreifend auf alle Bereiche unseres Lebens sowie der Gesellschaft. Übernehme Gutes und verbessere Unpassendes. Zum anderen wollen wir die Programmiersprache C++ lernen, die sich als Erweiterung der Programmierspache C versteht und somit auch auf eine Historie aufbaut.

Darum treffen wir häufig “Relikte” dieses Ursprungs an. Diesmal sind es die Arrays.

Was und Wozu?

Arrays werden im Deutschen Felder genannt und bilden eine Einheit aus einer festen Anzahl an Elementen des selben Datentyps. In diesem Satz hast du alle drei Charakteristiken eines Arrays:

  • Einheit aus Elementen
  • feste Anzahl
  • selber Datentyp

Okay, aber was bedeutet das genau? Das Array reserviert Platz im Speicher, um dort deine Datenelemente in geordneter Reihenfolge abzulegen. Es ist der rohe Speicher und kein Objekt im eigentlichen Sinn.

Die Deklaration besteht aus dem Datentyp der Elemente, dem Namen des Arrays und anschließend der Anzahl der Elemente im Indexoperator [] (siehe listing1).

datatype arrayName [numberOfElements];

// listing1: syntax of an array

Und welchen Nutzen hast du von diesem Konstrukt? Stell dir vor, du hast eine riesige Schallplattensammlung und alle stehen nebeneinander schön aufgereiht im Regal. Jetzt möchtest du gerne eine digitale Auflistung deines Bestands haben und eine kleine Datenbank für deine Platten erstellen.

Ein Ansatz ist für jede einzelne Schallplatten ein Objekt der Struktur vinyl anzulegen.

#include <iostream>

struct vinyl
{
    // elements
}

int main()
{
    vinyl lp1;  // declaring variable of type vinyl
    vinyl lp2;
    vinyl lp3;
    vinyl lp4;
    vinyl lp5;

    return 0;
}

// listing2: declaration of some structs

Das geht. Kannst du so machen. Kannst du genauso für 100 oder 1000 Schallplatten machen. Aber findest du das nicht auch sehr umständlich? Hier bietet sich ein Array für die Sammlung an. Statt 100 Objekten brauchst du einfach nur ein Feld von 100 Objekten anlegen.

#include <iostream>

struct vinyl
{
    // elements
};

int main()
{
    const int numberOfVinyls {5};       // initializing integer
    vinyl lpCollection[numberOfVinyls]; // declaring array

    return 0;
}

// listing3: array of some structs

Wenn du ein Array deklarierst, musst du gleichzeitig die feste Größe in Form der Anzahl an Elementen vorgeben. Danach ist es dir nicht möglich während der Laufzeit des Programms die Größe zu verändern und noch mehr Platten hinzuzufügen. Andererseits wird immer der gleiche Speicherplatz belegt, auch wenn du nur die Hälfte nutzt. Deshalb findest du auch die Bezeichnung statische Arrays.

Das Array befüllen

Also gut, jetzt reserviert der Compiler genügend Speicherplatz für 5 Elemente. Nun müssen wir den Platz mit Daten füllen. Der Indexoperator [] dient nicht nur zu festlegen der Größe des Arrays bei der Deklaration. Über ihn hast du Zugriff auf jedes einzelne Element im Feld.

Die Elemente haben eine feste Reihenfolge und sind durchnummeriert. Pass aber auf, das erste Element hat den Index 0. Diese Art der Nummerierung heißt zero-based und wird sehr häufig in informationsverarbeitenden System genutzt. Das liegt an der binären Logik. Dort geht es um Zustände der einzelnen Bits, die entweder Null oder Eins sein können. Der Zustand, in dem alle Bits auf 0 gesetzt sind, kannst du wie jeden anderen behandeln. Vergesse ihn nicht.

Wir möchten jetzt unser Schallplatten Array befüllen und beginnen bei dem Element mit Index 0. Dazu trägst du den Index in den Indexoperator des Felds ein und schon weiß der Compiler in welchen Bereich des Speichers du die Daten hinterlegen möchtest.

#include <iostream>

struct vinyl
{
    // elements
};

int main()
{
    vinyl lp1;  // declaring variable of type vinyl
    vinyl lp2;
    vinyl lp3;
    vinyl lp4;
    vinyl lp5;

    const int numberOfVinyls {5};
    vinyl lpCollection[numberOfVinyls];

    lpCollection[0] = lp1;  // assigning element at index 0
    lpCollection[1] = lp2;
    lpCollection[2] = lp3;
    lpCollection[3] = lp4;
    lpCollection[4] = lp5;

    return 0;
}

// listing4: assigning data to an array

Was ist da alles drin?

Der umgekehrte Fall funktioniert analog dazu. Möchtest du auf die Daten im Array zugreifen brauchst du nur den gewünschten Index angeben.

#include <iostream>

struct vinyl
{
    // elements
};

int main()
{
    vinyl lp1;  // declaring variable of type vinyl
    vinyl lp2;
    vinyl lp3;
    vinyl lp4;
    vinyl lp5;

    const int numberOfVinyls {5};
    vinyl lpCollection[numberOfVinyls];

    lpCollection[0] = lp1;  // assigning element at index 0
    lpCollection[1] = lp2;
    lpCollection[2] = lp3;
    lpCollection[3] = lp4;
    lpCollection[4] = lp5;

    vinyl actualElement = lpCollection[3];  // accessing element at index 3

    return 0;
}

// listing5: accessing data of an array

Und was passiert wenn ich einen Index wähle, der außerhalb meines Arrays liegt? Tja, genau hier liegt die Gefahr. Denn du wirst darauf nicht hingewiesen. Auch schmeißt der Compiler keinen Fehler. Er wird einfach die Daten nehmen, die an der Adresse im Speicher liegen, auf die der Index zeigen würde, hätte das Array diese Größe.

Abbildung 1: Memory of Arrays

Abbildung 1: Array im Speicher

Du musst also selbst darauf achten einen existierenden Index zu wählen. Dieser Fehler geschieht schnell, wenn du vergisst, dass die Nummerierung bei 0 beginnt. Merke dir am besten: N ist die Anzahl der Elemente des Arrays. Die Grenzen des Arrays liegen bei 0 und bei N-1;

If N is the number of elements,

then the boundaries of the array reach from 0 to N-1

Übrigens, wenn du ein Array nicht initialisierst, dann wird es mit irgendwelchen Werten (junk values) gefüllt.

How much is the …

Interessiert dich die Anzahl an Bytes, die dich unser Array an Speicherplatz kostet, hilft dir wieder einmal die Funkion sizeof(). Doch Vorsicht! In diesem Fall verhält sich die Funktion ein wenig anders. Wendest du sizeof() direkt auf das Feld an, dann ist das Ergebnis nicht die Menge an Bytes, sondern die Anzahl der Elemente.

Wir bleiben nicht verwundert, sondern sind clever. Wenn das Array aus einer festen Anzahl an Elementen des selben Datentyps besteht und nur den reinen Speicher ohne irgedwelchen Overhead darstellt, dann können wir Größe in Bytes selbst daraus bestimmen.

Mit sizeof() angewendet erhalten wir die Anzahl an Element. Diese multipliziert mit der Größe eines Elements (du erinnerst dich an die Bestimmung der Größe eines Datentyps) ergibt die gesammte Menge an Bytes des Arrays.

int numberOfElements = sizeof(anArray);
int sizeOfArray = sizeof(elementType) * numberOfElements; // in bytes

// listing6: size of an array

Für unsere Schallplattensammlung bedeutet das: int bytesOfCollection = sizeof(lpCollection) * sizeof(vinyl);

Multidimensional

Bisher haben wir uns eindimensionale Arrays angeschaut. Damit haben wir ein Regalbrett von nebeneinander stehenden Schallplatten abgebildet. Doch wir können auch ein Regal mit mehreren Brettern in einem mehrdimensionalen Array abbilden. Der Aufbau ist vergleichbar mit Zeilen und Spalten einer Matrix. Manchmal wird auch von einem “Array aus Arrays” gesprochen.

Abbildung 2: Bidimensional Array

Abbildung 2: Bidimensionales Array

Die Syntax ändert sich nur minimal. Für die hinzukommende Dimension benötigen wir nur einen weiteren Index Operator. So hat das Element eine eindeutige Koordinate im Feld.

Für ein zweidimensionales Array würde die Deklaration wie folgt aussehen:

datatype arrayName [numberOfRows][numberOfColumns];

// listing7: declaration of a bidimensional array

Wie der Prefix multi schon andeutet sind multidimensionale Arrays nicht auf zwei Dimensionen beschränkt. Du kannst so viele Index Operatoren anhängen wie du möchtest. Bedenke dabei, dass nicht nur die Anzahl der Elemente exponentiell steigt. Der benötigte Speicher wächst parallel dazu.

Eigentlich sind multidimensionale Arrays nur eine Abstraktion für dich als Programmierer. Du erreichst dasselbe Ergebnis auch indem du die Indizes eines eindimensionalen Arrays multiplizierst. Für den Überblick oder eine bessere Vorstellung von deinen Daten ist das aber wenig hilfreich.

Spezialfall: Char Arrays

In [eps1.4Datatypes](/posts/c++/datatypes-de/) haben wir den Datentypen _char kennengelernt. Mit ihm kannst du Zeichen speichern. Seine Größe von einem Byte reicht für die Darstellung des ASCII Zeichensatzes aus. Jetzt lässt sich mit einem Zeichen nicht so viel anfangen. Wie wäre es also, wenn wir ein Array aus chars anlegen und darin ganze Sätze als Zeichenketten zusammenfassen?

Die Idee ist natürlich nicht neu und von mir. Zeichenketten heißen im Englischen Strings und solche Objekte gibt es schon in der Programmiersprache C. S-Strings sind ein Spezialfall von Arrays aus chars.

Sie sind dir nicht völlig unbekannt. In unseren Beispielen kamen sie schon häufiger in Form eines Literals vor. In Listing6 siehst du die Ausgabe des String Literals "Hello World" und darauf folgt die äquivalente Initialisierung des String als Char Array.

#include <iostream>

int main()
{
    std::cout << "Hello World" << std::endl; // string literal

    char sayHello[] = {'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0'}; // initialize char array
    std::cout << sayHello << std::endl;

    return 0;
}

// listing8: char array

Jeder Buchstabe und auch das Leerzeichen sind ein Element des Feldes. Du kannst auf jedes Zeichen einzeln zugreifen oder das Array als Stream auslesen lassen.

Möchtest du einen Char Stream einlesen und damit eine Benutzereingabe ermöglichen, musst du vorher festlegen welchen Umfang der String haben darf. Die Größe des Array musst du bei der Deklaration angeben und kann während der Laufzeit nicht mehr geändert werden. Damit darf der Benutzer nur eine bestimmte Anzahl an Zeichen eingeben.

Er wird aber nicht aufgehalten mehr einzugeben. Die weiteren Zeichen liegen dann außerhalb des Arrays und gehören nicht zu dieser Einheit. Deshalb denke stets an das statische Verhalten von Char Arrays beim Entwickeln deines Codes.

#include <iostream>

int main()
{
    char sayHello[] = {'H','e','l','l','o',' ','W','o','r','l','d','\0'};
    std::cout << sayHello << std::endl;
    std::cout << "Size of array: " << sizeof(sayHello) << std::endl;

    std::cout << "Replacing space with null" << std::endl;
    sayHello[5] = '\0';
    std::cout << sayHello << std::endl;
    std::cout << "Size of array: " << sizeof(sayHello) << std::endl;
    return 0;
}

// listing9: escape code "\0" in char arrays

Sicherlich ist dir das “\0” Zeichen im letzten Element des Arrays aufgefallen. Dieser wird String-Terminating Character genannt, da er dem Compiler das Ende des Strings zeigt. Jedes Char Array muss mit dem NULL-Terminator enden. Im Fall des String Literals fügt der Compiler selbst “\0” an den Schluss hinzu.

Wenn du “\0” in die Mitte eines Strings schreibst, dann wird dieser dort abgeschnitten und der Rest nicht weiter vom Compiler beachtet. Die Größe des Strings im Speicher ändert sich allerdings nicht. Es werden nur nicht alle Informationen weiterverarbeitet, aber auch nichts gelöscht.

Der Backslash \ kündigt eine spezielle Anweisung für den Compiler an. In unserem Beispiel soll er eine Null einfügen und somit den String abschließen. Schreibst du einfach nur 0 wird das Zeichen als ASCII codiert interpretiert.

So eine Compiler Anweisung innerhalb eines Strings haben wir schon einmal diskutiert. Mit \n haben wir eine Alternative zu std::endl besprochen. Dort wird mit dem Backslash dem Compiler gezeigt, dass wir an dieser Stelle den String in einer neuen Zeile fortführen möchten.

Vergisst du den Char Array in listing7 mit \0 zu beenden, werden weitere Zeichen hinter Hello World ausgegeben. Das liegt daran, dass die Funktion std::cout den expliziten Abschluß erwartet und ohne Rücksicht auf die Grenzen des Arrays einfach die Daten, die darauf im Speicher folgen versucht darzustellen. Solange bis eine Null vorkommt.

Du siehst also, die Verwendung von Char Arrays ist umständlich. Zudem birgt es die Gefahr den Null-Terminator zu vergessen und einen Absturz deines Programms zu verursachen.

Das bleibt hängen

Das war unsere kleine “Geschichtsstunde” zu C-Style Arrays. Du brauchst sie, wenn du in C programmierst. In modernem C++ finden sie in der vorgestellten Art kaum noch Anwendung. Wichtig ist aber, dass du das Konzept hinter Arrays verstanden hast.

Du kennst nun die statischen Arrays aus C und das Char Array als Spezialfall. Du weißt, dass es sie gibt. Kannst sie auch benutzen. Doch C++ hält für dich die Evolution in Form von dynamischen Arrays in der C++ Standard Library bereit. Diese heißen Vektoren und du wirst sie im direkten Vergleich lieben! Zudem schauen wir uns ein weiteres Objekt der STL an. Mit C++ Strings lassen sich Zeichenketten viel komfortabler als mit Char Arrays handhaben. Dazu mehr beim nächsten Mal.

Ich wünsche dir maximalen Erfolg!


Quellen

  • [1] B. Stroustrup, A Tour of C++. Pearson Education, 2. Auflage, 29. Juni 2018.
  • [2] B. Stroustrup, Programming: Principles and Practice Using C++. Addison Wesley, 2. Auflage, 15. Mai 2014.
  • [3] U. Breymann, Der C++ Programmierer. C++ lernen – professionell anwenden – Lösungen nutzen. Aktuell zu C++17. München: Carl Hanser Verlag GmbH & Co. KG; 5. Auflage, 6. November 2017