AulaML Python · C++
C++ intermedio · 46 min · Estructuras de datos

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.

Lección 1 de 3 del módulo
Al terminar esta lección podrás
  • 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
  1. ¿Qué es un array y qué es un contenedor?
  2. ¿Para qué sirven estos contenedores?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis
  5. Ejemplo básico: recorrer y consultar un vector
  6. Ejemplo intermedio: reservar y calcular
  7. Caso práctico: analizar una lista de palabras
  8. Visualización: tamaño, capacidad y buffer
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. 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.

TipoTamañoDónde vive el bufferConoce su longitudCrece
int a[10]Fijo en compilaciónStack o dentro de otro objetoNo: hay que pasarla aparteNo
std::array<int, 10>Fijo en compilaciónDentro del objetoSí: .size()No
std::vector<int>Variable en ejecuciónHeap, gestionado por el vectorSí: .size()Sí, con push_back
std::stringVariable en ejecuciónHeap o buffer interno pequeñoSí: .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) lanza std::out_of_range si 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::string permite concatenar, buscar y convertir sin gestionar char* 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

CPP sintaxis.cpp
#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::array se declara con dos parámetros: el tipo y el número de elementos, que debe ser una constante de compilación.
  • std::vector y std::string se inicializan con llaves, con un tamaño o vacíos. El vector vacío tiene size() == 0 y capacity() == 0.
  • push_back añade una copia al final; emplace_back construye el elemento directamente en el buffer y evita una copia en objetos grandes.
  • size() devuelve std::size_t, un entero sin signo. Comparar con un int negativo genera avisos con -Wsign-compare.

Ejemplo básico: recorrer y consultar un vector

CPP basico.cpp
#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

CPP intermedio.cpp
#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

CPP palabras.cpp
#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.

CPP conversiones.cpp
#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.

Ejecución visual ·Crecimiento de un vector y efecto de reserve
1 / 7
1 std::vector<int> v;
2 v.push_back(10);
3 v.push_back(20);
4 v.push_back(30);
5 std::cout << v.size() << ' ' << v.capacity() << '\n';
6 v.reserve(8);
7 std::cout << v.capacity() << '\n';
El vector nace vacío: no hay buffer, size y capacity valen 0.
Variables
v = []
size = 0
capacity = 0
Salida
—

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. Usa at() cuando el índice venga de fuera.
  • Confundir size y capacity: v.capacity() no dice cuántos elementos hay. Un vector con capacidad 8 y tamaño 0 está vacío; recorrerlo hasta capacity() es un error.
  • Guardar data() antes de crecer: tras un push_back que reasigna, el puntero anterior apunta a memoria liberada. Vuelve a llamar a data() después de la inserción.
  • Usar sizeof para contar: sizeof(v) / sizeof(v[0]) sobre un std::vector da el tamaño del objeto, no el número de elementos. Utiliza v.size().
  • Olvidar la cabecera: std::vector necesita <vector>, std::array necesita <array> y std::string, <string>. El error típico es no member named 'vector' in namespace 'std'.
  • Indexar con enteros negativos: v[-1] convierte el -1 a 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::vector por defecto; usa std::array si el tamaño es constante y pequeño, y el array crudo solo para interoperar con APIs de C.
  • Llama a reserve cuando conozcas el número aproximado de elementos; evita resize si solo querías capacidad.
  • Recorre con for (const auto& x : v) para no copiar; usa auto& si necesitas modificar.
  • Accede con at en 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 reserve mal puesto puede ser peor que dejar crecer el vector.
  • Usa std::string y sus métodos en lugar de char*; para texto que no posees, std::string_view evita copias.
Crecer una lista y crecer un vector
Python
PYTHON lista.py
datos = []
for i in range(1, 6):
    datos.append(i * 1.5)
print(len(datos))   # 5
print(datos[-1])    # 7.5
C++
CPP vector.cpp
std::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.5

Python 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

Ejercicio 1

Estadísticas de un vector

Dado 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()).

Ejercicio 2

Contar palabras largas

Crea un 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.

Completar código Completa las operaciones del vector
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 con push_back en tiempo amortizado constante.
  • size es cuántos elementos hay; capacity cuántos caben antes de reasignar. reserve cambia solo la capacidad.
  • Una reasignación mueve el buffer e invalida punteros, referencias e iteradores anteriores.
  • at comprueba límites y lanza std::out_of_range; [] es más rápido y no comprueba.
  • std::string ofrece tamaño, concatenación, búsqueda y conversiones; stoi/stod y to_string cubren los números.
  • Elige el contenedor por el tamaño y las operaciones, no por costumbre.

Evaluación

Evaluación 1

Un std::vector<int> tiene size() == 3 y capacity() == 8. ¿Qué significa?

Evaluación 2

¿Qué provoca que un int* p = v.data(); quede colgante?

Evaluación 3

¿Cuál es la diferencia principal entre v[i] y v.at(i)?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.