C++
std::vector: el array dinámico de la STL
El contenedor que reemplaza al array de estilo C: crece solo, conoce su tamaño y gestiona su propia memoria.
De dónde venimos
En Arrays y cadenas de estilo C apareció la primera forma de guardar muchos valores del mismo tipo bajo un solo nombre. Funciona, pero esa misma página dejó anotadas dos limitaciones que no tienen arreglo dentro del propio array:
- El tamaño es fijo y se decide al compilar.
int notas[5];son cinco notas para siempre. Si el usuario quiere ingresar seis, no hay a dónde ponerlas. - El array no sabe cuántos elementos tiene cuando lo pasas a una función. Como se vio en Arrays y funciones: la primera sorpresa, al pasarlo se degrada a un puntero y el tamaño se pierde por el camino: hay que mandarlo aparte, en un segundo parámetro, y confiar en no equivocarse.
std::vector es la respuesta de la Standard Library a esas dos cosas a la vez. Es el contenedor que vas a usar en la enorme mayoría de los casos donde antes habrías escrito un array.
Esta página no reemplaza lo que aprendiste sobre arrays: lo aprovecha. Los índices siguen empezando en 0, [] sigue siendo [], y el for basado en rango funciona igual. Lo que cambia es quién se encarga del tamaño y de la memoria.
Qué es un vector
Un std::vector es una secuencia de elementos del mismo tipo, contigua en memoria como un array, pero que puede crecer y encogerse mientras el programa corre. Guarda internamente cuántos elementos tiene, y pide o libera memoria por su cuenta cuando hace falta.
La analogía útil: un array de estilo C es una caja de huevos de 12 huecos —ni uno más, ni uno menos—; un vector es una bolsa que se estira sola a medida que le metes cosas.
Vive en el header <vector>, y como todo lo estándar, dentro del namespace std:
#include <vector>El <int> entre ángulos indica qué tipo de elementos guarda. std::vector<int> es un vector de enteros, std::vector<double> de decimales, std::vector<std::string> de textos. Esa sintaxis se llama plantilla (template) y por ahora basta con leerla como “vector de int”.
Declarar un vector
Dos formas, según sepas o no los valores iniciales:
#include <iostream>
#include <vector>
int main() {
std::vector<int> vacio; // 0 elementos, listo para crecer
std::vector<int> numeros = {1, 2, 3}; // 3 elementos desde el inicio
std::cout << "vacio tiene " << vacio.size() << " elementos\n";
std::cout << "numeros tiene " << numeros.size() << " elementos\n";
std::cout << "El primero de numeros es " << numeros[0] << "\n";
return 0;
}g++ -std=c++17 -Wall -o vectores vectores.cpp
./vectoresvacio tiene 0 elementos
numeros tiene 3 elementos
El primero de numeros es 1Fíjate en el primero: std::vector<int> vacio; no lleva paréntesis ni tamaño. Un vector recién creado tiene cero elementos y eso es perfectamente válido; un array de cero elementos, en cambio, no tiene sentido.
La segunda forma usa llaves ({1, 2, 3}), la misma inicialización con llaves (brace initialization) que ya usabas con arrays. Es la forma moderna y uniforme de dar valores iniciales en C++; si quieres el detalle completo de por qué se prefieren las llaves, está en Declaración e inicialización a fondo.
std::vector es una clase, no un tipo básico del lenguaje: por eso tiene funciones propias que se llaman con un punto (numeros.size()), igual que hacías con .size() sobre un std::string. Esas funciones que pertenecen a un objeto se llaman métodos, y de dónde salen se explica en Clases. Para usar un vector no necesitas haber leído esa página; solo saber que el punto significa “pídele esto a este objeto”.
Agregar y contar elementos
Aquí está la diferencia práctica más visible con un array. Tres métodos que vas a usar todo el tiempo:
| Método | Qué hace |
|---|---|
.push_back(valor) | Agrega valor al final, haciendo crecer el vector en uno |
.size() | Devuelve cuántos elementos hay ahora mismo |
.empty() | Devuelve true si no hay ninguno (equivale a .size() == 0, pero se lee mejor) |
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas; // empieza vacío
std::cout << "Vacío al principio: " << (notas.empty() ? "sí" : "no") << "\n";
notas.push_back(7); // se agrega al final
notas.push_back(4);
notas.push_back(6);
std::cout << "Ahora tiene " << notas.size() << " elementos\n";
std::cout << "Vacío ahora: " << (notas.empty() ? "sí" : "no") << "\n";
return 0;
}Vacío al principio: sí
Ahora tiene 3 elementos
Vacío ahora: noNingún array de estilo C puede hacer eso: no existe una forma de “agregarle” un elemento a int notas[3].
El tamaño viaja con el vector
Este es el otro problema resuelto. Compara la función de abajo con la ponerEnCero(int datos[], int cantidad) de la página de arrays: aquí no hay segundo parámetro con la cantidad, porque el vector la lleva consigo.
#include <iostream>
#include <vector>
// Sin segundo parámetro: el tamaño viene dentro del propio vector
double promedio(const std::vector<double>& datos) {
if (datos.empty()) {
return 0.0;
}
double suma = 0.0;
for (double valor : datos) {
suma += valor;
}
return suma / datos.size();
}
int main() {
std::vector<double> notas = {7.0, 4.5, 6.0};
std::cout << "Cantidad de notas: " << notas.size() << "\n";
std::cout << "Promedio: " << promedio(notas) << "\n";
return 0;
}Cantidad de notas: 3
Promedio: 5.83333El parámetro es const std::vector<double>&: una referencia constante, igual que la que usarías para un std::string grande. El & evita copiar todos los elementos en cada llamada, y el const deja claro que la función solo va a leerlos.
A diferencia de un array, un vector sí se copia si lo pasas por valor (double promedio(std::vector<double> datos), sin &). Con tres notas da igual, pero con un millón de elementos estarías duplicando un millón de elementos en cada llamada, en silencio. La regla práctica: pasa los contenedores por referencia constante salvo que de verdad necesites una copia propia.
Acceso seguro e inseguro a los elementos
Hay dos formas de leer o escribir un elemento por su índice, y la diferencia entre ellas importa:
v[i]— exactamente igual que en un array: rápido y sin ninguna comprobación de límites.v.at(i)— el mismo resultado, pero antes verifica queiesté dentro del rango.
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas = {7, 4, 6};
std::cout << "Primera con []: " << notas[0] << "\n";
std::cout << "Primera con .at(): " << notas.at(0) << "\n";
notas[2] = 10; // modificar, igual que en un array
std::cout << "Tercera modificada: " << notas.at(2) << "\n";
return 0;
}Primera con []: 7
Primera con .at(): 7
Tercera modificada: 10Que un vector sea “de la Standard Library” no lo hace seguro por defecto. notas[5] en un vector de 3 elementos es exactamente el mismo comportamiento indefinido que notas[5] en un array crudo: compila sin una sola advertencia, puede leer basura, corromper otros datos o crashear, y las tres cosas pueden pasar en ejecuciones distintas del mismo programa. El vector te salva de gestionar la memoria, no de equivocarte con los índices.
Con .at(), en cambio, el error se detecta y el programa se detiene con un mensaje claro en vez de seguir corriendo con datos corruptos:
#include <iostream>
#include <vector>
int main() {
std::vector<int> notas = {7, 4, 6}; // índices válidos: 0, 1 y 2
std::cout << "Antes del acceso inválido\n";
std::cout << notas.at(5) << "\n"; // no existe el índice 5
std::cout << "Esta línea nunca se ejecuta\n";
return 0;
}Antes del acceso inválido
terminate called after throwing an instance of 'std::out_of_range'
what(): vector::_M_range_check: __n (which is 5) >= this->size() (which is 3)Ese mensaje viene de una excepción, el mecanismo de C++ para señalar errores en ejecución. No hace falta entenderlo a fondo todavía: lo único que importa aquí es el contraste. [] fuera de rango puede fallar silenciosamente y arruinarte una tarde de depuración; .at() fuera de rango se detiene en el acto y te dice qué índice pediste y cuántos elementos había.
Regla práctica mientras aprendes: usa .at() cuando el índice venga de una fuente que no controlas (una cuenta, una entrada del usuario, un cálculo) y [] cuando estés recorriendo con un índice que tú mismo generaste y sabes correcto. Y siempre que puedas, evita el problema entero recorriendo con for basado en rango, donde no hay índice del que pasarse.
Recorrer un vector
Sin novedad: es la misma sintaxis del for basado en rango que ya usaste con arrays.
#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> tareas = {"compilar", "probar", "documentar"};
for (const std::string& tarea : tareas) {
std::cout << "- " << tarea << "\n";
}
std::cout << "Total: " << tareas.size() << " tareas\n";
return 0;
}- compilar
- probar
- documentar
Total: 3 tareasEl const std::string& de la variable del bucle es, otra vez, para no copiar cada texto en cada vuelta. Con tipos pequeños como int o double se escribe directo (for (int nota : notas)) porque copiar un número no cuesta nada.
También puedes usar el for clásico con índice cuando necesites saber en qué posición estás; solo recuerda que el límite es notas.size() en vez de una constante escrita a mano.
Dónde vive la memoria
Un array de estilo C declarado dentro de una función vive en la pila (stack): el espacio se reserva al entrar en el bloque y desaparece al salir, y su tamaño tiene que ser conocido al compilar. Por eso no puede crecer.
Un std::vector guarda sus elementos en el heap (también llamado memoria dinámica): una zona que se pide al sistema operativo mientras el programa corre y cuyo tamaño se decide sobre la marcha. Cuando el vector se queda sin espacio, pide un bloque más grande, mueve los elementos y libera el anterior. Todo eso ocurre solo, sin que escribas una línea.
Aquí conviene detenerse un segundo, porque es el punto más importante de la página. La sección Lo que deliberadamente no vimos aquí de Punteros dejó pendiente justamente esa mitad del tema: pedirle memoria al sistema con new, devolverla con delete, y las fugas de memoria (memory leaks) que ocurren cuando te olvidas de devolverla. std::vector hace automáticamente, por dentro, exactamente eso que quedó como tema pendiente. Pide memoria cuando crece y la libera sola cuando el vector deja de existir.
Esa es la razón de fondo por la que en C++ moderno se prefiere std::vector incluso cuando un array bastaría: no es solo comodidad, es que elimina de raíz toda una familia de errores (olvidar liberar, liberar dos veces, usar memoria ya liberada) que en código con memoria manual son la primera causa de fallos difíciles de encontrar.
No te preocupes por cómo crece exactamente (cuánto pide de más, cada cuánto se reorganiza). Es un detalle de eficiencia que existe y está bien resuelto, y que solo necesitarás mirar de cerca cuando trabajes con volúmenes grandes de datos.
Ejemplo completo: promedio de notas del usuario
Este es el programa que no se puede escribir con un array de estilo C, y por eso vale la pena verlo entero: el usuario ingresa tantas notas como quiera y escribe -1 para terminar. Al escribir el código no sabemos cuántas serán, así que no hay ningún tamaño que poner entre corchetes.
#include <iostream>
#include <vector>
int main() {
std::vector<double> notas; // todavía no sabemos cuántas serán
double entrada = 0.0;
std::cout << "Ingresa una nota por línea. Escribe -1 para terminar.\n";
while (std::cin >> entrada && entrada != -1) {
notas.push_back(entrada); // el vector crece solo, sin límite fijo
}
if (notas.empty()) {
std::cout << "No ingresaste ninguna nota.\n";
return 0;
}
double suma = 0.0;
double maxima = notas.at(0);
for (double nota : notas) {
suma += nota;
if (nota > maxima) {
maxima = nota;
}
}
std::cout << "Notas ingresadas: " << notas.size() << "\n";
std::cout << "Promedio: " << suma / notas.size() << "\n";
std::cout << "Nota más alta: " << maxima << "\n";
return 0;
}g++ -std=c++17 -Wall -o promedio promedio.cpp
./promedioIngresa una nota por línea. Escribe -1 para terminar.
7
4.5
6
9
-1
Notas ingresadas: 4
Promedio: 6.625
Nota más alta: 9Tres piezas que ya conocías, combinadas:
- El
whilerepite mientras la lectura tenga éxito y el valor no sea el centinela (-1), es decir, el valor convenido que significa “terminé”. notas.push_back(entrada)va agrandando el vector una nota por vez.- El resto es el mismo cálculo de promedio de Variables y tipos, pero ahora sobre una cantidad de datos que se decide en ejecución.
Fíjate también en que no hay ningún static_cast<double>: como suma ya es double, la división no es entera. Y el if (notas.empty()) evita dividir entre cero si el usuario escribe -1 de entrada.
El std::cin >> entrada dentro de la condición sirve además como segunda salida del bucle: si el usuario escribe algo que no es un número (o el archivo de entrada se acaba), la lectura falla, la condición se vuelve falsa y el bucle termina en vez de quedarse colgado. Es un patrón muy común al leer datos por consola.
Cuándo usar cada contenedor
Los tres guardan elementos del mismo tipo en memoria contigua, así que la elección se reduce a cuándo se conoce el tamaño y quién administra la memoria:
| Opción | Elígela cuando |
|---|---|
Array de estilo C (int v[5]) | Estás obligado: interoperar con código C, con una API del sistema o con una librería que lo exige (Dear ImGui incluida). Para código nuevo tuyo, casi nunca |
std::array<int, 5> | El tamaño es fijo, conocido al compilar y no va a cambiar (los 7 días de la semana, las 3 coordenadas de un punto), y quieres los métodos y la seguridad de la Standard Library sin costo extra |
std::vector<int> | No sabes cuántos elementos habrá, o pueden agregarse y quitarse mientras el programa corre. Es la opción por defecto: ante la duda, vector |
Si dudas entre std::array y std::vector, empieza con std::vector. Cambiar después a std::array es un ajuste menor —los métodos que usaste (.size(), .at(), [], el for de rango) son los mismos—, mientras que descubrir a mitad de camino que necesitabas crecer y estar atrapado en un tamaño fijo obliga a reescribir.
Siguiente paso
Falta ver de cerca la opción del medio de esa tabla: el contenedor de tamaño fijo que no pierde su tamaño al pasarlo a una función y trae los mismos métodos que acabas de aprender. Continúa con std::array.