Operadores, expresiones y conversiones
Precedencia, promociones enteras y las conversiones implícitas que causan bugs sutiles. Dominar cómo C evalúa expresiones evita una clase entera de errores.
Las expresiones parecen lo trivial de un lenguaje, pero en C esconden reglas que sorprenden: promociones automáticas, conversiones silenciosas y una precedencia con trampas. Entenderlas es la diferencia entre código que hace lo que crees y bugs que solo aparecen con ciertos valores.
- Operadores y su precedencia (y las trampas clásicas).
- Las promociones enteras.
- Las conversiones aritméticas usuales.
- Operadores a nivel de bits.
Operadores esenciales
int a = 7, b = 2;
a / b // 3 — división ENTERA (trunca)
a % b // 1 — módulo (resto)
a & b // 2 — AND de bits
a | b // 7 — OR de bits
a ^ b // 5 — XOR de bits
a << 1 // 14 — desplazamiento a la izquierda (×2)
a >> 1 // 3 — desplazamiento a la derecha (÷2)
La trampa número uno: / entre dos enteros es división entera, trunca hacia cero. 7 / 2 es 3, no 3.5. Para obtener decimales, al menos un operando debe ser de coma flotante: 7.0 / 2 da 3.5. Muchísimos bugs de cálculo vienen de olvidar esto.
Precedencia: usa paréntesis
C tiene una tabla de precedencia larga y no siempre intuitiva. En vez de memorizarla entera, aplica una regla: cuando dudes, pon paréntesis.
// clásico: & tiene MENOS precedencia que ==
if (flags & MASK == 0) // ❌ se evalúa como flags & (MASK == 0)
if ((flags & MASK) == 0) // ✅ lo que querías
Nadie te va a felicitar por memorizar que && tiene más precedencia que || pero menos que ==. Los paréntesis explícitos hacen el código correcto y legible. El compilador con -Wall incluso te avisa de las combinaciones más peligrosas (-Wparentheses). No es señal de debilidad usarlos: es señal de que valoras tu tiempo.
Promociones enteras
Antes de operar, C promociona los tipos enteros pequeños (char, short) a int. Esto explica comportamientos raros:
char a = 100, b = 100;
char c = a + b; // a y b se promocionan a int: 200, que cabe en int
// pero al asignar a char (a menudo 8 bits con signo)
// 200 no cabe → resultado dependiente de implementación
La aritmética “ocurre en int” aunque los operandos sean más pequeños. Tenerlo presente evita sorpresas al mezclar tipos.
Las conversiones aritméticas usuales
Cuando operas tipos distintos, C convierte ambos a un tipo común siguiendo reglas fijas (el “de mayor rango gana”). El caso peligroso es mezclar con y sin signo:
unsigned u = 0;
int s = -1;
u - 1 // unsigned: da un número ENORME, no -1
s < u // -1 se convierte a unsigned gigante → false (¡trampa!)
Aquí vive una categoría entera de bugs que no crashean —solo dan el resultado equivocado con ciertos valores—. C convierte tipos por ti, sin avisar, siguiendo reglas que casi nadie recuerda de memoria. La defensa profesional es triple: (1) compila con -Wconversion -Wsign-compare para que el compilador te señale las conversiones sospechosas; (2) evita mezclar con y sin signo en la misma expresión; (3) cuando la conversión sea intencional, hazla explícita con un cast, para que quede claro que sabes lo que haces. El código que respeta esto no tiene esa clase de bugs fantasma.
Operadores a nivel de bits
En sistemas manipularás bits constantemente (flags, máscaras, registros de hardware):
#define LEER (1u << 0) // 0b001
#define ESCRIBIR (1u << 1) // 0b010
#define EJECUTAR (1u << 2) // 0b100
unsigned permisos = LEER | ESCRIBIR; // activar dos flags
permisos |= EJECUTAR; // activar uno
permisos &= ~ESCRIBIR; // desactivar uno
bool puede_leer = permisos & LEER; // comprobar
Este patrón —máscaras con |, &, ~, <<— es el pan de cada día del programador de sistemas. Usa siempre literales unsigned (1u) para evitar problemas de signo en los desplazamientos.
- Comprueba que
7 / 2es3y7.0 / 2es3.5. - Reproduce el bug de precedencia
flags & MASK == 0y arréglalo con paréntesis. - Provoca la trampa de signo
s < uy observa el aviso de-Wsign-compare. - Escribe un conjunto de flags con máscaras de bits: activa, desactiva y comprueba.