C++-Programmierung/ Weitere Grundelemente/ Felder

Aus Wikibooks


In C++ lassen sich mehrere Variablen desselben Typs zu einem Array (im Deutschen bisweilen auch Datenfeld oder Vektor genannt, selten auch Matrix, Tabelle, Liste) zusammenfassen. Auf die Elemente des Arrays wird über einen Index zugegriffen. Bei der Definition sind der Typ der Elemente und die Größe des Arrays (=Anzahl der Elemente) anzugeben. Folgende Möglichkeiten stehen zum Anlegen eines Array zur Verfügung:

// Array mit 10 Elementen vom Typ 'int'; Array-Name ist 'feld'.
int feld[10];                                      // Anlegen ohne Initialisierung
int feld[]   = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  // Mit Initialisierung (automatisch 10 Elemente)
int feld[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  // 10 Elemente, mit Initialisierung

Soll das Array initialisiert werden, verwenden Sie eine Aufzählung in geschweiften Klammern, wobei der Compiler die Größe des Arrays selbst ermitteln kann. Es wird empfohlen, diese automatische Größenerkennung nicht zu nutzen. Wenn die Größenangabe explizit gemacht wurde, gibt der Compiler einen Fehler aus, falls die Anzahl der Intitiallisierungselemente nicht mit der Größenangabe übereinstimmt.

Wie bereits erwähnt, kann auf die einzelnen Elemente eines Arrays mit dem Indexoperator [] zugegriffen werden. Beim Zugriff auf Arrayelemente beginnt die Zählung bei 0. Das heißt, ein Array mit 10 Elementen enthält die Elemente 0 bis 9. Ein Arrayindex ist immer ganzzahlig.

#include <iostream>

int main() {
    int feld[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 10 Elemente, mit Initialisierung

    for(int i = 0; i < 10; ++i) {
        std::cout << feld[i] << " ";   // Elemente 0 bis 9 ausgeben
    }
}
Ausgabe:
1 2 3 4 5 6 7 8 9 10

Mit Arrayelementen können alle Operationen wie gewohnt ausgeführt werden.

feld[4] = 88;
feld[3] = 2;
feld[2] = feld[3] - 5 * feld[7 + feld[3]];

if(feld[0] < 1){
    ++feld[9];
}

for(int n = 0; n < 10; ++n){
    std::cout << feld[n] << std::endl;
}


Hinweis

Beachten Sie, dass der Compiler keine Indexprüfung durchführt. Wenn Sie ein nicht vorhandenes Element, z.B. feld[297] im Programm verwenden, kann Ihr Programm bei einigen Durchläufen unerwartet abstürzen. Zugriffe über Arraygrenzen erzeugen undefiniertes Verhalten. In modernen Desktop-Betriebssystemen kann das Betriebssystem einige dieser Bereichsüberschreitungen abfangen und das Programm abbrechen („segmentation fault“). Manchmal überschreibt man auch einfach nur den eigenen Speicherbereich in anderen Variablen, was zu schwer zu findenden Bugs führt.

Zeiger und Arrays[Bearbeiten]

Der Name eines Arrays wird vom Compiler (ähnlich wie bei Funktionen) als Adresse des Arrays interpretiert. In Folge dessen haben Sie neben der Möglichkeit über den Indexoperator auch die Möglichkeit, mittels Zeigerarithmetik auf die einzelnen Arrayelemente zuzugreifen.

#include <iostream>

int main() {
    int feld[10] = {1,2,3,4,5,6,7,8,9,10};

    std::cout << feld[3]   << "\n"; // Indexoperator
    std::cout << *(feld + 3) << "\n"; // Zeigerarithmetik
}
Ausgabe:
4
4

Im Normalfall werden Sie mit der ersten Syntax arbeiten, es ist jedoch nützlich zu wissen, wie Sie ein Array mittels Zeigern manipulieren können. Es ist übrigens nicht möglich, die Adresse von feld zu ändern. feld verhält sich also in vielerlei Hinsicht wie ein konstanter Zeiger auf das erste Element des Arrays. Einen deutlichen Unterschied werden Sie allerdings bemerken, wenn Sie den Operator sizeof() auf die Arrayvariable anwenden. Als Rückgabe bekommen Sie die Größe des gesamten Arrays. Teilen Sie diesen Wert durch die Größe eines beliebigen Arrayelements, erhalten Sie die Anzahl der Elemente im Array.

#include <iostream>

int main() {
    int feld[10];

    std::cout << "   Array Größe: " << sizeof(feld)                   << "\n";
    std::cout << " Element Größe: " << sizeof(feld[0])                << "\n";
    std::cout << "Element Anzahl: " << sizeof(feld) / sizeof(feld[0]) << "\n";
}
Ausgabe:
Array Größe: 40
 Element Größe: 4
Element Anzahl: 10

Mehrere Dimensionen[Bearbeiten]

Auch mehrdimensionale Arrays sind möglich. Hierfür werden einfach Größenangaben für die benötigte Anzahl von Dimensionen gemacht. Das folgende Beispiel legt ein zweidimensionales Array der Größe 4 × 8 an, das 32 Elemente enthält. Theoretisch ist die Anzahl der Dimensionen unbegrenzt.

int feld[4][8];    // Ohne Initialisierung

int feld[4][8] = { // Mit Initialisierung
    {  1,  2,  3,  4,  5,  6,  7,  8 },
    {  9, 10, 11, 12, 13, 14, 15, 16 },
    { 17, 18, 19, 20, 21, 22, 23, 24 },
    { 25, 26, 27, 28, 29, 30, 31, 32 }
};

Wie Sie sehen, können auch mehrdimensionale Arrays initialisiert werden. Die äußeren geschweiften Klammern beschreiben die erste Dimension mit 4 Elementen. Die inneren geschweiften Klammern beschreiben dementsprechend die zweite Dimension mit 8 Elementen. Beachten Sie, dass die inneren geschweiften Klammern lediglich der Übersicht dienen, sie sind nicht zwingend erforderlich. Dementsprechend ist es für mehrdimensionale Arrays bei der Initialisierung nötig, die Größe aller Dimensionen anzugeben.

Genaugenommen wird eigentlich ein (1-dimensionales) Array von (1-dimensionalen) Arrays erzeugt. In unserem Beispiel ist feld ein Array mit 4 Elementen vom Typ „Array mit 8 Elementen vom Typ int“. Dementsprechend sieht auch der Aufruf mittels Zeigerarithmetik auf einzelne Elemente aus.

#include <iostream>

int main(){
    // Anlegen des Arrays mit Initialisierung von eben

    std::cout << feld[2][5]         << "\n"; // Indexoperator
    std::cout << *(*(feld + 2) + 5) << "\n"; // Zeigerarithmetik
}
Ausgabe:
22
22

Es sind ebenso viele Dereferenzierungen wie Dimensionen nötig. Um sich vor Augen zu führen, wie die Zeigerarithmetik für diese mehrdimensionalen Arrays funktioniert, ist es nützlich, sich einfach die Adressen bei Berechnungen anzusehen:

#include <iostream>

int main(){
    int feld[4][8];


    std::cout << "Größen\n";
    std::cout << "int[4][8]: " << sizeof(feld)     << "\n";
    std::cout << "   int[8]: " << sizeof(*feld)    << "\n";
    std::cout << "      int: " << sizeof(**feld)   << "\n";

    std::cout << "Adressen\n";
    std::cout << "       feld: " << feld        << "\n";
    std::cout << "   feld + 1: " << feld + 1    << "\n";
    std::cout << "(*feld) + 1: " << (*feld) + 1 << "\n";
}
Ausgabe:
Größen
int[4][8]: 128
   int[8]: 32
      int: 4
Adressen
       feld: 0x7fff2be5d400
   feld + 1: 0x7fff2be5d420
(*feld) + 1: 0x7fff2be5d404

Wie Sie sehen, erhöht feld + 1 die Adresse um den Wert 32 (Hexadezimal 20), was sizeof(int[8]) entspricht. Also der Größe aller verbleibenden Dimensionen. Die erste Dereferenzierung liefert wiederum eine Adresse zurück. Wird diese um 1 erhöht, so steigt der Wert lediglich um 4 (sizeof(int)).

Hinweis

Beachten Sie, dass auch für mehrdimensionale Arrays keine Indexprüfung erfolgt. Greifen Sie nicht auf ein Element zu, dessen Grenzen außerhalb des Arrays liegen. Beim Array int[12][7][9] können Sie auf die Elemente [0..11][0..6][0..8] zugreifen. Die Zählung beginnt also auch hier immer bei 0 und endet dementsprechend 1 unterhalb der Dimensionsgröße.

Arrays und Funktionen[Bearbeiten]

Arrays und Funktionen arbeiten in C++ nicht besonders gut zusammen. Sie können keine Arrays als Parameter übergeben und auch keine zurückgeben lassen. Da ein Array allerdings eine Adresse hat (und der Arrayname diese zurückliefert), kann man einfach einen Zeiger übergeben. C++ bietet (ob nun zum besseren oder schlechteren) eine alternative Syntax für Zeiger bei Funktionsparametern an.

void funktion(int *parameter);
void funktion(int parameter[]);
void funktion(int parameter[5]);
void funktion(int parameter[76]);

Jeder dieser Prototypen ist gleichwertig. Die Größenangaben beim dritten und vierten Beispiel werden vom Compiler ignoriert. Innerhalb der Funktion können Sie wie gewohnt mit dem Indexoperator auf die Elemente zugreifen. Beachten Sie, dass Sie die Arraygröße innerhalb der Funktion nicht mit sizeof(arrayname) feststellen können. Bei diesem Versuch würden Sie stattdessen die Größe eines Zeigers auf ein Array-Element erhalten.

Aufgrund dieses Verhaltens könnte man die Schreibweise des ersten Prototypen auswählen. Andere Programmierer argumentieren, dass bei der zweiten Schreibweise deutlich wird, dass der Parameter ein Array repräsentiert. Eine Größenangabe bei Arrayparametern ist manchmal anzutreffen, wenn die Funktion nur Arrays dieser Größe bearbeiten kann. Um es noch einmal zu betonen: Diese Größenangaben sind nur ein Hinweis für den Programmierer; der Compiler wird ohne Fehler und Warnung Ihr Array als einfachen Zeiger übernehmen. Eine eventuelle Angabe der Elementanzahl beim Funktionsparameter wird vom Compiler komplett ignoriert.

Bei mehrdimensionalen Arrays sehen die Regeln ein wenig anders aus, da diese Arrays vom Typ Array sind. Wie Sie wissen, ist es zulässig, Zeiger als Parameter zu übergeben. Entsprechend ist natürlich auch ein Zeiger auf ein Array zulässig. Die folgenden Prototypen zeigen, wie die Syntax bei mehrdimensionalen Arrays aussieht.

void funktion(int (*parameter)[8]);
void funktion(int parameter[][8]);
void funktion(int parameter[4][8]);

Alle diese Prototypen haben einen Parameter vom Typ „Zeiger auf Array mit acht Elementen vom Typ int“. Ab der zweiten Dimension geben Sie also tatsächlich Arrays an, somit müssen Sie natürlich auch die Anzahl der Elemente zwingend angeben. Daher können Sie sizeof() in der Funktion verwenden, um die Größe zu ermitteln. Dies ist allerdings nicht notwendig, da Sie bereits im Vorfeld wissen, wie groß der Array ist und vom welchem Typ er ist. Die Größe berechnet sich wie folgt:

sizeof(Typ)*Anzahl der Elemente. In unserem Beispiel entspricht dies 4*8 = 32. Auf ein zweidimensionales Array können Sie innerhalb der Funktion mit dem normalen Indexoperator zugreifen.

Beachten Sie, dass beim ersten Prototypen die Klammern zwingend notwendig sind, andernfalls hätten die eckigen Klammern auf der rechten Seite des Parameternamen Vorrang. Somit würde der Compiler dies wie oben gezeigt als einen Zeiger behandeln, natürlich unabhängig von der Anzahl der angegebenen Elemente. Ohne diese Klammern würden Sie also einen Zeiger auf einen Zeiger auf int deklarieren.

Tipp

Seit C++11 gibt es den Header <array> in welchem eine gleichnamige Datenstruktur definiert ist. Man könnte in diesem Zusammenhang vielleicht von C++-Arrays sprechen. Sie werden verwendet wie gewöhnliche C-Arrays, haben aber eine andere Deklaration. Ein Array mit 8 Elemente vom Typ int wird wie folgt definiert:

#include <array>
// ...
std::array< int, 8 > variablenname;

Für mehrdimensionale Arrays kann man statt int als Datentyp wieder ein Array angeben. Die Deklaration ist also etwas aufwendiger als bei C-Arrays, dafür kann ein solches C++-Array aber ganz normal an Funktionen übergeben werden.

void funktion(std::array< int, 8 > parameter);
void funktion(std::array< int, 8 > const& parameter);
void funktion(std::array< std::array< int, 8 >, 4 > const& parameter);

Im ersten Fall wird eine Kopie übergeben, im zweiten eine Referenz auf eine konstantes Array. In beiden Fällen können nur C++-Arrays mit genau 8 Elementen übergeben werden. Die dritte Zeile zeigt die Übergabe eines zweidimensionalen Arrays, auch hier müssen die beiden Dimensionen (4 und 8) natürlich exakt übereinstimmen.

Ein weiterer Vorteil ist, dass C++-Array-Objekte eine Funktion names size() haben, welche die Anzahl der Elemente zurückgibt.

Lesen komplexer Datentypen[Bearbeiten]

Sie kennen nun Zeiger, Referenzen und Arrays, sowie natürlich die grundlegenden Datentypen. Es kann Ihnen passieren, dass Sie auf Datentypen treffen, die all das in Kombination nutzen. Im Folgenden werden Sie lernen, solche komplexen Datentypen zu lesen und zu verstehen, wie man sie schreibt.

Tipp

Als einfache Regel zum Lesen von solchen komplexeren Datentypen können Sie sich merken:

  • Es wird ausgehend vom Namen gelesen.
  • Steht etwas rechts vom Namen, wird es ausgewertet.
  • Steht rechts nichts mehr, wird der Teil auf der linken Seite ausgewertet.
  • Mit Klammern kann die Reihenfolge geändert werden.

Die folgenden Beispiele werden zeigen, dass diese Regeln immer gelten:

int i;            // i ist ein int
int *j;           // j ist ein Zeiger auf int
int k[6];         // k ist ein Array von sechs Elementen des Typs int
int *l[6];        // l ist ein Array von sechs Elementen des Typs Zeiger auf int
int (*m)[6];      // m ist ein Zeiger auf ein Array von sechs Elementen des Typs int
int *(*&n)[6];    // n ist eine Referenz auf einen Zeiger auf ein Array von
                  // sechs Elementen des Typs Zeiger auf int
int *(*o[6])[5];  // o ist ein Array von sechs Elementen des Typs Zeiger auf ein
                  // Array von fünf Elementen des Typs Zeiger auf int
int **(*p[6])[5]; // p ist Array von sechs Elementen des Typs Zeiger auf ein Array
                  // von fünf Elementen des Typs Zeiger auf Zeiger auf int

Nehmen Sie sich die Zeit, die Beispiele nachzuvollziehen. Wenn Sie keine Probleme damit haben, sehen Sie sich das nächste sehr komplexe Beispiel an. Es soll die allgemeine Gültigkeit dieser Regel noch einmal demonstrieren:

int (*(*(**pFunc[5])(int*, double&))[6])();

pFunc ist ein Array mit fünf Elementen, das Zeiger auf Zeiger auf Funktionen enthält, die einen Zeiger auf int und eine Referenz auf double übernehmen und einen Zeiger auf Arrays mit sechs Elementen vom Typ Zeiger auf Funktionen, ohne Parameter, mit einem int als Rückgabewert zurückgeben. Einem solchen Monstrum werden Sie beim Programmieren wahrscheinlich selten bis nie begegnen, aber falls doch, können Sie es mit den obengenannten Regeln entschlüsseln. Wenn Sie nicht in der Lage sind, dem Beispiel noch zu folgen, brauchen Sie sich keine Gedanken zu machen: Nur wenige Menschen sind in der Lage, sich ein solches Konstrukt überhaupt noch vorzustellen. Wenn Sie es nachvollziehen können, kommen Sie sehr wahrscheinlich mit jeder Datentypdeklaration klar!

Hinweis

Wenn Sie in Ihren Programmen solche Strukturen deklarieren/anlegen, denken Sie daran, dass ein kurzes Kommentar es jedermann viel einfacher macht, die Struktur zu verstehen!