Para entender cómo se gestiona la memoria en C, no basta con saber dónde se guardan los bytes; debes distinguir entre dos conceptos que a menudo se confunden: el ámbito (scope) y la duración de almacenamiento (storage duration). El ámbito es una propiedad de los identificadores: determina qué partes del código pueden “ver” una variable por su nombre. La duración de almacenamiento, en cambio, es una propiedad de los objetos: define cuánto tiempo permanece ese objeto ocupando un lugar en la memoria.
Si confundes ambos, el compilador te dará errores de sintaxis (si intentas usar un nombre fuera de su ámbito) o, lo que es peor, el programa compilará pero fallará de forma catastrófica en tiempo de ejecución si intentas acceder a un objeto cuya vida ha terminado. Esta separación es lo que permite a C ser tan eficiente: el compilador puede decidir qué datos vivirán en la pila (stack) de forma automática y qué datos deben residir en el segmento de datos para persistir durante toda la ejecución.
¿Qué es esto exactamente? Es la dualidad entre la visibilidad de un nombre y la existencia física de un dato. ¿Por qué se diseñó así? Para separar la lógica de resolución de nombres (compilación) de la gestión de recursos (tiempo de ejecución). ¿Cuándo deberías usar cada una? Usa duración automática para datos temporales de funciones, estática para mantener estados sin usar el heap, y dinámica para datos cuyo tamaño o vida dependen de la lógica del programa. ¿Qué rompe un mal uso? El uso de punteros a memoria que ya ha sido liberada o que ha salido de su ámbito de la pila.
#include <stdio.h>
#include <stdlib.h>
#include <threads.h> // [POSIX/C11] para _Thread_local
/* Ámbito de archivo y duración estática: visible en todo el archivo,
existe durante toda la vida del programa. */
int g_contador_global = 0;
void demostrar_ciclos_vida() {
/* Ámbito de bloque y duración estática: la variable es visible
solo aquí, pero su valor persiste entre llamadas a la función. */
static int s_contador_interno = 0;
/* Ámbito de bloque y duración automática: vive en la pila (stack).
Se crea al entrar y se destruye al salir. */
int variable_temporal = 0;
/* Ámbito de bloque y duración dinámica: gestionada manualmente.
Su vida depende de malloc/free. */
int *p_dinamico = malloc(sizeof(int));
if (p_dinamico == NULL) {
perror("Fallo en malloc");
exit(EXIT_FAILURE);
}
*p_dinamico = 42;
s_contador_interno++;
variable_temporal++;
g_contador_global++;
printf("[Func] Global: %d, Static: %d, Auto: %d, Dynamic: %d\n",
g_contador_global, s_contador_interno, variable_temporal, *p_dinamico);
free(p_dinamico);
}
void demostracion_shadowing() {
int local_var = 10;
printf("Antes del shadowing: %d\n", local_var);
{
/* Shadowing: esta variable 'local_var' oculta a la de la función
superior. Solo la interna es visible en este bloque. */
int local_var = 20;
printf("Dentro del bloque (shadowing): %d\n", local_var);
}
printf("Después del bloque: %d\n", local_var);
}
int main(void) {
/* C11: _Thread_local asegura que cada hilo tenga su propia instancia. */
_Thread_local int t_id = 0;
t_id++;
printf("--- Inicio de demostración ---\n");
demostrar_ciclos_vida();
demostrar_ciclos_vida(); // El static mantendrá su valor
demostración_shadowing();
printf("--- Fin de demostración ---\n");
return 0;
}
Análisis del ejemplo
Fíjate en cómo g_contador_global se comporta: al estar declarada fuera de cualquier función, tiene ámbito de archivo y su duración es estática, lo que significa que reside en el segmento de datos del binario y su valor se mantiene mientras el proceso viva.
En la función demostrar_ciclos_vida, observa la diferencia crucial entre variable_temporal y s_contador_interno. Aunque ambas son visibles solo dentro de las llaves {} de la función, variable_temporal es automática (se asigna un espacio en la pila que se libera al ejecutar el }), mientras que s_contador_interno es estática, por lo que su valor se mantiene entre la primera y la segunda llamada a la función, permitiendo llevar un conteo de llamadas.
Cuando llamamos a malloc para p_dinamico, estamos solicitando duración dinámica. El nombre p_dinamico es una variable local (automática) que contiene una dirección, pero el objeto al que apunta vive en el heap hasta que ejecutamos explícitamente free.
En demostración_shadowing, vemos el fenómeno de shadowing. Al declarar int local_var dentro de un bloque anidado, el compilador prioriza la declaración más cercana al punto de uso. La variable original no ha muerto, simplemente se ha vuelto “invisible” para el compilador mientras estemos dentro de ese bloque.
Finalmente, t_id utiliza _Thread_local [C11]. Su duración es similar a la estática (vive durante la vida del hilo), pero su ámbito es específico para cada hilo de ejecución, evitando condiciones de carrera sin necesidad de mutex en este caso particular.
El error frecuente
Un error clásico que produce un puntero colgante (dangling pointer) es intentar devolver una variable con duración automática.
int* obtener_error() {
int x = 100; // Duración automática (stack)
return &x; // UB: la dirección de x es inválida al salir de la función
}
int main() {
int *ptr = obtener_error();
// El valor de *ptr es impredecible (Undefined Behavior)
// AddressSanitizer detectaría este error inmediatamente.
printf("%d\n", *ptr);
return 0;
}
En este caso, cuando obtener_error retorna, el marco de la pila (stack frame) de la función se marca como disponible. El puntero ptr en main contiene una dirección de memoria que ya no pertenece a ninguna variable válida. Si intentas acceder a ella, podrías leer basura o provocar un segmentation fault. Herramientas como Valgrind o AddressSanitizer son esenciales para detectar estos errores de vida de objeto que el compilador, por diseño, no siempre puede evitar.
N° 21