wandres.dev
ADEPTO · Arrays y strings

Arrays y strings

El decaimiento de array a puntero, la aritmética que subyace a los índices, los arrays de longitud variable, y por qué las cadenas de C son una fuente eterna de bugs.

⏱ 15 min

Arrays y strings parecen sencillos hasta que descubres que un array “se convierte” en puntero casi siempre, que no conoce su propio tamaño, y que las cadenas de C son solo bytes con un cero al final. Estos detalles son la causa de más vulnerabilidades que ningún otro tema en C.

🎯 Al terminar esta lección sabrás
  • Arrays y su relación con los punteros.
  • El decaimiento a puntero.
  • Cadenas: char arrays y el terminador nulo.
  • Los peligros clásicos de los strings de C.

Arrays: elementos contiguos

int nums[5] = {10, 20, 30, 40, 50};
nums[0];                // 10 — primer elemento (índice desde 0)
nums[4];                // 50 — último
int n = sizeof(nums) / sizeof(nums[0]);  // 5 — nº de elementos

nums[i] es, literalmente, *(nums + i): aritmética de punteros del nivel anterior. De hecho, i[nums] también compila (¡pero no lo hagas!), porque la suma es conmutativa.

El decaimiento a puntero

Aquí está el concepto clave y el que causa más confusión: en casi todos los contextos, un array decae a un puntero a su primer elemento. Pierde la información de su tamaño.

void procesar(int arr[]) {          // ¡mentira! esto es int *arr
    int n = sizeof(arr);            // ❌ NO es el tamaño del array
                                    //    es sizeof(int*) = 8, ¡el del puntero!
}

int datos[100];
procesar(datos);                    // datos decae a int*: se pierde el "100"
Un array no sabe cuánto mide (cuando lo pasas)

Este es el origen de innumerables bugs y agujeros de seguridad en C. Cuando pasas un array a una función, no pasas el array: pasas un puntero a su primer elemento, y el tamaño se queda atrás. La función no tiene forma de saber cuántos elementos hay. Por eso la convención universal en C es pasar siempre el tamaño junto al puntero: void procesar(int *arr, size_t n). Todas las funciones de la librería que manejan buffers lo hacen así. Interiorízalo: en C, un puntero a datos casi siempre necesita ir acompañado de su longitud, porque el puntero solo dice dónde empieza, nunca cuánto dura.

Cadenas: bytes con un cero al final

C no tiene un tipo string. Una cadena es un array de char terminado en un byte nulo \0:

char saludo[] = "Hola";   // en memoria: 'H' 'o' 'l' 'a' '\0' → 5 bytes

Ese \0 invisible es cómo las funciones saben dónde termina la cadena. Contar caracteres, copiar, comparar: todo se basa en buscar el terminador.

#include <string.h>
strlen("Hola");           // 4 (no cuenta el \0)
strcmp("a", "b");         // negativo (a < b)

Los peligros de los strings

Las funciones clásicas de cadenas no comprueban tamaños. Si el destino es más pequeño que el origen, escriben fuera — el clásico buffer overflow:

char dest[4];
strcpy(dest, "demasiado largo");   // 💥 desbordamiento: corrompe memoria
🛑
Buffer overflows: la vulnerabilidad más antigua y letal

La familia strcpy/strcat/sprintf sin límite de tamaño ha causado décadas de vulnerabilidades de seguridad graves — desde el gusano de Morris (1988) hasta exploits modernos. Escriben hasta encontrar un \0, sin importar cuánto espacio haya en el destino. La defensa: usa las variantes que reciben un tamaño máximo (snprintf, y con cuidado strncpy/strncat), valida siempre las longitudes, y en el nivel 24 los sanitizers cazarán los desbordamientos que se te escapen. En sistemas, cada buffer que manejas es una superficie de ataque potencial: trátalo con respeto.

Arrays de longitud variable (VLA)

C permite arrays cuyo tamaño se decide en tiempo de ejecución:

void f(int n) {
    int temp[n];   // VLA: tamaño según n
}

Son cómodos pero arriesgados: viven en el stack, y un n grande o negativo puede desbordarlo. En código de sistemas robusto suelen evitarse en favor de memoria dinámica (nivel 13). C23 los mantiene, pero úsalos con tamaños pequeños y controlados.

⚔️ Maneja arrays y cadenas con cuidado
  1. Calcula el número de elementos de un array con sizeof(arr)/sizeof(arr[0]).
  2. Pasa un array a una función e imprime sizeof dentro: comprueba que es el del puntero.
  3. Reescribe esa función pasando también el tamaño (int *arr, size_t n).
  4. Provoca un buffer overflow con strcpy en un buffer pequeño (bajo un sanitizer) y luego arréglalo con snprintf.