Beziehung zwischen Arrays und Zeigern
Array-zu-Zeiger-Decay, zusammenhängendes Layout und sichere Zeigerarithmetik.
In C++ sind Arrays und Pointer eng miteinander verbunden. Das liegt daran, dass Arrays im Speicher hintereinander gespeichert werden und der Name eines Arrays tatsächlich die Adresse seines ersten Elements darstellt. Daher ist es möglich, ein Array mithilfe von Zeigerarithmetik zu durchlaufen. In diesem Artikel lernen wir die Speicherstruktur von Arrays, ihre Beziehung zu Zeigern und praktische Anwendungsbeispiele kennen.
1. Array- und Speicheranordnung
Wenn in C++ ein Array definiert wird, werden alle Elemente nacheinander im Speicher abgelegt. Beispiel:
#include <iostream>
using namespace std;
int main() {
int zahlen[3] = {10, 20, 30};
cout << "Adresse des 1. Elements: " << &zahlen[0] << endl;
cout << "Adresse des 2. Elements: " << &zahlen[1] << endl;
cout << "Adresse des 3. Elements: " << &zahlen[2] << endl;
return 0;
}
Die ausgegebenen Adressen werden aufeinanderfolgend sein:
Adresse des 1. Elements: 0x61ff0c
Adresse des 2. Elements: 0x61ff10
Adresse des 3. Elements: 0x61ff14
Da jedes int 4 Byte Speicherplatz belegt, erhöhen sich die Adressen jeweils um 4.
2. Der Array-Name ist ein Pointer
Der Name des Arrays (zahlen) repräsentiert tatsächlich die Adresse des ersten Elements.
Das bedeutet, dass zahlen und &zahlen[0] dieselbe Adresse bezeichnen.
int zahlen[3] = {10, 20, 30};
cout << zahlen << endl; // Adresse des ersten Elements
cout << &zahlen[0] << endl; // gleiche Adresse
Daher ist es möglich, über ein Array mit Hilfe eines Pointers zu iterieren.
3. Zugriff auf Array-Elemente mit Zeigern
Wenn ein Pointer auf das erste Element eines Arrays zeigt, kann man mithilfe der Zeigerarithmetik auf die anderen Elemente zugreifen.
#include <iostream>
using namespace std;
int main() {
int zahlen[4] = {5, 10, 15, 20};
int *p = zahlen; // p = &zahlen[0]
cout << *p << endl; // 5
cout << *(p + 1) << endl; // 10
cout << *(p + 2) << endl; // 15
cout << *(p + 3) << endl; // 20
}
Der Ausdruck *(p + i) ist identisch mit zahlen[i].
Dies bildet die Grundlage der Zeigerarithmetik.
4. Zeigerarithmetik
Zeiger bewegen sich im Speicher automatisch um die passende Menge, abhängig von ihrem Datentyp. Beispiel:
int array[3] = {1, 2, 3};
int *ptr = array;
cout << ptr << endl; // Adresse des ersten Elements
ptr++;
cout << ptr << endl; // Adresse des nächsten Elements
Der Ausdruck ptr++ verschiebt die Adresse um 4 Byte (int = 4 Byte).
Dies wird automatisch anhand des Typs des Zeigers berechnet.
Bei double sind es 8 Byte, bei char nur 1 Byte.
5. Array-Schleifen mit Zeigern
Um die Elemente eines Arrays mit Zeigern zu durchlaufen, können Schleifen verwendet werden.
int zahlen[] = {2, 4, 6, 8, 10};
int *p = zahlen;
for (int i = 0; i < 5; i++) {
cout << *(p + i) << " ";
}
Alternativ kann der Zeiger direkt inkrementiert werden:
for (int *ptr = zahlen; ptr < zahlen + 5; ptr++) {
cout << *ptr << " ";
}
Beide Methoden liefern dasselbe Ergebnis:
2 4 6 8 10
6. Array-Parameter in Funktionen
Wenn ein Array an eine Funktion übergeben wird, wird tatsächlich die Adresse des Arrays übergeben. Auf diese Weise kann die Funktion das Array direkt ändern.
#include <iostream>
using namespace std;
void Fuellen(int *array, int laenge) {
for (int i = 0; i < laenge; i++)
array[i] = (i + 1) * 5;
}
void Drucken(const int *array, int laenge) {
for (int i = 0; i < laenge; i++)
cout << array[i] << " ";
}
int main() {
int zahlen[5];
Fuellen(zahlen, 5);
Drucken(zahlen, 5);
}
Da die Funktion Fuellen das Array direkt im Speicher ändert,
werden die Werte im Array der main()-Funktion aktualisiert.
7. Mehrdimensionale Arrays und Zeiger
Mehrdimensionale Arrays werden ebenfalls hintereinander im Speicher gespeichert. Allerdings muss die Zeigerarithmetik hier mit etwas mehr Vorsicht angewendet werden.
#include <iostream>
using namespace std;
int main() {
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
cout << *(*(matrix + 0) + 2) << endl; // 3
cout << *(*(matrix + 1) + 0) << endl; // 4
}
Hier gilt: matrix → ist die Adresse der ersten Zeile.
*(matrix + 1) → geht zur zweiten Zeile,
*(*(matrix + 1) + 0) → greift auf das erste Element dieser Zeile zu.
8. Zeiger und dynamische Arrays
Im Gegensatz zu Arrays mit fester Größe werden dynamische Arrays mit dem Schlüsselwort new erstellt.
Diese Arrays werden im Heap-Speicher abgelegt, und ihre Größe kann zur Laufzeit bestimmt werden.
int *array = new int[5];
for (int i = 0; i < 5; i++)
array[i] = (i + 1) * 10;
for (int i = 0; i < 5; i++)
cout << array[i] << " ";
delete[] array; // Speicher freigeben
Dynamische Arrays erfordern manuelle Speicherverwaltung.
Speicher, der mit new reserviert wurde, muss nach der Verwendung immer mit delete[] freigegeben werden.
9. Arrays mit const-Zeigern lesen
Wenn eine Funktion ein Array nicht verändern soll, kann der Zeigerparameter als const deklariert werden.
void Ausgeben(const int *array, int länge) {
for (int i = 0; i < länge; i++)
cout << array[i] << " ";
}
Diese Methode schützt die Datenintegrität und verhindert, dass das Array versehentlich verändert wird.
10. TL;DR
- Der Array-Name (
zahlen) → ist die Adresse des ersten Elements (&zahlen[0]). *(p + i)↔array[i]sind gleichwertig.- Die Zeigerarithmetik erhöht die Adresse automatisch entsprechend der Datentypgröße.
- Arrays werden als Adresse an Funktionen übergeben, nicht als Kopie.
- Mehrdimensionale Arrays können über Zeigerketten angesprochen werden (
*(*(matrix + i) + j)). - Dynamische Arrays sollten mit
newerstellt und mitdelete[]freigegeben werden. - Alle Beispiele können in Visual Studio 2022 oder mit dem GCC 11+ Compiler ausgeführt werden.