std::vector es un contenedor de la biblioteca estándar que implementa un array dinámico. A diferencia de un array tradicional de C, el std::vector puede cambiar su tamaño durante la ejecución de forma automática. Internamente, gestiona un bloque de memoria contiguo en el heap, lo que garantiza que los elementos estén uno al lado del otro en la memoria física.
Este diseño responde a una necesidad de rendimiento: al estar los datos contiguos, la CPU puede predecir con precisión qué memoria necesitará a continuación, aprovechando la caché de la CPU de forma extremadamente eficiente. Cuando el espacio asignado se agota tras un push_back, el vector realiza una realocación: reserva un nuevo bloque de memoria (normalmente el doble del tamaño actual), mueve los elementos al nuevo bloque y libera el antiguo.
Debes usar std::vector casi siempre que necesites una secuencia de elementos donde el acceso aleatorio sea frecuente y la inserción al final sea la operación principal. Es la opción por defecto debido a su excelente equilibrio entre velocidad de acceso y gestión de memoria. Sin embargo, si intentas acceder a un índice que no existe usando el operador [], entrarás en el terreno del Comportamiento Indefinido (Undefined Behavior), lo que puede causar un crash inmediato o, peor aún, que el programa continúe con datos corruptos sin avisar.
#include <iostream>
#include <vector>
#include <string>
struct Producto {
std::string nombre;
double precio;
// Constructor para demostrar la eficiencia de emplace_back
Producto(std::string n, double p) : nombre(std::move(n)), precio(p) {}
void imprimir() const {
std::cout << " - " << nombre << ": $" << precio << "\n";
}
};
int main() {
// 1. Inicialización con initializer_list
std::vector<Producto> inventario = {
{"Manzana", 0.5},
{"Pan", 1.2}
};
// 2. Gestión de capacidad: reserve() evita realocaciones innecesarias
// Sabemos que planeamos tener al menos 5 productos.
// Esto reserva espacio en memoria sin crear objetos todavía.
inventario.reserve(5);
// 3. push_back vs emplace_back
// push_back requiere un objeto ya construido (crea un temporal)
inventario.push_back(Producto("Leche", 1.1));
// emplace_back construye el objeto directamente en la memoria del vector.
// Pasamos los argumentos del constructor de Producto directamente.
// Esto evita la creación de un objeto temporal y la posterior copia/movimiento.
inventario.emplace_back("Huevo", 2.5);
// 4. Acceso a elementos
std::cout << "Contenido del inventario:\n";
for (size_t i = 0; i < inventario.size(); ++i) {
// operator[] es rápido pero no comprueba límites
std::cout << i << ": " << inventario[i].nombre << "\n";
}
// 5. Acceso seguro con at()
try {
// at() lanza std::out_of_range si el índice es inválido
std::cout << "Acceso seguro al índice 1: " << inventario.at(1).nombre << "\n";
} catch (const std::out_of_range& e) {
std::cerr << "Error: " << e.what() << "\n";
}
// 6. Propiedades de capacidad y tamaño
std::cout << "Tamaño (size): " << inventario.size() << "\n";
std::cout << "Capacidad (capacity): " << inventario.capacity() << "\n";
// 7. Modificación y limpieza
inventario.resize(6); // Expande el vector, creando elementos por defecto
inventario.clear(); // Destruye todos los elementos, pero mantiene la capacidad
return 0;
}
Análisis detallado
Fíjate en cómo inventario.reserve(5) cambia el comportamiento del contenedor. Al llamar a reserve, modificamos la capacidad (capacity), que es el espacio total reservado en memoria, sin alterar el tamaño (size), que es la cantidad de elementos que realmente existen. Esto es crucial para el rendimiento: si sabes que vas a insertar 1000 elementos, llama a reserve(1000) al principio para evitar que el vector tenga que reasignar y copiar toda la información cada vez que se quede sin espacio.
En la línea inventario.emplace_back("Huevo", 2.5), estamos aprovechando la construcción in-place. Mientras que push_back necesita un objeto Producto ya creado para poder copiarlo dentro del vector, emplace_back toma los argumentos "Huevo" y 2.5 y los pasa directamente al constructor del Producto dentro de la memoria ya reservada del vector. Esto elimina un paso de copia innecesario.
Cuando usamos inventario[i], el compilador simplemente calcula la dirección de memoria (dirección base + índice * tamaño del objeto) y accede. Es extremadamente rápido. En cambio, inventario.at(1) realiza una comprobación lógica: ¿Es el índice menor que el tamaño?. Si la respuesta es no, lanza una excepción. En código de alto rendimiento, usa []; en código donde la robustez ante errores de lógica es prioritaria, usa at().
Finalmente, inventario.clear() es una operación común pero malentendida. Al llamar a clear(), los objetos Producto se destruyen (sus destructores se ejecutan), pero la memoria sigue reservada. El capacity() seguirá siendo el mismo, lo que permite reutilizar ese espacio si decides volver a llenar el vector sin necesidad de nuevas asignaciones de memoria al sistema operativo.
El error frecuente
El error más peligroso con std::vector es la invalidación de iteradores o referencias debido a una realocación.
std::vector<int> numeros = {1, 2, 3};
int& referencia_al_primer_elemento = numeros[0]; // Referencia a memoria interna
numeros.emplace_back(4); // Esto podría disparar una realocación
// Si el vector tuvo que moverse a un nuevo bloque de memoria para crecer...
// ¡referencia_al_primer_elemento ahora apunta a memoria liberada (dangling reference)!
std::cout << referencia_al_primer_elemento; // Undefined Behavior: Crash o basura.
Si el vector crece y necesita más espacio, la dirección de memoria de todos sus elementos cambia. Cualquier puntero, referencia o iterador que tuvieras antes de la inserción queda invalidado. Si sospechas que el vector va a crecer, usa reserve() previamente para asegurar que la capacidad sea suficiente y evitar que la memoria se desplace.
N° 83