Menu

Tableaux 2D en C : tableaux multidimensionnels, disposition mémoire et matrices

Comment déclarer, initialiser et parcourir des tableaux 2D en C, ce que signifie réellement la disposition par lignes en mémoire, pourquoi passer un tableau 2D à une fonction exige le nombre de colonnes, et un exemple matriciel complet.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Un tableau multidimensionnel est un tableau dont les éléments sont eux-mêmes des tableaux. int grid[3][4]; n'est pas un type grille spécial - ce sont trois éléments, chacun étant un tableau de quatre entiers, stockés bout à bout. Une fois cela compris, le reste du sujet en découle : la disposition, l'arithmétique d'indexation, et la règle sinon déroutante sur leur passage aux fonctions.

Deux dimensions couvrent presque tous les usages pratiques - grilles, tables, matrices, plateaux de jeu, images - c'est donc avec cela que travaille cette page.

Déclarer et initialiser

int grid[3][4];        // 3 lignes, 4 colonnes - 12 int

Le premier nombre est le nombre de lignes, le second celui des colonnes. Les initialiseurs peuvent s'écrire à plat ou avec des accolades internes ; les accolades valent la peine car elles montrent la forme.

La forme int e[][3] compte : vous pouvez laisser le nombre de lignes vide et laisser l'initialiseur décider, mais le nombre de colonnes n'est jamais facultatif. La section suivante explique pourquoi.

La disposition par lignes

Le C stocke un tableau 2D en ordre par lignes : toute la ligne 0, puis toute la ligne 1, et ainsi de suite, dans un bloc de mémoire ininterrompu. Il n'y a pas de tableau de pointeurs de lignes en coulisses.

int grid[3][4] = {{ 1, 2, 3, 4},
                  { 5, 6, 7, 8},
                  { 9,10,11,12}};

comment vous l'imaginez          comment il est vraiment en memoire
+----+----+----+----+
|  1 |  2 |  3 |  4 |   ligne 0  +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+            | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
|  5 |  6 |  7 |  8 |   ligne 1  +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+             \_ ligne 0 _/\_ ligne 1 _/\_ ligne 2 _/
|  9 | 10 | 11 | 12 |   ligne 2
+----+----+----+----+            grid[i][j] est a l'indice i*4 + j

Cette formule, i * colonnes + j, est tout le mécanisme - et c'est pourquoi le compilateur doit connaître le nombre de colonnes pour indexer quoi que ce soit. Le nombre de lignes n'entre jamais dans le calcul.

Vous pouvez voir la disposition directement en affichant les adresses :

Les adresses montent de sizeof(int) sans aucun trou, y compris là où une ligne se termine et où la suivante commence. La boucle aplatie le prouve - flat[k] parcourt les douze éléments comme une seule suite.

Cette disposition a aussi une conséquence de performance à connaître : boucler lignes-puis-colonnes touche la mémoire dans l'ordre, ce que le cache du processeur apprécie. Inverser l'imbrication pour que la boucle interne descende une colonne saute d'une ligne entière à chaque fois et peut tourner plusieurs fois plus lentement sur un grand tableau.

Les boucles imbriquées

Deux dimensions veulent deux boucles for : l'externe choisit la ligne, l'interne balaie les colonnes de cette ligne.

Nommez les compteurs d'après ce qu'ils signifient (i/row pour les lignes, j/col pour les colonnes) et gardez l'ordre cohérent - grid[row][col] partout. La moitié des bugs de tableaux 2D est une paire d'indices transposée.

Les tailles en #define ne sont pas décoratives non plus : les bornes de boucle et la déclaration ne peuvent plus diverger quand vous changez la forme.

Passer un tableau 2D à une fonction

Voici la règle qui fait trébucher tout le monde : le paramètre de la fonction doit déclarer le nombre de colonnes.

La raison est la dégradation. Passer grid le convertit en pointeur sur son premier élément - et ses éléments sont des lignes, donc le type est int (*)[4] : pointeur sur un tableau de 4 int. Pour que grid[i][j] signifie quelque chose, le compilateur doit savoir quelle distance couvre une ligne, et c'est ce 4. Le nombre de lignes est réellement absent du type, c'est pourquoi il voyage en argument séparé.

Notez que int (*grid)[COLS] et int grid[][COLS] sont le même paramètre écrit de deux façons - les parenthèses sont obligatoires, puisque int *grid[COLS] serait plutôt un tableau de pointeurs. Cette distinction est couverte dans pointeurs et tableaux.

Si le nombre de colonnes n'est connu qu'à l'exécution, les paramètres à modification variable de C99 vous permettent de le passer en premier :

void print_any(int rows, int cols, int grid[rows][cols]);

rows et cols doivent être déclarés avant le paramètre tableau qui les utilise. Là où ce n'est pas disponible, l'alternative courante est un tableau 1D plat plus une arithmétique d'indices manuelle :

data[i * cols + j] est exactement ce que le compilateur écrit pour vous dans le cas à taille fixe. Le faire à la main coûte une ligne et fonctionne pour n'importe quelle forme décidée à l'exécution.

Un exemple matriciel

La multiplication de matrices réunit toute la page - trois boucles imbriquées sur un stockage par lignes.

Deux détails à copier. La boucle interne k apparie a[i][k] avec b[k][j] - un indice parcourt une ligne, l'autre une colonne. Et la transposition démarre sa boucle interne à j = i + 1 : partir de 0 échangerait chaque paire deux fois et laisserait la matrice inchangée.

Trois dimensions et au-delà

Le motif s'étend, et la règle sur les paramètres de fonction aussi - toutes les dimensions sauf la première doivent être déclarées.

En pratique, les trois dimensions sont l'endroit où les tableaux de taille fixe commencent à devenir encombrants, et la plupart du code bascule vers un bloc plat avec indices calculés ou un tableau de structures qui nomme ce que chaque axe signifie.

Erreurs courantes

  • Écrire grid[i, j]. L'opérateur virgule évalue i, le jette, et indexe avec j. Cela compile. C'est faux. Utilisez grid[i][j].
  • Transposer les indices. grid[col][row] lit un vrai élément au mauvais endroit, il n'y a donc aucune erreur pour l'attraper. Gardez l'ordre [row][col] partout.
  • Omettre la taille des colonnes dans un paramètre. void f(int grid[][]) ne compile pas, et c'est le compilateur qui vous sauve.
  • Sortir des bornes. Comme pour tout tableau, il n'y a pas de vérification de bornes. grid[0][5] sur une grille [3][4] lit silencieusement grid[1][1], car la disposition est contiguë et l'arithmétique s'en moque.

Questions fréquentes

Comment déclare-t-on un tableau 2D en C ?

Donnez deux tailles entre crochets : int grid[3][4]; déclare 3 lignes de 4 colonnes - 12 entiers au total. Lisez-le comme « un tableau de 3 choses, chacune étant un tableau de 4 int », ce qui est littéralement la façon dont le C le stocke.

Comment un tableau 2D est-il stocké en mémoire en C ?

Dans l'ordre par lignes (row-major) : tous les éléments de la ligne 0, puis tous ceux de la ligne 1, et ainsi de suite, dans un bloc contigu. grid[i][j] se trouve au décalage i * colonnes + j éléments depuis le début, c'est pourquoi le nombre de colonnes est le nombre dont le compilateur a besoin.

Comment passe-t-on un tableau 2D à une fonction en C ?

Le paramètre doit déclarer le nombre de colonnes : void print(int grid[][4], int rows) ou, de façon équivalente, void print(int (*grid)[4], int rows). Le nombre de lignes peut être omis car le tableau se dégrade en pointeur sur une ligne - mais sans la taille des colonnes, le compilateur ne peut pas calculer où commence une ligne.

Peut-on initialiser un tableau 2D entièrement à zéro ?

Oui : int grid[3][4] = {0}; met chaque élément à zéro, car tout élément non listé est initialisé à zéro. int grid[3][4] = {{1, 2}}; fixe les deux premières entrées de la ligne 0 et laisse les dix autres à zéro.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER