Arrays, std::vector y std::string
Del bloque de tamaño fijo al contenedor que crece y se libera solo
Un array crudo es rápido pero no sabe cuánto mide ni crece; std::array añade tamaño y seguridad sin coste; std::vector administra un buffer dinámico con size y capacity. Con std::string aplicarás las mismas ideas a texto.
- Elegir entre array crudo, std::array y std::vector según el problema
- Explicar la diferencia entre size y capacity y el efecto de push_back
- Reservar espacio con reserve y acceder con at cuando importa la seguridad
- Manipular texto con std::string y convertir números con stoi y to_string
- Evitar punteros e iteradores invalidados por una reasignación
Antes necesitas: RAII y punteros inteligentes · Referencias, new/delete y fugas de memoria
En esta lección
- ¿Qué es un array y qué es un contenedor?
- ¿Para qué sirven estos contenedores?
- ¿Cómo funciona por dentro?
- Sintaxis
- Ejemplo básico: recorrer y consultar un vector
- Ejemplo intermedio: reservar y calcular
- Caso práctico: analizar una lista de palabras
- Visualización: tamaño, capacidad y buffer
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Hasta ahora has visto arrays con int datos[5] y memoria dinámica con new[]. Ambos enfoques tienen el mismo problema: el tamaño no viaja con los datos. Un array crudo decae a puntero al pasarlo a una función y pierde la única información que tenía, su longitud. std::array y std::vector resuelven eso sin que pagues por ello.
La idea central de esta lección es que un contenedor es un objeto con tres responsabilidades: guardar los elementos, saber cuántos hay y liberar el buffer cuando muere. std::vector cumple las tres, crece cuando hace falta y aplica RAII. std::string es, en esencia, un vector de caracteres con operaciones de texto.
¿Qué es un array y qué es un contenedor?
Un array es una secuencia de elementos del mismo tipo colocados en posiciones contiguas de memoria. Esa contigüidad es lo que permite calcular la dirección de cualquier elemento con una multiplicación y acceder en tiempo constante. Un array crudo de C++ no es más que eso: un bloque, sin longitud almacenada y sin comprobaciones.
Un contenedor de la biblioteca estándar es una clase que envuelve ese bloque y añade una interfaz. std::array<T, N> guarda el bloque dentro del propio objeto y conoce N en tiempo de compilación. std::vector<T> guarda un puntero al buffer, un tamaño y una capacidad, y puede pedir un buffer nuevo cuando se queda corto. std::string hace lo mismo con char y añade búsqueda, subcadenas y conversiones.
| Tipo | Tamaño | Dónde vive el buffer | Conoce su longitud | Crece |
|---|---|---|---|---|
int a[10] | Fijo en compilación | Stack o dentro de otro objeto | No: hay que pasarla aparte | No |
std::array<int, 10> | Fijo en compilación | Dentro del objeto | Sí: .size() | No |
std::vector<int> | Variable en ejecución | Heap, gestionado por el vector | Sí: .size() | Sí, con push_back |
std::string | Variable en ejecución | Heap o buffer interno pequeño | Sí: .size() | Sí, con += o append |
¿Para qué sirven estos contenedores?
- Datos de tamaño desconocido: leer un CSV, acumular predicciones o guardar los píxeles de una imagen requiere crecer sobre la marcha.
- Pasar datos sin perder el tamaño:
const std::vector<double>&lleva elementos y longitud en un solo parámetro. - Seguridad en los límites:
at(i)lanzastd::out_of_rangesi te sales;v[i]no comprueba nada. - Rendimiento predecible: el buffer es contiguo, así que recorrer un vector es amigable con la caché y se vectoriza bien.
- Texto cómodo:
std::stringpermite concatenar, buscar y convertir sin gestionarchar*ni terminadores\0.
En Machine Learning, casi todo acaba siendo un vector o una matriz de números. Un lote de entrenamiento es un std::vector<double> o el buffer de una biblioteca de álgebra lineal; las etiquetas son otro vector; los nombres de las columnas, un std::vector<std::string>. Elegir bien el contenedor y reservar la capacidad correcta marca la diferencia entre un bucle que hace una sola reserva y otro que copia el buffer diez veces.
¿Cómo funciona por dentro?
std::vector mantiene tres valores: un puntero al inicio del buffer, el número de elementos construidos (size) y el número de elementos que caben sin reasignar (capacity). Mientras size < capacity, push_back construye el elemento al final y suma uno. Cuando size == capacity, pide un bloque nuevo —normalmente del doble de tamaño—, mueve o copia los elementos, destruye los viejos y libera el bloque anterior.
Ese crecimiento es amortizado constante: una operación concreta puede costar O(n) porque copia todo, pero repartida entre las n inserciones da O(1) por elemento. Si sabes de antemano cuántos elementos tendrás, reserve(n) elimina todas las reasignaciones intermedias. reserve cambia la capacidad, nunca el tamaño: el vector sigue teniendo los mismos elementos.
Sintaxis
#include <array>
#include <iostream>
#include <string>
#include <vector>
int main() {
int crudo[3] = {1, 2, 3}; // tamano fijo, sin size()
std::array<int, 3> fijo{4, 5, 6}; // fijo, pero con .size()
std::vector<int> dinamico{7, 8}; // crece en ejecucion
std::string texto = "hola"; // secuencia de char
dinamico.push_back(9);
std::cout << crudo[0] << '\n'; // 1
std::cout << fijo.size() << '\n'; // 3
std::cout << dinamico.size() << '\n'; // 3
std::cout << texto.size() << '\n'; // 4
std::cout << texto + " mundo" << '\n'; // hola mundo
return 0;
}std::arrayse declara con dos parámetros: el tipo y el número de elementos, que debe ser una constante de compilación.std::vectorystd::stringse inicializan con llaves, con un tamaño o vacíos. El vector vacío tienesize() == 0ycapacity() == 0.push_backañade una copia al final;emplace_backconstruye el elemento directamente en el buffer y evita una copia en objetos grandes.size()devuelvestd::size_t, un entero sin signo. Comparar con unintnegativo genera avisos con-Wsign-compare.
Ejemplo básico: recorrer y consultar un vector
#include <iostream>
#include <vector>
int main() {
std::vector<int> v{10, 20, 30};
v.push_back(40);
std::cout << v.size() << '\n'; // 4
std::cout << v.front() << ' ' << v.back() << '\n'; // 10 40
std::cout << v.at(2) << '\n'; // 30
std::cout << (v.capacity() >= v.size()) << '\n'; // 1
for (int x : v) {
std::cout << x << ' ';
}
std::cout << '\n'; // 10 20 30 40
return 0;
}front() y back() devuelven referencias al primero y al último; sobre un vector vacío son comportamiento indefinido. at(2) comprueba el índice y devuelve 30; si hubieras pedido at(10), se lanzaría std::out_of_range. La comparación de capacidad se imprime como 1 porque la capacidad siempre es al menos el tamaño. El rango for (int x : v) copia cada entero: con objetos grandes escribe const auto&.
Ejemplo intermedio: reservar y calcular
#include <iostream>
#include <vector>
double media(const std::vector<double>& datos) {
if (datos.empty()) {
return 0.0;
}
double suma = 0.0;
for (double x : datos) {
suma += x;
}
return suma / static_cast<double>(datos.size());
}
int main() {
std::vector<double> datos;
datos.reserve(5); // una sola reserva para cinco elementos
for (int i = 1; i <= 5; ++i) {
datos.push_back(i * 1.5);
}
std::cout << datos.size() << '\n'; // 5
std::cout << (datos.capacity() >= 5) << '\n'; // 1
std::cout << media(datos) << '\n'; // 4.5
return 0;
}reserve(5) pide espacio para cinco elementos, pero size() sigue siendo 0 hasta que insertas. Los valores son 1.5, 3.0, 4.5, 6.0 y 7.5; su suma es 22.5 y la media, 4.5. La función recibe const std::vector<double>& para no copiar el buffer y no poder modificarlo. Ese static_cast<double> evita la división entera si size() se usara directamente con un acumulador entero.
Caso práctico: analizar una lista de palabras
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> palabras{"vector", "de", "cadenas", "en", "cpp"};
std::size_t total = 0;
std::string larga;
for (const std::string& p : palabras) {
total += p.size();
if (p.size() > larga.size()) {
larga = p;
}
}
std::cout << palabras.size() << '\n'; // 5
std::cout << total << '\n'; // 20
std::cout << larga << '\n'; // cadenas
return 0;
}El recorrido usa const std::string& porque cada palabra ya vive en el vector y no necesitamos copiarla. total es std::size_t para que la suma de longitudes no mezcle signos. La palabra más larga se actualiza comparando tamaños, no contenidos: larga = p copia la cadena ganadora. Este patrón —recorrer, acumular y quedarse con un extremo— es el mismo que usarás para calcular estadísticas de un lote de datos.
Para convertir texto en números, std::stoi y std::stod leen desde el principio de la cadena y lanzan std::invalid_argument si no hay dígitos. La operación inversa, std::to_string, produce la representación con el formato por defecto. No hay conversión implícita entre std::string y números: el compilador no adivina si "42" debe ser un entero o una cadena.
#include <iostream>
#include <string>
int main() {
std::string numero = "42";
int n = std::stoi(numero);
std::cout << n + 1 << '\n'; // 43
std::string texto = std::to_string(3.14);
std::cout << texto << '\n'; // 3.140000
try {
std::cout << std::stoi("abc") << '\n';
} catch (const std::invalid_argument&) {
std::cout << "no es un numero\n";
}
return 0;
}Visualización: tamaño, capacidad y buffer
Avanza por los pasos y fíjate en tres columnas: los elementos, size y capacity. Mientras la capacidad alcanza, push_back escribe en el buffer existente. Cuando se llena, el vector reserva otro bloque, copia y libera el anterior: por eso la dirección del buffer cambia. Los valores de capacidad son los típicos de duplicar el bloque; la norma no obliga a un factor concreto.
std::vector<int> v;v.push_back(10);v.push_back(20);v.push_back(30);std::cout << v.size() << ' ' << v.capacity() << '\n';v.reserve(8);std::cout << v.capacity() << '\n';size y capacity valen 0.—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Errores comunes
- Salirse con
[]:v[v.size()]no compila mal ni avisa; es comportamiento indefinido y puede leer memoria ajena. Usaat()cuando el índice venga de fuera. - Confundir
sizeycapacity:v.capacity()no dice cuántos elementos hay. Un vector con capacidad 8 y tamaño 0 está vacío; recorrerlo hastacapacity()es un error. - Guardar
data()antes de crecer: tras unpush_backque reasigna, el puntero anterior apunta a memoria liberada. Vuelve a llamar adata()después de la inserción. - Usar
sizeofpara contar:sizeof(v) / sizeof(v[0])sobre unstd::vectorda el tamaño del objeto, no el número de elementos. Utilizav.size(). - Olvidar la cabecera:
std::vectornecesita<vector>,std::arraynecesita<array>ystd::string,<string>. El error típico esno member named 'vector' in namespace 'std'. - Indexar con enteros negativos:
v[-1]convierte el-1a un índice gigante sin signo. El compilador no lo detiene y el acceso es inválido. - Convertir sin comprobar:
std::stoi("12abc")devuelve 12 porque se detiene en la primera letra;std::stoi("abc")lanza. Ninguna de las dos valida la cadena completa por ti.
Buenas prácticas
- Prefiere
std::vectorpor defecto; usastd::arraysi el tamaño es constante y pequeño, y el array crudo solo para interoperar con APIs de C. - Llama a
reservecuando conozcas el número aproximado de elementos; evitaresizesi solo querías capacidad. - Recorre con
for (const auto& x : v)para no copiar; usaauto&si necesitas modificar. - Accede con
aten fronteras del programa (entrada de usuario, archivos) y con[]en bucles internos ya acotados. - Pasa los contenedores como
const std::vector<T>&; devuélvelos por valor, el movimiento es barato. - No midas el rendimiento por intuición: un
reservemal puesto puede ser peor que dejar crecer el vector. - Usa
std::stringy sus métodos en lugar dechar*; para texto que no posees,std::string_viewevita copias.
datos = []
for i in range(1, 6):
datos.append(i * 1.5)
print(len(datos)) # 5
print(datos[-1]) # 7.5std::vector<double> datos;
for (int i = 1; i <= 5; ++i) {
datos.push_back(i * 1.5);
}
std::cout << datos.size(); // 5
std::cout << datos.back(); // 7.5Python oculta la capacidad y comprueba índices negativos; C++ expone `size` y `capacity` y no acepta índices negativos. Esa transparencia permite reservar de antemano, pero exige que tú vigiles los límites.
Ejercicios
Estadísticas de un vector
std::vector<int> notas{7, 4, 9, 6, 10}, calcula el mínimo, el máximo y la media con un solo recorrido. Imprime los tres valores separados por espacios y comprueba que la media es un número decimal, no 7.Pista
Inicializa el mínimo y el máximo con notas.front(). Acumula en un double y divide entre static_cast<double>(notas.size()).
Contar palabras largas
std::vector<std::string> con las palabras uno, computadora, de, algoritmo, ia. Cuenta cuántas tienen más de 4 caracteres e imprime la cuenta y la palabra más larga. No copies las cadenas al recorrerlas.Pista
Recorre con const std::string&. Para la más larga, guarda un std::string y actualízalo cuando encuentres una mayor.
std::vector<int> v; v.(10); // anade un elemento std::cout << v.() << '\n'; // 1 std::cout << v.(0) << '\n'; // 10, con comprobacion de limites
Añadir al final es `push_back`; contar elementos, `size`; acceder comprobando el índice, `at`.
Soluciones explicadas
En el primer ejercicio, el error habitual es dividir dos enteros y perder la parte decimal. La conversión explícita a double antes de dividir obliga a que la operación sea en coma flotante. En el segundo, la clave es distinguir entre el tamaño de una cadena y su contenido: p.size() > 4 cuenta caracteres, no palabras, y la comparación p.size() > mayor.size() actualiza la ganadora. En ambos casos el contenedor conoce su longitud y te ahorra pasar un tamaño aparte; ese es el motivo por el que std::vector reemplaza al array crudo en casi todo el código nuevo.
Resumen
- Un array crudo es un bloque contiguo sin longitud ni comprobaciones; decae a puntero al pasarlo.
std::array<T, N>guarda el tamaño en el tipo, vive en el stack y permite.size().std::vector<T>administra un buffer en el heap y crece conpush_backen tiempo amortizado constante.sizees cuántos elementos hay;capacitycuántos caben antes de reasignar.reservecambia solo la capacidad.- Una reasignación mueve el buffer e invalida punteros, referencias e iteradores anteriores.
atcomprueba límites y lanzastd::out_of_range;[]es más rápido y no comprueba.std::stringofrece tamaño, concatenación, búsqueda y conversiones;stoi/stodyto_stringcubren los números.- Elige el contenedor por el tamaño y las operaciones, no por costumbre.
Evaluación
Un std::vector<int> tiene size() == 3 y capacity() == 8. ¿Qué significa?
¿Qué provoca que un int* p = v.data(); quede colgante?
¿Cuál es la diferencia principal entre v[i] y v.at(i)?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.