Arrays de longitud variable (VLA) en C99 y C11

Un array de longitud variable (VLA, por sus siglas en inglés) es un tipo de array cuyo tamaño no se define de forma constante durante la compilación, sino que se determina en tiempo de ejecución mediante una expresión entera. A diferencia de los arrays estáticos tradicionales, un VLA se reserva en el stack (pila) del proceso, lo que permite gestionar buffers de tamaño dinámico sin recurrir necesariamente al heap (montículo).

Esta funcionalidad fue introducida en C99 como una característica obligatoria del lenguaje. Sin embargo, en C11 el estándar cambió su postura y los VLA pasaron a ser opcionales, permitiendo que algunas implementaciones de sistemas embebidos con recursos extremadamente limitados puedan prescindir de ellos (puedes verificarlo mediante la macro __STDC_NO_VLA__).

¿Por qué esto es útil? Porque te permite crear buffers temporales en la pila que se adaptan al tamaño de los datos de entrada, evitando la sobrecarga de gestión de memoria que implica llamar a malloc y free. Sin embargo, no es una solución mágica: solo debes usarlo para tamaños moderados y predecibles; si el usuario introduce un valor masivo, provocarás un stack overflow que colapsará el programa, ya que la pila es mucho más pequeña y está menos protegida que el heap. Además, si intentas usar un VLA en un contexto donde el compilador requiere tamaños conocidos de antemano, como en variables static o globales, el código simplemente no compilará.

#include <stdio.h>
#include <stdlib.h>

/* 
 * En C, para pasar un array multidimensional a una función, 
 * es crucial comunicar la dimensión de la segunda dimensión 
 * (el número de columnas) para que el compilador sepa cómo 
 * calcular el desplazamiento (stride) en memoria.
 */
void procesar_matriz(int filas, int cols, int matriz[filas][cols]) {
    if (filas <= 0 || cols <= 0) return;

    long suma = 0;
    for (int i = 0; i < filas; i++) {
        for (int j = 0; j < cols; j++) {
            // El compilador usa 'cols' para calcular la dirección:
            // &matriz[i][j] = base + (i * cols + j) * sizeof(int)
            suma += matriz[i][j];
        }
    }

    double promedio = (double)suma / (filas * cols);
    printf("Matriz de %dx%d procesada. Promedio: %.2f\n", filas, cols, promedio);
}

int main(void) {
    int n, m;

    printf("Introduce dimensiones (filas columnas): ");
    if (scanf("%d %d", &n, &m) != 2 || n <= 0 || m <= 0) {
        fprintf(stderr, "Dimensiones no válidas.\n");
        return 1;
    }

    /* 
     * Declaración de un VLA en el scope local.
     * El tamaño se reserva en el stack en el momento en que 
     * se evalúan 'n' y 'm'.
     */
    int matriz[n][m];

    // Inicializamos el VLA.
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            matriz[i][j] = i + j;
        }
    }

    /*
     * Para un VLA, el operador sizeof() no se calcula en 
     * tiempo de compilación, sino en tiempo de ejecución.
     */
    printf("Tamaño total del array en bytes: %zu\n", sizeof(matriz));

    procesar_matriz(n, m, matriz);

    return 0;
}

Análisis del código

Fíjate en la función procesar_matriz. El parámetro int matriz[filas][cols] no está creando un nuevo array local; es una forma de indicarle al compilador que el puntero que recibirá la función tiene una estructura de dimensiones conocidas mediante los parámetros filas y cols. Esto es vital para la aritmética de punteros: sin la información de cols, el compilador no sabría que para pasar a la siguiente fila debe saltar cols * sizeof(int) bytes.

En el main, cuando declaramos int matriz[n][m], estamos instruyendo al compilador para que ajuste el stack frame de la función main. Si n o m son valores extremadamente grandes (por ejemplo, $10^6$), el programa fallará con un segmentation fault antes de siquiera empezar a ejecutar el bucle, porque hemos intentado asignar más memoria de la que el sistema permite para la pila.

Un detalle técnico importante es el comportamiento de sizeof(matriz). En un array estático como int arr[10], el compilador traduce sizeof(arr) a una constante inmediata en el ensamblador. Con un VLA, el compilador debe generar instrucciones adicionales para multiplicar n * m * sizeof(int) en tiempo de ejecución, lo que añade una mínima carga computacional.

El error frecuente

Un error muy común al transicionar de lenguajes de alto nivel es intentar inicializar un VLA en su declaración. El estándar prohíbe esto:

int n = 5;
int arr[n] = {0}; // ERROR: Prohibido por el estándar

Esto falla porque la inicialización de arrays requiere que el compilador conozca el tamaño para determinar la disposición de los datos en el segmento de memoria durante la fase de compilación. Si necesitas limpiar la memoria de un VLA, debes hacerlo manualmente mediante memset o mediante un bucle for. Ten en cuenta que herramientas como AddressSanitizer detectarán errores de acceso si intentas usar un VLA con un índice que exceda los límites calculados dinámicamente, lo cual es un riesgo latente al trabajar con tamaños controlados por el usuario.

55

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio