HOWTO · C++

2D-Array mit dem Operator new in C++ deklarieren

Dieser Artikel erklärt, wie Sie mit dem C++-Operator new dynamische zweidimensionale Arrays darstellen und verwalten.

Auf dieser Seite

Eine Matrix mit Abmessungen, die erst zur Laufzeit feststehen, lässt sich in C++ auf verschiedene Arten darstellen. new erzeugt jedoch kein eingebautes mehrdimensionales Array. Sie entscheiden, wie die Elemente angeordnet sind und wem der Speicher gehört. Die folgenden Beispiele verwenden zwei Zeilen und drei Spalten, damit Sie nachvollziehen können, wie die Indizes den gespeicherten Werten zugeordnet sind. Das Beispiel zur Zeilenbelegung wurde in der bereitgestellten Ausführungsumgebung erfolgreich ausgeführt.

Darstellung eines dynamischen zweidimensionalen Arrays

Eine eingebaute Deklaration wie int data[2][3] hat eine feste Größe. Für Abmessungen, die erst zur Laufzeit feststehen, kommen unter anderem ein int ** mit separat angelegten Zeilen, eine einzelne flache Speicherbelegung mit berechnetem Index oder verschachtelte std::vector-Objekte infrage. Mit all diesen Darstellungen können Sie auf Zeilen und Spalten zugreifen, doch Speicherlayout und Besitzverhältnisse unterscheiden sich. C++ prüft nicht, ob Zeilen- und Spaltenindizes innerhalb ihrer gültigen Bereiche liegen.

Zeilen für den Zugriff mit arr[i][j] separat anlegen

Ein int ** zeigt auf ein Array von Zeilenzeigern; es zeigt nicht auf einen einzigen rechteckigen Speicherblock mit allen Elementen. Legen Sie zuerst das Zeigerarray und danach jede einzelne Zeile an. So bleibt die vertraute Schreibweise matrix[i][j] erhalten. Außerdem können die Zeilen unterschiedlich lang sein, sofern Sie ihre jeweilige Länge selbst festhalten.

Führen Sie dieses Beispiel aus:

Der Code und die angezeigten Bezeichnungen oder Ausgaben in diesem Abschnitt bleiben auf Englisch.

#include <iostream>

int main() {
    const int rows = 2;
    const int columns = 3;
    int **matrix = new int*[rows];

    for (int i = 0; i < rows; ++i) {
        matrix[i] = new int[columns]{};
    }

    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < columns; ++j) {
            matrix[i][j] = i * columns + j + 1;
            std::cout << matrix[i][j] << (j + 1 == columns ? '\n' : ' ');
        }
    }

    for (int i = 0; i < rows; ++i) {
        delete[] matrix[i];
    }
    delete[] matrix;
}

Jedes new[] muss mit delete[] freigegeben werden: zuerst die Zeilen, dann das Zeigerarray. Schlägt eine Speicherbelegung während des Aufbaus fehl, benötigt eine Implementierung mit rohen Zeigern zusätzliche Bereinigungslogik. Verwenden Sie in Produktionscode vorzugsweise einen besitzenden Container oder Smart Pointer, damit Sie die Lebensdauer nicht manuell verwalten müssen.

Elemente in einer zusammenhängenden Speicherbelegung ablegen

Eine einzelne Belegung mit new[] speichert alle Elemente zusammenhängend. Bei zeilenweiser Anordnung liegt das Element in Zeile i und Spalte j am Offset i * columns + j. Sie müssen die Abmessungen separat festhalten, denn der Zeiger allein speichert weder die Zeilen- noch die Spaltenanzahl. Achten Sie darauf, dass sich das Produkt zur Berechnung der Belegungsgröße mit den in Ihrem Programm verwendeten Typen und Abmessungen darstellen lässt.

Wenn Sie beispielsweise rows * columns Elemente in int *data belegen, greifen Sie mit data[i * columns + j] auf ein Element zu. Geben Sie die Belegung einmal mit delete[] data frei. Dieses Layout hält die Elemente zusammenhängend im Speicher, bietet aber keinen direkten Zugriff mit data[i][j]. Eine Hilfsfunktion oder eine Container-Abstraktion kann die Offsetberechnung übernehmen. Aus dem Speicherlayout allein lässt sich keine Leistungsrangfolge ableiten. Wenn die Geschwindigkeit entscheidend ist, messen Sie daher die vollständige Arbeitslast.

std::vector für automatische Speicherverwaltung verwenden

Für gewöhnlichen Anwendungscode bieten verschachtelte Vektoren meist die klarste Besitzverwaltung. Die inneren Vektoren besitzen jeweils eine Zeile und geben deren Elemente automatisch frei, wenn der äußere Vektor seinen Gültigkeitsbereich verlässt. Die folgende Initialisierung erzeugt zwei Zeilen mit jeweils drei Elementen:

std::vector<std::vector<int>> matrix(2, std::vector<int>(3, 0));

Greifen Sie mit matrix[i][j] auf ein Element zu oder durchlaufen Sie Zeilen und Werte mit bereichsbasierten Schleifen. Sie können jede Zeile unabhängig vergrößern oder verkleinern, was für unregelmäßig lange Zeilen nützlich ist. Wie bei rohen Arrays prüft der Vektorzugriff mit operator[] die Grenzen nicht. Verwenden Sie at(), wenn ein geprüfter Zugriff passend ist. Ein verschachtelter Vektor garantiert außerdem nicht, dass alle Zeilen in einem einzigen zusammenhängenden Speicherblock liegen.

Einen Ansatz auswählen und seine Grenzen beachten

Wählen Sie separat angelegte Zeilen, wenn sie unterschiedlich lang sein dürfen oder Ihre Schnittstelle die Zeiger-auf-Zeiger-Form erfordert. Eine einzelne flache Belegung eignet sich, wenn ein zusammenhängender Speicherblock und eine explizite zeilenweise Zuordnung zu Ihrer Schnittstelle passen. Bevorzugen Sie std::vector, wenn automatische Freigabe, Größenänderungen und eine einfache Besitzverwaltung wichtig sind. Sie ist im Allgemeinen der sicherere Ausgangspunkt als der eigene Besitz roher new-Belegungen.

Ein eingebautes Array fester Größe ist einfach, wenn die Abmessungen zur Compile-Zeit bekannt sind, zum Beispiel int matrix[2][3]. Speichern Sie bei unregelmäßig langen Zeilen mit manueller Belegung die Länge jeder Zeile und verwenden Sie sie für Zugriffe und Freigabe. Prüfen Sie bei jeder Darstellung die Abmessungen, bevor Sie auf Elemente zugreifen: Ein Zugriff außerhalb des gültigen Bereichs über rohe Zeiger oder operator[] führt zu undefiniertem Verhalten. Ordnen Sie jeder mit new[] angelegten Array-Belegung das passende delete[] zu. Kopieren Sie rohe Zeiger auf besessenen Speicher nur, wenn Sie den Besitz gezielt übertragen oder gemeinsam verwalten.