Dos preguntas sobre cada variable
Cada variable en C tiene dos propiedades distintas, y confundirlas es el origen de casi todas las sorpresas de esta página:
- Ámbito: dónde se puede usar el nombre. Una cuestión de tiempo de compilación.
- Tiempo de vida: cuánto existe el almacenamiento. Una cuestión de tiempo de ejecución.
Normalmente van juntos (una local corriente es visible en su bloque y vive exactamente lo mismo), pero static los desacopla, y por eso es la palabra clave que vale la pena entender aquí.
Ámbito de bloque
Una variable declarada dentro de llaves es visible desde su declaración hasta la llave de cierre, y en ningún otro sitio.
El bloque interno puede ver outer, porque está anidado dentro de él. El bloque externo no puede ver inner. Esto aplica a todo par de llaves: cuerpos de funciones, cuerpos de if, cuerpos de bucles o un bloque suelto como el de arriba.
Los bucles lo hacen concreto. Un contador declarado en un encabezado for pertenece al bucle:
for (int i = 0; i < 5; i++) {
/* i vive aqui */
}
/* i no existe aqui */
Que es exactamente lo que quieres: dos bucles en la misma función pueden usar cada uno i sin interferencia, y el contador no se puede leer por accidente después del bucle. Si necesitas el valor después —el índice donde se detuvo una búsqueda— decláralo antes del bucle.
Los parámetros de una función tienen el ámbito del cuerpo de la función, así que se comportan como locales declaradas al inicio de él.
C99 permite una declaración en cualquier punto de un bloque, no solo al principio. Declara las variables donde las necesites por primera vez; una variable con un ámbito corto es una variable con menos formas de estar mal.
Tiempo de vida: almacenamiento automático
Una local corriente tiene almacenamiento automático: nace cuando el control entra en su bloque y se destruye cuando el control sale. Su memoria vive en la pila.
Imprime 1 tres veces. Cada llamada recibe un count flamante, inicializado a 0, y descartado al retornar. Es también por esto que cada marco de una función recursiva tiene su propia copia de cada local.
Dos consecuencias que conviene decir claramente. Una local sin inicializar contiene basura, no cero: los bytes que hubiera en la pila. Y devolver un puntero a una local es comportamiento indefinido, porque el almacenamiento desaparece en el momento en que la función retorna:
/* BUG: buf no existe despues del return */
char *broken(void) {
char buf[64] = "hello";
return buf;
}
Locales static: memoria entre llamadas
Ponle static a una local y su tiempo de vida pasa a ser el programa entero, mientras que su ámbito queda exactamente igual.
Ahora el contador imprime 1, 2, 3. El = 0 se ejecuta una vez, antes de que main empiece, no en cada llamada.
Tres reglas para las locales static:
- El inicializador debe ser una expresión constante, porque se aplica al arrancar el programa, no en tiempo de ejecución.
- Sin inicializador, una
staticse inicializa a cero (a diferencia de una local automática, que contiene basura). - La variable sigue siendo privada de la función. Ningún otro código puede nombrarla.
Ese último punto es todo el atractivo: una función puede recordar algo entre llamadas sin exponer una global que cualquier cosa pudiera modificar. Úsalo para contadores de llamadas, tablas de consulta cacheadas que se construyen en el primer uso y banderas de inicialización única.
El costo es que la función deja de ser una correspondencia pura de entrada a salida —los mismos argumentos pueden dar respuestas distintas— lo que la hace más difícil de probar e insegura de llamar desde varios hilos a la vez sin protección. Usa las locales static deliberadamente, no por comodidad.
Ámbito de archivo: las globales
Una variable declarada fuera de toda función tiene ámbito de archivo. Es visible desde su declaración hasta el final del archivo, en cada función por debajo de ella, y su tiempo de vida es el programa entero.
Las globales se inicializan a cero por defecto, así que totalOperations empieza en 0 incluso sin el = 0.
Por defecto una global tiene además enlace externo: otros archivos .c del mismo programa pueden alcanzarla declarándola extern:
/* en stats.c */
int totalOperations = 0; /* la definicion: exactamente una en el programa */
/* en main.c */
extern int totalOperations; /* una declaracion: "existe en alguna parte" */
Pon la declaración extern en un archivo de cabecera para que todos los usuarios vean la misma. Fíjate en la asimetría: la declaración extern dice que la variable existe, y exactamente un archivo .c debe definirla de verdad.
Por qué muerden las globales
Las globales son la forma más fácil de compartir datos y la forma más fácil de crear errores que no puedes localizar. Cuatro problemas concretos:
Cualquier cosa puede cambiarlas. Cuando totalOperations tiene un valor equivocado, el culpable es cualquier línea de cualquier archivo. Con un parámetro, los sospechosos son los puntos de llamada que puedes ver.
Las funciones se vuelven imposibles de probar. Una función que lee una global no se puede llamar de forma aislada; tienes que preparar el estado global primero y acordarte de restablecerlo después.
El nombre abarca todo el programa. Una global llamada count o buffer acabará chocando con la de alguien más.
Sus inicializadores deben ser constantes en tiempo de compilación. En C, una global solo se puede inicializar con una expresión constante: int limit = readConfig(); no compila, y int b = a * 2; en el ámbito de archivo tampoco. Cualquier cosa calculada tiene que asignarse en tiempo de ejecución desde alguna función de preparación, y todo archivo que toque la global antes de que esa llamada se ejecute ve un 0 silencioso.
Las alternativas casi siempre están disponibles: pasa valores como parámetros, devuelve resultados y agrupa el estado relacionado en una estructura que se pase explícitamente. Existen excepciones genuinas —un objeto de configuración de todo el programa, un manejador de registro— e incluso esas quedan mejor como static en el ámbito de archivo con funciones de acceso, que es la siguiente sección.
static en el ámbito de archivo: privacidad
Sobre una variable global o una función, static significa algo completamente distinto de lo que significa sobre una local: enlace interno. El nombre se vuelve privado de su propio archivo .c y no puede alcanzarse desde ningún otro.
/* counter.c */
#include "counter.h"
static int count = 0; /* privado de este archivo: ningun otro puede tocarlo */
static void validate(void) { /* un auxiliar privado, fuera de la interfaz */
if (count < 0) count = 0;
}
void increment(void) { /* publico: declarado en counter.h */
count++;
validate();
}
int getCount(void) { /* publico */
return count;
}
Otro archivo puede llamar a increment y getCount, pero no puede ver count ni llamar a validate: el enlazador no resolverá esos nombres. Ese es el sistema de módulos de C, tal cual: una cabecera que declara las funciones públicas y static en todo lo demás.
Dos beneficios prácticos más allá del orden. Dos archivos pueden tener cada uno una static void validate(void) sin colisión, cosa imposible si las funciones fueran públicas. Y el compilador sabe que una función static no tiene llamadas fuera del archivo, así que puede integrarla de forma más agresiva o avisar de que no se usa.
Así que la palabra clave tiene dos significados distintos, decididos por dónde aparece:
static en una variable LOCAL -> el tiempo de vida pasa a ser permanente (ambito igual)
static en el AMBITO DE ARCHIVO -> el enlace pasa a ser interno (tiempo de vida igual)
Ocultamiento (shadowing)
Declarar en un ámbito interno un nombre que ya existe en uno externo oculta el nombre externo: dentro del ámbito interno, el nombre se refiere a la variable nueva y la externa queda inalcanzable.
Esto es legal y, de vez en cuando, intencionado. Más a menudo es un accidente, y uno costoso: una función pensada para actualizar una global actualiza en cambio una local con el mismo nombre, y la global nunca cambia. Lo mismo pasa cuando una local oculta a un parámetro, así que la asignación que querías hacerle a la entrada no va a ninguna parte.
gcc -Wshadow reporta cada caso. No está en -Wall, así que actívala explícitamente:
gcc -Wall -Wextra -Wshadow program.c -o program
El hábito que evita el problema por completo es nombrar por el papel y no por el tipo: totalScore e itemScore no pueden ocultarse entre sí, mientras que dos variables llamadas ambas n acabarán haciéndolo.
Referencia rápida
declarada dentro de un bloque ambito de bloque, vida automatica, basura si no se inicializa
parametro de funcion ambito del cuerpo de la funcion, una copia del argumento
static dentro de una funcion ambito de bloque, vida del PROGRAMA, a cero, se conserva entre llamadas
declarada fuera de toda funcion ambito de archivo, vida del programa, a cero, visible a otros archivos
static fuera de toda funcion ambito de archivo, vida del programa, PRIVADA de este archivo .c
declaracion extern nombra una variable definida en otro archivo
Preguntas frecuentes
¿Qué es el ámbito en C?
La región de código donde un nombre es visible. Una variable declarada dentro de un bloque (un par de llaves) es visible solo en ese bloque: eso es el ámbito de bloque. Una declarada fuera de toda función tiene ámbito de archivo y es visible desde su declaración hasta el final del archivo.
¿Qué le hace static a una variable local en C?
Cambia el tiempo de vida de la variable sin cambiar su ámbito. Una local static se crea una vez, se inicializa una vez y conserva su valor entre llamadas, pero sigue siendo visible solo dentro de su función. Es la forma de darle memoria a una función sin usar una global.
¿Cuál es la diferencia entre static y global en C?
Una global corriente es visible para todos los archivos del programa, que pueden alcanzarla con extern. Una variable o función static en el ámbito de archivo es privada de ese único archivo .c: los demás no pueden enlazarse a ella. static en el ámbito de archivo va de ocultar; static en una local va de tiempo de vida.
¿Por qué las variables globales son malas en C?
Cualquier función puede cambiarlas, así que un error puede originarse en cualquier parte del programa; las funciones que las leen no se pueden probar de forma aislada; y el nombre ocupa el programa entero, invitando a colisiones. Pasa los valores como parámetros y devuelve resultados, y donde el estado compartido sea genuinamente necesario, hazlo static en el ámbito de archivo para que solo un archivo pueda tocarlo.