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 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 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
}
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
- 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 - Sé cuándo usar
size_tpara tamaños e índices - Entiendo que
size_tes unsigned y sus trampas
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.