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 y bibliotecas usan bitpacking para optimizar el uso de memoria.

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.

Tipos especiales para propósitos específicos

size_t: El tipo para tamaños y conteos

size_t es fundamental en C++ para trabajar con tamaños y índices de manera portable.

#include <cstddef>  // Define size_t
#include <vector>
#include <string>

// size_t es el tipo correcto para:
// 1. Resultados de sizeof()
size_t tamañoInt = sizeof(int);        // Típicamente 4

// 2. Tamaños de contenedores
std::vector<int> vec = {1, 2, 3, 4, 5};
size_t elementos = vec.size();         // 5 elementos

// 3. Índices en loops (evita warnings de comparación)
for (size_t i = 0; i < vec.size(); ++i) {
    std::cout << vec[i] << " ";
}

// 4. Operaciones con strings
std::string texto = "Hola mundo";
size_t longitud = texto.length();      // 10 caracteres

// ⚠️ CUIDADO: size_t es UNSIGNED
size_t a = 10;
size_t b = 20;
// size_t diff = a - b;  // ¡OVERFLOW! No da -10, da un número enorme

// Solución para restas:
if (a > b) {
    size_t diff = a - b;  // Seguro solo si a > b
}
Trampa común con size_t

Como size_t es unsigned, restar puede causar overflow silencioso:

// MAL - Loop infinito si vec está vacío
for (size_t i = vec.size() - 1; i >= 0; --i) {  // i nunca es < 0
    // Cuando i=0 y se decrementa, i se vuelve SIZE_MAX (enorme)
}

// BIEN - Evitar la comparación con 0
for (size_t i = vec.size(); i > 0; --i) {
    size_t index = i - 1;  // Usar index para acceder
    std::cout << vec[index];
}

ptrdiff_t: Para diferencias entre punteros

ptrdiff_t es el equivalente con signo de size_t.

#include <cstddef>  // Define ptrdiff_t

int arr[] = {10, 20, 30, 40, 50};
int* ptr1 = &arr[1];
int* ptr2 = &arr[4];

// ptrdiff_t puede ser negativo
ptrdiff_t diff = ptr2 - ptr1;  // 3
ptrdiff_t diff2 = ptr1 - ptr2; // -3

// Útil cuando necesitas índices que pueden ser negativos
ptrdiff_t buscar(const std::vector<int>& vec, int valor) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (vec[i] == valor) {
            return static_cast<ptrdiff_t>(i);
        }
    }
    return -1;  // Valor no encontrado (imposible con size_t)
}
¿Por qué size_t existe? Intermedio

Portabilidad: En un sistema de 32 bits, size_t es de 32 bits (puede indexar hasta 4GB). En un sistema de 64 bits, es de 64 bits (puede indexar mucho más). Si usaras int (siempre 32 bits), no podrías trabajar con arrays mayores a 2GB en sistemas de 64 bits.

Semántica clara: Al ver size_t, sabes inmediatamente que representa un tamaño o contador, no un valor numérico general.

Evita warnings: Los compiladores modernos advierten cuando comparas signed con unsigned. Usar size_t consistentemente evita estos warnings.

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 →