HOWTO · C++

Comment déclarer un tableau 2D avec l’opérateur `new` en C++

Cet article explique comment déclarer un tableau 2D dynamique en C++ à l’aide de l’opérateur `new`.

Sur cette page

En C++, une matrice dont les dimensions sont déterminées à l’exécution peut être représentée de plusieurs façons, mais new ne crée pas de tableau multidimensionnel intégré au langage. Vous devez choisir la disposition des éléments et la manière dont leur stockage est géré. Les exemples ci-dessous utilisent deux lignes et trois colonnes pour montrer comment les indices correspondent aux valeurs stockées. L’exemple d’allocation des lignes a été exécuté avec succès dans l’environnement fourni.

Représenter un tableau bidimensionnel dynamique

Une déclaration intégrée comme int data[2][3] a une taille fixe. Pour des dimensions déterminées à l’exécution, les options courantes sont un int ** dont les lignes sont allouées séparément, une allocation linéaire unique avec un indice calculé, ou des objets std::vector imbriqués. Ces représentations permettent toutes d’accéder aux lignes et aux colonnes, mais leur disposition en mémoire et la gestion de leur propriété diffèrent. C++ ne vérifie pas que les indices de ligne et de colonne restent dans leurs limites valides.

Allouer les lignes séparément pour accéder avec arr[i][j]

Un int ** pointe vers un tableau de pointeurs de lignes ; il ne pointe pas vers un bloc rectangulaire unique contenant tous les éléments. Allouez d’abord le tableau de pointeurs, puis chaque ligne. Cette représentation conserve la notation familière matrix[i][j] et permet aussi d’utiliser des lignes de longueurs différentes, à condition d’en suivre vous-même la longueur.

Exécutez cet exemple :

Le code et les libellés ou sorties affichés dans cette section restent en anglais.

#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;
}

Chaque new[] doit être associé à delete[] : libérez les lignes, puis le tableau de pointeurs. Si une allocation échoue pendant la construction, une implémentation à base de pointeurs bruts nécessite une logique de nettoyage supplémentaire. En production, préférez un conteneur propriétaire ou des pointeurs intelligents pour éviter de gérer manuellement la durée de vie.

Stocker les éléments dans une seule allocation contiguë

Une seule allocation new[] stocke tous les éléments ensemble. Dans une disposition par lignes, l’élément de la ligne i et de la colonne j se trouve à l’offset i * columns + j. Vous devez conserver les dimensions, car le pointeur à lui seul n’indique ni le nombre de lignes ni celui de colonnes. Vérifiez que le produit utilisé pour calculer la taille de l’allocation peut être représenté avec les types et les dimensions de votre programme.

Par exemple, après avoir alloué rows * columns éléments dans int *data, accédez à un élément avec data[i * columns + j]. Libérez l’allocation une seule fois avec delete[] data. Cette disposition rend le stockage contigu, mais ne permet pas directement l’indexation data[i][j] ; une fonction auxiliaire ou une abstraction de conteneur peut masquer le calcul de l’offset. La disposition seule ne permet pas de conclure à une supériorité en matière de performances : si la vitesse est déterminante, mesurez la charge de travail complète.

Utiliser std::vector pour gérer automatiquement la mémoire

Dans le code d’application courant, les vecteurs imbriqués offrent généralement une gestion claire de la propriété. Chaque vecteur interne possède une ligne, et son destructeur libère automatiquement les éléments lorsque le vecteur externe sort de portée. L’initialisation suivante crée deux lignes de trois éléments chacune :

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

Utilisez matrix[i][j] pour accéder à un élément, ou des boucles basées sur les plages pour parcourir les lignes et leurs valeurs. Chaque ligne peut être redimensionnée indépendamment, ce qui est utile pour les données irrégulières. Comme pour les tableaux bruts, l’indexation des vecteurs avec operator[] ne vérifie pas les limites ; utilisez at() lorsque vous avez besoin d’un accès vérifié. Un vecteur imbriqué ne garantit pas non plus que toutes les lignes occupent un seul bloc contigu.

Choisir une approche et en connaître les limites

Choisissez des lignes allouées séparément lorsque leurs longueurs peuvent varier ou que l’interface exige une forme pointeur vers pointeur. Choisissez une seule allocation linéaire lorsque votre interface convient à un bloc unique dont vous gérez la propriété et à un mappage explicite par lignes. Préférez std::vector lorsque le nettoyage automatique, le redimensionnement et une gestion simple de la propriété sont importants ; c’est généralement un point de départ plus sûr que la gestion d’allocations brutes de new.

Un tableau intégré de taille fixe est simple lorsque les dimensions sont connues à la compilation, par exemple int matrix[2][3]. Pour des lignes irrégulières allouées manuellement, stockez la longueur de chaque ligne et servez-vous-en pour accéder aux éléments et libérer la mémoire. Quelle que soit la représentation, vérifiez les dimensions avant l’indexation : un accès hors limites au moyen de pointeurs bruts ou de operator[] entraîne un comportement indéfini. Associez chaque allocation de tableau effectuée avec new[] à son delete[] correspondant, et évitez de copier des pointeurs bruts propriétaires, sauf si la propriété est délibérément transférée ou partagée.