Resumen
Un std::array exige conocer al compilar cuántos elementos tendrá; la mayoría de los problemas, en cambio, no lo dicen hasta que los datos llegan. Esta clase presenta std::vector, la secuencia que crece según los datos: cómo se crea, cómo se le añaden elementos con push_back y cómo se leen con él series de longitud desconocida. Siguen los tres recorridos canónicos de toda programación con colecciones (acumular, filtrar y transformar), con un bucle y con los algoritmos de la biblioteca; el paso de un vector a una función, y por qué devolver un vector nuevo es barato. Cierra el interior del vector: su tamaño y su capacidad, el bloque que se reemplaza por otro del doble cuando se llena, y la referencia a un elemento que por eso puede quedar apuntando a memoria liberada.
Objetivos de aprendizaje
- Crear un
std::vector, añadirle elementos conpush_backy recorrerlo, leyendo con él una serie de datos de longitud desconocida. - Escribir funciones que reciban un vector por referencia constante para leerlo y por referencia para modificarlo, y que devuelvan un vector nuevo cuando el resultado es una secuencia.
- Implementar recorridos de acumulación, de filtrado y de transformación, reconocer en un problema el patrón que le corresponde y abreviarlo, cuando la biblioteca lo permite, con
std::ranges::count,std::ranges::maxostd::ranges::fold_left. - Explicar con un ejemplo la diferencia entre el tamaño y la capacidad de un vector, y por qué una referencia a un elemento puede invalidarse al añadir otro.
La secuencia que crece
Crear y llenar un vector
La clase Arreglos presentó la secuencia indexada: elementos del mismo tipo, uno tras otro, numerados desde cero. Su forma moderna de tamaño fijo, std::array<int, 7>, lleva el tamaño en el tipo, y el compilador debe conocerlo. Ahora bien, quien escribe un programa que promedia las notas de un curso no sabe cuántos estudiantes tendrá. De ello se sigue la necesidad de una secuencia cuyo tamaño sea un dato más, que cambie durante la ejecución: es el vector, std::vector<T>, de la cabecera <vector>, donde T es el tipo de los elementos.
#include <print>#include <vector>int main(){ std::vector<int> squares{}; std::println("Al comenzar: tamaño {}, vacío: {}", squares.size(), squares.empty()); for (int k{1}; k <= 5; ++k) { squares.push_back(k * k); } std::println("{}", squares); std::println("Tamaño {}, primero {}, último {}", squares.size(), squares[0], squares.back()); squares[1] = 0; squares.pop_back(); std::println("{}", squares);}Al comenzar: tamaño 0, vacío: true[1, 4, 9, 16, 25]Tamaño 5, primero 1, último 25[1, 0, 9, 16]La línea 6 crea un vector vacío de enteros: a diferencia del arreglo, no hay que anunciar cuántos elementos tendrá. size() devuelve cuántos tiene, y empty() dice si no tiene ninguno. El bucle de las líneas 8 a 10 añade cinco cuadrados con push_back, que coloca su argumento al final y aumenta el tamaño en uno. La línea 11 escribe el vector entero con el formato de rangos que la clase anterior usó con los arreglos. El acceso por índice es el de siempre, y squares.back() abrevia squares[squares.size() - 1]. La línea 13 asigna a un elemento existente, y la 14 quita el último con pop_back. Las operaciones de esta clase son, en resumen, las siguientes.
v.push_back(x)añadexal final;v.pop_back()quita el último (con el vector vacío, es comportamiento indefinido).v.size()es el tamaño, de tipostd::size_t, el entero sin signo de los tamaños que presentó la clase 6.1;v.empty()equivale av.size() == 0.v[i]yv.at(i)dan el elemento de índicei, que debe cumpliri < v.size();v.front()yv.back()dan el primero y el último, y exigen que el vector no esté vacío.v.resize(n)deja el vector connelementos: quita los que sobran al final o añade ceros.
Como en los arreglos, v[i] no garantiza ninguna comprobación, y v.at(i) sí: con un índice 7 y cinco elementos, detiene el programa con el mensaje vector::_M_range_check: __n (which is 7) >= this->size() (which is 5), que nombra el índice y el tamaño. El mecanismo que produce esa detención, una excepción, se estudia en Programación en C++ II.
Llaves y paréntesis
El curso inicializa todo con llaves, y el vector es el primer tipo del curso en que cambiar las llaves por paréntesis cambia el resultado. Con llaves, los valores escritos son los elementos; con paréntesis, el primer número es la cantidad de elementos y el segundo, si lo hay, el valor de cada uno.
#include <print>#include <vector>int main(){ const std::vector<int> a{5}; const std::vector<int> b(5); const std::vector<int> c{3, 7}; const std::vector<int> d(3, 7); std::println("{} {} {} {}", a, b, c, d);}[5] [0, 0, 0, 0, 0] [3, 7] [7, 7, 7]a tiene un elemento, el 5; b, cinco ceros; c, el 3 y el 7; d, tres sietes. La lista entre llaves dice, por tanto, «estos son los elementos», y los paréntesis se reservan para pedir n copias de un mismo valor, como un vector de contadores en cero.
El último de un vector vacío
Un vector vacío es un caso legítimo (la entrada sin datos, el filtro que nadie pasa), y cada fórmula con su tamaño debe revisarse en ese caso; la más traicionera es v.size() - 1. Como v.size() es un entero sin signo, restarle 1 cuando vale 0 no da , sino que da la vuelta, como se vio en la clase Los enteros y su representación, hasta el mayor valor de std::size_t.
#include <cstddef>#include <print>#include <vector>int main(){ const std::vector<int> values{}; std::println("values.size() - 1 = {}", values.size() - 1); // Cota errónea: con el vector vacío, la condición es verdadera. for (std::size_t i{0}; i <= values.size() - 1; ++i) { std::println("{}", values[i]); }}values.size() - 1 = 18446744073709551615/usr/include/c++/15/bits/stl_vector.h:1282: constexpr std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::const_reference std::vector< <template-parameter-1-1>, <template-parameter-1-2> >::operator[](size_type) const [with _Tp = int; _Alloc = std::allocator<int>; const_reference = const int&; size_type = long unsigned int]: Assertion '__n < this->size()' failed.El sistema detuvo el programa con la señal 6.
La primera línea de la salida es , es decir, . De ello se sigue que la condición del bucle, 0 <= 18446744073709551615, es verdadera, y que la primera vuelta lee values[0] de un vector que no tiene elemento 0: comportamiento indefinido. Lo que se observa depende de las opciones, como en los arreglos: compilada sin optimización, la biblioteca de GCC 15 comprueba el índice de operator[] y detiene el programa con una aserción que dice __n < this->size(); con optimización esa comprobación desaparece, y el bucle, que no se detiene a tiempo, intenta leer donde no hay ningún elemento (comprobado en Linux con -O2: como el vector vacío no tiene bloque, el programa termina con una violación de segmento, sin mensaje que explique la causa). La corrección es escribir la cota sin resta, i < values.size(), que con el vector vacío es falsa desde el comienzo; y donde la resta parezca necesaria, preguntar antes por values.empty().
Cargando el contenido…