Contenedores Secuenciales en C++ STL: Una Guía Práctica

La Biblioteca de Plantillas Estándar (STL) de C++ ofrece varios contenedores secuenciales diseñados para almacenar y gestionar datos de manera ordenada. Los principales contenedores secuenciales son:

  • std::vector: Ideal para acceso aleatorio rápido a los elementos.
  • std::list: Optimizado para inserciones y eliminaciones rápidas en cualquier parte de la secuencia.
  • std::deque: Una cola de doble extremo que permite inserciones y eliminaciones eficientes en ambos extremos.

Además, la STL proporciona adaptadores de contenedores que imponen un comportamiento específico a los contenedores subyacentes:

  • std::stack: Implementa una estructura de datos LIFO (Último en Entrar, Primero en Salir).
  • std::queue: Implementa una estructura de datos FIFO (Primero en Entrar, Primero en Salir).
  • std::priority_queue: Una cola donde los elementos se extraen según su prioridad.

La idea de "adaptador" puede ser un poco abstracta al principio, similar a cómo un adaptador de corriente transforma un tipo de enchufe en otro. En el contexto de la STL, un adaptador toma un contenedor existente (como std::vector o std::deque) y expone un conjunto de operaciones más restringido y específico para un patrón de uso particular.

  1. std::vector

Un std::vector es una excelente alternativa a los arrays C-style cuando se necesita un tamaño dinámico o la capacidad de añadir o eliminar elementos fácilmente. A diferencia de los arrays, cuyo tamaño se fija en la declaración, un std::vector puede crecer o encogerse según sea necesario.

Para usar std::vector, primero debes incluir su encabezado:

#include <vector>

Si no quieres prefijar cada declaración con std::, puedes añadir:

using std::vector;

Aquí tienes un ejemplo de cómo inicializar un std::vector a partir de un array C-style:

#include <iostream>
#include <vector>
#include <string>

struct Estudiante
{
    int codigo;
    std::string nombre;
};

int main()
{
    Estudiante datosEstudiantes[] = {{4, "Daniel"}, {2, "Beatriz"}, {6, "Fernanda"}, {3, "Carlos"}, {1, "Ana"}, {5, "Elena"}};
    // Inicializa el vector con un rango de elementos del array
    std::vector<Estudiante> listaEstudiantes(datosEstudiantes, datosEstudiantes + sizeof(datosEstudiantes) / sizeof(Estudiante));

    // Itera y muestra los elementos
    for (auto it = listaEstudiantes.begin(); it != listaEstudiantes.end(); ++it) {
        std::cout << it->codigo << ", " << it->nombre << std::endl;
    }

    return 0;
}

std::vector ofrece varios constructores, incluyendo uno que permite inicializar el vector como una copia de un rango de elementos de otro contenedor o array. El acceso a los elementos se puede realizar mediante iteradores o índices. Sin embargo, es importante tener en cuenta que el uso de índices para la inserción o eliminación de elementos es ineficiente y desaconsejado; estas operaciones deben realizarse preferentemente con métodos específicos como push_back(), insert(), erase(), etc.

Los iteradores, como los devueltos por begin() y end(), actúa de manera similar a los punteros. begin() apunta al primer elemento, y end() apunta a la posición *después* del último elemento. Se pueden realizar operaciones aritméticas con los iteradores de vector para moverse rápidamente a posiciones específicas, como:

auto it_medio = listaEstudiantes.begin() + listaEstudiantes.size() / 2;

Una consideración clave con std::vector es que las operaciones de inserción o eliminación pueden invalidar los iteradores existentes si provocan un reasignamiento de memoria. Por lo tanto, después de tales operaciones, es prudente volver a obtener los iteradores necesarios.

  1. std::list

Al igual que con vector, se incluye el encabezado <list> y opcionalmente using std::list;.

std::list se diferencia principalmente de std::vector en su eficiencia para inserciones y eliminaciones en cualquier punto de la secuencia. Además, std::list proporciona una función miembro sort() para ordenar sus elementos.

Consideremos el ejemplo anterior, pero ahora usando std::list y ordenándolo:

#include <iostream>
#include <list>
#include <string>

struct Estudiante
{
    int codigo;
    std::string nombre;
};

// Función de comparación para ordenar por código de estudiante
bool compararEstudiantes(const Estudiante& a, const Estudiante& b)
{
    return a.codigo < b.codigo; // Orden ascendente
}

int main()
{
    Estudiante datosEstudiantes[] = {{4, "Daniel"}, {2, "Beatriz"}, {6, "Fernanda"}, {3, "Carlos"}, {1, "Ana"}, {5, "Elena"}};
    std::list<Estudiante> listaEstudiantes(datosEstudiantes, datosEstudiantes + sizeof(datosEstudiantes) / sizeof(Estudiante));

    // Ordenar la lista usando la función de comparación
    listaEstudiantes.sort(compararEstudiantes);

    // Iterar y mostrar los elementos ordenados
    for (auto it = listaEstudiantes.begin(); it != listaEstudiantes.end(); ++it) {
        std::cout << it->codigo << ", " << it->nombre << std::endl;
    }

    return 0;
}

Si los elementos tienen un operador < sobrecargado que define el orden natural, se puede usar listaEstudiantes.sort(); sin argumentos. Para tipos definidos por el usuario, como nuestra estructura Estudiante, es necesario proporcionar una función de comparación personalizada. Esta función toma dos elementos y devuelve true si el primer elemento debe preceder al segundo en el orden deseado.

A diferencia de std::vector, std::list no soporta el acceso por índice. Esto se debe a su naturaleza de lista enlazada, donde el acceso a un elemento específico requiere recorrer la lista desde el principio o el final.

Consideraciones Adicionales

Al trabajar con adaptadores como std::stack, es un error común intentar iterar sobre él de forma no estándar. Por ejemplo, acceder repetidamente a top() y luego a pop() mientras se incrementa un contador puede llevar a resultados incorrectos, ya que se modifican los elementos del stack.

La utilización de plantillas (templates) en C++ es un concepto poderoso. El ejemplo con bitset<32> ilustra cómo los parámetros de plantilla pueden ser valores enteros. La sintaxis template<Tipo NombreParametro> se utiliza para definir plantillas, y los parámetros pueden ser tipos, valores enteros o incluso punteros a objetos (con ciertas restricciones).

Intentar usar un objeto de tipo no estático como parámetro de planitlla directa (template<Student stu>) no es válido. Los parámetros de plantilla deben ser evaluables en tiempo de compilación. En su lugar, se pueden usar punteros a objetos si estos son de ámbito global o estático, como se muestra en la corrección:

template<Student* ptrEstudiante>
class Persona
{
    int valor;
public:
    Persona()
    {
        valor = ptrEstudiante->codigo;
        std::cout << valor << std::endl;
    }
};

Estudiante miEstudiante = {10, "Ejemplo"};

int main()
{
    // Pasar la dirección de un objeto global/estático
    Persona<&miEstudiante> p;
    return 0;
}

La exploración continua de la STL y las características avanzadas de C++ como las plantillas es fundamental para el dsearrollo eficiente y robusto.

Etiquetas: C++ STL vector List contenedores secuenciales

Publicado el 7-17 08:23