C++-Programmierung/ Weitere Grundelemente/ Felder
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.
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;
}
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
BearbeitenDer 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.
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.
Mehrere Dimensionen
BearbeitenAuch 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.
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";
}
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)
).
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
BearbeitenArrays 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.
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:
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
BearbeitenSie 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.
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:
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!
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!