Cuando declaras int m[3][4], no estás creando una entidad abstracta de dos dimensiones, sino un array de 3 elementos, donde cada elemento es, a su vez, un array completo de 4 int. Para el compilador, esto es un bloque contiguo de memoria donde los elementos se disponen en row-major order (orden de fila dominante). Esto significa que la memoria se organiza fila por fila: primero están los 4 elementos de la primera fila, inmediatamente después los 4 de la segunda, y así sucesivamente.
Debido a esta naturaleza lineal, para que el compilador pueda resolver la expresión m[i][j], necesita conocer el stride (el salto). Matemáticamente, la ubicación de un elemento se calcula como *((int*)m + i * número_de_columnas + j). Por eso, cuando pasas un array multidimensional a una función, la primera dimensión es opcional, pero las dimensiones restantes son obligatorias. Sin el tamaño de las columnas, el compilador no sabría cuántos elementos debe saltar en la memoria para llegar a la siguiente fila.
Para evitar este problema en contextos de diseño genérico, [C99] introdujo los Variable Length Arrays (VLA), que permiten usar parámetros para definir las dimensiones. En términos de rendimiento, entender el layout es crítico para la cache locality: iterar sobre el índice de la derecha (el más rápido en variar) asegura que estés accediendo a direcciones de memoria contiguas, aprovechando las líneas de caché de la CPU y evitando cache misses costosos.
#include <stdio.h>
#include <stdlib.h>
/* La sintaxis 'int (*m)[cols]' es un puntero a un array de 'cols' enteros.
Es fundamental para manejar matrices dinámicas con un solo malloc. */
void imprimir_matriz(int (*m)[4], size_t filas) {
for (size_t i = 0; i < filas; i++) {
for (size_t j = 0; j < 4; j++) {
/* m[i][j] es equivalente a *(m + i)[j] o *(*(m + i) + j) */
printf("%02d ", m[i][j]);
}
printf("\n");
}
}
/* Uso de VLA [C99] para permitir dimensiones dinámicas en la firma. */
void imprimir_vla(int filas, int cols, int m[filas][cols]) {
for (size_t i = 0; i < (size_t)filas; i++) {
for (size_t j = 0; j < (size_t)cols; j++) {
printf("%d ", m[i][j]);
}
printf("\n");
}
}
int main(void) {
const size_t FILAS = 3;
const size_t COLS = 4;
/* Asignación de una matriz 2D como un único bloque contiguo.
Esto es más eficiente que un array de punteros (int **). */
int (*matriz_dinamica)[COLS] = malloc(FILAS * sizeof(*matriz_dinamica));
if (!matriz_dinamica) {
return EXIT_FAILURE;
}
/* Inicialización manual para el ejemplo */
for (size_t i = 0; i < FILAS; i++) {
for (size_t j = 0; j < COLS; j++) {
matriz_dinamica[i][j] = (int)(i * COLS + j);
}
}
printf("Matriz estática/dinámica contigua:\n");
imprimir_matriz(matriz_dinamica, FILAS);
/* Ejemplo de VLA con dimensiones que no conocemos en tiempo de compilación */
int n = 2, m = 3;
int (*vla_array)[m] = malloc(n * sizeof(*vla_array)); // VLA en heap
if (vla_array) {
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
vla_array[i][j] = i + j;
printf("\nMatriz usando VLA (dimensiones variables):\n");
imprimir_vla(n, m, vla_array);
free(vla_array);
}
free(matriz_dinamica);
return EXIT_SUCCESS;
}
Análisis técnico
En la línea int (*matriz_dinamica)[COLS] = malloc(FILAS * sizeof(*matriz_dinamica));, estamos realizando una asignación de un puntero a un array. El sizeof(*matriz_dinamica) es la clave: no devuelve el tamaño de un int, sino el tamaño de una fila completa (COLS * sizeof(int)). Esto nos permite tratar al bloque de memoria como una matriz real de dos dimensiones sin necesidad de realizar múltiples malloc para cada fila, manteniendo la contigüidad que la CPU tanto agradece.
Cuando llamamos a imprimir_matriz(matriz_dinamica, FILAS), el compilador utiliza el tipo int (*m)[4] para calcular la dirección de m[i]. Si intentáramos declarar la función como void imprimir_matriz(int **m, int filas), el programa fallaría o corrompería la memoria. Un int ** es un puntero a un puntero (una dirección que apunta a otra dirección), mientras que int m[3][4] es un bloque de valores contiguos.
En el bucle de imprimir_matriz, la iteración de j (la columna) es el índice más interno. Como los elementos de una fila están contiguos en el layout de memoria, cada incremento de j accede a la siguiente posición de memoria inmediata, lo que garantiza que los datos ya estén en la caché L1 tras la primera carga.
El error frecuente
Un error clásico al transicionar a un nivel intermedio es confundir la firma de una función para matrices estáticas con una matriz dinámica de punteros.
/* ERROR: Esto causará un error de tipo o comportamiento indefinido */
void error_func(int **m, size_t filas, size_t cols) {
printf("%d", m[0][0]);
}
int main(void) {
int matriz[3][4]; // Array bidimensional real (contiguo)
matriz[0][0] = 10;
// ERROR: 'int (*)[4]' no es compatible con 'int **'
error_func(matriz, 3, 4);
}
El compilador rechazará la llamada o, si se fuerza mediante casts, el programa fallará en tiempo de ejecución. int **m espera que m sea un array de punteros, donde cada puntero apunta a una fila que podría estar en cualquier parte de la memoria. En cambio, int matriz[3][4] es un bloque único de 12 enteros. Para que la función funcione con matrices de tamaño variable, debes usar la sintaxis de VLA int m[filas][cols] o pasar el puntero al primer elemento y calcular el desplazamiento manualmente.
N° 54