Programación en C++/Plantillas

De Wikilibros, la colección de libros de texto de contenido libre.

Editores:
Oscar E. Palacios

Estructuras II Excepciones

Plantillas[editar]

Introducción[editar]

Con el objeto de explicar la razón de la necesidad de la existencia de las plantillas debemos reflexionar sobre tres paradígmas de programación anteriores, estas son: programación clásica o procedimental, programación estructurada y programación orientada al objeto POO.

Programación clásica

En el tipo de programación conocida como clásica existe una clara diferenciación entre los datos y su manipulación, es decir, entre los datos y el conjunto de algoritmos para manejarlos. Los datos son tipos muy simples y generalmente los algoritmos se agrupan en funciones orientadas de forma muy específica a los datos que deben manejar. Por ejemplo, si se escribe una función ( sort ) para ordenar en forma ascendente o descendente los números contenidos en un arreglo de números enteros, dicha función puede aplicarse a cualquier arreglo de enteros más no a arreglos de otro tipo. Aun así, la programación clásica provee el soporte necesario para la reutilización de código ya que el código de la función se escribe solamente una vez y su reutilización se da por medio de un mecanismo conocido como llamada de función.

Programación estructurada

En la medida en que los datos que había de manipular se iban haciendo cada vez más complejos se busco la forma de agruparlos de alguna manera bajo un mismo nombre, es así como surjen las estructuras de datos. Muchos autores se refieren a la programación estructura como a la suma de funciones y/o procedimientos más estructura de datos.

Programación Orientada al Objeto

La Programación Orientada al Objeto ( POO ) introduce nuevas facilidades y se extiende el concepto de dato, permitiendo que existan tipos más complejos, es decir, si la programación estructurada establece las bases para la manipulación de funciones y datos estructurados, la POO establece las bases para manipular datos y funciones como un solo objeto. Esta nueva habilidad viene acompañada por ciertas mejoras adicionales: la posibilidad de ocultación de determinados detalles internos irrelevantes para el usuario y la capacidad de herencia simple o múltiple.

Notas: El ocultamiento de código así como la herencia están presentes ( en una forma simple ) en la programación estructurada, y los mismos adquieren mucha más relevancia en la POO. Por ejemplo, en la programación estructurada si usted escribe una librería de funciones, al usuario de dicha librería solamente le informará de la existencia de tal o cual función, así como el objetivo y la forma correcta para comunicarse con estas, pero no es necesario que se le explique el funcionamiento interno de las mismas. Por otro lado, es bien conocido que lenguajes tales como Pascal y C, por ejemplo, dan el soporte para la creación de datos estructurados ( Record en Pascal, y struct en C ), y que dichas estructuras pueden contener a otras estructuras previamente definidas. De tal manera vemos como a los usuarios de las funciones se les oculta el código de las mismas, y que una estructura que contiene a otra hereda los miembros de la estructura contenida.

Programación genérica

La programación genérica está mucho más centrada en los algoritmos que en los datos, y su postulado fundamental puede sintetizarse en una palabra: generalización. Significa que, en la medida de lo posible, los algoritmos deben ser parametrizados al máximo y expresados de la forma más independiente posible de detalles concretos, permitiendo así que puedan servir para la mayor variedad posible de tipos y estructuras de datos.

Con el objetivo de mostrar de una manera practica las diferencias entre los tres tipos de programación mencionados arriba tomaremos como base el modelo de una vieja estructura de datos amiga de los programadores, me refiero a un array, también conocida por muchos como arreglo, tabla o lista. Para no entrar en polemicas de estándarización de nombrado en este capítulo diremos que la estructura modelo con la que trabajaremos es un vector ( arreglo unidimensional ).

Pues bien, dado un vector cualquiera se presenta la necesidad de crear un cierto número de funciones que sean las encargadas de la manipulación de los datos dentro del vector. Para comenzar, podemos pensar en las funciones:

  1. mostrar, para desplegar o imprimir los elementos del vector.

  2. ordenar, para ordenar los elementos del vector.

  3. buscar, para determinar la presencia o ausencia de un determinado elemento dentro del vector.

  4. insertar, para agregar componentes al vector.

  5. eliminar, para quitar componentes del vector.

  6. capacidad, para obtener la capacidad máxima del vector.

  7. cuenta, para obtener el número de elementos actuales contenidos por el vector.

Al hacer un análisis muy detallado del problema planteado y al tratar de resolver el mismo mediante la programación clásica, veremos que, si bien es cierto que se puede llegar a la solución, para lograrlo se tendrían que establecer una serie de medidas que nos permitierán controlar de alguna manera detalles tales como: el total de elementos soportados por el vector y el número de elementos actuales contenidos por el vector. Por ejemplo, con la instrucción:

int vector[120];

se crea un arreglo con capacidad para 120 componentes de tipo entero . Ahora bien, el compilador sabe perfectamente que deberá reservar un espacio de 120 enteros para la memoria del vector, pero no se garantiza que cualquier función trate de leer o escribir datos fuera de los limites del vector. Es decir, nuestro programa no podría saber el tamaño del vector a menos que usaramos algún truco, por ejemplo usar el primer elemento del vector para contener el tamaño del mismo. Otra forma de alcanzar una solución, sería que a cada una de las funciones que tengan que ver con el vector se le pasará los parámetros: puntero de vector y número de elementos en el vector, de tal manera que, por ejemplo, la función mostrar podría declararse como:

int mostrar(int *vector, int cuenta);

Un paso hacia adelante[editar]

Si continuamos en el ámbito de la programación clásica podemos dar un paso más si es que nos valemos de tipos estructurados más elaborados. Por ejemplo, podemos definir una estructura llamada vector la cual posea los miembros: capacidad, cuenta y data como se muestra en seguida:

typedef struct vector {
    int capacidad;
    int cuenta;
    int *data;
};

De tal manera que podriamos escribir funciones que operen sobre un solo parámetro del tipo estructurado vector. Por ejemplo, la función mostrar podría declararse como:

int mostrar(vector *v);

En este punto tendríamos que detenernos y pensar en lo siguiente:
La estructura vector ( definida arriba con typedef ) es solamente un nuevo tipo de dato, es decir, podemos declarar tantas copias ( variables ) de la misma como sean necesarias, pero carece de un método constructor adecuado. Por ejemplo, la declaración:

vector nombre_var;

es válida siempre y cuando que el tipo vector ya haya sido definido, pero la variable nombre_var contendrá sólo basura hasta que no se establezcan los valores adecuados para cada uno de sus miembros ( capacidad, cuenta y data ).

Una mejor solución[editar]

Para el tipo de problemas como en el ejemplo vector y otros similares, surge la POO, misma que facilita en gran medida la solución del mismo, pero aún queda pendiente la resolución a otro problema, es decir, hasta aquí hemos mencionado la estructura vector como un contenedor de números enteros, pero un vector podría contener caracteres, números de punto flotante y otros tipos estructurados; y los mismos algoritmos usados para ( mostrar, ordenar, buscar, etc. ) empleados en un vector de enteros, tendrían su aplicación sobre vectores de cualquier tipo. Es así como surge lo que se conoce como PLANTILLAS o lo que es lo mismo, la programación genérica.

La clase vector desde la perspectiva de la POO[editar]

Con el objetivo de mostrar en la práctica los conceptos que hemos venido mencionando presentaremos un pequeña implementación de la clase vector. Se debe aclarar que la implementación de la misma se hará para vectores contenedores de datos tipo int solamente. Para simplificar el ejemplo, para la clase vector solamente se implementarán los métodos mostrar(), ordenar() e insertar(), así como un método constructor base y un método destructor.

#include <iostream>
#include <cstdio>
#include <ctime>

using namespace std;

#define VECTOR_DEFAULT_SIZE 128

class vector
{
    // atributos
    int capacidad;
    int cuenta;
    int *data;

public:
    // constructor base
    vector()
    {
        capacidad = VECTOR_DEFAULT_SIZE;
        cuenta = 0;
        data = new int[VECTOR_DEFAULT_SIZE];
    }

    // destructor
    ~vector() { delete[] data; }

    // despliega todos los elementos contenidos por el vector
    void mostrar()
    {
        for (int t = 0; t < cuenta; t++)
        cout << "elemento " << t << ", valor " << data[t] << endl;
    }

    // inserta un elemento al vector
    int insertar(int d)
    {
        if (cuenta == capacidad) return -1;
        data[cuenta] = d;
        cuenta ++;
        return cuenta;
    }

    // ordena en forma ascendente los elementos del vector
    void ordenar()
    {
        int i, j, temp;
        int fin = cuenta;

        i = 0;
        while (i < fin )
        {
            for ( j = i ; j < fin-1; j++)
            if ( data[j] > data[j+1] )
            {
                temp = data[j];
                data[j] = data[j+1];
                data[j+1] = temp;
            }
            fin --;
        }
    }
};


#define MAX 10
int main()
{
    vector v;
    srand( time(NULL) );
    for (int r = 0; r < MAX; r++) v.insertar( rand() % 100);    
    cout << "\nvector v sin ordenar\n";
    v.mostrar();

    v.ordenar();
    cout << "\nvector v ordenado\n";
    v.mostrar(); 

    getchar();
    return 0;
}

Una plantilla para la clase vector[editar]

Una vez que se llegado al entendimiento de la programación estructurada así como de la programación orientada al objeto, se puede observar que, si bien es cierto que ambas ofrecen soluciones a problemas fundamentales también es cierto que las soluciones se presentan como casos especializados. Esta última afirmación la podemos ilustrar si nos fijamos en el caso del programa presentado anteriormente, en dicho programa se presenta la clase vector como un contenedor especial para números enteros (int), ahora bien, si prestamos aún aun más atención podemos llegar a la conclusión de que todos los algoritmos o funciones aplicadas en la clase vector pueden operar con cualquier otro tipo de datos y, por lo tanto, la única diferencia sería el tipo de datos contenidos por el vector. De tal manera que aparece lo que se llama generalización o programación genérica y esta, a su vez, nos permite la creación de plantillas basadas en una lógica operatoria previamente concebida.

Con el objetivo de mostrar un ejemplo práctico retomaremos la clase vector del programa de la sección anterior y crearemos, a raiz del mismo, una plantilla. La plantilla resultante tendrá la capacidad para crear vectores de cualquier tipo.

#include <iostream>
#include <cstdio>
#include <ctime>

using namespace std;

#define VECTOR_DEFAULT_SIZE 128

template <class T> class vector
{
    // atributos
    int capacidad;
    int cuenta;
    T   *data;

public:
    // constructor base
    vector()
    {
        capacidad = VECTOR_DEFAULT_SIZE;
        cuenta = 0;
        data = new T[VECTOR_DEFAULT_SIZE];
    }

    // destructor
    ~vector() { delete[] data; }

    void mostrar();
    int  insertar(T d);
    void ordenar();
};

// implementación del método mostrar
template <class T> void vector<T>::mostrar()
{
    for (int t = 0; t < cuenta; t++)
    cout << "elemento " << t << ", valor " << data[t] << endl;
}

// implementación del método insertar
template <class T> int vector<T>::insertar(T d)
{
    if (cuenta == capacidad) return -1;
    data[cuenta] = d;
    cuenta ++;
    return cuenta;
}

// implementación del método ordenar
template <class T> void vector<T>::ordenar()
{
    T temp;
    int i, j, fin = cuenta;

    i = 0;
    while (i < fin )
    {
        for ( j = i ; j < fin-1; j++)
        if ( data[j] > data[j+1] )
        {
            temp = data[j];
            data[j] = data[j+1];
            data[j+1] = temp;
        }
        fin --;
    }
}


#define TEST 10
int main()
{
    // prueba de un vector de números de punto flotante
    vector<double> v;
    srand( time(NULL) );
    for (int r = 0; r < TEST; r++) v.insertar( (rand() % 10) * 0.5);    
    cout << "\nvector v sin ordenar\n";
    v.mostrar();
    v.ordenar();
    cout << "\nvector v ordenado\n";
    v.mostrar(); 

    // prueba de un vector de números long int
    vector<long int> v2;
    srand( time(NULL) );
    for (int r = 0; r < TEST; r++) v2.insertar( (rand() % 100) );    
    cout << "\nvector v2 sin ordenar\n";
    v2.mostrar();
    v2.ordenar();
    cout << "\nvector v2 ordenado\n";
    v2.mostrar();

    getchar();
    return 0;
}


Estructuras II Arriba Excepciones