1. Qué es una variable
En Bash, una variable era un nombre asociado a un texto, y existía desde que se le asignaba algo. En C, una variable es una región de la memoria, de un tamaño fijo, a la que el programa le dio un nombre y un tipo. Esa definición tiene más partes de las que parece, y cada una tiene su sección en este tema:
| Propiedad | Qué es | En int edad = 30; dentro de main |
|---|---|---|
| Nombre | El identificador con que el código se refiere a ella. | edad |
| Tipo | Cuántos bytes ocupa y cómo se interpretan (tema 5). | int, 4 bytes |
| Valor | El contenido actual de esos bytes. | 30 |
| Dirección | Dónde está en la memoria; se obtiene con &. | Algo como 0x7ffe…, en la pila |
| Alcance | La parte del código donde el nombre es visible. | Desde la declaración hasta el } de main |
| Duración | Durante cuánto tiempo existe la memoria de la variable. | Mientras main se esté ejecutando |
| Enlace | Si el nombre puede usarse desde otros archivos del programa. | Ninguno: es local |
El alcance y la duración se confunden con facilidad, pero son cosas distintas. El alcance es una propiedad del texto del programa: dónde se puede escribir el nombre. La duración es una propiedad de la ejecución: cuándo existe la memoria. Una variable puede existir sin que su nombre sea visible, y veremos que ese caso, el de static, es muy útil.
2. Declarar e inicializar
Una declaración presenta la variable: su tipo y su nombre. Si además le da un valor, ese valor es su inicialización:
int contador; /* declarada, sin valor inicial */
int intentos = 3; /* declarada e inicializada */
unsigned long bytes = 0, total = 0; /* dos variables del mismo tipo */
double promedio = 7.5;
char separador = ':';
Inicializar y asignar se escriben parecido, pero no son lo mismo. La inicialización forma parte de la declaración y da el valor con que la variable nace; la asignación, contador = 0;, es una sentencia que cambia el valor de una variable que ya existía. La diferencia importa en tres casos que veremos en este tema: las constantes, que se pueden inicializar pero no asignar; las variables static, cuya inicialización ocurre una sola vez; y los arreglos, que se inicializan con llaves pero no se pueden asignar enteros.
Desde C99, una variable se puede declarar en cualquier lugar de un bloque, no solo al principio, y también en la primera parte de un for: for (int i = 0; …). La costumbre moderna, que seguimos en el curso, es declarar cada variable lo más cerca posible de donde se usa por primera vez, y con su valor inicial. Hay dos trampas al declarar varias variables en una línea:
$ gcc -std=c17 -Wall -Wextra -o multi multi.c
multi.c: In function ‘main’:
multi.c:5:5: warning: ‘a’ is used uninitialized [-Wuninitialized]
…
multi.c:5:5: warning: ‘c’ is used uninitialized [-Wuninitialized]
En int a, b = 0, c;, el = 0 se aplica solo a b; quien lee rápido puede creer que las tres empiezan en cero. La otra trampa, todavía peor, aparecerá con los punteros en el tema 15: en int* p, q;, solo p es un puntero. Por eso la tabla de estilo del tema 3 pedía una variable por declaración. Y una variable no se puede declarar dos veces en el mismo bloque: int x = 1; int x = 2; da error: redefinition of ‘x’.
3. Las variables sin inicializar
¿Qué valor tiene una variable que nadie inicializó? Depende de dónde se declaró, y la respuesta es uno de los errores clásicos de C. Este programa llama a una función que deja un valor en una variable local, y luego a otra que lee una variable local sin inicializar:
#include <stdio.h>
static void preparar(void)
{
int secreto = 4242; /* queda en la pila al volver */
printf("preparar: secreto = %d\n", secreto);
}
static void usar(void)
{
int sin_valor; /* nunca recibe un valor */
printf("usar: sin_valor = %d\n", sin_valor);
}
int global_sin_valor; /* fuera de toda función */
int main(void)
{
preparar();
usar();
printf("main: global_sin_valor = %d\n", global_sin_valor);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o basura basura.c
basura.c: In function ‘usar’:
basura.c:14:5: warning: ‘sin_valor’ is used uninitialized [-Wuninitialized]
…
$ ./basura
preparar: secreto = 4242
usar: sin_valor = 4242
main: global_sin_valor = 0
$ gcc -std=c17 -O2 -o basura basura.c 2>/dev/null && ./basura
preparar: secreto = 4242
usar: sin_valor = 0
main: global_sin_valor = 0
$ gcc -std=c17 -ftrivial-auto-var-init=pattern -o basura basura.c && ./basura
preparar: secreto = 4242
usar: sin_valor = -16843010
main: global_sin_valor = 0
$ gcc -std=c17 -ftrivial-auto-var-init=zero -o basura basura.c && ./basura
preparar: secreto = 4242
usar: sin_valor = 0
main: global_sin_valor = 0
La primera ejecución es la más reveladora: usar muestra 4242, el valor de una variable de otra función. No hay magia. Las variables locales viven en la pila, y usar ocupó el mismo lugar de la pila que acababa de dejar libre preparar; como nadie lo limpió, sin_valor encontró allí el 4242. Una variable local sin inicializar no vale cero ni ningún valor en particular: vale lo que haya quedado en esa memoria, y usarla es comportamiento indefinido. Con -O2 el resultado cambió a 0, sin que cambiara el fuente; con otro compilador, otra cosa.
Las dos últimas compilaciones usan una opción de gcc que existe desde la versión 12 precisamente por este problema: -ftrivial-auto-var-init llena cada variable local sin inicializar con un patrón llamativo (-16843010 es 0xfefefefe), que ayuda a detectar el error al depurar, o con ceros, que neutraliza el error en producción. Muchas distribuciones compilan el núcleo Linux con la segunda. Pero ninguna de las dos reemplaza a la regla: toda variable local se inicializa al declararla.
La variable global, en cambio, valió 0 en las cuatro ejecuciones, y eso sí está garantizado. Las variables que existen durante todo el programa, las globales y las static, se inicializan a cero si no se indica otra cosa. La diferencia viene de dónde viven, y la explica la sección 10.
4. Las constantes
Un número que aparece en el código sin explicación, como 512 o 3, se llama número mágico: quien lo lee no sabe qué significa, y quien quiere cambiarlo tiene que encontrar todas sus apariciones. C ofrece varias formas de darle un nombre, cada una con sus propiedades:
#include <stdio.h>
#define TAM_BUFFER 512 /* macro: el preprocesador la reemplaza */
enum { MAX_INTENTOS = 3 }; /* constante entera con nombre */
static const double PI = 3.14159265358979; /* variable de solo lectura */
int main(void)
{
char buffer[TAM_BUFFER];
const int limite = 10;
printf("buffer de %zu bytes, %d intentos, pi = %.5f, límite %d\n",
sizeof buffer, MAX_INTENTOS, PI, limite);
limite = 20;
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o constantes constantes.c
constantes.c: In function ‘main’:
constantes.c:16:12: error: assignment of read-only variable ‘limite’
16 | limite = 20;
| ^
$ sed -i '/limite = 20;/d' constantes.c
$ gcc -std=c17 -Wall -Wextra -o constantes constantes.c && ./constantes
buffer de 512 bytes, 3 intentos, pi = 3.14159, límite 10
El calificador const convierte una variable en de solo lectura: se inicializa al declararla y el compilador rechaza cualquier asignación posterior. Las tres formas de nombrar una constante se comparan así:
#define TAM 512 | enum { TAM = 512 }; | const int tam = 512; | |
|---|---|---|---|
| Qué es | Texto que el preprocesador reemplaza. | Una constante entera del lenguaje. | Una variable que no se puede modificar. |
| Tipo | El de lo que se escriba: 512 es int. | Siempre int. | Cualquiera: double, uint64_t, una estructura. |
| Ocupa memoria | No. | No. | Sí, tiene dirección. |
| Respeta el alcance | No: vale desde el #define hasta el final del archivo. | Sí. | Sí. |
Sirve como tamaño de un arreglo global o en un case | Sí. | Sí. | No, en C (sección 5). |
Visible en gdb | No: el compilador nunca la vio. | Sí. | Sí. |
En la práctica, el código de sistemas usa las tres. #define es lo tradicional, y lo que encontrarás en todos los encabezados del sistema: EXIT_SUCCESS, INT_MAX, O_RDONLY. enum es una buena alternativa para constantes enteras, sobre todo cuando forman un grupo, como veremos en el tema 26. Y const es lo natural para valores de otros tipos y, sobre todo, para proteger datos que una función no debe cambiar, que es su uso más importante y aparecerá con los punteros en el tema 19. La convención de nombres, como vimos en el tema 3, es escribir las macros en mayúsculas.
5. Lo que const no es
El nombre engaña: en C, una variable const no es una constante en el sentido del lenguaje, es decir, un valor conocido al compilar. Es una variable con una restricción. Por eso no puede usarse donde el lenguaje exige una constante:
#include <stdio.h>
const int TAMANO = 16;
int tabla[TAMANO]; /* arreglo global de tamaño TAMANO */
int main(void)
{
int opcion = 2;
switch (opcion) {
case TAMANO:
printf("dieciséis\n");
break;
}
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o noconstante noconstante.c
noconstante.c:5:5: error: variably modified ‘tabla’ at file scope
5 | int tabla[TAMANO]; /* arreglo global de tamaño TAMANO */
| ^~~~~
noconstante.c: In function ‘main’:
noconstante.c:12:5: error: case label does not reduce to an integer constant
12 | case TAMANO:
| ^~~~
$ sed 's/^const int TAMANO/constexpr int TAMANO/' noconstante.c > noc23.c
$ gcc -std=c23 -Wall -Wextra -o noc23 noc23.c && echo compila
compila
Ni el tamaño de un arreglo global ni una etiqueta de case aceptan una variable const. (En C++ sí, y es una diferencia frecuente entre los dos lenguajes.) Por eso los programas en C usan #define o enum para esos casos. C23 agregó una solución propia: constexpr, que declara un objeto cuyo valor debe conocerse al compilar, y que sí sirve en ambos lugares.
La otra cosa que const no es: una protección absoluta. Es una promesa que el compilador verifica en el código que la respeta, pero un programa puede quitar el const a la fuerza, con una conversión de punteros. El resultado es comportamiento indefinido, y este programa muestra dos de sus formas:
#include <stdio.h>
const int global = 1;
int main(void)
{
const int local = 1;
int *p;
p = (int *) &local; /* quitar el const a la fuerza */
*p = 2;
printf("local = %d\n", local);
p = (int *) &global;
*p = 2;
printf("global = %d\n", global);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o trampa trampa.c && ./trampa
local = 2
Segmentation fault
$ gcc -std=c17 -Wall -Wextra -O2 -o trampa trampa.c && ./trampa
local = 1
Segmentation fault
$ gcc -std=c17 -Wall -Wextra -Wcast-qual -c trampa.c 2>&1 | grep warning
trampa.c:10:9: warning: cast discards ‘const’ qualifier from pointer target type [-Wcast-qual]
trampa.c:14:9: warning: cast discards ‘const’ qualifier from pointer target type [-Wcast-qual]
Todavía no vimos los punteros, pero la idea se entiende: p guarda la dirección de la variable y *p = 2 escribe en ella. Con la variable local, sin optimizar, la escritura «funcionó»; con -O2, el compilador, confiado en el const, ya había reemplazado local por un 1 y ni siquiera la volvió a leer. Con la variable global, el resultado es más drástico: el programa muere. Una constante global va a la sección .rodata, que el sistema carga en páginas de memoria marcadas como de solo lectura, y el procesador detiene el intento de escribir en ellas. Esa protección no la da el lenguaje sino el sistema operativo, y la veremos en la sección 10. -Wcast-qual advierte de estas conversiones.
6. El alcance
El alcance de un nombre es la región del fuente en la que se puede usar. En C hay dos alcances principales para las variables:
- De bloque. Una variable declarada dentro de un bloque, entre
{y}, es visible desde su declaración hasta el final de ese bloque, incluidos los bloques anidados. Las variables de una función, sus parámetros y la variable de unfortienen alcance de bloque. Se las llama variables locales. - De archivo. Una variable declarada fuera de toda función es visible desde su declaración hasta el final del archivo, en todas las funciones que vengan después. Se las llama variables globales.
La variable de un for merece una mención: su alcance es el propio for, y al terminar el bucle deja de existir para el compilador:
$ gcc -std=c17 -Wall -Wextra -o ifuera ifuera.c
ifuera.c: In function ‘main’:
ifuera.c:5:12: error: ‘i’ undeclared (first use in this function)
5 | return i;
| ^
Es justamente lo que se quiere: un contador de bucle no tiene sentido fuera del bucle, y limitar su alcance evita usarlo por error. En general, cuanto más pequeño es el alcance de una variable, más fácil es razonar sobre ella: para saber quién puede modificarla, basta con mirar unas pocas líneas.
7. El ocultamiento
Dos variables distintas pueden tener el mismo nombre si están en alcances distintos. Cuando un alcance interior declara un nombre que ya existía en uno exterior, la declaración interior oculta a la exterior mientras dure su bloque:
#include <stdio.h>
int nivel = 0; /* alcance de archivo */
static void mostrar(void)
{
printf("mostrar: nivel = %d\n", nivel);
}
int main(void)
{
printf("main: nivel = %d\n", nivel);
int nivel = 1; /* oculta a la global */
printf("main: nivel = %d\n", nivel);
{
int nivel = 2; /* oculta a la de main */
printf("bloque: nivel = %d\n", nivel);
mostrar();
}
for (int i = 0; i < 1; i++)
printf("for: i = %d\n", i);
/* aquí i ya no existe */
printf("main: nivel = %d\n", nivel);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o alcance alcance.c && ./alcance
main: nivel = 0
main: nivel = 1
bloque: nivel = 2
mostrar: nivel = 0
for: i = 0
main: nivel = 1
$ gcc -std=c17 -Wall -Wextra -Wshadow -o alcance alcance.c 2>&1 | grep warning
alcance.c:13:9: warning: declaration of ‘nivel’ shadows a global declaration [-Wshadow]
alcance.c:17:13: warning: declaration of ‘nivel’ shadows a previous local [-Wshadow]
Hay tres variables llamadas nivel, y cada línea usa la más cercana que esté a la vista. El primer printf de main, antes de la declaración local, todavía ve la global. Dentro del bloque se ve la tercera. Y el caso de mostrar es el más instructivo: aunque se llama desde el bloque donde nivel vale 2, muestra 0. El alcance es una propiedad del texto, no de la ejecución: mostrar está escrita fuera de main, y en su texto el único nivel visible es el global. A esta regla se la llama alcance léxico, y es distinta de lo que ocurría en Bash, donde una función sí veía las variables local de quien la llamaba.
Ocultar variables es legal, pero rara vez es buena idea: en un programa largo, es fácil creer que se está modificando una variable cuando en realidad se modifica otra del mismo nombre. -Wshadow, que ya vimos en el tema 4, señala cada caso.
8. La duración
La duración de una variable, que el estándar llama storage duration, es el período de la ejecución durante el cual su memoria existe y conserva su valor. C tiene cuatro:
| Duración | Existe | Quiénes la tienen | Dónde vive | Sin inicializar vale |
|---|---|---|---|---|
| Automática | Desde que se entra en su bloque hasta que se sale. | Las variables locales y los parámetros. | La pila. | Basura. |
| Estática | Todo el programa, desde antes de main hasta el final. | Las variables globales y las locales declaradas static. | .data, .bss o .rodata. | Cero. |
| Asignada | Desde malloc hasta free. | La memoria pedida al ejecutar. | El montículo (heap). | Basura (con malloc). |
| De hilo | Mientras exista el hilo. | Las variables _Thread_local. | Una zona propia de cada hilo. | Cero. |
La duración asignada es el tema del bloque 7, y la de hilo, del curso Procesos e Hilos. Aquí nos ocupan las dos primeras, que se ven en el siguiente diagrama para un programa que llama tres veces a una función:
Que una variable automática muera al salir de su bloque tiene una consecuencia que veremos con detalle en el tema 12: su memoria queda libre para la siguiente llamada, como pasó en basura.c, y cualquier intento de usarla después, por ejemplo devolviendo su dirección, es un error grave.
9. static en una variable local
La palabra static delante de una variable local cambia su duración sin cambiar su alcance: el nombre sigue siendo visible solo dentro de la función, pero la variable existe durante todo el programa y conserva su valor entre una llamada y la siguiente.
#include <stdio.h>
static int siguiente_id(void)
{
static int ultimo = 0; /* se inicializa una sola vez */
int llamadas_aqui = 0; /* se crea en cada llamada */
ultimo++;
llamadas_aqui++;
printf("ultimo = %d, llamadas_aqui = %d\n", ultimo, llamadas_aqui);
return ultimo;
}
int main(void)
{
siguiente_id();
siguiente_id();
siguiente_id();
return 0;
}
$ ./contador
ultimo = 1, llamadas_aqui = 1
ultimo = 2, llamadas_aqui = 1
ultimo = 3, llamadas_aqui = 1
La línea static int ultimo = 0; no se ejecuta en cada llamada: es una inicialización, y una variable de duración estática se inicializa una sola vez, antes de que el programa arranque. llamadas_aqui, en cambio, se crea con el valor 0 cada vez, y nunca pasa de 1. Es el patrón clásico para generar identificadores, contar llamadas o recordar un resultado ya calculado, sin exponer la variable al resto del programa como haría una global.
Como la inicialización ocurre antes de ejecutar nada, su valor tiene que conocerse al compilar. Ni una variable static ni una global pueden inicializarse con algo que dependa de la ejecución:
$ gcc -std=c17 -Wall -Wextra -c sinit.c
sinit.c: In function ‘f’:
sinit.c:3:20: error: initializer element is not constant
3 | static int y = x;
| ^
$ gcc -std=c17 -Wall -Wextra -c ginit.c
ginit.c:3:13: error: initializer element is not constant
3 | int doble = largo * 2;
| ^~~~~
Hay una advertencia importante para más adelante. Una función con variables static tiene estado oculto: dos llamadas con los mismos argumentos pueden dar resultados distintos. Y si dos hilos la llaman a la vez, ambos modifican la misma variable sin coordinación. Varias funciones viejas de la biblioteca de C tienen este problema, como strtok, y por eso existen versiones alternativas como strtok_r, que veremos en el tema 22.
10. Dónde vive cada variable
La duración decide dónde termina cada variable, y se puede ver en el ejecutable. Este programa declara una variable de cada clase y muestra sus direcciones:
#include <stdio.h>
int con_valor = 7; /* global con valor inicial */
int sin_valor; /* global sin valor inicial */
static int privada = 3; /* global, solo de este archivo */
const int de_lectura = 42; /* global constante */
int main(void)
{
static int persistente = 1; /* local, pero de duración estática */
int automatica = 5; /* local y automática */
printf("con_valor %p\n", (void *) &con_valor);
printf("sin_valor %p\n", (void *) &sin_valor);
printf("privada %p\n", (void *) &privada);
printf("de_lectura %p\n", (void *) &de_lectura);
printf("persistente %p\n", (void *) &persistente);
printf("automatica %p\n", (void *) &automatica);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o donde donde.c && ./donde
con_valor 0x59f873984010
sin_valor 0x59f873984020
privada 0x59f873984014
de_lectura 0x59f873982004
persistente 0x59f873984018
automatica 0x7ffcdcf0f4e4
$ nm -n donde | grep -E ' (main|con_valor|sin_valor|privada|de_lectura|persistente.*)$'
0000000000001169 T main
0000000000002004 R de_lectura
0000000000004010 D con_valor
0000000000004014 d privada
0000000000004018 d persistente.0
0000000000004020 B sin_valor
Cinco de las seis variables están en el ejecutable, con su lugar ya asignado por el enlazador, y la sexta no. Las direcciones del programa son las de nm más una base al azar, 0x59f873980000, por el ASLR. La letra de nm dice en qué sección está cada una:
| Variable | nm | Sección | Por qué |
|---|---|---|---|
de_lectura | R | .rodata | Es constante: va con los datos de solo lectura, como las cadenas literales. |
con_valor | D | .data | Tiene un valor inicial distinto de cero, que se guarda en el archivo. |
privada | d | .data | Igual, pero la minúscula indica que es static: local al archivo. |
persistente | d | .data | Una static local es, para el enlazador, otra variable de archivo; el .0 evita choques con otras del mismo nombre. |
sin_valor | B | .bss | Empieza en cero, y de esa sección el archivo solo guarda el tamaño. |
automatica | (no aparece) | la pila | Se crea al ejecutar main; no existe en el archivo. |
Aquí está la explicación de los ceros garantizados de la sección 3. El sistema crea la sección .bss llenándola de ceros al cargar el programa, de modo que toda variable global o static sin inicializar empieza en cero sin costo alguno. La pila, en cambio, se reutiliza en cada llamada sin limpiarse, y por eso las variables automáticas contienen lo que haya quedado. Y la protección de .rodata que mató a trampa.c se ve en el mapa de memoria del proceso, que Linux muestra en /proc/self/maps:
$ ./mapa | grep -E 'mapa|stack|^0x'
0x5e60c4a3c010 0x5e60c4a3a004 0x7ffcc8a5d8e4
5e60c4a38000-5e60c4a39000 r--p 00000000 08:30 52289 /home/ana/c/tema06/mapa
5e60c4a39000-5e60c4a3a000 r-xp 00001000 08:30 52289 /home/ana/c/tema06/mapa
5e60c4a3a000-5e60c4a3b000 r--p 00002000 08:30 52289 /home/ana/c/tema06/mapa
5e60c4a3b000-5e60c4a3c000 r--p 00002000 08:30 52289 /home/ana/c/tema06/mapa
5e60c4a3c000-5e60c4a3d000 rw-p 00003000 08:30 52289 /home/ana/c/tema06/mapa
7ffcc8a3f000-7ffcc8a61000 rw-p 00000000 00:00 0 [stack]
Cada línea es una zona de memoria con sus permisos: r lectura, w escritura, x ejecución. La variable global modificable, en …c010, cae en la zona rw-p; la constante, en …a004, en una zona r--p, sin permiso de escritura; y la automática, en [stack], la pila. Escribir en una página r--p es lo que produjo el Segmentation fault. El tema 27 recorre este mapa completo.
11. El enlace: static fuera de las funciones
La palabra static tiene un segundo significado, completamente distinto, cuando se usa fuera de las funciones. Una variable global ya tiene duración estática, de modo que static no puede cambiar eso; lo que cambia es su enlace: si su nombre es visible para los demás archivos del programa.
| Declaración | Duración | Alcance | Enlace |
|---|---|---|---|
int x; dentro de una función | Automática | Bloque | Ninguno |
static int x; dentro de una función | Estática | Bloque | Ninguno |
int x; fuera de las funciones | Estática | Archivo | Externo: otros archivos pueden usarla |
static int x; fuera de las funciones | Estática | Archivo | Interno: solo este archivo |
Es la diferencia entre la D mayúscula de con_valor y la d minúscula de privada en la salida de nm: los símbolos en mayúscula son globales para el enlazador, y otro archivo del programa podría usarlos; los de minúscula son invisibles fuera de su archivo. Lo mismo vale para las funciones: por eso las funciones auxiliares de los ejemplos de este tema, como preparar o siguiente_id, están declaradas static.
La regla práctica es declarar static todo lo que no necesite verse desde otro archivo. Evita choques de nombres entre archivos, como el multiple definition del tema 4, y le dice al lector que esa variable o función es un detalle interno. Cómo se comparte deliberadamente una variable entre archivos, con extern, es parte del tema 13.
12. volatile y otras palabras
Quedan algunas palabras clave relacionadas con las variables que conviene reconocer:
| Palabra | Significado | Uso hoy |
|---|---|---|
volatile | El valor puede cambiar por causas ajenas al código, de modo que el compilador debe leerlo de la memoria en cada uso, sin optimizar los accesos. | Registros de dispositivos mapeados en memoria, y variables modificadas por un manejador de señales (volatile sig_atomic_t). |
register | Una sugerencia de guardar la variable en un registro del procesador. No se puede tomar su dirección. | Obsoleta: los compiladores deciden mejor solos. |
auto | Hasta C17, «duración automática», lo que ya son las locales por omisión. En C23, deducir el tipo a partir del valor inicial. | En C17, nunca. En C23, auto n = 10; declara un int. |
constexpr | C23: un objeto cuyo valor se conoce al compilar. | Reemplazo moderno de #define para constantes con tipo. |
_Thread_local | C11: una copia de la variable por cada hilo. En C23, thread_local. | Curso Procesos e Hilos. |
volatile es la única que tiene un papel real en la programación de sistemas, y es fácil de malinterpretar. No hace que una variable sea segura para compartir entre hilos: eso requiere operaciones atómicas o mecanismos de sincronización. Lo que hace es impedir que el compilador elimine lecturas o escrituras que, desde el punto de vista del código, parecen innecesarias, como leer en un bucle un registro de hardware que el código nunca modifica.
13. Las variables globales
Las variables globales son cómodas: cualquier función puede leerlas y modificarlas sin recibirlas como parámetro. Ese es justamente su problema. En un programa grande, una global puede cambiar desde cualquier parte, y para entender su valor en un punto hay que leer todo el programa. Las funciones que dependen de globales son difíciles de probar por separado y no se pueden usar con seguridad desde varios hilos. La alternativa casi siempre es pasar los datos como parámetros, que veremos en el tema 11.
Aun así, hay usos legítimos, y el sistema mismo ofrece ejemplos:
- Estado del programa entero, como el nivel de detalle de los mensajes de una herramienta, elegido una vez con una opción de la línea de órdenes.
- Interfaces del sistema:
errno, la variable que indica el último error, del tema 37;environ, con las variables de entorno;stdin,stdoutystderr. - Comunicación con un manejador de señales, que no puede recibir parámetros propios, en el curso Procesos e Hilos.
Cuando una global es necesaria, conviene limitar el daño: declararla static para que solo la vea su archivo, darle un nombre descriptivo, y modificarla desde pocas funciones bien identificadas.
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
| Usar una variable local sin inicializar | Toma lo que haya en la pila; el resultado cambia con las opciones de compilación. | Inicializar cada variable al declararla; -Wall y -O2 ayudan a detectarlo. |
Creer que int a, b = 0; inicializa las dos | Solo b empieza en 0. | Una variable por declaración. |
Usar una variable const como tamaño de un arreglo global o en un case | Error: no es una constante para el lenguaje. | #define, enum o, en C23, constexpr. |
Quitar el const con una conversión para modificar el valor | Comportamiento indefinido; si es global, Segmentation fault. | Si hay que modificarla, no es una constante. |
| Ocultar una variable con otra del mismo nombre | Se modifica una creyendo modificar la otra. | Nombres distintos; -Wshadow. |
Usar la variable de un for después del bucle | Error: undeclared. | Si hace falta su valor final, declararla antes del for. |
| Esperar que una local conserve su valor entre llamadas | Se crea de nuevo en cada llamada. | Declararla static, o mejor, devolver o recibir el valor. |
Inicializar una global o static con una expresión no constante | Error: initializer element is not constant. | Inicializarla con una constante y asignarle el valor al ejecutar. |
15. Resumen
- Una variable tiene nombre, tipo, valor, dirección, alcance, duración y enlace. El alcance es una propiedad del texto; la duración, de la ejecución.
- Inicializar da el valor con que la variable nace; asignar cambia el de una variable existente. Se declara cada variable cerca de su primer uso, una por línea y con su valor inicial.
- Una variable local sin inicializar contiene lo que haya quedado en la pila: usarla es comportamiento indefinido. Las globales y las
staticempiezan en cero. -ftrivial-auto-var-init=patterno=zerollena las locales sin inicializar, como red de seguridad.- Las constantes se nombran con
#define,enum,consto, en C23,constexpr.consthace una variable de solo lectura, pero no una constante del lenguaje. - Quitar el
consta la fuerza es comportamiento indefinido; una constante global está en páginas de solo lectura, y escribirla termina el programa. - Las variables locales tienen alcance de bloque; las globales, de archivo. Un nombre interior oculta a uno exterior;
-Wshadowlo advierte. - El alcance es léxico: una función ve las variables de donde está escrita, no de donde se la llama.
- La duración automática dura lo que el bloque; la estática, todo el programa.
staticen una local la hace estática, y su inicialización ocurre una sola vez, con un valor constante. - Las variables de duración estática viven en
.data,.bsso.rodata, ynmlas muestra; las automáticas, en la pila. staticfuera de las funciones da enlace interno: el nombre no es visible desde otros archivos.volatileobliga a leer la memoria en cada acceso; no sincroniza hilos.registeres obsoleta, yautocambió de significado en C23.- Las variables globales se evitan salvo para estado verdaderamente global, y conviene declararlas
static.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué diferencia hay entre el alcance y la duración de una variable?
El alcance es la región del texto del programa donde se puede usar el nombre de la variable. La duración es el período de la ejecución durante el cual existe su memoria. Una variable static local tiene alcance de bloque, porque su nombre solo se ve dentro de la función, pero duración estática, porque existe durante todo el programa.
2. En basura.c, ¿por qué sin_valor mostró 4242, el valor de una variable de otra función?
Porque las variables locales viven en la pila, y usar ocupó el mismo lugar que acababa de liberar preparar. La pila no se limpia al volver de una función, de modo que sin_valor contenía el 4242 que había dejado secreto. Es comportamiento indefinido: con -O2 salió otro valor.
3. ¿Por qué una variable global sin inicializar vale siempre cero?
Porque el estándar lo garantiza para toda variable de duración estática, y la implementación lo cumple sin costo: esas variables van a la sección .bss, que el sistema crea llena de ceros al cargar el programa.
4. ¿Por qué const int TAMANO = 16; no sirve como tamaño de un arreglo global ni como etiqueta de case?
Porque en C una variable const no es una expresión constante: es una variable que no se puede modificar, cuyo valor no forma parte de lo que el lenguaje considera conocido al compilar. Esos lugares exigen una constante, y se usa #define, enum o, en C23, constexpr.
5. En trampa.c, ¿por qué la constante local «cambió» sin optimizar pero no con -O2, y por qué la global hizo terminar el programa?
Modificar un objeto const es comportamiento indefinido. Sin optimizar, el compilador volvió a leer la variable de la pila y encontró el 2; con -O2, confió en el const y usó directamente el valor 1. La global está en .rodata, cargada en páginas de memoria sin permiso de escritura, y el procesador detuvo el intento con un Segmentation fault.
6. En alcance.c, ¿por qué mostrar imprime 0 aunque se la llame desde el bloque donde nivel vale 2?
Porque el alcance en C es léxico: depende de dónde está escrito el código, no de desde dónde se lo llama. mostrar está definida fuera de main, y en su texto la única variable nivel visible es la global, que vale 0.
7. En contador.c, ¿por qué ultimo crece y llamadas_aqui se queda en 1?
ultimo es static: tiene duración estática, se inicializa una sola vez antes del arranque y conserva su valor entre llamadas. llamadas_aqui es automática: se crea de nuevo, con valor 0, en cada llamada, y desaparece al volver.
8. ¿Qué significan las letras D, d, B y R en la salida de nm donde?
D es una variable en .data, con valor inicial, visible para otros archivos; d, lo mismo pero con enlace interno, es decir, static; B, una variable en .bss, que empieza en cero; y R, un dato en .rodata, de solo lectura.
9. ¿Qué dos significados tiene static según dónde se use?
Dentro de una función, cambia la duración de la variable de automática a estática: existe todo el programa y conserva su valor, aunque su nombre siga siendo local. Fuera de las funciones, aplicado a variables o funciones, cambia el enlace de externo a interno: el nombre deja de ser visible desde otros archivos.
10. ¿Por qué static int y = x; no compila dentro de una función con parámetro x?
Porque una variable de duración estática se inicializa una sola vez, antes de que el programa empiece a ejecutarse, y su valor inicial tiene que conocerse al compilar. El valor de x solo se conoce durante una llamada. Si hace falta, se inicializa con una constante y se le asigna el valor después.
11. ¿Qué hace volatile, y qué no hace?
Obliga al compilador a leer y escribir la variable en la memoria en cada acceso, sin eliminar accesos que parezcan innecesarios; sirve para registros de hardware y para variables modificadas por manejadores de señales. No hace que la variable sea segura para compartir entre hilos: no garantiza atomicidad ni orden entre procesadores.
Para practicar
- Agrega a
basura.cuna segunda variable local enpreparary otra enusar, y observa qué valores aparecen en cada una sin optimizar. ¿Qué pasa si cambias el orden de las declaraciones? - Reescribe
noconstante.cprimero con#definey después conenum. Compila las dos versiones con-gy comprueba engdbconprint TAMANOcuál de los dos nombres conoce el depurador. - Modifica
contador.cpara quesiguiente_idreciba un parámetroreiniciar: si vale 1, el contador vuelve a 0. ¿Se puede hacer sinstatic? - Agrega a
donde.cun arreglo global de 4096 bytes sin inicializar y otro inicializado con{1}. Compara el tamaño del ejecutable y la salida desizeantes y después. ¿Cuál de los dos arreglos agrandó el archivo? - Compila
alcance.ccon-Wshadowy elimina todos los ocultamientos cambiando nombres, sin alterar lo que imprime el programa. - Escribe un programa con una función que devuelva cuántas veces fue llamada, usando una variable
static, y otra que haga lo mismo con una variable global. Declara la globalstaticy comprueba connmla diferencia de letra.