Cuando programas en C, te encontrarás con la necesidad de fijar valores que no deben cambiar durante la ejecución. Sin embargo, no todas las formas de hacerlo son iguales. Existen tres mecanismos principales para definir valores fijos: macros con #define, enumeraciones con enum y variables con const. Aunque parezcan cumplir el mismo propósito, actúan en momentos distintos del proceso de construcción de tu programa: el preprocesador, el compilador o incluso durante la ejecución.
Si eliges mal, podrías enfrentarte a errores de tipos, problemas de memoria o comportamientos inesperados en el depurador. Para decidir correctamente, debes entender si necesitas una simple sustitución de texto, un grupo de enteros con nombre o una variable de solo lectura con un tipo específico.
El preprocesador es como un editor de texto automático que corre antes de que el compilador vea tu código; si usas #define, simplemente estás reemplazando palabras por valores. Por otro lado, el compilador es quien entiende qué es un número, un texto o un decimal; aquí es donde entran los enum y las variables const. Finalmente, existe la fase de ejecución, donde la memoria se asigna y los valores se mantienen fijos mediante la restricción de “solo lectura”.
#include <stdio.h>
/* El preprocesador sustituye este nombre por el número antes de compilar */
#define MAX_RETRIES 3
/* Los enum definen una lista de nombres para valores enteros relacionados */
enum DeviceStatus {
STATUS_IDLE,
STATUS_BUSY,
STATUS_ERROR
};
int main(void) {
/* 'const' define una variable de solo lectura con un tipo de dato claro */
const float VOLTAGE_THRESHOLD = 12.4f;
enum DeviceStatus current_state = STATUS_BUSY;
printf("Máximo de reintentos: %d\n", MAX_RETRIES);
printf("Umbral de voltaje: %.2fV\n", VOLTAGE_THRESHOLD);
if (current_state == STATUS_BUSY) {
printf("El dispositivo está ocupado.\n");
}
return 0;
}
En el ejemplo anterior, MAX_RETRIES no es realmente una variable; es una directiva de preprocesamiento. Cuando el compilador recibe el código, ya no ve MAX_RETRIES, sino el número 3. Esto es muy eficiente porque no consume memoria de variables, pero tiene un peligro: no tiene tipo. Si intentas usarlo en un contexto donde se requiere un tipo muy específico, el compilador podría dar errores extraños porque solo está viendo texto sustituido.
La enumeración DeviceStatus es distinta. Aquí, STATUS_IDLE, STATUS_BUSY y STATUS_ERROR son valores enteros (0, 1 y 2, respectivamente). La ventaja es que el depurador (debugger) puede mostrarte el nombre STATUS_BUSY en lugar de simplemente un 1, lo que facilita enormemente la vida al encontrar errores. Además, como es un tipo de dato real, el compilador puede verificar que estés usando enteros.
Por último, VOLTAGE_THRESHOLD es una variable de tipo float. A diferencia de las macros, esta tiene una dirección en la memoria y un tipo de dato estricto. La palabra clave const le dice al compilador: “esta variable se inicializa una vez y no permitas que nadie la cambie”. Esto es vital para valores complejos como estructuras o flotantes que no podrían ser manejados por un enum o un #define.
El error frecuente
Un error clásico para quienes vienen de otros lenguajes es intentar usar una variable const donde el lenguaje exige una constante de tiempo de compilación. Por ejemplo, en C, las etiquetas de un switch deben ser constantes conocidas antes de que el programa corra:
const int VALOR_FIJO = 10;
int x = 5;
switch (x) {
case VALOR_FIJO: // ERROR: 'VALOR_FIJO' is not a constant expression
// ...
break;
}
Aunque parezca que VALOR_FIJO no cambia, para el compilador de C, es una variable de “solo lectura” en la memoria, no un número estático en el código. Herramientas como AddressSanitizer no te avisarán de esto, pero el compilador fallará al intentar generar el código de salto del switch. En el estándar C23 se ha introducido constexpr para resolver precisamente esto, permitiendo que una variable sea tratada como una constante real en todo momento.
N° 24