Memoria & Bajo Nivel

Capítulo 3: Tipos de Datos Primitivos

En lenguajes como JavaScript o Python, no piensas mucho en cuánta memoria usa una variable. En C++, tú tienes el control total. Aquí aprenderás cómo los datos se representan realmente en la memoria de tu computadora.

Objetivos

Conceptos fundamentales: bits y bytes

Antes de hablar de tipos de datos en C++, necesitas entender cómo funciona la memoria a nivel fundamental.

¿Qué es un bit?

Un bit (binary digit) es la unidad más pequeña de información en una computadora. Solo puede tener dos valores: 0 o 1. Esta representación binaria es fundamental porque los transistores en un chip solo pueden estar en dos estados: apagado (0) o encendido (1).

Toda la información en tu computadora—texto, imágenes, videos, programas—se reduce finalmente a secuencias de bits.

¿Qué es un byte?

Un byte es un grupo de 8 bits. Es la unidad básica de memoria direccionable en la mayoría de computadoras modernas. Con 8 bits puedes representar 2^8 = 256 valores diferentes (de 0 a 255 si es unsigned, o de -128 a 127 si es signed).

Por ejemplo, un byte con el patrón 10100110 puede representar el número 166, o el carácter '¦', o parte de una instrucción de máquina—depende del contexto.

Anatomía de un Byte

flowchart LR
    subgraph "Un byte = 8 bits"
    B7["1"] --- B6["0"] --- B5["1"] --- B4["0"] --- B3["0"] --- B2["1"] --- B1["1"] --- B0["0"]
    end
    MSB["MSB<br/>(Bit más significativo)"] -.-> B7
    LSB["LSB<br/>(Bit menos significativo)"] -.-> B0
    Dec["En decimal: 128+32+4+2 = 166"] -.-> B7
    Hex["En hexadecimal: 0xA6"] -.-> B7

    style B7 fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style B6 fill:#f5f5f5,stroke:#757575,stroke-width:2px
    style B5 fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style B4 fill:#f5f5f5,stroke:#757575,stroke-width:2px
    style B3 fill:#f5f5f5,stroke:#757575,stroke-width:2px
    style B2 fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style B1 fill:#ffcdd2,stroke:#c62828,stroke-width:2px
    style B0 fill:#f5f5f5,stroke:#757575,stroke-width:2px
Cuantos valores puede representar N bits? Intermedio

Con N bits, puedes representar 2^N valores diferentes:

1 bit  → 2 ^ 1 = 2 valores(0, 1) 2 bits → 2 ^ 2 =
                     4 valores(00, 01, 10, 11) 3 bits → 2 ^ 3 =
                         8 valores(000 a 111) 8 bits → 2 ^ 8 =
                             256 valores(0 a 255) 16 bits → 2 ^ 16 = 65,
             536 valores 32 bits → 2 ^ 32 = 4, 294, 967,
             296 valores 64 bits → 2 ^ 64 = 18, 446, 744, 073, 709, 551,
             616 valores

Por eso un uint8_t (unsigned 8 bits) puede almacenar valores de 0 a 255, y un uint32_t puede almacenar hasta ~4.3 billones.

Tipos enteros con tamaño fijo

En C++, los tipos como int, long, etc. pueden tener diferentes tamaños dependiendo de la plataforma (32-bit vs 64-bit, compilador, etc.). Esto es un problema para código portable.

Problema de portabilidad
int x = 100;
// Cuantos bytes ocupa x?
// - En Windows de 32 bits: 4 bytes
// - En algunos sistemas embebidos: 2 bytes
// - Depende del compilador!

Para evitar esto, C++11 introdujo tipos con tamaño garantizado en el header <cstdint>:

El código

#include <cstdint>
#include <iostream>

int main() {
    // Enteros CON signo (signed): pueden ser negativos
    std::int8_t tiny = -128;                      // 1 byte: -128 a 127
    std::int16_t small = -32000;                  // 2 bytes: -32,768 a 32,767
    std::int32_t medium = -2000000000;            // 4 bytes: -2^31 a 2^31-1
    std::int64_t large = -9000000000000000000LL;  // 8 bytes

    // Enteros SIN signo (unsigned): solo positivos
    std::uint8_t u_tiny = 255;                        // 1 byte: 0 a 255
    std::uint16_t u_small = 65000;                    // 2 bytes: 0 a 65,535
    std::uint32_t u_medium = 4000000000U;             // 4 bytes: 0 a 2^32-1
    std::uint64_t u_large = 18000000000000000000ULL;  // 8 bytes

    std::cout << "int32_t: " << medium << "
";
    std::cout << "uint32_t: " << u_medium << "
";

    return 0;
}

Desglose de la nomenclatura

Anatomía de std::int32_t

flowchart TB
    A["std::int32_t"] --> B["std"]
    A --> C["int"]
    A --> D["32"]
    A --> E["_t"]

    B --> B1["namespace de la<br/>biblioteca estándar"]
    C --> C1["integer (entero)"]
    D --> D1["Número de BITS<br/>(no bytes!)"]
    E --> E1["'type' (convención<br/>de naming)"]

    F["Ejemplos:"] --> F1["int8_t → 8 bits = 1 byte, con signo"]
    F --> F2["uint16_t → 16 bits = 2 bytes, sin signo"]
    F --> F3["int64_t → 64 bits = 8 bytes, con signo"]

    style A fill:#e1f5fe,stroke:#0277bd,stroke-width:3px
    style B fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style C fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px
    style D fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style E fill:#fce4ec,stroke:#c2185b,stroke-width:2px

Los tipos signed y unsigned

Signed (con signo): puede representar números positivos y negativos.

Unsigned (sin signo): solo números positivos (incluido el 0).

Ambos usan la misma cantidad de bits, pero los interpretan diferente. Por ejemplo, con 8 bits: int8_t va de -128 a 127, mientras que uint8_t va de 0 a 255.

determinan si un tipo puede representar números negativos.

int8_t vs uint8_t

flowchart TB
    subgraph S["int8_t (signed - con signo)"]
    S1["8 bits = 2^8 = 256 valores posibles"]
    S2["Rango: -128 a 127"]
    S3["Incluye negativos, 0 y positivos"]
    end

    subgraph U["uint8_t (unsigned - sin signo)"]
    U1["8 bits = 2^8 = 256 valores posibles"]
    U2["Rango: 0 a 255"]
    U3["Solo positivos (incluye 0)"]
    end

    style S fill:#ffebee,stroke:#c62828,stroke-width:2px
    style U fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
    style S1 fill:#fff3e0,stroke:#e65100,stroke-width:1px
    style S2 fill:#fff3e0,stroke:#e65100,stroke-width:1px
    style S3 fill:#fff3e0,stroke:#e65100,stroke-width:1px
    style U1 fill:#f1f8e9,stroke:#558b2f,stroke-width:1px
    style U2 fill:#f1f8e9,stroke:#558b2f,stroke-width:1px
    style U3 fill:#f1f8e9,stroke:#558b2f,stroke-width:1px
¡Overflow silencioso!
std::uint8_t x = 255;
x = x + 1;                         // Que valor tiene x ahora?
std::cout << static_cast<int>(x);  // Imprime: 0

// ¡El valor se "envuelve" (wraps around)!
// 255 + 1 = 0 en aritmética modular (mod 256)

C++ no lanza errores en overflow de enteros unsigned. Simplemente "da la vuelta". Con signed, es undefined behavior (¡aún peor!).

Como se representan los números negativos? (Complemento a 2) Avanzado

Los números negativos se representan usando complemento a dos (two's complement):

Para representar - 5 en int8_t : 1. Escribe 5 en binario
    : 00000101 2. Invierte todos los bits : 11111010(complemento a 1)3. Suma 1
    : 11111011(complemento a 2)

        Resultado : 11111011 = -5

Por que este sistema?

  • La suma funciona igual para positivos y negativos (hardware más simple)
  • Solo hay un cero (no existe +0 y -0)
  • El bit más significativo indica el signo (1 = negativo, 0 = positivo)

Sufijos de literales numéricos

Los sufijos le dicen al compilador qué tipo usar para un número literal:

42         // int (por defecto)
    42U    // unsigned int
    42L    // long
    42UL   // unsigned long
    42LL   // long long (64 bits)
    42ULL  // unsigned long long
Cuando usar unsigned?

Usa unsigned cuando:

  • El valor nunca puede ser negativo (ej: contador, índice, tamaño)
  • Necesitas el rango extra (uint32_t va hasta ~4.3B vs int32_t ~2.1B)
  • Trabajas con bits/máscaras (bitwise operations)

Evita mezclar signed y unsigned en la misma expresión:

int a = -1;
unsigned int b = 1;
if (a < b) {  // ⚠️ a se convierte a unsigned
    // Este bloque NUNCA se ejecuta!
    // -1 se interpreta como 4,294,967,295 (unsigned)
}

Números de punto flotante

Para representar números con decimales, C++ usa el estándar IEEE 754:

#include <iostream>

int main() {
    float f = 3.14f;               // 32 bits (sufijo 'f' obligatorio)
    double d = 3.141592653589793;  // 64 bits (por defecto)

    std::cout << "float:  " << f << "
";
    std::cout << "double: " << d << "
";

    return 0;
}

Tamaños y Precisión

flowchart LR
    subgraph "Tipos de Punto Flotante"
    F["float"] --> F1["4 bytes / 32 bits"]
    F1 --> F2["~7 dígitos de precisión"]
    F2 --> F3["Rango: ±3.4 × 10^38"]

    D["double"] --> D1["8 bytes / 64 bits"]
    D1 --> D2["~15-16 dígitos de precisión"]
    D2 --> D3["Rango: ±1.7 × 10^308"]
    end

    style F fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
    style D fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style F1 fill:#e8eaf6,stroke:#3949ab,stroke-width:1px
    style F2 fill:#e8eaf6,stroke:#3949ab,stroke-width:1px
    style F3 fill:#e8eaf6,stroke:#3949ab,stroke-width:1px
    style D1 fill:#f3e5f5,stroke:#8e24aa,stroke-width:1px
    style D2 fill:#f3e5f5,stroke:#8e24aa,stroke-width:1px
    style D3 fill:#f3e5f5,stroke:#8e24aa,stroke-width:1px
¡Los floats NO son exactos!

Los números de punto flotante son aproximaciones. Esto puede causar errores sutiles:

float a = 0.1f;
float b = 0.2f;
float c = a + b;

if (c == 0.3f) {
    std::cout << "Igual
";
} else {
    std::cout << "Diferente
";  // ← ¡Se imprime esto!
}

// c es 0.30000001192092896 (aproximadamente)

Solución: Nunca compares floats con ==. Usa un epsilon:

#include <cmath>

const float EPSILON = 0.00001f;
if (std::abs(c - 0.3f) < EPSILON) {
    std::cout << "Suficientemente cercano
";
}

Los valores especiales NaN e Infinity

IEEE 754 define valores especiales para situaciones excepcionales:

Peculiaridad: NaN != NaN siempre es verdadero. Es la única entidad en C++ que no es igual a sí misma.

permiten manejar casos extremos en cálculos de punto flotante.

Otros tipos primitivos

Boolean

bool is_valid = true;  // true or false
bool is_ready = false;

// En memoria: típicamente 1 byte (aunque solo usa 1 bit de info)
Por qué bool ocupa 1 byte si solo necesita 1 bit?

Por eficiencia de acceso a memoria. La mayoría de CPUs no pueden direccionar bits individuales, solo bytes completos. Usar 1 byte permite acceso directo más rápido.

(Aunque en arrays de bools, algunos compiladores optimizan con bitpacking)

Carácter

char letra = 'A';  // 1 byte
// Internamente: ASCII 65 = 0x41 = 01000001

std::cout << letra << "\n";                    // A
std::cout << static_cast<int>(letra) << "\n";  // 65

Aunque char vs int8_t

Técnicamente ambos ocupan 1 byte, pero tienen propósitos diferentes:

char puede ser signed o unsigned dependiendo del compilador. Para texto usa char, para números de 1 byte usa int8_t o uint8_t.

ocupan el mismo espacio, tienen propósitos semánticos diferentes.

Checklist

Preguntas para reflexionar

¿Qué pasa si asigno 300 a un uint8_t? (max = 255)
Respuesta:

Se produce un overflow. El valor se 'envuelve' usando aritmética modular: 300 % 256 = 44. El resultado sería 44.

C++ no lanza errores en overflow de unsigned. Con signed es undefined behavior (puede causar cualquier cosa).

¿Por qué float x = 0.1 + 0.2; no resulta exactamente en 0.3?
Respuesta:

Los números de punto flotante son aproximaciones binarias. 0.1 y 0.2 no se pueden representar exactamente en binario (como 1/3 = 0.333... en decimal).

El resultado es aproximadamente 0.30000001192092896. Por eso nunca debes comparar floats con ==, usa un epsilon: std::abs(x - 0.3f) < 0.00001f

¿Cuál es el mayor número que puede almacenar un int16_t?
Respuesta:

int16_t es signed (con signo) de 16 bits = 2 bytes.

Rango: -32,768 a 32,767

Cálculo: 2^15 - 1 = 32,767 (el bit 16 se usa para el signo)

¿Por qué mezclar signed y unsigned en comparaciones es peligroso?
Respuesta:

C++ convierte automáticamente el signed a unsigned, lo que puede causar resultados inesperados:

int a = -1;
unsigned int b = 1;
if (a < b) ( /* NUNCA se ejecuta! */ )

-1 se convierte a unsigned y se interpreta como 4,294,967,295, que es mayor que 1.

¿Qué ocupa más espacio: un double o dos float?
Respuesta:

Ocupan exactamente lo mismo:

  • double = 8 bytes
  • float = 4 bytes × 2 = 8 bytes

Pero double tiene mayor precisión (~15-16 dígitos) que float (~7 dígitos).

Próximo Capítulo

En el Capítulo 3 aprenderás sobre enumeraciones (enum class), una forma de crear tus propios tipos con valores nombrados de manera segura. También profundizaremos en switch statements.

Siguiente capítulo: Enumeraciones →