Menu

Arrays 2D en C: arrays multidimensionales, disposición en memoria y matrices

Cómo declarar, inicializar y recorrer arrays 2D en C, qué significa realmente la disposición por filas en memoria, por qué pasar un array 2D a una función exige la cantidad de columnas y un ejemplo resuelto con matrices.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Un array multidimensional es un array cuyos elementos son a su vez arrays. int grid[3][4]; no es un tipo especial de cuadrícula: son tres elementos, cada uno de los cuales es un array de cuatro ints, almacenados uno detrás de otro. En cuanto eso encaja, el resto del tema se deduce: la disposición, la aritmética de índices y la regla por lo demás desconcertante sobre pasarlos a funciones.

Dos dimensiones cubren casi cualquier uso práctico —cuadrículas, tablas, matrices, tableros de juego, imágenes— así que eso es con lo que trabaja esta página.

Declarar e inicializar

int grid[3][4];        // 3 filas, 4 columnas: 12 ints

El primer número es cuántas filas, el segundo cuántas columnas. Los inicializadores pueden escribirse planos o con llaves internas; vale la pena usar las llaves porque muestran la forma.

La forma int e[][3] importa: puedes dejar en blanco la cantidad de filas y dejar que el inicializador la decida, pero la cantidad de columnas nunca es opcional. La siguiente sección explica por qué.

Disposición por filas

C guarda un array 2D en orden por filas (row-major): la fila 0 entera, luego la fila 1 entera, y así, en un bloque de memoria ininterrumpido. No hay ningún array de punteros a filas entre bastidores.

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

como te lo imaginas              como esta en memoria de verdad
+----+----+----+----+
|  1 |  2 |  3 |  4 |   fila 0   +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+            | 1| 2| 3| 4| 5| 6| 7| 8| 9|10|11|12|
|  5 |  6 |  7 |  8 |   fila 1   +--+--+--+--+--+--+--+--+--+--+--+--+
+----+----+----+----+             \_ fila 0 __/\_ fila 1 __/\_ fila 2 _/
|  9 | 10 | 11 | 12 |   fila 2
+----+----+----+----+            grid[i][j] esta en el indice i*4 + j

Esa fórmula, i * columnas + j, es todo el mecanismo, y es por lo que el compilador debe conocer la cantidad de columnas para indexar cualquier cosa. La cantidad de filas nunca entra en el cálculo.

Puedes ver la disposición directamente imprimiendo direcciones:

Las direcciones suben de sizeof(int) en sizeof(int) sin huecos, incluido donde una fila termina y empieza la siguiente. El bucle aplanado lo demuestra: flat[k] recorre los doce elementos como una sola tira.

Esta disposición también tiene una consecuencia de rendimiento que vale la pena conocer: recorrer primero las filas y luego las columnas toca la memoria en orden, cosa que a la caché de la CPU le gusta. Invertir el anidamiento para que el bucle interno baje por una columna salta una fila entera cada vez y puede correr varias veces más lento en un array grande.

Bucles anidados

Dos dimensiones piden dos bucles for: el externo elige la fila, el interno recorre las columnas de esa fila.

Nombra los contadores según lo que significan (i/row para filas, j/col para columnas) y mantén el orden consistente: grid[row][col] en todas partes. La mitad de los errores con arrays 2D son un par de índices intercambiados.

Los tamaños con #define tampoco son decoración: ahora los límites de los bucles y la declaración no pueden desincronizarse cuando cambies la forma.

Pasar un array 2D a una función

Aquí está la regla que hace tropezar a todo el mundo: el parámetro de la función debe declarar la cantidad de columnas.

La razón es el decaimiento. Pasar grid lo convierte en un puntero a su primer elemento, y sus elementos son filas, así que el tipo es int (*)[4]: puntero a un array de 4 ints. Para que grid[i][j] signifique algo, el compilador debe saber cuánto ocupa una fila, y eso es el 4. La cantidad de filas está genuinamente ausente del tipo, que es por lo que viaja como un argumento aparte.

Fíjate en que int (*grid)[COLS] e int grid[][COLS] son el mismo parámetro escrito de dos formas: los paréntesis son obligatorios, ya que int *grid[COLS] sería en cambio un array de punteros. Esa distinción se cubre en punteros y arrays.

Si la cantidad de columnas solo se conoce en tiempo de ejecución, los parámetros de tipo modificado variablemente de C99 te dejan pasarla primero:

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

rows y cols deben declararse antes del parámetro de tipo array que los usa. Donde eso no esté disponible, la alternativa habitual es un array 1D plano más aritmética de índices hecha a mano:

data[i * cols + j] es exactamente lo que el compilador escribe por ti en el caso de tamaño fijo. Hacerlo a mano cuesta una línea y funciona para cualquier forma decidida en tiempo de ejecución.

Un ejemplo con matrices

La multiplicación de matrices junta toda la página: tres bucles anidados sobre un almacenamiento por filas.

Dos detalles que vale la pena copiar. El bucle interno k empareja a[i][k] con b[k][j]: un índice recorre una fila y el otro recorre una columna. Y la transposición empieza su bucle interno en j = i + 1: empezar en 0 intercambiaría cada par dos veces y dejaría la matriz igual.

Tres dimensiones y más allá

El patrón se extiende, y también la regla sobre los parámetros de las funciones: toda dimensión salvo la primera debe declararse.

En la práctica, tres dimensiones es donde los arrays de tamaño fijo empiezan a sentirse incómodos, y la mayoría del código pasa a un bloque plano con índices calculados o a un array de estructuras que nombra lo que significa cada eje.

Errores comunes

  • Escribir grid[i, j]. El operador coma evalúa i, lo descarta e indexa con j. Compila. Está mal. Usa grid[i][j].
  • Intercambiar los índices. grid[col][row] lee un elemento real del lugar equivocado, así que no hay ningún error que lo detecte. Mantén el orden [fila][columna] en todas partes.
  • Omitir el tamaño de columna en un parámetro. void f(int grid[][]) no compila, y eso es el compilador salvándote.
  • Salirse de los límites. Como con cualquier array, no hay comprobación de límites. grid[0][5] en una cuadrícula [3][4] lee en silencio grid[1][1], porque la disposición es contigua y a la aritmética le da igual.

Preguntas frecuentes

¿Cómo se declara un array 2D en C?

Da dos tamaños entre corchetes: int grid[3][4]; declara 3 filas de 4 columnas, 12 ints en total. Léelo como "un array de 3 cosas, cada una de las cuales es un array de 4 ints", que es literalmente como C lo almacena.

¿Cómo se guarda un array 2D en memoria en C?

En orden por filas: todos los elementos de la fila 0, luego todos los de la fila 1, y así, en un solo bloque contiguo. grid[i][j] vive a i * columnas + j elementos del inicio, que es por lo que la cantidad de columnas es el número que el compilador necesita.

¿Cómo se pasa un array 2D a una función en C?

El parámetro debe declarar la cantidad de columnas: void print(int grid[][4], int rows) o, equivalentemente, void print(int (*grid)[4], int rows). La cantidad de filas puede omitirse porque el array decae a un puntero a una fila, pero sin el tamaño de columna el compilador no puede calcular dónde empieza una fila.

¿Se puede inicializar un array 2D todo a ceros?

Sí: int grid[3][4] = {0}; pone a cero cada elemento, porque cualquier elemento que no listes se inicializa a cero. int grid[3][4] = {{1, 2}}; fija las dos primeras entradas de la fila 0 y deja las otras diez en cero.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR