tutoriales.com

Explorando la Biblioteca Estándar de C++: Un Viaje por `std::vector`, `std::map` y `std::algorithm`

Este tutorial te guiará a través de los componentes esenciales de la Biblioteca Estándar de C++ (STL), centrándose en contenedores como `std::vector` y `std::map`, e iteradores y algoritmos. Aprenderás a escribir código C++ más limpio, eficiente y robusto aprovechando las herramientas que la STL pone a tu disposición.

Intermedio20 min de lectura8 views
Reportar error

La Biblioteca Estándar de C++ (STL) es una colección de clases y funciones de plantilla que proporcionan soluciones a problemas comunes de programación. Es una parte fundamental del C++ moderno y dominarla es crucial para cualquier desarrollador. Este tutorial te sumergirá en sus componentes clave: contenedores, iteradores, algoritmos y utilidades.

🚀 Introducción a la Biblioteca Estándar (STL)

La STL es mucho más que una simple colección de utilidades; es un framework que promueve la reutilización de código, la eficiencia y la seguridad de tipos. Se basa en el paradigma de la Programación Genérica, permitiendo que sus componentes trabajen con cualquier tipo de datos que cumpla con ciertos requisitos, a menudo a través de plantillas.

🎯 Componentes Principales de la STL

La STL se organiza en varias categorías principales:

  • Contenedores: Objetos que almacenan colecciones de otros objetos (ej. std::vector, std::list, std::map).
  • Iteradores: Objetos que permiten recorrer y acceder a los elementos de los contenedores de una manera uniforme, similar a los punteros.
  • Algoritmos: Funciones que realizan operaciones comunes en rangos de elementos definidos por iteradores (ej. std::sort, std::find, std::for_each).
  • Adaptadores: Proporcionan interfaces diferentes para contenedores existentes (ej. std::stack, std::queue, std::priority_queue).
  • Utilidades: Clases y funciones misceláneas que complementan los componentes anteriores (ej. std::pair, std::function, std::chrono).
💡 Consejo: Familiarizarse con la documentación de la STL (como cppreference.com) es una habilidad invaluable. ¡No intentes memorizarlo todo, aprende a buscarlo!

📦 Contenedores STL: Almacenando tus Datos

Los contenedores son la columna vertebral de la STL, permitiéndote almacenar y organizar datos de diversas maneras. Veremos dos de los más utilizados: std::vector y std::map.

std::vector: El Array Dinámico

std::vector es un contenedor de secuencia que gestiona un array dinámico, lo que significa que puede cambiar de tamaño. Es eficiente para añadir elementos al final y acceder a elementos por índice.

Características clave:

  • Acceso aleatorio rápido (O(1)).
  • Inserción y eliminación al final (amortizado O(1)).
  • Inserción y eliminación en el medio o al principio (O(n)).
  • Los elementos se almacenan contiguamente en memoria.
#include <iostream>
#include <vector>
#include <string>

int main() {
    // Declarar un vector de enteros
    std::vector<int> numeros;

    // Añadir elementos al final
    numeros.push_back(10);
    numeros.push_back(20);
    numeros.push_back(30);

    // Acceder a elementos por índice
    std::cout << "Primer elemento: " << numeros[0] << std::endl; // Salida: 10

    // Iterar sobre el vector con un bucle for-each
    std::cout << "Elementos del vector: ";
    for (int n : numeros) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // Salida: 10 20 30 

    // Obtener el tamaño del vector
    std::cout << "Tamaño del vector: " << numeros.size() << std::endl; // Salida: 3

    // Eliminar el último elemento
    numeros.pop_back();
    std::cout << "Tamaño después de pop_back: " << numeros.size() << std::endl; // Salida: 2

    // Redimensionar el vector (inicializa nuevos elementos si se hace más grande)
    numeros.resize(5); 
    std::cout << "Vector después de resize(5): ";
    for (int n : numeros) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // Salida: 10 20 0 0 0 (los nuevos elementos se inicializan a 0)

    // Vector de strings
    std::vector<std::string> nombres = {"Alice", "Bob", "Charlie"};
    nombres.emplace_back("David"); // Añade eficientemente un elemento al final

    for (const std::string& nombre : nombres) {
        std::cout << nombre << " ";
    }
    std::cout << std::endl;

    return 0;
}
🔥 Importante: Cuando un `std::vector` necesita redimensionarse para acomodar más elementos, puede reasignar todo su contenido a una nueva ubicación en memoria. Esto invalida cualquier iterador o puntero a sus elementos anteriores. Tenlo en cuenta en operaciones de alto rendimiento.

std::map: El Diccionario Ordenado

std::map es un contenedor asociativo que almacena pares de clave-valor ordenados por clave. Cada clave debe ser única. Es excelente para búsquedas rápidas basadas en clave.

Características clave:

  • Búsqueda, inserción y eliminación eficientes (O(log n)).
  • Los elementos se almacenan ordenados por clave.
  • Las claves deben ser únicas.
#include <iostream>
#include <map>
#include <string>

int main() {
    // Declarar un map de string a int (nombre a edad)
    std::map<std::string, int> edades;

    // Insertar elementos
    edades["Alice"] = 30; // Acceso tipo array, inserta si no existe, actualiza si existe
    edades.insert({"Bob", 25}); // Usando el método insert
    edades.emplace("Charlie", 35); // Otra forma eficiente de insertar

    // Acceder a elementos por clave
    std::cout << "Edad de Alice: " << edades["Alice"] << std::endl; // Salida: 30

    // Intentar acceder a una clave que no existe puede insertarla con un valor por defecto
    // std::cout << "Edad de David: " << edades["David"] << std::endl; // ¡CUIDADO!

    // Buscar un elemento de forma segura
    auto it = edades.find("Bob");
    if (it != edades.end()) {
        std::cout << "Bob encontrado. Edad: " << it->second << std::endl; // Salida: 25
    } else {
        std::cout << "Bob no encontrado." << std::endl;
    }

    // Iterar sobre el map (los elementos están ordenados por clave)
    std::cout << "Contenido del map:" << std::endl;
    for (const auto& par : edades) {
        std::cout << par.first << ": " << par.second << std::endl;
    }
    /* Salida:
    Alice: 30
    Bob: 25
    Charlie: 35
    */

    // Eliminar un elemento
    edades.erase("Bob");
    std::cout << "Tamaño del map después de eliminar Bob: " << edades.size() << std::endl; // Salida: 2

    return 0;
}
📌 Nota: Si necesitas un mapa que no ordene sus elementos por clave y que ofrezca un rendimiento promedio O(1) para la mayoría de las operaciones (a cambio de un peor caso O(n) y mayor consumo de memoria), considera `std::unordered_map`.

🚶‍♂️ Iteradores: Navegando por los Contenedores

Los iteradores son el puente entre los contenedores y los algoritmos. Proporcionan una forma genérica de acceder a los elementos de una secuencia, independientemente del tipo de contenedor. Actúan como punteros inteligentes y ofrecen una interfaz uniforme.

Tipos de Iteradores (simplificado):

  • Input Iterators: Pueden leer y avanzar (++).
  • Output Iterators: Pueden escribir y avanzar (++).
  • Forward Iterators: Combinan Input y Output, pueden avanzar (++).
  • Bidirectional Iterators: Pueden avanzar y retroceder (++, --). std::vector, std::list, std::map los soportan.
  • Random Access Iterators: Soportan todas las operaciones de los bidireccionales y además acceso arbitrario (+, -, []). Solo std::vector y std::deque los soportan entre los contenedores estándar.

Todos los contenedores de la STL proporcionan los métodos begin() y end() para obtener iteradores al principio y "uno más allá del final" de la secuencia, respectivamente.

#include <iostream>
#include <vector>
#include <list>

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::list<char> l = {'a', 'b', 'c'};

    // Usando iteradores para recorrer un vector
    std::cout << "Vector con iteradores: ";
    for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // Usando iteradores constantes para una lista
    std::cout << "Lista con iteradores constantes: ";
    for (std::list<char>::const_iterator it = l.cbegin(); it != l.cend(); ++it) {
        // *it = 'x'; // Esto daría un error de compilación si fuera const_iterator
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // C++11 y posteriores: auto simplifica el código
    std::cout << "Vector con auto iterador: ";
    for (auto it = v.begin(); it != v.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}
Contenedor (ej. vector, map) Iterador (begin(), end()) Algoritmo (sort, find)

✨ Algoritmos STL: Operaciones Potentes

Los algoritmos STL son funciones de plantilla que realizan operaciones comunes en rangos de elementos. Son independientes del tipo de contenedor y funcionan con iteradores, lo que los hace increíblemente flexibles y potentes. Se encuentran en el <algorithm> header.

Ejemplos Comunes de Algoritmos

  • std::sort: Ordena los elementos en un rango.
  • std::find: Busca el primer elemento que coincide con un valor.
  • std::count: Cuenta las ocurrencias de un valor.
  • std::for_each: Aplica una función a cada elemento de un rango.
  • std::transform: Aplica una función a cada elemento y almacena el resultado en otro rango.
  • std::remove: Elimina lógicamente elementos de un rango.
#include <iostream>
#include <vector>
#include <algorithm> // Para std::sort, std::find, std::count, std::for_each
#include <string>
#include <numeric>   // Para std::accumulate

int main() {
    std::vector<int> datos = {5, 2, 8, 2, 1, 9, 2, 7};

    // 1. Ordenar el vector
    std::sort(datos.begin(), datos.end());
    std::cout << "Vector ordenado: ";
    for (int n : datos) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // Salida: 1 2 2 2 5 7 8 9 

    // 2. Buscar un elemento
    auto it_find = std::find(datos.begin(), datos.end(), 5);
    if (it_find != datos.end()) {
        std::cout << "El 5 se encuentra en la posición: " << std::distance(datos.begin(), it_find) << std::endl; 
    } else {
        std::cout << "El 5 no se encontró." << std::endl;
    }

    // 3. Contar ocurrencias
    int count_twos = std::count(datos.begin(), datos.end(), 2);
    std::cout << "El número 2 aparece " << count_twos << " veces." << std::endl; // Salida: 3

    // 4. Aplicar una función a cada elemento (con lambda)
    std::cout << "Elementos duplicados: ";
    std::for_each(datos.begin(), datos.end(), [](int& n) { 
        n *= 2; 
        std::cout << n << " ";
    });
    std::cout << std::endl;
    // Salida: 2 4 4 4 10 14 16 18 (y el vector 'datos' ahora contiene estos valores)

    // 5. Transformar elementos y guardar en otro vector
    std::vector<int> cuadrados;
    cuadrados.resize(datos.size()); // Asegúrate de que el vector destino tenga espacio
    std::transform(datos.begin(), datos.end(), cuadrados.begin(), 
                   [](int n) { return n * n; });
    
    std::cout << "Cuadrados de los elementos: ";
    for (int n : cuadrados) {
        std::cout << n << " ";
    }
    std::cout << std::endl;
    // Salida: 4 16 16 16 100 196 256 324

    // 6. Sumar todos los elementos (usando std::accumulate de <numeric>)
    int suma = std::accumulate(datos.begin(), datos.end(), 0); // 0 es el valor inicial de la suma
    std::cout << "Suma total de los elementos: " << suma << std::endl;
    // Salida: 80 (2+4+4+4+10+14+16+18)

    // 7. Eliminar elementos con `std::remove` (requiere `erase` posterior)
    std::vector<int> numeros_remove = {1, 2, 3, 2, 4, 5, 2};
    // std::remove mueve los elementos no eliminados al principio y devuelve un iterador al nuevo 'fin'
    auto new_end = std::remove(numeros_remove.begin(), numeros_remove.end(), 2);
    // Ahora 'new_end' apunta después del último elemento útil. Los elementos después de 'new_end' son basura.
    // Para realmente eliminar, usamos erase-remove idiom.
    numeros_remove.erase(new_end, numeros_remove.end());

    std::cout << "Vector después de remover 2s: ";
    for (int n : numeros_remove) {
        std::cout << n << " ";
    }
    std::cout << std::endl; // Salida: 1 3 4 5 

    return 0;
}
¿Por qué el idiom erase-remove?Los algoritmos STL operan en rangos de iteradores y no pueden cambiar el tamaño de los contenedores subyacentes. `std::remove` es un algoritmo que 'mueve' los elementos que *no* deben ser eliminados al principio del rango y devuelve un iterador al nuevo final lógico. Los elementos entre este nuevo final lógico y el final físico original del contenedor son inválidos o redundantes, pero siguen ahí. Por lo tanto, debes llamar explícitamente al método `erase()` del contenedor para eliminar físicamente estos elementos y redimensionar el contenedor.

🛠️ Adaptadores de Contenedores y Funciones

Además de los contenedores y algoritmos básicos, la STL ofrece adaptadores para modificar el comportamiento de los contenedores o para crear objetos de función personalizados.

std::stack, std::queue, std::priority_queue

Estos son adaptadores de contenedores que proporcionan interfaces específicas para estructuras de datos conocidas, construidas sobre contenedores existentes (por defecto std::deque para stack y queue, y std::vector para priority_queue).

  • std::stack: LIFO (Last-In, First-Out). Pila. Operaciones: push, pop, top, empty, size.
  • std::queue: FIFO (First-In, First-Out). Cola. Operaciones: push, pop, front, back, empty, size.
  • std::priority_queue: Almacena elementos en un orden de prioridad, el elemento con mayor prioridad está siempre al frente. Operaciones: push, pop, top, empty, size.
#include <iostream>
#include <stack>   // Para std::stack
#include <queue>   // Para std::queue y std::priority_queue
#include <vector>  // std::priority_queue usa std::vector por defecto
#include <functional> // Para std::greater en priority_queue

int main() {
    // === std::stack ===
    std::stack<int> pila;
    pila.push(10);
    pila.push(20);
    pila.push(30);

    std::cout << "Elementos de la pila: ";
    while (!pila.empty()) {
        std::cout << pila.top() << " "; // Acceder al elemento superior
        pila.pop(); // Eliminar el elemento superior
    }
    std::cout << std::endl; // Salida: 30 20 10

    // === std::queue ===
    std::queue<std::string> cola;
    cola.push("Tarea A");
    cola.push("Tarea B");
    cola.push("Tarea C");

    std::cout << "Elementos de la cola: ";
    while (!cola.empty()) {
        std::cout << cola.front() << " "; // Acceder al elemento frontal
        cola.pop(); // Eliminar el elemento frontal
    }
    std::cout << std::endl; // Salida: Tarea A Tarea B Tarea C

    // === std::priority_queue ===
    // Por defecto, es una cola de máxima prioridad (el elemento más grande es el top)
    std::priority_queue<int> pq_max;
    pq_max.push(50);
    pq_max.push(10);
    pq_max.push(80);

    std::cout << "Cola de prioridad (max): ";
    while (!pq_max.empty()) {
        std::cout << pq_max.top() << " ";
        pq_max.pop();
    }
    std::cout << std::endl; // Salida: 80 50 10

    // Cola de mínima prioridad (el elemento más pequeño es el top)
    std::priority_queue<int, std::vector<int>, std::greater<int>> pq_min;
    pq_min.push(50);
    pq_min.push(10);
    pq_min.push(80);

    std::cout << "Cola de prioridad (min): ";
    while (!pq_min.empty()) {
        std::cout << pq_min.top() << " ";
        pq_min.pop();
    }
    std::cout << std::endl; // Salida: 10 50 80

    return 0;
}
90% Cubierto

✅ Buenas Prácticas y C++ Moderno con STL

Dominar la STL no es solo conocer sus componentes, sino también saber usarlos de manera efectiva en un contexto de C++ moderno.

Preferir Algoritmos a Bucles Manuales

Siempre que sea posible, utiliza algoritmos STL en lugar de escribir bucles for o while manuales. Esto hace el código más legible, menos propenso a errores y a menudo más eficiente (los algoritmos STL están altamente optimizados).

Mal:

// Encontrar un elemento manualmente
std::vector<int> vec = {1, 2, 3, 4, 5};
bool found = false;
for (int x : vec) {
    if (x == 3) {
        found = true;
        break;
    }
}

Bien:

// Usando std::find
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = std::find(vec.begin(), vec.end(), 3);
bool found = (it != vec.end());

Usar Lambdas con Algoritmos

Las expresiones lambda, introducidas en C++11, son compañeros perfectos para los algoritmos STL, permitiéndote definir funciones anónimas inline para operaciones personalizadas.

#include <iostream>
#include <vector>
#include <algorithm>

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};

    // Sumar solo los números pares usando std::accumulate con una lambda
    int sum_pares = 0;
    std::for_each(nums.begin(), nums.end(), [&](int n) {
        if (n % 2 == 0) {
            sum_pares += n;
        }
    });
    std::cout << "Suma de pares: " << sum_pares << std::endl; // Salida: 6 (2+4)

    // Contar elementos mayores que 3 usando std::count_if con una lambda
    int count_gt_3 = std::count_if(nums.begin(), nums.end(), [](int n) { 
        return n > 3; 
    });
    std::cout << "Elementos mayores que 3: " << count_gt_3 << std::endl; // Salida: 2 (4, 5)

    return 0;
}

Referencias const y auto

Utiliza const auto& al iterar sobre colecciones para evitar copias innecesarias y para indicar que los elementos no serán modificados.

std::vector<std::string> palabras = {"hola", "mundo", "c++"};

// CORRECTO: Evita copias y permite modificaciones (si se quita const)
for (auto& palabra : palabras) {
    // Modificar palabra
}

// MEJOR: Evita copias y garantiza que los elementos no serán modificados
for (const auto& palabra : palabras) {
    std::cout << palabra << std::endl; // Solo lectura
}

// MAL: Crea copias de cada string, ineficiente
for (auto palabra : palabras) {
    // 'palabra' es una copia
}

🔚 Conclusión

La Biblioteca Estándar de C++ es una herramienta increíblemente poderosa y versátil que te permite escribir código C++ más eficiente, legible y robusto. Al dominar sus contenedores, iteradores y algoritmos, estarás sentando una base sólida para el desarrollo moderno en C++.

Recuerda que este tutorial apenas rasca la superficie de lo que la STL tiene para ofrecer. Hay muchos más contenedores (std::list, std::set, std::deque, std::unordered_map), algoritmos y utilidades que esperan ser explorados. La clave es entender los principios subyacentes y saber cuándo y cómo aplicar las herramientas adecuadas.

¡Sigue practicando y explorando para convertirte en un maestro de la STL!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!