Promoción de enteros y conversiones aritméticas en C

Cuando escribes una expresión aritmética en C, el compilador no opera directamente con los tipos tal cual están definidos en tus variables; aplica un proceso de transformación automática para asegurar que la ALU (Unidad Aritmético-Lógica) de la CPU trabaje con tipos de tamaño fijo y rangos consistentes. Este proceso se divide en dos etapas críticas: la promoción de enteros (integer promotion) y las conversiones aritméticas habituales (usual arithmetic conversions).

La primera etapa, la promoción de enteros, ocurre antes de que cualquier operador sea evaluado. Cualquier tipo de entero que sea más pequeño que un int (como char o short) se convierte automáticamente en un int (o un unsigned int si el valor no cabe en un int con signo). Esto se hace para evitar errores de desbordamiento en cálculos intermedios. La segunda etapa, las conversiones aritméticas habituales, actúa cuando intentas realizar una operación binaria (como +, - o *) con dos operandos de distinto tipo. Aquí entra en juego una jerarquía estricta: el tipo de menor rango se “eleva” al tipo de mayor rango. Si uno de los operandos es long double, el otro se convierte en long double; si ambos son enteros pero uno es unsigned, la jerarquía suele dictar que el tipo con signo sea convertido a unsigned, lo que puede ser una trampa mortal.

Debes tener cuidado con esto cuando trabajes con operaciones de bits o comparaciones lógicas. Si aplicas un operador bit a un unsigned char, la promoción a int llenará los bits superiores con ceros, pero si el tipo original fuera un signed char con valor negativo, se llenarán con unos (sign extension). Si ignoras estas reglas, podrías terminar comparando un número negativo contra un unsigned int y obtener false cuando lógicamente esperabas true, o realizar operaciones bit a bit que resulten en valores masivos inesperados debido a la extensión de signo.

#include <stdio.h>
#include <stdbool.h>

int main(void) {
    /* 1. Demostración de Integer Promotion */
    unsigned char c = 0xFF; // 255 en decimal
    /* Al aplicar ~, c se promueve a int (0x000000FF en 32 bits).
       ~0x000000FF resulta en 0xFFFFFF00, que es -256 en un int de 32 bits. */
    int bitwise_not = ~c;

    /* 2. Demostración de Usual Arithmetic Conversions (Jerarquía) */
    int i = 10;
    double d = 5.5;
    /* d es double, por lo tanto i se promueve a double antes de la suma. */
    double sum = i + d;

    /* 3. La trampa de Signed vs Unsigned */
    int negative = -1;
    unsigned int upos = 1;
    /* En la comparación, negative se promueve a unsigned int.
       -1 como unsigned es el valor máximo (ej. 4294967295).
       Por tanto, (unsigned int)-1 > 1 es TRUE. */
    bool comparison = (unsigned int)negative > upos;

    printf("Bitwise (~unsigned char 255): %d\n", bitwise_not);
    printf("Suma (int + double): %.2f\n", sum);
    printf("Comparación (-1 as unsigned > 1): %s\n", comparison ? "true" : "false");

    return 0;
}

Análisis del código

En el primer caso, al realizar ~c, el compilador no opera sobre un byte de 8 bits. Debido a la promoción de enteros, c se trata como un int de 32 bits con el valor 0x000000FF. El operador bit a bit ~ invierte todos esos bits, resultando en 0xFFFFFF00. Si no existiera esta promoción, el resultado de ~0xFF sería 0x00, pero en la máquina real, el resultado es un valor negativo debido a cómo se interpreta ese int resultante.

En la suma i + d, aplicamos las conversiones aritméticas habituales. Como d es de tipo double (un tipo de punto flotante de mayor rango que int), el operando i se convierte de int a double antes de que la CPU ejecute la instrucción de suma. Esto garantiza que la precisión de la coma flotante no se pierda en el proceso.

Finalmente, en la comparación (unsigned int)negative > upos, hemos forzado la conversión de -1 a unsigned int. Dado que los tipos con signo y sin signo del mismo rango tienen reglas de conversión específicas, el valor -1 se representa internamente como un número extremadamente grande en su forma sin signo (todos los bits en uno). Esto hace que la comparación sea verdadera, un comportamiento que suele romper la lógica de validación de límites en sistemas embebidos.

El error frecuente
Un error clásico ocurre al comparar un entero con signo con uno sin signo en una estructura condicional:

int limite = -5;
unsigned int valor = 10;

if (valor > limite) {
    // Este bloque NO se ejecutará.
    // 'limite' se convierte en unsigned int: 4294967291.
    // 10 > 4294967291 es FALSO.
}

Este error es sutil porque el código compila sin advertencias si no tienes activadas las banderas de seguridad. Para detectarlo, debes usar -Wsign-compare en gcc o clang. Si el error es un desbordamiento de tipo en una conversión implícita, AddressSanitizer o -Wconversion te avisarán que estás perdiendo información.

72

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio