Práctica 1 | P2 GIR Saltearse al contenido

Práctica 1

A continuación se encuentra el enunciado de la práctica 1. Lee cuidadosamente el enunciado y sigue las instrucciones. Se recomienda consultar el apartado de Teoría para tratar de resolver dudas de concepto. También puedes consultar el enunciado de la práctica 0, donde puedes encontrar algún ejemplo sobre vectores del Standard Template Library (STL) que podría serte útil en esta práctica.

1 Cambios

1.1 Versión 1.0.0

  • Enunciado original.

2 Archivos de partida.

  • Aquí se encuentran los archivos de partida. Descárgalos y utilizalos como base para resolver la práctica.

3 Objetivos

  • Adquirir práctica y destreza con las características de C++ que lo hacen un mejor C.
  • Profundizar en los conocimientos aprendidos en Programación I.
  • Hacer uso de las herramientas estudiadas en la práctica 0.

4 Enunciado

Esta práctica consta de un ejercicio en el que trabajarás con una matriz de datos que contendrá el stock de ejemplares de una biblioteca en C++.

La biblioteca tiene varios almacenes (depósitos) y gestiona varios libros (títulos). La matriz representa cuántos ejemplares de cada libro hay en cada almacén:

  • Las filas se corresponden con los almacenes.
  • Las columnas representan los libros.

Es decir, si tenemos 3 almacenes y 5 libros, data dentro del registro LibraryStock será un puntero que apunte a un array de 3 elementos (los 3 almacenes) y, a su vez, cada uno de esos almacenes será otro puntero que apuntará a la lista de stocks por libro, en este caso a un array de 5 valores.

Emplearemos los siguientes tipos de datos y constantes que se encuentran en p1.h:

/** El tipo de los elementos de la matriz de bajo nivel. */
using copies_t = uint32_t;
/// El tipo de la fila de la matriz de bajo nivel
using warehouse_t = copies_t*;
//! El tipo matriz de bajo nivel.
using matrix_t = warehouse_t*;
// Estructura que contiene los datos de la matriz de stock de biblioteca
struct LibraryStock {
uint32_t warehouses;
uint32_t books;
matrix_t data;
};
// Constante que representa un dato de tipo LibraryStock que no tiene información
// (con la matriz nula y número de almacenes y de libros con el valor 0)
const LibraryStock empty_stock = {0, 0, nullptr};

Nota: En esta práctica, por simplicidad, el stock en cada celda se codifica como un entero en el rango 0..10 (ambos incluidos).

4.1 Funciones de gestión de los datos.

Para poder implementar las funciones, se recomienda seguir el mismo orden en el que se encuentran en el fichero p1.h. Si te fijas, las primeras funciones que aparecen son funciones básicas para trabajar con el registro LibraryStock: para crear una nueva variable (stock_reserve), para liberar la memoria reservada una vez que quiera destruir dicha variable (stock_free), para mostrar la información que contiene (stock_show) y para modificar el stock de un almacén en un libro concreto (set_copies). Vamos a ver en detalle primero estas funciones y más adelante veremos las restantes.

/* Funciones para crear un registro de LibraryStock utilizando memoria dinámica
para construir los datos de acuerdo a la cantidad de almacenes y libros.
Inicializa los valores con el parámetro v, cuyo valor por defecto es 0.
*/
LibraryStock stock_reserve(uint32_t warehouses, uint32_t books, copies_t v = 0);
// Libera la memoria creada con memoria dinámica
void stock_free(LibraryStock& s);
// Visualiza en pantalla los datos del stock en forma de matriz.
// Las filas son los almacenes y las columnas los libros.
void stock_show(const LibraryStock& s);
// Función para modificar el stock de un almacén para un libro concreto
void set_copies(LibraryStock& s, uint32_t warehouse_idx, uint32_t book_idx, copies_t new_copies);

Veamos lo que tienen que hacer estas funciones:

  1. LibraryStock stock_reserve(uint32_t warehouses, uint32_t books, copies_t v = 0) :

    • Crea y devuelve un registro de tipo LibraryStock que contiene una matriz con el stock de la biblioteca.
    • data contendrá una matriz con un número de filas igual al número de almacenes (warehouses) y un número de columnas igual al número de libros (books). Fíjate que data es de tipo matrix_t, que a su vez es de tipo warehouse_t\*. Es decir, data será un puntero que apuntará a un array de tipo warehouse_t, y a su vez, cada uno de los elementos de ese array será otro puntero que apuntará a un array de copies_t, que contendrá el stock del almacén para todos los libros.
    • Para rellenar los datos de data, necesitarás trabajar con memoria dinámica, utilizando el operador new.
    • Si el número de almacenes o de libros solicitado es 0, entonces no debes reservar memoria y debes devolver empty_stock (definido en p1.h).
    • Ten en cuenta que si por ejemplo tienes warehouses = 5 y books = 0, se devolverá empty_stock ya que no se ha podido generar la matriz data. En este caso, tanto warehouses como books dentro del nuevo registro creado tendrán el valor 0, por coherencia con empty_stock.
    • La función inicializará todos los valores de la matriz con el valor proporcionado por el parámetro v, que es opcional y su valor por defecto es 0. Debes comprobar que v tiene un valor entre 0 y 10 (ambos incluidos). Si v no tiene un valor correcto, entonces se devolverá empty_stock.
  2. void stock_free(LibraryStock& s); :

    • Libera la memoria dinámica de la matriz data dentro del parámetro que se recibe por referencia s. Cuando termine la función, s debe de ser como empty_stock, es decir, data tendrá el valor nullptr y los atributos warehouses y books tendrán el valor 0.
  3. void stock_show(const LibraryStock& s); :

    • Muestra la matriz data que contiene s por pantalla como tienes en los ejemplos más abajo. Si la matriz está vacía, no se mostrará nada.
  4. void set_copies(LibraryStock& s, uint32_t warehouse_idx, uint32_t book_idx, copies_t new_copies);:

    • Modifica el stock de un almacén warehouse_idx para un libro específico book_idx. Estos dos parámetros contienen la posición donde se encuentra el almacén y el libro dentro de la matriz. La primera posición será la 0.
    • Si se reciben índices que no existen dentro de la matriz o un valor en new_copies que no está entre 0 y 10, no se debe realizar ninguna modificación ni mostrar ningún mensaje en pantalla.

4.2 Funciones de cálculo.

Se nos pide implementar tres funciones que utilizarán los datos de la matriz para calcular promedios y también para filtrar los datos seleccionando unos pocos. En esta parte de la práctica, trabajarás con vectores.

Al igual que con las funciones anteriores, en p1.h tienes el prototipo de las funciones que tienes que implementar:

// Función que filtra el stock creando un nuevo registro con una copia de los datos
// para determinados almacenes y libros.
LibraryStock stock_filter(const LibraryStock& s,
const std::vector<uint32_t> &warehouse_idxs,
const std::vector<uint32_t> &book_idxs);
// Función que devuelve el promedio del stock por libro (columnas) en forma de vector.
std::vector<copies_t> average_per_book(const LibraryStock& s);
// Función que devuelve el promedio del stock por almacén (filas) en forma de vector.
std::vector<copies_t> average_per_warehouse(const LibraryStock& s);

Y su funcionamiento es el siguiente:

  1. LibraryStock stock_filter(const LibraryStock& s, const std::vector<uint32_t> &warehouse_idxs, const std::vector<uint32_t> &book_idxs); :
    • Esta función filtrará los datos de la matriz para seleccionar solo los datos de algunos almacenes y libros indicados por los parámetros de la función.

    • El objetivo de esta función es construir un nuevo registro LibraryStock a partir de los datos del parámetro s, y de los vectores warehouse_idxs y book_idxs, que representan los índices de los almacenes y de los libros de los cuales se quiere obtener la información. La idea es que la función devuelva un registro nuevo que contendrá únicamente los datos de los almacenes en las posiciones indicadas por warehouse_idxs y de los libros en las posiciones indicadas por book_idxs.

    • Si se da el caso de que todos los índices de almacenes y libros existen dentro del parámetro s, entonces la nueva matriz tendrá un número de filas equivalente al tamaño de warehouse_idxs y un número de columnas equivalente al tamaño de book_idxs.

    • Si hay algún índice de almacenes o de libros en warehouse_idxs o book_idxs que no exista dentro de s, se ignorará ese índice y se pasará al siguiente.

    • Si se da el caso de que alguno de los vectores warehouse_idxs o book_idxs no contiene ningún índice válido, entonces se asume que el nuevo registro no se puede rellenar, por lo que el contenido del registro estará vacío. Esto quiere decir que la matriz data no tendrá datos (tendrá el valor nullptr) y el número de almacenes warehouses y de libros books será 0, por lo tanto su contenido será como el que tiene empty_stock, definido en p1.h. Debes devolver lo mismo si tras comprobarlo, se obtienen 0 almacenes o 0 libros.

    • La primera posición será la 0 en los índices recibidos por parámetro.

    • Recuerda que los almacenes se representan por filas y los libros por columnas dentro de data.

    • Si la función recibe índices repetidos, se deben ignorar los duplicados y sólo utilizar el índice la primera vez que aparece en el vector. Esto aplica tanto a warehouse_idxs como a book_idxs. Hay muchas formas de hacer esto, pero aquí tienes una pista de una posible solución:

      • Pista: puedes declarar un nuevo vector (inicialmente vacío) donde copiar los valores que se encuentran en el vector original, copiandolos únicamente si no se han copiado antes a ese nuevo vector. Si lo haces así, puede serte útil crear una función simple que compruebe si existe o no un índice dentro de un vector. Recuerda no mezclar índices de libros con índices de almacenes.

Veamos un ejemplo (tienes más en mainp1.cc). Supongamos este ejemplo con 3 almacenes y 4 libros:

[ 5 3 6 8 ]
[ 7 10 8 9 ]
[ 3 8 1 4 ]

Si usamos la función stock_filter pidiendo los almacenes 0 y 2 (en el vector warehouse_idxs) y los libros 1 y 3 (en el vector book_idxs), el resultado sería:

[ 3 8 ]
[ 8 4 ]

Si usamos la función stock_filter pidiendo los almacenes 0, 2 y 21 (en el vector warehouse_idxs) y los libros 1, 3 y 122 (en el vector book_idxs), el resultado sería exactamente el mismo (se ignoran índices inválidos).

Si usamos stock_filter pidiendo el almacén 21 (en el vector warehouse_idxs) y los libros 1 y 3 (en el vector book_idxs), el resultado tendŕia el mismo contenido que empty_stock porque no hay ningún índice correcto de almacenes.

Si usamos stock_filter pidiendo un almacén válido (por ejemplo el 2) y los índices de libros no son válidos (por ejemplo 200, 300 y 400), el resultado tendrá el mismo contenido que empty_stock porque no hay ningún índice correcto de libros.

Por otro lado, si utilizamos stock_filter pidiendo los almacenes 0, 2, 2, 1 (en el vector warehouse_idxs) y los libros 1, 1 y 3 (en el vector book_idxs), el resultado sería:

[ 3 8 ]
[ 8 4 ]
[ 10 9 ]
  1. std::vector<copies_t> average_per_book(const LibraryStock& s); :

    • Calculará el promedio del stock por cada libro (por columna), devolviendo un vector de tamaño igual al número de libros.

    • Si el stock no tiene almacenes o libros, devolverá un vector vacío (sin elementos).

    • Nota: como copies_t es uint32_t, el resultado no tendrá valor decimal y se truncará (no se aproximará).

  2. std::vector<copies_t> average_per_warehouse(const LibraryStock& s); :

    • Calculará el promedio del stock por almacén (por fila), devolviendo un vector de tamaño igual al número de almacenes.

    • Si el stock no tiene almacenes o libros, devolverá un vector vacío (sin elementos).

    • También se trunca por ser entero.

4.3 Ejemplo de mainp1.cc.

Tienes un programa de ejemplo en mainp1.cc y su salida debería ser:

Matriz de stock completa:
[ 7 5 9 6 ]
[ 4 8 6 7 ]
[ 9 6 5 8 ]
Promedios por libro:
Libro 0: 6
Libro 1: 6
Libro 2: 6
Libro 3: 7
Promedios por almacén:
Almacén 0: 6
Almacén 1: 6
Almacén 2: 7
Matriz filtrada (almacenes 0 y 2, libros 1 y 3):
[ 5 6 ]
[ 6 8 ]

Consejo 1: No te conformes con el main que dispones en los archivos de partida. Asegúrate de probar tu código tanto como puedas. Puedes modificar la función main del fichero mainp1.cc tanto como necesites para incluir todas las pruebas que consideres oportunas. No se utilizará en la evaluación (solo se utilizará p1.cc).

Consejo 2: cuando pruebes tu código, diseña las pruebas sin mirar tu implementación. Diseñalas solo mirando la especificación del enunciado.

5 Estructura del proyecto.

  • En programación es muy común que los proyectos tengan varios ficheros que separen ciertas partes del código.

  • Por un lado, los archivos .cc, que incluyen la lógica del programa y la implementación de las funciones. La función main está separada en un fichero aparte mainp1.cc. Por otro lado, cada fichero de implementación (excepto mainp1.cc) tendrá un fichero con extensión .h que incluirá la interfaz del código implementado en el archivo .cc con mismo nombre.

  • Aclaración sobre la estructura: imagínate el siguiente supuesto: Has implementado unas funciones que trabajan con fechas en fecha.cc y necesitas invocarlas desde otro fichero de código diferente main.cc. Para poder hacerlo, necesitas declarar la interfaz (las funciones y/o tipos de datos y/o constantes que queremos que sean públicos y visibles desde fuera de fecha.cc). Esto se hace en fecha.h. Después, para utilizarla desde main.cc solo necesitamos añadir la instrucción \#include"fecha.h". Esto lo puedes ver en esta práctica, en los ficheros p1.h y mainp1.cc.

En este caso, la estructura es la siguiente:

irp2-p1.tgz
└── irp2-p1
├── mainp1.cc (incluye la función principal. Puedes modificarlo para probar tu código. No se evaluará.)
├── p1.h (no puedes modificarlo)
├── p1.cc (aquí implementarás tu código. Solo se evaluará este fichero.)
└── Makefile (sirve para configurar la herramienta make. No debes modificarlo)
  • Tendremos un único archivo de cabecera ( p1.h ) con los prototipos de las funciones además de los \#include necesarios para que no de errores de compilación. No debes modificarlo.

  • Este archivo hará uso de la guarda para protegerlo de su inclusión más de una vez que hemos visto en el tema-1.

  • Tendremos un único archivo de implementación ( p1.cc) en la que debes implementar todas las funciones que se piden y añadir los \#include necesarios para que no haya errores de compilación (también incluirá a p1.h). Este archivo no contendrá ningún programa principal (main) pero sí podrá contener las funciones auxiliares que necesites.

  • Para poder probar tus funciones puedes modificar la función main que tienes en el archivo mainp1.cc (el cual incluirá a p1.h) y compilarlo y enlazarlo con el anterior así: g++ -g -Wall --std=c++17 mainp1.cc p1.cc -o p1.

  • También puedes emplear la herramienta make para compilar y enlazar ya que dispones del fichero Makefile que la configura. Solo necesitas escribir en consola make.

  • Solo se evaluará p1.cc en esta práctica, pero el archivo entregado puede tener más ficheros. El resto de ficheros no se utilizarán en la evaluación. Lo importante es que esté p1.cc.

  • Todos los archivos .h y .cc se encontrarán dentro de una carpeta llamada irp2-p1.

6 Uso de make.

El fichero makefile incluye la configuración de la herramienta make con varias opciones que podrían serte útiles. A continuación tienes una lista de las cosas que puedes hacer:

ComandoDescripción
makeCompila y enlaza el código generando el ejecutable.
make runEjecuta la práctica (deberías compilarla primero con make)
make runvEjecuta la práctica activando Valgrind para detectar fugas de memoria.
make vstatsEjecuta la práctica activando Valgrind y el resumen de detección de fugas de memoria.
make debugEjecuta la práctica en modo depuración con GDB y el entorno gráfico.
make tgzBorra archivos compilados y genera el archivo comprimido irp2-p1.tgz que debes entregar.
make cleanElimina los archivos compilados de los dos ejercicios (ejecutables, objetos, etc.).
  • Si has compilado tu programa y vuelves a compilar sin modificar nada del código, make detectará que no necesita compilar nada. Si aún así necesitas compilar, puedes usar primero make clean y después make.

  • Para la ejecución con valgrind necesitas tener instalado el programa valgrind. Puedes instalarlo en linux así:

    • sudo apt-get install valgrind
    • Te será muy útil para detectar accesos a memoria fuera de rango (por ejemplo, si tienes un array de 10 elementos e intentas acceder a la posición 10 (ya que va de 0 a 9), o si utilizas memoria dinámica pero luego se te ha olvidado liberarla cuando ya no la necesitas).

7 Entrega. Requisitos técnicos.

Requisitos que tiene que cumplir este trabajo práctico para considerarse válido y ser evaluado (si no se cumple alguno de los requisitos la calificación será cero):

  • El archivo entregado se llama irp2-p1.tgz (todo en minúsculas), no es necesario entregar ningún programa principal, sólo es necesario tu archivo p1.cc en una carpeta llamada irp2-p1 dentro del comprimido irp2-p1.tgz. Este archivo lo puedes crear así en la terminal, si te sitúas en la carpeta padre donde tienes la carpeta irp2-p1:

tar cfz irp2-p1.tgz irp2-p1

  • También puedes emplear el archivo Makefile que tienes en la carpeta de la práctica para generar el fichero de la entrega tal y como hemos visto en clase de prácticas situandote en la terminal dentro de la carpeta que contiene el fichero Makefile:

make tgz.

  • Una vez comprimido, ábrelo y asegúrate de que tiene la estructura que se pide.

  • Al descomprimir el archivo irp2-p1.tgz se crea un directorio de nombre irp2-p1 (todo en minúsculas).

  • Dentro del directorio irp2-p1 estará al menos el archivo p1.cc (todo en minúsculas).

  • Recuerda que el fichero p1.h no se debe modificar, de lo contrario tus ficheros .cc no compilarán con el corrector. No es necesario entregarlos, pero si se hace no pasa nada.

  • Las clases, métodos y funciones implementados se llaman como se indica en el enunciado (respetando en todo caso el uso de mayúsculas y minúsculas). También es imprescindible respetar estrictamente los textos y los formatos de salida que se indican en este enunciado.

  • Al principio de todos los ficheros fuente (.h y .cc) entregados y escritos por ti se debe incluir un comentario con el nombre y el NIF (o equivalente) de la persona que entrega la práctica, como en el siguiente ejemplo:

// NIF: 12345678-Z
// NOMBRE: GARCIA PEREZ, LAURA
  • Un error de compilación/enlace implicará un cero en esa parte de la práctica.

  • Se utilizará valgrind para comprobar que no haya fugas de memoria en tu programa. Puedes probarlo antes de realizar la entrega para asegurarte de corregir posibles errores relacionados con la gestión de la memoria.

  • Lugar y fecha de entrega : según se publique en la plataforma de prácticas.

8 Detección de plagios/copias.

  • IMPORTANTE:
    • Cada práctica debe ser un trabajo original de la persona que la entrega.
    • En caso de detectarse copia se tomarán las medidas disciplinarias correspondientes.