</>waridocu

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.

cppstandard-librarystlvectorcontenedoresmemoria

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:

  1. 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.
  2. 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.

Nota

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:

El header que activa std::vectorcpp
#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:

Ejemplo: dos formas de crear un vectorcpp
#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;
}
bashbash
g++ -std=c++17 -Wall -o vectores vectores.cpp
./vectores
texttext
vacio tiene   0 elementos
numeros tiene 3 elementos
El primero de numeros es 1

Fí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.

Nota

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étodoQué 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)
Ejemplo: agregar elementos y consultar el tamañocpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> notas;              // empieza vacío

    std::cout << "Vacío al principio: " << (notas.empty() ? "" : "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() ? "" : "no") << "\n";
    return 0;
}
texttext
Vacío al principio: sí
Ahora tiene 3 elementos
Vacío ahora: no

Ningú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.

Ejemplo: una función que recibe un vector sí conoce su tamañocpp
#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;
}
texttext
Cantidad de notas: 3
Promedio: 5.83333

El 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.

Advertencia

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 que i esté dentro del rango.
Ejemplo: acceder a los elementos con [] y con .at()cpp
#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;
}
texttext
Primera con []:    7
Primera con .at(): 7
Tercera modificada: 10
Advertencia

Que 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:

Ejemplo: qué pasa al pasarse de rango con .at()cpp
#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;
}
texttext
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.

Tip

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.

Ejemplo: recorrer un vector con for basado en rangocpp
#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;
}
texttext
- compilar
- probar
- documentar
Total: 3 tareas

El 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.

Importante

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.

Nota

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.

Ejemplo: promediar tantas notas como el usuario quiera ingresarcpp
#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;
}
bashbash
g++ -std=c++17 -Wall -o promedio promedio.cpp
./promedio
texttext
Ingresa 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:    9

Tres piezas que ya conocías, combinadas:

  1. El while repite mientras la lectura tenga éxito y el valor no sea el centinela (-1), es decir, el valor convenido que significa “terminé”.
  2. notas.push_back(entrada) va agrandando el vector una nota por vez.
  3. 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.

Tip

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ónElí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
Tip

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.