C++ Teil 8 – Arrays

Einleitung

Ein Array (zu Deutsch: ein Feld) ist eine zusammenhängende Folge von Elementen eines bestimmen Datentyps.

Arrays können ein oder mehrdimensional sein. Ein eindimensionales Feld entspricht einer einspaltigen Tabelle von Elementen(Variablen), zweidimensionales einem Schachbrett, dreidimensionales einem „Block aus Würfeln“, Felder mit mehr als drei Dimensionen sind bildlich kaum vorstellbar.

Eindimensionales Array
Eindimensionales Array
Zweidimensionales Array
Zweidimensionales Array
Dreidimensionales Array
Dreidimensionales Array

Deklaration & Initialisierung

Statische Felder werden ähnlich einer einfacher Variable deklariert, wobei am Ende des Arraynamens eckige Klammern dazu kommen. In der Klammer steht die Anzahl der Elemente, die ein Array haben soll.

// Deklaration
Datentyp Arrayname[Anzahl der Arrayelemente];
	
// int-Array für 10 Zahlen
int zahlenreihe[10];

Die Anzahl der Elemente muss logischerweise eine Ganzzahl sein. Zusätzlich muss es sich um eine Konstante handeln, damit der Compiler bereits beim Kompilieren weiß, wie groß das Array sein muss. Diese Beschränkung bezieht sich nur auf statische Arrays, um die es hier geht.

Wie auch bei einer Variablendeklaration ist auch bei den Arrays sehr wichtig diese auch zu initialisieren. Dies geschieht entweder gleichzeitig mit der Deklaration oder man initialisiert später jedes Element einzeln.
Schauen wir uns zuerst die direkte Initialisierung an.

(Datentyp) Arrayname[Anzahl der Elemente(N)] = {Wert1, Wert2, Wert3, …, WertN};

Beispiel:

// eine Tabelle mit Zahlen
int primzahlen[10] = {2, 3, 5, 7, 11, 13, 17, 19, -5, 4};
1D Beispielarray mit Zahlen
1D Beispielarray mit Zahlen

Wenn man nicht alle Elemente Initialisiert, so werden sie mit 0 belegt.

// Beispiel
int zahlen[10] = {1,-2,4,7};
Eindimensionales Array
Eindimensionales Array

Da C++ eine typsichere Programmiersprache ist, müssen die zugewiesenen Werte den gleichen Datentyp, wie das Array haben. Daraus folgt, dass ein Array nicht Elemente mit verschiedenen Datentypen beinhalten kann. So würde beispielsweise folgender Code einen Compilerfehler verursachen:

// erzeugt einen Fehler
float kommazahlen[5] = {3.14, "hallo", 1, 'a', 5};

Initialisiert man alle Elemente per Hand, so kann man die Arraylänge bei der Deklaration weglassen, sie entspricht dann der Anzahl der Elemente bei der Initialisierung. Das ist besonders praktisch, wenn man ein Array für Buchstaben erstellt. Somit muss man die einzelnen Buchstaben nicht zählen.

// Eine Zeichenkette
char zeichenkette[] = "Das ist ein Text";

Wie man erkennt, sieht die Initialisierung von char-Arrays etwas handlicher aus, so dass man nicht jeden einzelnen Buchstaben mit Komma getrennt angeben muss, was aber auch geht.

char zeichenkette[] =  {'H', 'a', 'u', 's', '\0'};
// entspricht 
char zeichenkette[] = "Haus";
Zeichenkette
Zeichenkette

Damit haben wir auch gleich kennengelernt, wie in C++ Texte in Variablen gespeichert werden, nämlich in char-Arrays. Man beachte, dass bei einer manuellen Initialisierung eines Zeichenarrays am Ende ein ‚\0‘ draufgehängt werden muss. Daran erkennen die Funktionen, wie zum Beispiel std::cout, dass die Zeichenkette an dieser Stelle zu Ende ist.

Führt man zum Beispiel folgenden Code aus, so wird die Zeichenkette nur bis zum ‚\0‘-Escape-Sequenz ausgegeben.

// gibt nur den Text bis '\0' aus
char zeichenkette[] = "Das ist \0ein Text";
std::cout << zeichenkette; 

Für das Arbeiten mit Strings gibt es eine bessere Lösung als char-Arrays, nämlich die Klasse std::string, die man für den produktiven Einsatz den Arrays immer vorziehen sollte. Trotzdem stellen die Arrays die absolute Grundlage für die Verwendung von Zeichenketten dar, so arbeitet auch die string-Klasse intern mit Arrays.

Zugriff

Der Lese- und Schreibzugriff auf ein Element in einem Array erfolgt über die eckige Klammer, wobei darin die Elementnummer angegeben wird.

Beispiel:

// Array erstellen
int array[5] = {1,3,5,7,9};

// viertes Element ausgeben
std::cout << "4. Element" << array[3] << std::endl;

Nein, das ist kein Fehler, das vierte Element in dem Array wird mit dem Index 3 angesprochen, weil die Indexzählung in C++, wie bei fast allen modernen Programmiersprachen, bei 0 beginnt. Man zählt also nicht 1,2,3,4,5 , sondern 0,1,2,3,4. Dies muss man immer im Hinterkopf behalten, ansonsten sind Laufzeitfehler vorprogrammiert.

Möchte man auf ein Arrayelement zugreifen, dass gar nicht vorhanden ist, dann führt es meistens (aber nicht immer, was gerade das tückische daran ist) zu einem Laufzeitfehler und somit zu einem Programmabsturz. Dies ist in Verbindung mit Zeigern wohl eine der häufigsten Ursachen für Programmabstürze überhaupt.

Dazu ein paar Beispiele zum selbst ausprobieren.

// führt zur Laufzeitfehlern (nicht zwangsläufig)
int zarray[5] = {1,3,5,7,9};

zarray[5]; // Zugriff auf das sechste Element => Fehler
zarray[-1]; // Zählung beginnt bei 0 => Fehler

Schauen wir uns noch ein Beispiel zum Schreibzugriff an.

int zarray[5] = {1,3,5,7,9};

// viertes Element ausgeben
std::cout << "4. Element" << zarray[3] << std::endl;

// Wert an der 4ten Position ändern
zarray[3] = 3121;

// viertes Element ausgeben
std::cout << "4. Element" << zarray[3] << std::endl;

Wir haben uns angeschaut wie man eindimensionale Felder erstellt, initialisiert, die Werte darin ausliest und verändert. Als nächstes betrachten wir höherdimensionale Felder.

Mehrdimensionale Felder

Die Deklaration bei mehrdimensionalen Feldern ist nicht weiter kompliziert und geht analog zu eindimensionalen Arrays. Pro eine zusätzliche Dimension hängt man eine [Anzahl der Elemente]-Klammer bei der Initialisierung dran.

Datentyp arrayname[Anzahl der Elemente(N)][Anzahl der Elemente (M)]… [Anzahl der …] ;

Beispiel für ein 2D Feld:

char array2d[2][3] = {{'a', 'b', 'c'}, {'x', 'y', 'z'}};

Ein zweidimensionales Array ist praktisch eindimensionales Array, welches als Datentyp ein eindimensionales Array hat.

Initialisierung eines 2D-Arrays
Initialisierung eines 2D-Arrays

Wenn man das erst mal verinnerlicht hat, dann wird die oben verwende Syntax für die Initialisierung sofort schlüssig. Man Initialisiert ein eindimensionales Array, das aus zwei weiteren eindimensionalen Arrays besteht. Diese Betrachtung kann weiter auf Felder höherer Dimensionen übernommen werden.

Aufpassen sollte man beim Zugriff auf die Elemente des Arrays. Der erste Index spiegelt bei den zweidimensionalen Arrays die Y-Richtung und der zweite die X-Richtung. Will man beispielsweise in dem oberen Array das ‚z‘ durch ‚f‘ ersetzen, so greift man auf das Feld mit array2d[1][2] zu und nicht array2d[2][1], denn dieses Element existiert nicht. Am besten wir schauen uns ein weiteres Beispiel an.

// Array erstellen (drei Zeilen mit je 4 "Zellen")
int array2d[3][4] = {{1, 3, 5, 7}, 
					 {2, 4, 6, 8}, 
                     {9,11,13,15}};

// die 5 holen.
int zahl = array2d[0][2];

// die 2 holen
zahl = array2d[1][0];

// die 15 holen
zahl = array2d[2][3];
Zugriff auf ein 2D-Array
Zugriff auf ein 2D-Array

Als nächstes noch ein Beispiel für ein 3D-Array.
Nun wird in einer Arrayzelle nicht nur ein Wert gespeichert, sondern ein Array in einer Größe für 6 char-Werte.

// Array erstellen
// drei zeilen mit je 2 "zellen". jede zelle kann 6 char-werte beinhalten
char array3d[3][2][6] = {{"Das", "ist"}, 
					 {"ein", "3D"}, 
                     {"Array", "."}};


// das Wort '3D' ausgeben
std::cout << array3d[1][1] << std::endl;

// den Buchstaben 3 aus 3D augeben
std::cout << array3d[1][1][1] << std::endl;

Damit sollten die Grundlagen der statischen Felder klar sein. Als nächstes betrachten wir einige zusätzliche Informationen.

Mehrdimensionale Arrays auf eindimensionale reduzieren

Intern werden die mehrdimensionalen Felder als eindimensionale verwaltet. Die Darstellung mit den mehrfachen Klammern in mehreren Dimensionen ist nur dafür da, um dem Programmierer die Handhabung zu erleichtern.

Schauen uns wieder das obere 2D-Feld Beispiel mit Zahlen an.

2D Array
2D Array

Dieses 2D-Array wird intern als folgendes eindimensionales Array dargestellt.

1D Array
1D Array

Die einzelnen Zeilen liegen im Speicher einfach nacheinander. Die Länge dieses Arrays entspricht logischerweise dem Produkt aus der Höhe und Breite des 2D-Arrays.

// Array erstellen. entsprich int array2d[hoehe][breite]
const int hoehe = 3, breite = 4;
int array2d[hoehe*breite] = {1, 3, 5, 7, 2, 4, 6, 8, 9, 11, 13, 15};

Der Zugriff auf die Elemente ist auch nicht weiter schwierig. Bezeichnen wir den ersten Index mit y und den zweiten als x, so können wir mit arrayname[y*breite+x] auf jedes Element in dem eindimensionalen Array zugreifen, als ob es ein zweidimensionales wäre.

// Array erstellen. entsprich int array2d[hoehe][breite]
const int hoehe = 3, breite = 4;
int array2d[hoehe*breite] = {1, 3, 5, 7, 2, 4, 6, 8, 9, 11, 13, 15};

// die 5 holen. entspricht array2d[0][2]
int zahl = array2d[0*breite+2];

// die 2 holen. entspricht array2d[1][0]
zahl = array2d[1*breite+0];

// die 15 holen. entspricht array2d[2][3]
zahl = array2d[2*breite+3];

Man überzeuge sich selbst durch Nachzählen und durch das Ausführen des Beispiels (was man sowieso immer tun sollte), dass die Methode funktioniert.
Diese Methode wird vor allem im Zusammenhang mit Bilddateien bzw. Texturen benutzt, weil die Daten dabei meistens in Form eines eindimensionalen Arrays vorliegen.

Belegter Speicher

Manchmal ist es interessant zu wissen, wie viel Speicherplatz ein Array belegt. Dies kann man mit der C++-Internen Funktionen int sizeof() bewerkstelligen.

Beispiel:

// Array erstellen und Text ausgeben
wchar_t eintext[] = L"Dieses Beispiel zaehlt die Anzahl der Zeichen in diesem Satz.";
std::wcout << eintext << std::endl;

// die Größe des Arrays in Bytes bestimmen
int satzgroesse = sizeof(eintext);
std::cout << "Satzgroesse in Bytes:" << satzgroesse << std::endl;

// die Größe des ersten Elements bestimmen (sie ist für alle Elemente gleich)
int elementgroesse = sizeof(eintext[0]);
std::cout << "Groesse eines Arrayelements in Bytes:" << elementgroesse << std::endl;

// die Länge berechnen
std::cout << "Zeichen Anzahl: " << (satzgroesse / elementgroesse) << std::endl;

Das ist eine, wenn auch umständliche, Art die Anzahl der Arrayelemente zu bestimmen. Dieses Beispiel sollte nur die Anwendung von sizeof() verdeutlichen, weil dies vor allem später im Zusammenhang mit dynamischer Speicherverwaltung recht wichtig sein kann.

Etwas Anwendung

Im Grunde haben wir bereits alles erfahren, um mit den statischen Arrays zu arbeiten. Im Folgenden werden wir noch einige Beispiele durchgehen.

Alle Elemente eines Arrays ausgeben

Alle Elemente durchlaufen und mit cout ausgeben.

// array erstellen
const int laenge = 10;
int zahlen[laenge] = {10,2,-4,4,1,7,3,7,9,-2};

// alle elemente des arrays durchlaufen
for(int i = 0; i < laenge; i++)
{
	// auf das Element mit dem Index i zugreifen und ausgeben
	std::cout << "Element Nr. "<< i << ": " << zahlen[i] << std::endl;
}

Einen String nach einem bestimmten Buchstaben durchsuchen.

Eine lineare Suche in einem Array: Man geht alle Elemente der Reihe nach durch und schaut ob es eine Übereinstimmung mit dem gesuchten Element gibt.

// zeichenkette anlegen
char satz[] = "Hier gibt es was Wichtiges zu erfahren.";

// nach diesen buchstaben wird gesucht
char gesucht = 'z';

// das ganze array durchlaufen
for(int i = 0; i < sizeof(satz)/sizeof(char); i++)
{
	// stimmt das arrayelement mit dem gesuchten zeichen überein?
	if(satz[i] == gesucht)
	{ 
		std::cout << "Zeichen "<<gesucht << " an der Position "<< (i+1) << " gefunden." << std::endl;
	}
}

Zwei Werte im Array vertauschen.

Dieses Beispiel zeigt, wie man mit Hilfe einer Variablen zwei Werte im Array vertauschen kann.

// array anlegen
int zahlen[10] = {10,2,-4,4,1,7,3,7,9,-2};

// wert 1 in einer variable zwischenspeichern
int temp = zahlen[4];

// die 9 auf die position der der eins schreiben
zahlen[4] = zahlen[8];

// die eins auf die position der ursprünglichen 9 schreiben
zahlen[8] = temp;

Bubblesort

Aufbauend auf dem letzten Beispiel werden wir einen einfachen Sortieralgorithmus implementieren – den Bubblesort-Algorithmus.

Die Idee besteht darin alle Elemente des unsortierten Arrays zu durchlaufen und zu schauen ob jeweils zwei benachbarte Werte in der richtigen Reihenfolge stehen. Wenn sie nicht richtig stehen, dann werden sie, wie oben gezeigt, vertauscht. Natürlich genügt ein Durchgang nicht, weil nicht alle Zahlen in einem Durchgang auf die richtige Position gebracht werden können.

Eine Beispielimpementierung:

// ein int-array mit ungeordneten zahlen
const int laenge = 8;
int intarray[8] = {9,3,2,4,5,8,0,4};

// diese variable gibt an, ob das Feld sortiert ist
bool sortiert = false;

// so lange das Feld nicht sortiert ist, wiederhole den Sortiervorgang
while(sortiert == false)
{
	// erst mal annehmen, dass das Array sortiert ist
	sortiert = true;

	// alle (bis auf das letzte) Elemente eines Arrays durchlaufen
	for(int i = 0; i < laenge-1; i++)
	{
		// benachbarte Elemente stehen in falschen Reihenfolge..
		if(intarray[i] > intarray[i+1])
		{
			// ...das heißt das Array ist nicht sortiert =>
			// man sollte es nach diesem Durchgang noch mal durchlaufen
			sortiert = false;

			// beide Elemente vertauschen
			int temp = intarray[i];
			intarray[i] = intarray[i+1];
			intarray[i+1] = temp;
		}
	}
}

Dies waren ein paar Beispiele zu Arrays, die ihre Handhabung verdeutlichen sollten. Aber alle Beispiele der Welt bringen nichts, wenn man nicht selbst übt, etwas Code eintippt und etwas Eigenes ausprobiert. Aus diesem Grund sollte man auf jeden Fall die bereitgestellten Übungsaufgaben lösen.

Abschließend möchte ich noch deutlich machen, dass wir hier nur mit statischen Arrays gearbeitet haben. Dynamische Arrays, die zur Laufzeit ihre Größe ändern können und die wir später kennen lernen werden, setzen das Wissen von Zeigern voraus. Das Konzept der Zeiger werde ich im nächsten Abschnitt vorstellen.

Viel Spaß!

Übungsaufgaben

  1. Beschreiben Sie in eigenen Worten was Arrays sind.
  2. Wie deklariert man Arrays?
  3. Erstellen Sie ein Feld und initialisieren Sie es mit dem Satz „Ich lerne C++“. Geben Sie den Inhalt auf dem Bildschirm aus.
  4. Wie kann man sich mehrdimensionale Arrays vorstellen? Wie werden sie deklariert und initialisiert? Erstellen Sie min. 3 Beispielfelder mit unterschiedlichen Datentypen und initialisieren Sie sie.
  5. Welche Bedeutung hat der erste Zugriffsindex bei einem 2D-Array?
  6. Erstellen Sie ein beliebiges TicTacToe Feld und lasse Sie es auf dem Bildschirm ausgeben.
  7. Fordern Sie den Benutzer auf X- und Y-Koordinate(1,2,3) und das gewünschte Symbol(X oder O) für das TicTacToe-Feld einzugeben. Daraufhin sollte das Feld mit dem neu gesetzten Symbol wieder ausgegeben werden. Tipp: mit system(„cls“); auf Windows- bzw. system(„clear“); auf Linux-Systemen kann der Inhalt der Konsole geleert werden.
  8. Erweitern Sie das TicTacToe Beispiel so, dass abwechselnd zwei Spielen ihre Symbole eingeben können. Nach jeder Eingabe, soll überprüft werden, ob einer der beiden Spieler gewonnen hat oder ob es unentschieden steht. Sinnvollerweise verpackt man die Spiellogik in einer Schleife. Empfehlung: Überlegungen macht man sich auf einem Blatt Papier.
  9. Erweitern Sie da das TicTacToe-Beispiel zu einem vollwertigen Spiel.
  10. Erweitern Sie das Bubblesort-Beispiel mit einer Textausgabe nach jedem Schritt und überlegen Sie sich warum es Bubble(Luftblase)sort heißt. Tipp: den Kopf nach links neigen kann hilfreich sein =)

11 Gedanken zu „C++ Teil 8 – Arrays“

  1. Man muss irgendwie sicherstellen, dass die Schleife so lange wiederholt wird bis es bei einem Durchgang keine Vertauschungen gibt. Der obere Code macht genau das. Überlege es dir mit einem Beispiel oder laufe der Code mit einem visuellen Debugger (in Visual C++) Schritt für Schritt durch, dann siehst du es auch.

  2. Das würde nicht funktionieren, weil du die Variable „sortieren“ in einem Schleifendurchgang setzen würdest. z.B. hast du in einem Schritt eine Vertauschung, dann sagst du „aha, die Liste ist nicht sortiert“, dann setze ich sortieren=false. Das nächste Zahlenpaar braucht dann aber keine Vertauschung und du setzt sortieren=true. Was aber falsch wäre.

    Man kann die Steuerung auch mit „else sortiert = true“ umschreiben, aber dann muss es auch im if-block und die Initialisierung davor ändern.

    Was genau klappt denn nicht? Poste deinen Code.

  3. #include
    #include
    #include
    using namespace std;
    int main(int argc, char *argv[])
    {
    int zahlarray[7];
    srand(time(0));
    for (int i = 0; i < 7; i++)
    {
    zahlarray[i] = rand() % 50 +1;
    cout << zahlarray[i] << " ";
    }
    const int derwert = 7;
    bool richtig = false;
    while (richtig == false)
    {

    for (int i = 0; i zahlarray[i+1])
    {
    richtig = false;
    int temp = zahlarray[i];
    zahlarray[i] = zahlarray[i+1];
    zahlarray[i+1] = temp;
    }
    else
    {
    richtig = true;
    }

    }
    }
    return 0;

  4. ahh hab bemerkt dass nicht danach ausgegeben ^^ aber wo muss ich das dann hinschreiben? bei mir kommt dan dauernt ein Fehler

  5. Tja, deswegen sage ich: benutze einen Debugger und führe deinen Algorithmus Schritt für Schritt aus. In Visual C++ ist ein guter Debugger drin, man muss ihn nur nutzen.

Schreibe einen Kommentar