Tipos enteros: Tamaños garantizados y no garantizados

Los tipos enteros en C++ representan números sin decimales, pero su tamaño no es una constante universal. El estándar define un rango mínimo de capacidad, pero deja la decisión del tamaño exacto en manos del compilador y la arquitectura de la CPU. Esto se hace para optimizar el rendimiento: si tu procesador trabaja de forma nativa con 32 bits, usar un int de ese tamaño es mucho más eficiente que forzar un tipo de 64 bits. Sin embargo, esto introduce un problema de portabilidad crítico: un long puede medir 32 bits en Windows (modelo LLP64) pero 64 bits en Linux/macOS (modelo LP64). Si escribes código asumiendo que long siempre tiene el mismo tamaño, romperás tu programa al compilarlo en otra plataforma.

Para evitar este caos, cuando el tamaño sea crítico —como al diseñar un protocolo de red, leer un archivo binario o definir una estructura que se guardará en disco— debes usar los tipos de ancho fijo definidos en <cstdint>. Tipos como int32_t garantizan exactamente 32 bits, sin importar si estás en un microcontrolador o en un servidor. Por otro lado, para manejar tamaños de memoria o índices de arrays, el estándar provee size_t, un tipo unsigned (sin signo) que garantiza ser lo suficientemente grande como para indexar cualquier objeto en la máquina actual. Si necesitas calcular la diferencia entre dos punteros, ptrdiff_t es la opción correcta, ya que esa diferencia puede ser negativa.

Si usas mal estos tipos, te enfrentarás a desbordamientos (overflow) silenciosos o errores de comparación entre tipos con signo y sin signo que el compilador podría no detectar, pero que destruirán la lógica de tu programa.

#include <iostream>
#include <cstdint>   // Para tipos de ancho fijo (int32_t, uint64_t, etc.)
#include <cstddef>   // Para size_t y ptrdiff_t
#include <vector>

// Función para imprimir el tamaño de un tipo de forma genérica
template <typename T>
void imprimir_info(std::string_view nombre, T valor) {
    std::cout << nombre << ": " << valor 
              << " (bytes: " << sizeof(T) << ")\n";
}

int main() {
    // 1. Tipos estándar (tamaño variable según la plataforma)
    int i_std = 2147483647;
    long l_std = 2147483647;
    long long ll_std = 2147483647LL;

    std::cout << "--- Tipos Estándar ---\n";
    imprimir_info("int", i_std);
    imprimir_info("long", l_std);
    imprimir_info("long long", ll_std);

    // 2. Tipos de ancho fijo (garantizados por <cstdint>)
    // Úsalos para serialización o hardware.
    int32_t i32 = 100;
    uint64_t u64 = 100ULL;
    
    std::cout << "\n--- Tipos de Ancho Fijo (Exactos) ---\n";
    imprimir_info("int32_t", i32);
    imprimir_info("uint64_t", u64);

    // 3. Tipos para memoria y aritmética de punteros
    std::vector<int> vec = {10, 20, 30, 40, 50};
    
    // size_t: Siempre unsigned, ideal para tamaños y rangos de memoria
    std::size_t tamano = vec.size();
    
    // ptrdiff_t: Signed, para la distancia entre punteros
    std::ptrdiff_t diferencia = &vec[4] - &vec[0];

    std::cout << "\n--- Tipos de Sistema ---\n";
    imprimir_info("size_t (vec.size())", tamano);
    imprimir_info("ptrdiff_t (offset)", diferencia);

    return 0;
}

En el código anterior, fíjate en cómo imprimir_info utiliza sizeof para revelar la realidad del hardware. Si ejecutas esto en Windows, verás que long y int suelen medir lo mismo (32 bits), pero en un sistema Linux de 64 bits, long saltará a los 64 bits, mientras que int se mantendrá en 32. Esta es la razón por la cual int32_t es la única forma de asegurar que tu variable i32 ocupe siempre exactamente 4 bytes.

Cuando llamamos a vec.size(), el tipo devuelto es std::size_t. Es fundamental que, cuando trabajes con contenedores de la STL, uses size_t para tus contadores de bucles; de lo contrario, podrías tener problemas de comparación si intentas comparar un int con un size_t. La variable diferencia usa std::ptrdiff_t porque la resta de dos direcciones de memoria (&vec[4] - &vec[0]) es una operación aritmética de punteros que puede dar como resultado un valor negativo si restas el puntero mayor al menor.

El uso de uint64_t para u64 garantiza que, sin importar la arquitectura, ese valor siempre ocupará 64 bits de memoria, lo cual es vital si ese dato se va a enviar por un socket o se va a escribir en un archivo binario que será leído por otra máquina.

El error frecuente

Un error clásico ocurre al comparar un entero con signo con uno sin signo, algo extremadamente común cuando usas size_t para evitar warnings de compilación.

#include <iostream>
#include <vector>

void error_critico() {
    std::vector<int> datos = {1, 2, 3};
    int limite = -1; // Queremos decir "no hay límite" o "vacío"

    // Error: -1 convertido a size_t es un número gigantesco (ej. 18446744073709551615)
    if (limite < datos.size()) {
        std::cout << "Esto no se imprimirá como esperas...\n";
    }
    
    // El resultado de (limite < datos.size()) es FALSE porque -1 es un valor 
    // inmenso en el mundo de los unsigned.
}

Este bug es silencioso y no suele disparar advertencias si no compilas con -Wsign-compare en GCC o Clang. Para detectarlo, usa siempre AddressSanitizer (-fsanitize=undefined) o activa los warnings de comparación de signos. La regla de oro: si vas a comparar un índice o un tamaño, asegúrate de que ambos tipos sean del mismo “lado” de la línea (ambos con signo o ambos sin signo).

15

Dejar un comentario

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

Scroll al inicio