Introducción¶
En el ámbito de la programación, una matriz se define como una estructura de datos que facilita el almacenamiento de un conjunto homogéneo de elementos, organizados en una disposición bidimensional de filas y columnas. En el lenguaje de programación C, esta abstracción se materializa mediante la implementación de arreglos bidimensionales (2D), los cuales pueden ser conceptualizados como arreglos cuyos elementos son, a su vez, otros arreglos.
Las matrices son fundamentales en numerosas aplicaciones: desde operaciones matemáticas básicas hasta algoritmos complejos de procesamiento de imágenes, simulaciones físicas, análisis de datos, representación de grafos, implementación de juegos como el tres en raya o ajedrez, y sistemas de coordenadas bidimensionales. Su comprensión es esencial para el desarrollo de software eficiente y estructurado.
Desarrollo¶
Matrices (Arreglos Bidimensionales)
Relación con el álgebra lineal¶
Las matrices en programación están íntimamente relacionadas con el concepto matemático de matriz del álgebra lineal. Esto permite aplicar directamente teoremas y algoritmos matemáticos en implementaciones de software, especialmente en campos como gráficos por computadora, machine learning, y simulaciones científicas.
Extensión a múltiples dimensiones¶
Técnicamente, no están limitadas a dos dimensiones. Podés tener arreglos
tridimensionales (int cubo[3][4][5]) o de mayor dimensionalidad. Sin embargo,
las aplicaciones prácticas se vuelven menos claras y la complejidad de manejo
aumenta considerablemente. Todos los conceptos presentados aquí se extienden
naturalmente a estas dimensiones superiores.
Declaración¶
La declaración de una matriz requiere la especificación del tipo de dato de sus elementos, un identificador único y las dimensiones correspondientes al número de filas y columnas.
Sintaxis
tipo_dato nombre_matriz[CANTIDAD_FILAS][CANTIDAD_COLUMNAS];Ejemplo
int miMatriz[3][4]; // Matriz de 3 filas y 4 columnasFigure 1:Disposición física contigua de una matriz 2D en memoria RAM (Row-Major order).
Inicialización¶
Podemos inicializar nuestras matrices, esencialmente, de dos formas diferentes, con un inicializador como con los arreglos, o con código.
Figure 2:Inicialización por filas de una matriz bidimensional.
Inicialización completa¶
Este proceso se realiza mediante el uso de llaves anidadas, donde cada conjunto de llaves interno corresponde a una fila de la matriz.
1 2 3 4int matriz[2][3] = { {1, 2, 3}, // Fila 0 {4, 5, 6} // Fila 1 };
Inicialización con declaración implícita¶
En C, es posible omitir la primera dimensión (filas) durante la inicialización, pero todas las dimensiones subsecuentes deben ser especificadas explícitamente. Esto se debe a que el compilador necesita conocer el tamaño de cada “sub-arreglo” para calcular las posiciones de memoria.
1 2 3 4 5// Válido: el compilador infiere 2 filas basándose en el inicializador. int matriz[][3] = { {1, 2, 3}, // Fila 0 {4, 5, 6} // Fila 1 };
La forma int matriz[][] es inválida y no compilará, ya que el compilador
no tendría forma de saber dónde termina una fila y empieza la siguiente.
Inicialización manual¶
Constituye un método más flexible y programático. El uso de macros en mayúsculas
para las dimensiones (Regla 0x3011h: Si una función recibe un puntero genérico para operaciones de solo lectura, la firma de la función debe utilizar const void*) y de size_t para los índices
(Regla 0x3010h: Las variables que representan tamaños o índices de arreglos deben ser de tipo size_t) son buenas prácticas que mejoran la legibilidad y portabilidad.
#define FILAS 3
#define COLUMNAS 4
int matriz[FILAS][COLUMNAS];
for (size_t i = 0; i < FILAS; i++) {
for (size_t j = 0; j < COLUMNAS; j++) {
matriz[i][j] = i * 10 + j;
}
}Asignación de valores mediante lazo anidados
Acceso a los Elementos¶
El acceso a un elemento específico de la matriz se realiza mediante la especificación de sus índices de fila y columna, los cuales son de base cero.
Sintaxis
nombre_matriz[indice_fila][indice_columna];Ejemplo de L-Value y R-Value
matriz[0][1] = 100; // Asigna 100 al elemento en la fila 0, columna 1.
int valor = matriz[2][3]; // Toma el valor del elemento en la fila 2, columna 3.Patrones de Recorrido y Localidad de Memoria (Caché)¶
El procesamiento sistemático de todos los elementos de una matriz requiere el uso de lazos anidados. La comprensión de la relación entre el almacenamiento en memoria y el hardware de la CPU es crucial tanto para la corrección del algoritmo como para el rendimiento del programa.
A nivel físico, la memoria RAM es unidimensional. Para almacenar una matriz bidimensional, C utiliza el esquema Row-Major Order (ordenación por filas), disponiendo los elementos de la fila 0 de forma consecutiva, seguidos inmediatamente por los de la fila 1, y así sucesivamente.
Cuando el programa solicita un elemento de la matriz, la CPU no lee una única variable directamente desde la RAM. En su lugar, el hardware lee un bloque contiguo completo de datos (línea de caché) y lo transfiere a la memoria caché del procesador. Este mecanismo responde al principio de localidad espacial: si accedés a un dato, es altamente probable que necesités los datos adyacentes a la brevedad.
Recorrido por Filas (Row-Major): Alto Rendimiento¶
El patrón más común y eficiente es el recorrido por filas, donde se accede a todos los elementos de una fila antes de pasar a la siguiente.
Si recorrés la matriz fila por fila (lazo externo en filas i, lazo interno en
columnas j), el orden de acceso del programa coincide exactamente con la
disposición lineal en el hardware. Los elementos contiguos ya se encontrarán
precargados en la caché, generando un acierto de caché (cache hit) y
agilizando notablemente el procesamiento, respetando la regla de estilo
Regla 0x0000h: La claridad y prolijidad son de máxima importancia.
1 2 3 4 5 6 7 8// Lazo externo: filas (i) for (size_t i = 0; i < FILAS; i++) { // Lazo interno: columnas (j) for (size_t j = 0; j < COLUMNAS; j++) { printf("%d ", matriz[i][j]); // Acceso lineal contiguo } printf("\n"); // Salto de línea al final de cada fila }
Recorrido fila por fila (Cache-Friendly) - patrón recomendado
Recorrido por Columnas (Column-Major): Bajo Rendimiento¶
Si recorrés la matriz columna por columna (lazo externo en columnas j, lazo
interno en filas i), forzás al procesador a realizar “saltos” en memoria
física. Cada incremento de i requiere avanzar una distancia de COLUMNAS * sizeof(tipo) bytes.
Esto invalida la caché constantemente, produciendo un fallo de caché (cache miss) en cada paso, obligando a la CPU a suspender momentáneamente la ejecución para esperar lecturas de la lenta memoria principal (RAM).
// Lazo externo: columnas (j)
for (size_t j = 0; j < COLUMNAS; j++) {
// Lazo interno: filas (i)
for (size_t i = 0; i < FILAS; i++) {
printf("%d ", matriz[i][j]); // Salto de fila en cada paso
}
printf("\n"); // Nueva línea al final de cada columna
}Recorrido columna por columna (Cache-Unfriendly)
Figure 3:Recorrido por filas vs. recorrido por columnas.
Figure 4:Acceso a memoria y fallos de caché según el orden del lazo.
Recorrido Diagonal¶
Para matrices cuadradas, es común necesitar acceder a las diagonales.
#define DIM 4
int matriz_cuadrada[DIM][DIM];
// Diagonal principal (i == j)
printf("Diagonal principal: ");
for (size_t i = 0; i < DIM; i++) {
printf("%d ", matriz_cuadrada[i][i]);
}
printf("\n");
// Diagonal secundaria (i + j == DIM - 1)
printf("Diagonal secundaria: ");
for (size_t i = 0; i < DIM; i++) {
printf("%d ", matriz_cuadrada[i][DIM - 1 - i]);
}
printf("\n");Acceso a diagonal principal y secundaria
Figure 5:Diagonal principal e inversa en una matriz cuadrada.
Pasando matrices a funciones (Método Clásico)¶
Al pasar una matriz como argumento a una función, el estándar de C requiere que se especifiquen explícitamente todas las dimensiones, a excepción de la primera. Esto es necesario para que el compilador pueda calcular el desplazamiento en memoria de cada elemento.
1 2 3 4 5 6 7 8 9 10 11#define COLUMNAS 4 // Es crucial pasar las dimensiones para cumplir con la regla {ref}`0x300Ch`. void imprimir_matriz(int mat[][COLUMNAS], size_t filas, size_t columnas) { for (size_t i = 0; i < filas; i++) { for (size_t j = 0; j < columnas; j++) { printf("%d\t", mat[i][j]); } printf("\n"); } }
¿Acaso las columnas no están ya en el macro COLUMNAS? Para garantizar la
consistencia y minimizar los efectos secundarios en las funciones que operan con
matrices, es fundamental ser explícito con el contexto en el que deben trabajar.
En este caso, las columnas como argumento evita que nuestro código dependa de un valor que es esencialmente externo a la misma y aunque funciona perfectamente sin él, en el momento en que veamos memoria dinámica, el código no funcionará sin mayores cambios.
Cálculo de Desplazamiento de Memoria¶
Dicha información es indispensable para que el compilador pueda calcular
correctamente el desplazamiento de memoria necesario para localizar cualquier
elemento matriz[i][j], utilizando una fórmula análoga a:
Pasando matrices a funciones (Método ALV)¶
Aunque el uso de ALV en el stack está estrictamente prohibido por seguridad (riesgo de desborde de pila), la sintaxis de parámetros ALV en firmas de funciones (introducida en el estándar C99) es una herramienta sumamente útil y segura para crear funciones genéricas capaces de operar sobre matrices de dimensiones arbitrarias sin recurrir a macros estáticas.
Al declarar la matriz en los parámetros de la función utilizando variables previamente declaradas como dimensiones, el compilador puede generar código para calcular el desplazamiento de memoria de manera dinámica y precisa.
// Correcto: filas y cols se conocen antes de que el compilador procese
matriz[filas][cols]
void procesar_matriz(size_t filas, size_t cols, int matriz[filas][cols]) {
printf("\nProcesando matriz de %zu x %zu\n", filas, cols);
for (size_t i = 0; i < filas; i++) {
for (size_t j = 0; j < cols; j++) {
matriz[i][j] *= 2; // Ejemplo: duplicar cada valor
}
}
}Matrices Multidimensionales¶
El lenguaje C no impone un límite de dos dimensiones para los arreglos; es posible declarar arreglos multidimensionales. Un arreglo tridimensional, por ejemplo, puede conceptualizarse como un cubo de datos.
Figure 6:Representación lógica y orden de almacenamiento de una matriz tridimensional.
// Arreglo tridimensional: 2 capas, 3 filas, y 4 columnas.
int cubo[2][3][4];
// Acceso a un elemento
cubo[1][0][2] = 99;
// Recorrido con tres lazos anidados
for (size_t i = 0; i < 2; i++) { // Capas
for (size_t j = 0; j < 3; j++) { // Filas
for (size_t k = 0; k < 4; k++) { // Columnas
cubo[i][j][k] = i + j + k;
}
}
}Declaración y recorrido de un arreglo 3D
Operaciones Matemáticas con Matrices¶
En el ámbito de la programación en C y otras áreas de la computación, el manejo de matrices es fundamental. A continuación, se presentan los algoritmos y las expresiones matemáticas para las operaciones básicas entre matrices.
Figure 7:Suma y transposición lógica de matrices.
Suma de Matrices¶
La suma de dos matrices, A y B, de las mismas dimensiones (), guarda el resultado en una matriz C de la misma dimensión. Cada elemento de C es la suma de los elementos correspondientes en A y B.
Expresión de Suma de Matrices¶
Para dos matrices A y B de tamaño , la matriz resultante C se define como:
donde representa la fila y la columna.
Expansión de Suma de Matrices¶
Visualmente, la suma de dos matrices de 2x2 se vería así:
Algoritmo de Suma de Matrices en Pseudocódigo¶
El algoritmo recorre ambas matrices y suma los elementos en la misma posición.
1 2 3 4 5 6 7 8 9 10PROCEDIMIENTO sumar_matrices(A, B, C, m, n) // A y B son matrices de dimensión m x n de entrada // C es la de dimensión m x n de salida (por referencia) PARA i DESDE 0 HASTA m - 1 PARA j DESDE 0 HASTA n - 1 C[i][j] = A[i][j] + B[i][j] FIN PARA FIN PARA FIN PROCEDIMIENTO
Algoritmo para la suma de dos matrices A y B.
Resta de Matrices¶
De manera análoga a la suma, la resta de dos matrices A y B de idénticas dimensiones guarda el resultado en una matriz C donde cada elemento es la diferencia de los elementos correspondientes.
Expresión de Resta de Matrices¶
Para dos matrices A y B de tamaño , la matriz resultante C se define como:
Algoritmo de Resta de Matrices en Pseudocódigo¶
El procedimiento es idéntico al de la suma, pero se realiza una resta.
1 2 3 4 5 6 7 8 9 10PROCEDIMIENTO restar_matrices(A, B, C, m, n) // A y B son matrices de dimensión m x n de entrada // C es la de dimensión m x n de salida (por referencia) PARA i DESDE 0 HASTA m - 1 PARA j DESDE 0 HASTA n - 1 C[i][j] = A[i][j] - B[i][j] FIN PARA FIN PARA FIN PROCEDIMIENTO
Algoritmo para la resta de dos matrices A y B.
Multiplicación de Matrices¶
La multiplicación de una matriz A de dimensión por una matriz B de dimensión guarda el resultado en una matriz C de dimensión . Es crucial que el número de columnas de A sea igual al número de filas de B.
Figure 8:Proceso físico de multiplicación de matrices (fila por columna).
Expresión de Multiplicación de Matrices¶
El elemento de la matriz resultante C se calcula como la suma de los productos de los elementos de la fila de A por los elementos de la columna de B.
Expansión de Multiplicación de Matrices¶
Cada elemento de la matriz resultante se calcula realizando el producto escalar del vector fila de la matriz A con el vector columna de la matriz B.
Dadas las matrices:
El elemento se calcula como:
Por ejemplo, para calcular el elemento de una multiplicación de matrices de 2x2:
Donde .
Algoritmo de Multiplicación de Matrices en Pseudocódigo¶
Este algoritmo requiere tres lazos anidados para calcular el producto escalar de cada fila de A con cada columna de B.
Algoritmo en Pseudocódigo Optimizado (Cache-Friendly)¶
Para realizar la multiplicación minimizando los fallos de caché, es conveniente reordenar los lazos del algoritmo clásico () al orden optimizado (). De esta forma, el lazo más interno recorre consecutivamente las columnas de las matrices en memoria principal, garantizando localidad espacial.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23PROCEDIMIENTO multiplicar_matrices(A, B, C, m, p, n) // A es una matriz de m x p de entrada // B es una matriz de p x n de entrada // C es la de dimensión m x n de salida (por referencia). Se asume inicializada en 0. // Inicializar la matriz de resultados C en cero PARA i DESDE 0 HASTA m - 1 PARA j DESDE 0 HASTA n - 1 C[i][j] = 0 FIN PARA FIN PARA // Multiplicación en orden i, k, j para optimizar el acceso a caché PARA i DESDE 0 HASTA m - 1 PARA k DESDE 0 HASTA p - 1 factor = A[i][k] PARA j DESDE 0 HASTA n - 1 C[i][j] = C[i][j] + (factor * B[k][j]) FIN PARA FIN PARA FIN PARA FIN PROCEDIMIENTO
Algoritmo optimizado para la multiplicación de una matriz A (m x p) por una matriz B (p x n) en orden i-k-j.
Validación y Manejo de Errores¶
En aplicaciones robustas, es fundamental implementar validaciones para prevenir
accesos fuera de límites y operaciones inválidas. Esto es especialmente crítico
en C, donde no existe verificación automática de límites (Regla 0x300Ch: Verificá siempre los límites de los arreglos antes de acceder a sus elementos).
Figure 9:Validación de dimensiones y coherencia en operaciones con matrices.
Validación de Índices¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21#include <stdbool.h> #include <stdio.h> #define MAX_COLUMNAS 100 bool indice_valido(size_t fila, size_t columna, size_t max_filas, size_t max_columnas) { return (fila < max_filas && columna < max_columnas); } int acceso_seguro_matriz(size_t filas, size_t columnas, int matriz[filas][columnas], size_t fila, size_t columna) { if (!indice_valido(fila, columna, filas, columnas)) { fprintf(stderr, "Error: Índices fuera de límites (%zu, %zu)\n", fila, columna); return -1; // Valor de error } return matriz[fila][columna]; }
Función para validar acceso seguro a matriz
Validación de Operaciones¶
Para operaciones matemáticas entre matrices, debemos verificar la compatibilidad de dimensiones antes de proceder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22typedef enum { MATRIZ_OK, MATRIZ_ERROR_DIMENSIONES, MATRIZ_ERROR_MEMORIA, MATRIZ_ERROR_SINGULAR } resultado_matriz_t; resultado_matriz_t validar_suma(size_t filas_a, size_t columnas_a, size_t filas_b, size_t columnas_b) { if (filas_a != filas_b || columnas_a != columnas_b) { return MATRIZ_ERROR_DIMENSIONES; } return MATRIZ_OK; } resultado_matriz_t validar_multiplicacion(size_t filas_a, size_t columnas_a, size_t filas_b, size_t columnas_b) { if (columnas_a != filas_b) { return MATRIZ_ERROR_DIMENSIONES; } return MATRIZ_OK; }
Validación para operaciones con matrices
Mejores Prácticas y Optimizaciones¶
Uso de Macros para Dimensiones¶
Utilizá siempre macros para definir las dimensiones de tus matrices, siguiendo
la regla de estilo Regla 0x3011h: Si una función recibe un puntero genérico para operaciones de solo lectura, la firma de la función debe utilizar const void*. Esto facilita el mantenimiento y la
modificación del código.
#define MAX_FILAS 100
#define MAX_COLUMNAS 100
int matriz[MAX_FILAS][MAX_COLUMNAS];Definición de dimensiones con macros
Funciones Auxiliares¶
Creá funciones auxiliares para operaciones comunes, siguiendo la regla de
claridad Regla 0x0000h: La claridad y prolijidad son de máxima importancia:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30void imprimir_matriz(int matriz[][MAX_COLUMNAS], size_t filas, size_t columnas) { for (size_t i = 0; i < filas; i++) { for (size_t j = 0; j < columnas; j++) { printf("%4d ", matriz[i][j]); } printf("\n"); } } void inicializar_con_ceros(int matriz[][MAX_COLUMNAS], size_t filas, size_t columnas) { for (size_t i = 0; i < filas; i++) { for (size_t j = 0; j < columnas; j++) { matriz[i][j] = 0; } } } bool son_matrices_iguales(int a[][MAX_COLUMNAS], int b[][MAX_COLUMNAS], size_t filas, size_t columnas) { for (size_t i = 0; i < filas; i++) { for (size_t j = 0; j < columnas; j++) { if (a[i][j] != b[i][j]) { return false; } } } return true; }
Funciones auxiliares para matrices
Apéndice Avanzado: Operaciones Matriciales de Álgebra Lineal¶
Cálculo de Determinantes¶
El determinante es un valor escalar que se puede calcular para toda matriz cuadrada. Este valor encapsula propiedades importantes de la matriz, como la invertibilidad. Se denota como det(A) o |A|.
Definición Matemática¶
Para una matriz de 2x2, el cálculo es directo:
Para matrices de mayor tamaño , un método común es la expansión por cofactores. El determinante se calcula expandiendo a lo largo de una fila o columna. Usando la primera fila, la fórmula es:
Donde:
es el elemento en la primera fila y la columna .
es la matriz menor, que es la submatriz que resulta de eliminar la fila 1 y la columna de A.
El término se conoce como el cofactor del elemento .
Algoritmo Recursivo (Basado en Cofactores)¶
Este método matemático se traduce de forma natural en un algoritmo recursivo. La idea es reducir el problema de un determinante al cálculo de varios determinantes , hasta llegar al caso base de una matriz 2x2.
Costo Computacional
Este algoritmo es conceptualmente claro, pero computacionalmente ineficiente para matrices grandes, con una complejidad de . Para aplicaciones de alto rendimiento, se utilizan otros métodos como la descomposición LU.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36FUNCIÓN calcular_determinante(A, n) // A es una matriz cuadrada de dimensión n x n SI n == 1 ENTONCES RETORNAR A[0][0] FIN SI SI n == 2 ENTONCES RETORNAR (A[0][0] * A[1][1]) - (A[0][1] * A[1][0]) FIN SI determinante_total = 0 PARA j_actual DESDE 0 HASTA n - 1 // 1. Crear la submatriz (menor) M CREAR submatriz M de tamaño (n-1) x (n-1) PARA i DESDE 1 HASTA n - 1 col_sub = 0 PARA j DESDE 0 HASTA n - 1 SI j != j_actual ENTONCES M[i-1][col_sub] = A[i][j] col_sub = col_sub + 1 FIN SI FIN PARA FIN PARA // 2. Calcular el signo del cofactor signo = (-1)^j_actual // o potencia( -1, j_actual) // 3. Suma recursiva sub_determinante = calcular_determinante(M, n-1) determinante_total = determinante_total + (signo * A[0][j_actual] * sub_determinante) FIN PARA RETORNAR determinante_total FIN FUNCIÓN
Algoritmo recursivo para el cálculo del determinante.
Inversión de Matrices¶
La inversa de una matriz cuadrada A, denotada como , es aquella matriz que al multiplicarla por A da como resultado la matriz identidad I.
Condiciones para la Inversión¶
Una matriz es invertible si y solo si cumple dos condiciones:
Es una matriz cuadrada.
Su determinante es distinto de cero. A las matrices con determinante cero se las llama singulares y no tienen inversa.
Método de la Matriz Adjunta¶
Un método para encontrar la inversa se basa en el determinante y la matriz adjunta. La fórmula es:
Donde es la matriz adjunta de A, que se define como la transpuesta de la matriz de cofactores de A.
Algoritmo (Basado en la Adjunta)¶
El algoritmo consiste en seguir los pasos de la fórmula matemática.
Calcular el determinante: Si es cero, la matriz no es invertible.
Calcular la matriz de cofactores: Para cada elemento , su cofactor es .
Calcular la matriz adjunta: Transponer la matriz de cofactores.
Obtener la inversa: Multiplicar la matriz adjunta por el escalar .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41FUNCIÓN invertir_matriz(A, n) // 1. Calcular determinante determinante = calcular_determinante(A, n) SI determinante == 0 ENTONCES RETORNAR ERROR "La matriz es singular y no se puede invertir." FIN SI // 2. Calcular la matriz de cofactores CREAR matriz_cofactores de tamaño n x n PARA i DESDE 0 HASTA n - 1 PARA j DESDE 0 HASTA n - 1 // a. Crear la submatriz menor M(i,j) CREAR submatriz M de (n-1) x (n-1) omitiendo fila i y columna j de A // b. Calcular el signo y el determinante del menor signo = (-1)^(i+j) det_menor = calcular_determinante(M, n-1) matriz_cofactores[i][j] = signo * det_menor FIN PARA FIN PARA // 3. Calcular la matriz adjunta (transpuesta de la de cofactores) CREAR matriz_adjunta de tamaño n x n PARA i DESDE 0 HASTA n - 1 PARA j DESDE 0 HASTA n - 1 matriz_adjunta[j][i] = matriz_cofactores[i][j] FIN PARA FIN PARA // 4. Calcular la inversa dividiendo la adjunta por el determinante CREAR matriz_inversa de tamaño n x n factor_inversion = 1.0 / determinante PARA i DESDE 0 HASTA n - 1 PARA j DESDE 0 HASTA n - 1 matriz_inversa[i][j] = matriz_adjunta[i][j] * factor_inversion FIN PARA FIN PARA RETORNAR matriz_inversa FIN FUNCIÓN
Algoritmo para la inversión de una matriz A.
Matrices Dinámicas en el Heap¶
Cuando las dimensiones de una matriz no se conocen en tiempo de compilación y no
querés incurrir en el riesgo de usar Arreglos de Longitud Variable (ALV/VLA) en
la pila (violando la regla Regla 0x5001h: Los arreglos estáticos deben ser creados con un tamaño fijo en tiempo de compilación), debés recurrir a la asignación de
memoria dinámica en el Heap.
En C, existen dos formas de modelar matrices dinámicas:
1. Modelo de Bloque Único Contiguo (Recomendado para rendimiento)¶
Consiste en alocar un único bloque unidimensional continuo en el Heap que
contenga todos los elementos de la matriz (). Luego, se calcula el
desplazamiento manualmente para indexar los elementos: matriz[i * columnas + j].
Ventajas:
Localidad espacial máxima: Todos los elementos son físicamente contiguos en memoria, lo que optimiza el uso de la memoria caché y reduce drásticamente los fallos de caché (cache misses).
Menor sobrecarga (overhead): Solo realizás una llamada a
malloc/calloc, lo que reduce el costo de metadatos en el Heap y acelera la liberación.Evita la fragmentación: No fragmenta el Heap con múltiples pequeñas asignaciones.
Desventajas:
La sintaxis de indexación es manual (
matriz[i * columnas + j]) y puede ser menos intuitiva quematriz[i][j].
Ejemplo de implementación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30#include <stdio.h> #include <stdlib.h> int *crear_matriz_contigua(size_t filas, size_t columnas) { if (filas == 0 || columnas == 0) { return NULL; } // Alocación de un único bloque físico contiguo int *matriz = malloc(filas * columnas * sizeof(*matriz)); if (matriz == NULL) { perror("Error al asignar memoria para la matriz contigua"); return NULL; } // Inicialización a cero for (size_t i = 0; i < filas * columnas; i++) { matriz[i] = 0; } return matriz; } void destruir_matriz_contigua(int **matriz) { if (matriz == NULL || *matriz == NULL) { return; } free(*matriz); *matriz = NULL; // Aniquilación del puntero post-free }
2. Modelo de Arreglo de Punteros (Matriz Deshilachada o Jagged Matrix)¶
Consiste en alocar un arreglo de punteros (de tamaño ) donde cada elemento
del arreglo apunta a una fila alocada de forma independiente en el Heap (de
tamaño ). Esto permite la sintaxis nativa matriz[i][j].
Ventajas:
Sintaxis intuitiva idéntica a las matrices estáticas:
matriz[i][j].
Desventajas:
Pérdida de localidad espacial: Cada fila puede estar alocada en cualquier parte del Heap, lo que rompe la contigüidad física e incrementa los fallos de caché.
Fragmentación física: Se realizan llamadas a alocadores, lo que introduce un alto overhead de metadatos en el Heap.
Complejidad de liberación: Se requiere un lazo para liberar cada fila individualmente antes de liberar el arreglo de punteros.
Ejemplo de implementación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48#include <stdio.h> #include <stdlib.h> int **crear_matriz_punteros(size_t filas, size_t columnas) { if (filas == 0 || columnas == 0) { return NULL; } // Asignación del arreglo de punteros a filas int **matriz = malloc(filas * sizeof(*matriz)); if (matriz == NULL) { perror("Error al asignar el arreglo de filas"); return NULL; } // Asignación individual de cada fila for (size_t i = 0; i < filas; i++) { matriz[i] = calloc(columnas, sizeof(*(matriz[i]))); if (matriz[i] == NULL) { // Lazo de liberación en caso de fallo intermedio for (size_t j = 0; j < i; j++) { free(matriz[j]); matriz[j] = NULL; } free(matriz); matriz = NULL; return NULL; } } return matriz; } void destruir_matriz_punteros(int ***matriz, size_t filas) { if (matriz == NULL || *matriz == NULL) { return; } int **m = *matriz; for (size_t i = 0; i < filas; i++) { if (m[i] != NULL) { free(m[i]); m[i] = NULL; } } free(m); *matriz = NULL; // Aniquilación del puntero a nivel de cliente }
Ejercicios de Autoevaluación¶
Definición y Declaración¶
Solution to Exercise 1
La declaración correspondiente es:
float temperaturas[5][10];El tamaño total en bytes se calcula multiplicando el número de filas por el de columnas por el tamaño en bytes del tipo básico:
Solution to Exercise 2
El criterio Row-Major Order (orden de fila principal) dispone las filas una
detrás de otra en la memoria contigua. La secuencia física en RAM será:
[10, 20, 30, 40, 50, 60]
Físicamente en memoria, el elemento M[0][2] (30) es inmediatamente adyacente
a M[1][0] (40).
Solution to Exercise 3
La declaración del arreglo tridimensional es:
int sensores_3d[3][4][5];El número total de celdas de almacenamiento entero reservadas en memoria se calcula como el producto de todas sus dimensiones:
Inicialización y Acceso¶
Solution to Exercise 4
Es inválida porque la segunda dimensión (columnas) no está especificada.
En C, para calcular la dirección física del elemento M[i][j], el compilador
requiere saber de forma exacta cuántos elementos contiene cada fila
(). Sin esta dimensión, el compilador es incapaz de
computar la fórmula de direccionamiento en memoria contigua, provocando un error de traducción. La
primera dimensión es la única que puede ser implícita.
Solution to Exercise 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17#include <stdio.h> #define N 4 int main() { int identidad[N][N]; for (size_t i = 0; i < N; i++) { for (size_t j = 0; j < N; j++) { if (i == j) { identidad[i][j] = 1; } else { identidad[i][j] = 0; } } } return 0; }
Solution to Exercise 6
Debido al uso de índices de base cero en C, los rangos válidos son:
Para filas: de
0aFILAS - 1.Para columnas: de
0aCOLUMNAS - 1. El índicematriz[FILAS][COLUMNAS]apunta a un elemento situado completamente fuera de la memoria reservada para el arreglo (específicamente, es la dirección adyacente a la primera posición de la fila posterior inexistente). Leer o escribir en esta dirección provoca un comportamiento indefinido, el cual puede resultar en corrupción de variables adyacentes en el stack o en un fallo de segmentación (Segmentation Fault).
Recorridos y Memoria¶
Solution to Exercise 7
La diagonal secundaria de una matriz cuadrada de orden cumple que la suma de sus índices de fila y columna es igual a . Por lo tanto, .
1 2 3 4 5 6 7 8 9 10 11#include <stddef.h> #define N 4 int sumar_diagonal_secundaria(const int matriz[N][N]) { int suma = 0; for (size_t i = 0; i < N; i++) { // Acceso directo a la diagonal secundaria suma += matriz[i][N - 1 - i]; } return suma; }
Solution to Exercise 8
En C, las matrices se disponen linealmente en memoria por filas.
Recorrido por filas: Accede a elementos secuenciales que se encuentran de forma adyacente en memoria física. El hardware precarga estos bloques en la rápida memoria caché (localidad espacial), resultando en aciertos de caché (cache hits).
Recorrido por columnas: Provoca saltos en memoria equivalentes al tamaño de una fila entera en cada iteración. Esto invalida constantemente los bloques cargados en caché, forzando a la CPU a buscar los datos en la memoria RAM principal lenta (fallos de caché o cache misses), ralentizando el procesamiento.
Solution to Exercise 9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16#define FILAS 4 #define COLUMNAS 5 int M[FILAS][COLUMNAS]; // Recorrido de los bordes periféricos for (size_t i = 0; i < FILAS; i++) { for (size_t j = 0; j < COLUMNAS; j++) { // Si pertenece a la primera o última fila, o a la primera o última columna if (i == 0 || i == FILAS - 1 || j == 0 || j == COLUMNAS - 1) { printf("%d\t", M[i][j]); } else { printf("\t"); // Espacio para el interior vacío } } printf("\n"); }
Funciones y Operaciones¶
Solution to Exercise 10
Para que una matriz sea simétrica, debe ser necesariamente cuadrada (filas == columnas).
1 2 3 4 5 6 7 8 9 10 11 12 13 14#include <stdbool.h> #include <stddef.h> bool es_matriz_simetrica(size_t n, const int matriz[n][n]) { for (size_t i = 0; i < n; i++) { for (size_t j = i + 1; j < n; j++) { // Solo verificamos el triángulo superior con el inferior if (matriz[i][j] != matriz[j][i]) { return false; } } } return true; }
Solution to Exercise 11
Solution to Exercise 12
1 2 3 4 5 6 7 8 9 10 11 12#include <stddef.h> void transponer_matriz(size_t filas_a, size_t cols_a, const int A[filas_a][cols_a], int B[cols_a][filas_a]) { for (size_t i = 0; i < filas_a; i++) { for (size_t j = 0; j < cols_a; j++) { // El elemento A[i][j] se copia en B[j][i] B[j][i] = A[i][j]; } } }
Glosario¶
- memoria caché
Una memoria caché (del francés cacher, “esconder”) es un componente de hardware o software que almacena datos para que las futuras solicitudes de esos datos puedan ser atendidas más rápidamente. Se trata de una memoria auxiliar, de alta velocidad y menor capacidad, situada entre la unidad central de procesamiento (CPU) y la memoria de acceso aleatorio (RAM).
El objetivo principal de una caché es acelerar el acceso a los datos que se utilizan con mayor frecuencia. Cuando la CPU necesita leer o escribir datos, primero busca en la caché. Si los datos se encuentran allí (lo que se conoce como un acierto de caché o cache hit), se accede a ellos de forma casi inmediata, evitando el acceso mucho más lento a la memoria principal. Si los datos no están en la caché (fallo de caché o cache miss), se deben recuperar de la RAM y, por lo general, se copian en la caché para futuros accesos.
Existen diferentes niveles de caché (L1, L2, L3), que se diferencian por su tamaño, velocidad y proximidad a los núcleos de la CPU. La caché L1 es la más pequeña y rápida, mientras que la L3 es la más grande y lenta de las tres.
¿Pero por que no todo es memoria caché? La relación costo capacidad. Las memorias mas cercanas al procesador y las mas rápidas, son las mas caras, tengan en cuenta que una computadora moderna tiene algunos kilobytes de memoria L1 y unos pocos megabytes en L3.
- localidad espacial
Un principio fundamental en el diseño de sistemas de memoria que establece que si un programa accede a una ubicación de memoria, es muy probable que también acceda a ubicaciones cercanas en un futuro próximo. Este principio es especialmente relevante para las matrices almacenadas en row-major order, donde los elementos de una fila son adyacentes en memoria. Al acceder secuencialmente por filas, se aprovecha esta localidad y se optimiza el uso de la caché.
- localidad temporal
Principio que indica que si un programa accede a una ubicación de memoria, es probable que vuelva a acceder a la misma ubicación en un futuro cercano. En el contexto de matrices, esto se aprovecha cuando se realizan múltiples operaciones sobre los mismos elementos o cuando se recorren matrices varias veces con diferentes propósitos.
- row-major order
Método de almacenamiento de matrices en memoria donde los elementos se disponen fila por fila de forma consecutiva. En una matriz de 3×4, los elementos se almacenan como: [matriz[0][0], matriz[0][1], matriz[0][2], matriz[0][3], matriz[1][0], ...]. Este es el orden usado por C, C++, Python (NumPy) y Java, entre otros.
- column-major order
Método alternativo de almacenamiento donde los elementos se almacenan columna por columna. Usado por lenguajes como Fortran y MATLAB. En una matriz de 3×4, el orden sería: [matriz[0][0], matriz[1][0], matriz[2][0], matriz[0][1], ...]. Es importante conocer este concepto al interoperar con código de otros lenguajes.
- matriz singular
Una matriz cuadrada cuyo determinante es igual a cero. Las matrices singulares no tienen inversa y representan transformaciones que “colapsan” el espacio, reduciendo su dimensionalidad. En términos geométricos, una matriz singular proyecta vectores de dimensión n en un subespacio de menor dimensión.
- determinante
Un valor escalar que se puede calcular para cualquier matriz cuadrada. Proporciona información importante sobre las propiedades de la matriz: si es cero, la matriz es singular; si es positivo o negativo, indica orientación; y su magnitud representa el factor de escalamiento del volumen en transformaciones lineales.
- matriz identidad
Una matriz cuadrada especial donde todos los elementos de la diagonal principal son 1 y todos los demás elementos son 0. Actúa como el elemento neutro en la multiplicación de matrices: A × I = I × A = A. Es fundamental en operaciones como la inversión de matrices.
Síntesis y Resumen¶
Las matrices en C se almacenan de forma contigua en memoria siguiendo un orden de fila mayor (row-major). Recorrer una matriz fila por fila maximiza el uso del caché de la CPU, mientras que hacerlo por columnas degrada el rendimiento. Al pasar matrices a funciones, es necesario indicar todas las dimensiones (excepto opcionalmente la primera) para permitir que el compilador calcule el desplazamiento físico correcto. Se deben validar rigurosamente los límites de los índices para evitar accesos fuera de rango.
Referencias y Lecturas Complementarias¶
Kernighan & Ritchie (2014). Capítulo 5: Pointers and Arrays.
King (2008). Capítulo 8: Arrays.
- Kernighan, B. W., & Ritchie, D. M. (2014). C Programming Language, 2nd Edition.
- King, K. N. (2008). C Programming: A Modern Approach (2nd ed.). W. W. Norton & Company.