12/04/2013 - Clase 2: Laboratorio y Memoria Dinámica Manual en C

Fecha de publicación: 12-abr-2013 20:34:59

Configurando el Entorno

Arrancamos copiando del DVD solo los componentes que necesitábamos.

Vimos que las máquinas eran windows de 32 bits, así que solo copiamos:

    • eclipse/eclipse-cpp*-win32.zip
    • java/*win*i586.exe
    • c-compiler/mingw
    • python/PyDev-*.zip

Para el eclipse no hacía falta instalarlo, símplemente se descomprime en una carpeta donde querramos que quede luego.

Tengamos en cuenta que las máquinas del laboratorio las usan también personas para otras materias y otros cursos. Así que intentemos ordenar los archivos que copiamos.

Háganse una carpeta "Programacion II" o bien en la home del usuario o bien en el escritorio. Y descompriman el eclipse ahí.

Tanto java como mingw se instalan.

Para el instalador de java necesitábamos permisos de administrador, así que ahí nos trabamos un rato, hasta que Diego nos ayudó con esto.

Después vimos que teníamos un problema para buildear con el eclipse C, así que ahí cortamos con las máquinas y pasamos a ver el tema de la clase.

Quedó pendiente entonces tener todo funcionando. Vamos a estar instalando todo en un windows nosotros y escribiendo las instrucciones completas. Les avisamos luego por la lista.

Gestión Manual de Memoria Dinámica en C

Vimos inicialmente las funciones básicas de C para memoria: malloc y free.

Pueden ver una descripción de esto en ésta página de la unidad 1.

Nuestro primer ejemplo era bastante simple para alocar dinámicante memoria para un int. Es el mismo que está en la página antes mencionada.

Luego vimos un segundo ejemplo que constaba de un programa que le preguntaba al usuario cuántos números quería ingresar. Luego alocaba memoria ya no solo para 1 int, sino para los N que haya dicho el usuario. Después de que el usuario ingresaba los números éstos se mostraban ordenados.

La versión inicial de este programa, aunque sin el "reordenamiento", es la que figura en la misma página que mencionamos.

La que vimos tiene las siguientes diferencias:

    • tiene las funciones para ordenar e imprimir el array
    • mismo en la clase cambiamos la lógica que iteraba y leí los valores, para utilizar aritmética de punteros en lugar de utilizar el puntero como un array (ejemplo original). Ésto es la implementación de la función leerNumeros.

Acá el código del archivo principal

#include <stdio.h>
#include <stdlib.h>
#include "prog2-utiles.h"
int leerCantidadAIngresar() {
    printf("Cuantos numeros va a ingresas ?: ");
    int cantidad;
    scanf("%i", &cantidad);
    return cantidad;
}
void leerNumeros(int *numeros, int cantidad) {
    printf("Ingrese los %i numeros\n", cantidad);
    int* puntero;
    int* punteroAlFinal = numeros + cantidad;
    for (puntero = numeros; puntero < punteroAlFinal; puntero++) {
        scanf("%i", puntero);
    }
}
void imprimirNumeros(int *numeros, int cantidad) {
    printf("Sus %i numeros ordenados son: ", cantidad);
    imprimirArray(numeros, cantidad, ',');
}
int main(void) {
    int cantidad = leerCantidadAIngresar();
    int *numeros = malloc(cantidad * sizeof(int));
    leerNumeros(numeros, cantidad);
    ordenar(numeros, cantidad);
    imprimirNumeros(numeros, cantidad);
    free(numeros);
    return 0;
}

Y, como vimos, teníamos funciones útiles codificadas aparte para poder reutilizarlas.

Para eso las definimos en un archivo header "prog2-utils.h":

#ifndef PROGR2_UTILES_H_
#define PROGR2_UTILES_H_
void ordenar(int *vector, size_t longitud);
void imprimirArray(int *array, size_t cantidad, char separador);
#endif /* PROGR2_UTILES_H_ */

Y en otro archivo "prog2-utils.c" (mismo nombre pero .c), la implementación:

int compararInts(const void* elem1, const void* elem2) {
    int* punteroElem1 = (int*) elem1;
    int f = *punteroElem1;
    int s = *((int*) elem2);
    if (f > s) { return  1; }
    if (f < s) { return -1; }
    return 0;
}
void ordenar(int *vector, size_t longitud) {
    qsort(vector, longitud, sizeof(int), compararInts);
}
void imprimirArray(int *array, size_t cantidad, char separador) {
    int i;
    for (i = 0; i < cantidad; i++) {
        printf("%i", array[i]);
        if (i < cantidad - 1) {
            printf("%c", separador);
        }
    }
}

Acá estuvimos un rato hablando de varias cosas que en realidad no necesáriamente tiene que ver con memoria dinámica, pero que son parte de la programación.

Funciones de Orden Superior

Por un lado dijimos que la función ordenar no tiene toooodo el código para ordenar, si no que utilizar una función ya existente en C que se llama qsort (algoritmo de ordenamiento quick sort).

Vimos que ésta funcion qsort es lo que en el paradigma funcional se denomina función de orden superior, ya que es una función que recibe como parámetro otra función que es la que realmente va a saber comparar dos elementos y ver cual es el más grande.

Ésto es así porque qsort es una función genérica que no solo permite ordenar arreglos de int's si no cualquier tipo de arreglos. La función ya implementa toda la lógica para ir recorriendo y comparando los elementos entre sí, y luego se toma el trabajo de ir intercambiando los contenidos entre las posiciones del array para que quede ordenado. Pero, como cada tipo de dato se compara de forma distinta, no puede tener ya esa lógica de la comparación en código "fijo". Por lo tanto, se parametríza, es decir que nosotros, desde afuera, le damos ese comportamiento.

Cómo lo hacemos ? pasando como parámetro el nombre de una función, en nuestro caso compararInts, que es la que sabe comparar dos ints.

Ya vamos a ver un poco más de funciones de orden superior cuando comencemos a trabajar con Python y estructuras de datos.

Casteos

Luego, como consecuencia vimos la idea de casteo. Ya que qsort espera que la función que le pasamos por parámetro reciba dos argumentos de tipo void *.

De paso vimos que "void *" es la forma de que tiene C para referirse al tipo de dato puntero genérico, es decir un puntero a una sección de memoria, de la cual no se qué tipo de datos tiene.

Entonces, como nosotros mismos somos los que estamos usando compararInts y sabemos que la vamos a usar para comparar enteros, podemos asumir que esos dos parámetros, van a ser en realidad punteros a int.

Así que le decimos a C: "vos confía, yo se, esta variable es de tipo int*", que es lo que se llama castear.

Esto se hace en las primeras 3 lineas del método compararInts(). Las dos primeras son para el primer argumento, donde en la primera casteamos asignando a una nueva variable, y en la segunda accedemos al contenido apuntado por el puntero.

En la 3er linea, para el segundo argumento, estamos haciendo todo en una misma linea: casteamos y luego accedemos al contenido. Por eso usamos los paréntesis allí, para ordenar la precedencia de operadores:

Entonces un casteo se hace precediendo una variable con un tipo rodeado de paréntesis.

float miVariableFloat =  2.1;
int miVariableInt = (int) miVariableFloat;

Y ojo porque los paréntesis también los usamos para demarcar precedencia, que no tiene nada que ver con castear. Por ejemplo vimos que para acceder a un miembro de un struct que tenemos referenciado por un puntero teníamos que: primero "viajar" por el puntero hasta el contenido:

*punteroAStruct

y luego acceder al miembro, como haríamos

persona.nombre

Para eso necesitamos demarcar la precedencia (qué operador tiene que ejecutar primero, y qué después):

(*punteroAStruct).nombre

Alocando memoria para Struct's

Luego pasamos a un ejemplo un poco más complicado, donde queríamos tener una aplicación para cargar facturas o compras.

Para eso dividimos el problema en dos partes. Primero simplemente ver cómo crear un Producto e imprimirlo.

Acá va la implementación:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Producto {
    int id;
    char nombre[20];
    float precio;
};
// declaraciones de funciones
struct Producto* crearProducto(int id, char* nombre, float precio);
void imprimirProducto(struct Producto* producto);
// main
int main(void) {
    struct Producto *producto = crearProducto(213, "Aceite de Oliva", 35.5);
    imprimirProducto(producto);
    free(producto);
    return 0;
}
// implementacion de funciones
struct Producto* crearProducto(int id, char* nombre, float precio) {
    struct Producto* producto = malloc(sizeof(struct Producto));
    producto->id = id;
    strcpy(producto->nombre, nombre);
    producto->precio = precio;
    return producto;
}
void imprimirProducto(struct Producto* producto) {
    printf("Producto[id=%i, nombre='%s', precio=$%.2f]", producto->id, producto->nombre, producto->precio);
}

Vemos que no hay demasiada diferencia a cuando alocábamos memoria para un int. En este caso hacemos:

malloc(sizeof(struct Producto)))

Es importante el uso del sizeof para evitar "hardcodear" tamaños en términos de bytes. Además, porque el tamaño suele ser dependiente de la arquictectura.

Por otro lado, dijimos que es importante organizar nuestro código en funciones chiquitas que hagan una sola cosa.

Una particularidad de estos métodos que implementamos es que se manejan con punteros a los structs, en lugar de structs.

Si usaran simplemente struct Producto sucedería que, por ejemplo, crearProducto no necesitaría del malloc. Estaría reservando memoria local al método para un Struct. Luego el return estaría copiando toda la estructura a otro especio de memoria local (en pila) para el main. Y lo mismo, al llamar a imprimirProducto() se estarían copiando las estructuras.

Para evitar todas estas copias, pasamos el puntero.

Pero no es exacta o exclusívamente proque nos preocupen la cantidad de memoria y la duplicación, si no, que, anticipamos, que en los lenguajes orientados a objetos es natural que cuando paso un objeto por parámetro (así como acá pasamos un struct), siempre se pasan por referencia, automáticamente.

En C tenemos que usar punteros explícitamente.

Ejemplo completo con Factura e Item

Luego implementamos el ejemplo completo de la Factura.

Apareció la idea del Item que representa la cantidad y tipo de producto que el cliente está comprando. Y una Factura tiene 1 o muchos Item's.

Acá va el código completo:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Producto {
    int codigo;
    float precio;
    char nombre[20];
};
struct Factura {
    int numero;
    char cliente[20];
    struct Item* items;
    int cantidadItems;
};
struct Item {
    int cantidad;
    struct Producto* producto;
};
struct Factura* crearFactura(int numero, char* cliente) {
    struct Factura* factura = malloc(sizeof(struct Factura));
    factura->numero = numero;
    strcpy(factura->cliente, cliente);
    factura->cantidadItems = 0;
    factura->items = NULL;
    return factura;
}
void agregarItem(struct Factura* factura, struct Producto* producto, int cantidad) {
    //reasigna el tamaño de la memoria que teniamos reservada
    factura->items = realloc(factura->items, sizeof(struct Item) * (factura->cantidadItems + 1));
    // popula el item
    factura->items[factura->cantidadItems].producto = producto;
    factura->items[factura->cantidadItems].cantidad = cantidad;
    factura->cantidadItems++;
}
float costoItem(struct Item* item) {
    return item->cantidad * item->producto->precio;
}
float costo(struct Factura* factura) {
    int i;
    float costo = 0;
    for (i = 0; i < factura->cantidadItems; ++i) {
        costo += costoItem(&factura->items[i]);
    }
    return costo;
}
struct Producto* crearProducto(int codigo, float precio, char* nombre) {
    struct Producto* producto = malloc(sizeof(struct Producto));
    producto->codigo = codigo;
    producto->precio = precio;
    strcpy(producto->nombre, nombre);
    return producto;
}
void imprimirProducto(struct Producto* producto) {
    printf("Producto codigo=%i, precio=%.2f, nombre=%s", producto->codigo, producto->precio, producto->nombre);
}
int main(void) {
    struct Factura* factura = crearFactura(8823, "Leo");
    agregarItem(factura, crearProducto(123, 2.5, "Salsa Tomate"), 2);
    agregarItem(factura, crearProducto(123, 7.25, "Fideos"), 1);
    float costoTotal = costo(factura);
    printf("Costo Total: %.2f", costoTotal);
    return EXIT_SUCCESS;
}

Arrancamos tirando el código desde el main() y viendo "cómo nos gustaría que quede el código del que va a usar nuestro programa". En lugar de empezar "desde adentro", desde cómo implementar la Factura, el Item etc, arrancamos pensando cómo quedaría nuestro código que va a crear una factura, agregarle items y luego consultar el costo total. Aunque no compilara inicialmente.

Dijimos que esta idea de construir software se denomina diseño basado en la interfaz. Donde interfaz sería los métodos y objetos que expone nuestro programa para ser usados por otro programa o desarrollador.

Luego sí, nos metimos a tratar de crear todas esas funciones e implementarlas, para que funcione.

La creación de la Factura era similar a la del producto. Alocando memoria con malloc.

Lo más complicado apareción al implementar el agregarItem.

Como nuestras facturas podían tener un número dinámico y cambiante de items, deberíamos tener un puntero a una zona de memoria que contega los Item's.

Por eso struct Factura tiene un puntero a item (a la primer posición de memoria que contendrá items contiguos -uno detrás de otro-).

Pero como ya vimos antes, con esto no alcanza, porque necesitamos saber cuántos items tenemos alocados, de otra forma no sabríamos hasta donde terminar de leer al recorrer la memoria. Entonces necesitamos otro miembro de Factura: cantidadItems. Quedaba así:

    struct Item* items;
    int cantidadItems;

Luego, al agregar un nuevo item, debíamos agrandar la cantidad de memoria asignada, para que tenga lugar para un nuevo item.

Para esto vimos que hay otra función útil llamada realloc que sirve para re-dimensionar una porción de memoria. Es decir que si tengo un puntero a una posición de memoria con tamaño suficiente para alocar 3 Items, ahora necesitamos agrandarla, para que tenga más lugar y poder alocar 4 items.

La función realloc recibe:

    • puntero: el puntero a la posición de memoria que queremos reajustar
    • tamaño: el nuevo tamaño (podría ser más grande o más chico en base a si queremos agrandar/achicar).

Eso es lo que hacemos en esta linea:

    factura->items = realloc(factura->items, sizeof(struct Item) * (factura->cantidadItems + 1));

Parece medio complicada, porque el nuevo tamaño es en realidad el tamaño actual + 1. Porque necesitamos un lugar más, un Item más.

A ver si, así queda más legible

    int nuevoTamanio = sizeof(struct Item) * (factura->cantidadItems + 1);
    factura->items = realloc(factura->items, nuevoTamanio);

Y, otra cosa a notar es que la función realloc quizás no podría expander la zona actual apuntada por nuestro puntero. En cambio podría suceder que la única zona libre que encuentre para el nuevo tamaño esté "lejos" de la posición inicial. En este caso, C se va a encargar de alocar la nueva zona, copiar el contenido de la original y luego liberar la incial.

Por esto es que realloc retorna un puntero, y siempre debemos asignarlo al puntero que le pasamos por parámetro. Por si cambia la posición.

Finalmente, como estamos alocando tamaño para sizeof(struct Item), quiere decir que al alocar, ya va a haber un nuevo Item. Así que no hace falta hacer un malloc para los Items. Simplemente usamos la posición de memoria del último item. Eso está en las lineas siguientes:

    factura->items[factura->cantidadItems].producto = producto;
    factura->items[factura->cantidadItems].cantidad = cantidad;
    factura->cantidadItems++;

Con factura->items[factura->cantidadItems] accedemos al último struct Item que acabamos de alocar.

Para evitar duplicar las lineas para asignarle producto y cantidad., podríamos calcularlo una sola vez:

    struct Item * itemNuevo = &(factura->items[factura->cantidadItems]);
    itemNuevo->producto = producto;
    itemNuevo->cantidad = cantidad;

itemNuevo es una nueva variable de tipo puntero que apunta al último Item.

Ojo que en este caso necesitamos sí o sí usar una variable de tipo puntero y no de tipo struct Item. De otra forma estaríamos declarando un nuevo item que se guardaría en la pila.

Lo que viene

La clase que viene entonces vamos a terminar de instalar el ambiente y trabajar en el trabajo práctico que ya publicamos acá. Y se puede acceder desde "Material->TP's y Ejercicios".

Como dijimos el TP se puede hacer en grupos de dos, o bien individual. En caso de armar grupos avisen por mail a Leo y a mí (Javi), así agregamos el grupo a la planilla.

Seguramente la clase que viene vamos a estar repasando un poco estos temas de punteros, structs, malloc, etc, además de continuar con el programa.

Saludos!