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.
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.
- 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"
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
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.
- Calcula el número de elementos de un array con
sizeof(arr)/sizeof(arr[0]). - Pasa un array a una función e imprime
sizeofdentro: comprueba que es el del puntero. - Reescribe esa función pasando también el tamaño (
int *arr, size_t n). - Provoca un buffer overflow con
strcpyen un buffer pequeño (bajo un sanitizer) y luego arréglalo consnprintf.