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
- Entender cómo los datos se almacenan como bits y bytes en memoria
- Conocer los tipos primitivos y sus tamaños garantizados
- Dominar la diferencia entre signed y unsigned
- Comprender el punto flotante y sus limitaciones
- Saber cuándo usar cada tipo de dato
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.
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 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 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 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 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 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 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 ocupan el mismo espacio, tienen propósitos semánticos diferentes.
Checklist
- Entiendo que 1 byte = 8 bits y puede representar 256 valores
- Sé la diferencia entre signed y unsigned
- Puedo calcular el rango de un tipo: 2^N valores
- Sé cuándo usar
int32_tvsuint32_t - Entiendo por qué los floats no son exactos
- Nunca comparo floats con
== - Uso sufijos (
f,U,LL) correctamente
Preguntas para reflexionar
¿Qué pasa si asigno 300 a un uint8_t? (max = 255)
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?
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?
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?
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?
Ocupan exactamente lo mismo:
double= 8 bytesfloat= 4 bytes × 2 = 8 bytes
Pero double tiene mayor precisión (~15-16 dígitos) que float (~7 dígitos).
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.