Curso-lenguaje-C/fundamentos-programacion/PR2
2024-05-05 13:38:22 +02:00
..
soluciones_c Add saldo exercise 2024-05-05 13:07:27 +02:00
README.md Add saldo exercise 2024-05-05 13:38:22 +02:00

PR 2

Volver a la página principal de "Fundamentos de la Programación"

Índice

12. Modularidad

La modularidad es una técnica de programación que consiste en dividir un programa en partes más pequeñas y manejables. Cada parte se denomina módulo y puede ser un subprograma, una función, una acción, un procedimiento, una rutina, un método, etc. La modularidad permite dividir un problema complejo en problemas más simples y fáciles de resolver. Además, facilita la reutilización de código, la depuración y el mantenimiento de los programas.

12.1 Aproximación intuitiva

Un ejemplo con una comida:

algorithm almuerzo
  var
    primerPlato, salsa, segundoPlato, postres: tPlato;
  end var

  primerPlato:= encargarPaella(marisco, 5);
  segundoPlato:= encargarPescado(bacalao, ajoaceite, 5);
  postres:= encargarPostres(pastel, chocolate, velas, 5);
end algorithm

Podemos ver cómo nuestro algoritmo, a base de solicitar a expertos que hagan el trabajo, se convierte en un algoritmo simple y fácil de entender. Lo único que hemos tenido que hacer son peticiones en las que hemos añadido valores, a los que llamamos parámetros, que indican exactamente cómo queremos cada plato.

Los lenguajes de programación permiten trabajar de una forma similar a como lo hemos hecho en el menú. Es decir, se pueden utilizar subalgortimos, o se pueden definir otros nuevos, que quedan aparte del algoritmo principal y que permiten dividir un algoritmo complicado en subalgoritmos más simples. Además, como veremos en los siguientes apartados, otra de las ventajas que proporcionan es el hecho de que estos subalgoritmos se pueden reutilizar evitando la reescritura de código.

Estos subalgoritmos reciben el nombre de acciones y funciones y es como si empaquetásemos un conjunto de instrucciones que resuelven un problema y no nos tenemos que preocupar de lo que hay dentro.

12.1.1. Ejemplo - Cálculo de combinaciones

Supongamos que tenemos que diseñar un algoritmo que permita saber cuántas combinaciones de colores se pueden hacer si disponemos de n telas, cada una de un color diferente. Concretamente se quiere disponer de un algoritmo genérico que dados n colores calcule cuantas combinaciones se pueden hacer utilizando cada vez m colores, donde m y n son enteros y m ≤ n.

Para diseñar el algoritmo deberemos leer los valores de n y m y haberlo indicado en la fórmula. Quedaría:

algorithm combinations
  var
    {definimos las variables, dos enteros que hay que leer y una donde almacenar el resultado}
    totalNumColors, colorsToCombine: integer ;
    result: integer;
  end var

  {leemos los dos números}
  totalNumColors:= readInteger();
  colorsToCombine:= readInteger();

  {Hacemos un esbozo del cálculo, suponiendo que disponemos de una manera de calcular el factorial de un número }
  result := totalNumColors! div (colorsToCombine! * (totalNumColors-colorsToCombine)!) ;
  writeInteger(result);
end algorithm

#include <stdio.h>

int main() {
  int totalNumColors;
  int colorsToCombine;
  int result;
  result = totalNumColors! / (colorsToCombine! * (totalNumColors-colorsToCombine)!) ;
  printf ("%d ", result);
  return 0;
}

Ahora bien, para calcular el factorial de las posible combinaciones se necesita hacer una iteración para ir calculando el producto de todos los enteros, además de la necesidad de variables donde guardar los cálculos parciales::

var
  factorial: integer;
  i: integer;
  n: integer;
end var

n:= readInteger();
factorial:= 1;
i:=1;

while i  n do
  factorial:=factorial*i;
  i:=i+1;
end while

#include <stdio.h>

int main() {
  int factorial;
  int i;
  int n;

  scanf("%d", &n);
  factorial=1;
  i=1;

  while(i <= n) {
    factorial = factorial*i;
    i++;
  }
  return 0;
}

Si incorporamos este código en el algoritmo anterior, el código se complica y se hace menos legible. Deberíamos repetir el código cada vez que necesitaramos el cálculo. La solución es encapsular este código en una acción o función que se encargue de calcular el factorial de un número cada vez que se necesite con una llamada que pase el parámetro. De esta forma, el algoritmo principal quedaría más limpio y fácil de entender.

12.2 Funciones

Una función es un conjunto independiente de instrucciones que resuelven una tarea concreta, que devuelve un valor y que puede ser referenciada (invocada o llamada) desde otro punto del programa.

12.2.1. Sintaxis para la declaración de una función

Para definir una función necesitaremos los siguientes elementos:

  • Nombre para poder identificarla.
  • Una serie de valores sobre los que se quieran hacer los cálculos, que se denominan parámetros formales de la función. Es necesario indicar el nombre y el tipo de dato de cada uno de los parámetros. Actúan como variables locales a la función.
  • Las instrucciones de la función, es decir, el cuerpo de la función.
  • El valor resultante de la función al que se debe indicar el tipo de dato que se va a devolver. Este valor se devuelve mediante la instrucción return.

Sintaxis:

function name(param1: type1 , param2: type2, ..., paramn: typen): returnType
  ...
  ...
  return expression;
end function

returnType name(type1 param1, type2 param2, ..., typen paramn) {
  ...
  ...
  /* returnValue es el valor que calcula la función */
  return returnValue;
}

La cabecera es la parte que describe cómo debe llamarse a una función. Indica su nombre, los parámetros que espera y el tipo que retorna. La estructura de la cabecera es la siguiente:

function name(param1: type1 , param2: type2, ..., paramn: typen): returntype;

returnType name(type1 param1, type2 param2, ..., typen param);

Volviendo a nuestro ejemplo del factorial, la función sería:

function factorial (number: integer): integer
  var
    fact: integer;
    i: integer;
  end var

  fact:= 1;
  i:=1;

  while i  number do
    fact:= fact*i;
    i:= i+1;
  end while

  return fact;

end function

#include <stdio.h>

int factorial(int number) {
  int fact;
  int i;

  fact=1;
  i=1;

  while(i <= number) {
    fact = fact*i;
    i++;
  }
  return fact;
}

Aquí vemos:

  • El nombre factorial para identificar la función.
  • Un único parámetro, en este caso un entero, para identificar el número sobre el que queremos hacer el cálculo del factorial.
  • El tipo de retorno, que también será entero.
  • El cuerpo de la función, que hace los cálculos.

Recordemos que las variables son locales a la función, es decir, que no se pueden utilizar fuera de la función.

12.3. Acciones

A veces interesa un subalgoritmo que realice una serie de instrucciones sin devolver un valor. En estos casos, el lenguaje algorítmico generaliza el concepto de función y define la acción.

Una acción es un conjunto de instrucciones que resuelven una tarea concreta y que puede ser referenciada (invocada o llamada). Se diferencia de una función porque:

  • No devuelve ningún valor.
  • Cuando modifica un parámetro formal, puede estar también modificando el parámetro real.
  • Se debe indicar delante del nombre del parámetro cómo se pasa. Utilizamos las palabras clave in, out o inout.

12.3.1. Sintaxis para la declaración de una función

Para definir una acción necesitaremos los siguientes elementos:

  • Nombre para poder identificarla.
  • Una serie de valores sobre los que se quieran hacer los cálculos, que se denominan parámetros formales de la acción. Es necesario indicar el nombre y el tipo de dato de cada uno de los parámetros. Actúan como variables locales a la acción.
  • Las instrucciones de la acción, es decir, el cuerpo de la acción.

Sintaxis:

action name(class1 param1: type1 , class2 param2: type2, ..., classn paramn: typen)
  ...
  ...
end action

void name(type1 param1, type2 param2, ..., typen paramn) {
  ...
  ...
}

12.4. Cómo utilizar (llamar) a una función y/o acción

Una vez definida la acción o función esta se puede utilizar desde el algoritmo principal o desde otra función o desde otra acción.

Para llamar a una función o acción se utiliza el nombre de la función o acción seguido de los parámetros que se le pasan entre paréntesis. Los parámetros reales pueden ser variables, constantes o expresiones, que deben coincidir en número, tipo y orden con los parámetros formales.

12.4.1. Llamada a una función

Ejemplo:

{Si suponemos que tenemos la siguiente cabecera}
function name(param1: integer , param2: real, param3: char): integer;

{y suponemos que tenemos la declaración de las siguientes variables}
var
  a: integer;
  b: real;
  c: char;
  result: integer;
end var

{La manera de llamar a la función sería:}
result := name(a, b, c);
{Las variables a, b y c son los parámetros actuales de la función}

//Si suponemos que tenemos la siguiente cabecera:
int name(int param1, oat param2, char param3);

//Y suponemos que tenemos la declaración de las siguientes variables:
int a;
oat b;
char c;
int result;

//La manera de llamar a la función en C sería:
result = name(a, b, c);
//Las variables a, b y c son los parámetros actuales de la función

En el caso del factorial, la cabecera era:

function factorial (number: integer): integer;

Las posibles maneras de llamar a la función serían:

n:= factorial(5); /* utilizando una constante como parámetro actual*/

n:= factorial(p) /* utilizando una variable como parámetro actual */

n:= factorial(m+2) /* utilizando una expresión como parámetro actual */

Ahora ya podemos ver el algoritmo completo del factorial:

function factorial (number: integer): integer
  var
    fact: integer;
    i: integer;
  end var

  fact:= 1;
  i:=1;

  while i  number do
    fact:=fact*i;
    i:=i+1;
  end while

  return fact;

end function

algorithm combinations
  var
    {definimos las variables, dos enteros que hay que leer y una donde almacenar el resultado }
    totalNumColors, colorsToCombine: integer ;
    result: integer;
  end var

  {leemos los números}
  totalNumColors:= readInteger();
  colorsToCombine:= readInteger();

  result := factorial(totalNumColors) div ( factorial(colorsToCombine) * factorial(totalNumColors-colorsToCombine));

  writeInteger(result);

end algorithm

#include <stdio.h>

int factorial(int number) {
  int fact;
  int i;

  fact = 1;
  i = 1;

  while(i <= number) {
    fact = fact*i;
    i++;
  }
  return fact;
}

int main() {
  int totalNumColors;
  int colorsToCombine;
  int result;

  scanf("%d", &totalNumColors);
  scanf("%d", &colorsToCombine);

  result = factorial(totalNumColors) / ( factorial(colorsToCombine) * factorial(totalNumColors-colorsToCombine));

  printf ("%d ", result);

  return 0;

}

12.4.2. Llamada a una acción

Ejemplo:

{Si suponemos que tenemos la siguiente cabecera de una acción con todos los parámetros de entrada}
action name(in param1: integer , in param2: real, in param3: char);

{y suponemos que tenemos la declaración de las siguientes variables}
var
  a: integer;
  b: real;
  c: char;
end var

{La manera de llamar a la acción sería:}
name(a, b, c);
{Las variables a, b y c son los parámetros actuales de la acción}

//Si suponemos que tenemos la siguiente cabecera:
void name(int param1, oat param2, char param3);

//Y suponemos que tenemos la declaración de las siguientes variables:
int a;
oat b;
char c;

//La manera de llamar a la acción en C sería:
name(a, b, c);
//Las variables a, b y c son los parámetros actuales de la acción

12.5. Clases de parámetros de una acción

Los parámetros de una acción pueden ser de tres tipos:

  • Parámetro de entrada (in). El valor del parámetro solamente será consultado y utilizado dentro de la acción. Esta manera de pasar parámetros es típica de funciones matemáticas y es la única manera que puede usarse en las funciones.
  • Parámetro de salida (out). Se utiliza para asignar un valor al parámetro. Si el parámetro ya tiene un valor inicial, este valor no se debería utilizar dentro de la acción. Normalmente se pasan los parámetros de esta manera para que sean inicializados por la acción. Puesto que la acción dejará un valor en el parámetro, este debe ser obligatoriamente una variable.
  • Parámetro de entrada/salida (inout). Se utilizan para actualizar el valor del parámetro. En este caso, el valor inicial del parámetro se puede utilizar y también se puede modificar, devolviendo un nuevo valor a quien ha llamado a la acción. Puesto que la acción dejará un nuevo valor en el parámetro, este debe ser obligatoriamente una variable.

12.5.1. Parámetros de entrada

Disponemos de una acción que, dados el número de piezas compradas a un mayorista y el precio por pieza, calcula el coste total y lo muestra por el canal estándar, teniendo en cuenta que según el número de piezas, se aplicará un tipo de descuento u otro:

const
  MAX_DISCOUNT: real = 0.15;
  MIN_DISCOUNT: real = 0.05;
  LIMIT: integer = 1000;
end const

action computeCost(in num: integer, in price: real)
  var
    val: real;
    discount: real;
  end var

  if num  LIMIT then
    discount:= MAX_DISCOUNT;
  else
    discount:= MIN_DISCOUNT;
  end if

  val:= integerToReal(num) * price * (1.0 - discount);
  writeReal(val);
end action

algorithm example1
  var
    num: integer;
    price: real;
  end var

  num:= 1005;
  price:= 10.0;
  computeCost(num, price);
end algorithm

#include <stdio.h>

#define MAX_DISCOUNT 0.15
#define MIN_DISCOUNT 0.05
#define LIMIT 1000

void computeCost(int num, oat price) {
  oat val;
  oat discount;

  if (num >= LIMIT) {
    discount = MAX_DISCOUNT;
  } else {
    discount = MIN_DISCOUNT;
  }

  val = (oat) (num)*price* (1.0 - discount);
  printf("%f", val);
}

int main() {
  oat price;
  int num;
  num = 1005;
  price = 10.0;

  computeCost(num, price);

  return 0;
}

Comentarios:

  • Aunque los parámetros formales y actuales tengan el mismo nombre, se trata de variables diferentes.
  • Podemos ver que los parámetros actuales no han cambiado.

12.5.2. Parámetros de salida

El ejemplo anterior pero con una acción que tiene un parámetro de salida para devolver el coste:

const
  MAX_DISCOUNT: real = 0.15;
  MIN_DISCOUNT: real = 0.05;
  LIMIT: integer = 1000;
end const

action computeCost(in num: integer, in price: real, out val: real)
  var
    discount: real;
  end var

  if num  LIMIT then
    discount:= MAX_DISCOUNT;
  else
    discount:= MIN_DISCOUNT;
  end if

  val:= integerToReal(num) * price * (1.0 - discount);
end action

algorithm example2
  var
    num: integer;
    price: real;
    cost: real;
  end var

  num:= 1005;
  price:= 10.0;

  computeCost(num, price, cost);
  writeReal(cost);

end algorithm

#include <stdio.h>

#define MAX_DISCOUNT 0.15
#define MIN_DISCOUNT 0.05
#define LIMIT 1000

void computeCost(int num, oat price, oat *val) {
  oat discount;

  if (num >= LIMIT) {
    discount = MAX_DISCOUNT;
  } else {
    discount = MIN_DISCOUNT;
  }

  *val = (oat) (num)*price* (1.0 - discount);
}

int main() {
  oat price;
  int num;
  oat cost;
  num = 1005;
  price = 10.0;

  computeCost(num, price, &cost);

  printf("%f", cost);

  return 0;

}

Comentarios:

  • Al tratarse de una acción, se llama sin asignar el retorno a una variable, ya que las acciones no retornan ningún valor.
  • La acción tiene un parámetro formal de salida. Esto quiere decir que cada vez que se modifique este parámetro dentro de la acción, automáticamente se modificará con el mismo valor el parámetro actual correspondiente cost.
  • Podemos ver que en lenguaje C, los parámetros de salida son en realidad Punteros. Más adelante se muestra un ejemplo.

12.5.3. Parámetros de entrada/salida

Supongamos ahora que el coste final es la suma de unos gastos mínimos (coste de entrega, etc.) más el coste de venta. El coste mínimo viene dado por una constante BASIC_COST.

const
  MAX_DISCOUNT: real = 0.15;
  MIN_DISCOUNT: real = 0.05;
  LIMIT: integer = 1000;
  BASIC_COST: real = 100.0;
end const

action computeCost(in num: integer, in price: real, inout val: real)
  var
    discount: real;
  end var

  if num  LIMIT then
    discount:= MAX_DISCOUNT;
  else
    discount:= MIN_DISCOUNT;
  end if

  val:= val + integerToReal(num) * price * (1.0 - discount);

end action

algorithm example3
  var
    num: integer;
    price: real;
    cost: real;
  end var

  num:= 1005;
  price:= 10.0;
  cost:= BASIC_COST;

  computeCost(num, price, cost);

  writeReal(cost);

end algorithm

#include <stdio.h>

#define MAX_DISCOUNT 0.15
#define MIN_DISCOUNT 0.05
#define LIMIT 1000
#define BASIC_COST 100.0

void computeCost(int num, oat price, oat *val) {
  oat discount;

  if (num >= LIMIT) {
    discount = MAX_DISCOUNT;
  } else {
    discount = MIN_DISCOUNT;
  }

  *val = *val + (oat) (num) * price * (1.0 - discount);
}

int main() {
  oat price;
  int num;
  oat cost;
  num = 1005;
  price = 10.0;
  cost = BASIC_COST;

  computeCost(num, price, &cost);

  printf("%f", cost);

  return 0;
}

Comentarios:

  • La única diferencia con el ejemplo 2 es que el parámetro val ahora es de entrada y salida y su valor se actualiza sumándole el coste de venta.

12.6. Funciones contra acciones

Función: el criterio para usar una función es evaluar si esta puede actuar como una función matemática que, dados unos parámetros, puede retornar un valor concreto que se puede utilizar como un miembro de una expresión. Si es así, claramente debemos escribir una función.

Acción: si necesitamos actualizar uno de los parámetros o inicializar uno que no lo está, claramente debemos usar una acción. También se utiliza cuando no se retorna ningún valor o cuando se deben devolver más de uno.

Muchas veces las acciones tienen algunos parámetros de salida, pero se podría dar el caso de que no tenga ninguno como, por ejemplo, en el caso de que la acción lea la información del canal estándar y saque también los resultados directamente por el canal estándar.

12.7. Acciones y funciones predefinidas

Tanto en el lenguaje algorítmico como en los de programación, hay algunas acciones y funciones ya definidas por defecto.

Entre otras tenemos:

  • Funciones de conversión de tipo.
  • Acciones y funciones de entrada y salida de datos.

Las funciones de conversión de tipo son esas que transforman una variable de un tipo a otro:

function integerToReal(x: integer): real;
function realToInteger(x: real): integer;
function charToCode(c: char): integer;
function codeToChar(x: integer): char;

#include <stdio.h>

int main() {
  int x;
  oat y;
  char c;

  y = (oat) x;
  x = (int) y;
  x = (int) c;
  c = (char) x;
}

Las acciones/funciones de entrada y salida de datos son esas que permiten recibir datos del exterior (teclado, ficheros, etc.) y mostrar datos al exterior:

function readInteger(): integer;
function readReal(): real;
function readChar(): char;
function readString(): string;

action writeInteger(in x: integer);
action writeReal(in x: real);
action writeChar(in x: char);
action writeString(in x: string);

#include <stdio.h>

int main() {
  int x;
  oat y;
  char c;
  string s;

  scanf("%d", &x);
  scanf("%f", &y);
  scanf("%c", &c);
  scanf("%s", s);

  printf("%d ", x);
  printf("%f ", y);
  printf("%c ", c);
  printf("%s ", s);

  return 0;
}

12.8. Modularización

Para mantener el conjunto de acciones y funciones ordenadas y que se puedan utilizar en diferentes programas de manera fácil, los lenguajes permiten crear librerías.

Estas librerías constan de dos partes:

  • Un fichero de texto que inclute las cabeceras de las acciones y funciones que la componen y que en C tienen siempre la extensión .h.
  • El código de las acciones y funciones, que también tendrá un extensión .c, pero que solamente tendrá el código de la acciones y/o funciones, sin tener un main.

Cuando se tendan que emplear desde algún otro programa solamente se incluirán en el programa el fichero de cabeceras.h. La sintaxis:

#include "nomLlibreria.h"

Concretamente, la librería que incluye las cabeceras de las acciones y funciones de entrada y salida de datos se llama en C stdio.h:

#include <stdio.h>

Hay muchos ejemplos de librerías, por ejemplo:

  • Librerías de funciones matemáticas. math.h, stdlib.h, etc.
  • Librerías de funciones y acciones de tratamiento de fechas. time.h, date.h, etc.
  • Librerías de funciones de tratamiento de cadenas de caracteres. string.h, etc.

La posibilidad de crear las librerías facilita la reutilización del código y estandarización de la manera de trabajar.

12.9 Ejercicios

  1. Diseñad una función que, dado un entero n, calcule la suma de los primeros n números de la serie Fibonacci.

Solución en lenguaje C

  1. Diseñad una acción que, dados un real que representa el saldo de una cuenta corriente y otro real que representa un cargo o imposición, actualice el saldo de la cuenta.

Solución en lenguaje C

  1. Diseñad una función que calcule n (real) elevado a la potencia m (entero).

Solución en lenguaje C

  1. Diseñad una acción que, dados tres números reales que representan los coeficientes de una ecuación de segundo grado, retorne sus raíces, en caso de que existan. En caso contrario, que inicialice las raíces a 0. Recordad que dada la ecuación A * x2 + B * x + C las raíces se calculan:
x1 = (-B + sqrt(B2 - 4 * A * C)) / (2 * A)

Si la expresión dentro de la raíz es negativa quiere decir que la ecuación no tiene raíces. Podéis suponer que la función raíz cuadrada está ya definida y su cabecera es:

function squareRoot(x: real): real;

Solución en lenguaje C

4b. Declarad las variables necesarias para llamar a la acción del ejercicio anterior e indicad cómo se llamaría.

Solución en lenguaje C

  1. Para cada apartado siguiente, decidid si es mejor una acción o una función y definid su cabecera (solamente se pide la cabecera, no hay que diseñar ningún algoritmo).
  • Dadas las dimensiones de una pared (altura y anchura) y el precio de pintura por metro cuadrado, determinad el coste total de pintura necesario para pintar la pared.
  • Dado el consumo total de agua acumulado desde el mes de enero hasta junio (ambos incluidos), leed por el canal estándar el consumo del resto de meses (desde julio a diciembre, ambos incluidos), y retornadlo acumulado al total del consumo de los 6 primeros meses.
  • Dados dos números enteros intercambiad los valores de ambas variables siempre que ambas sean diferentes de cero.
  • Dado un entero retornad la suma de sus divisores.

Solución en lenguaje C

5b. Declarad las variables necesarias para llamar a las acciones del ejercicio anterior e indicad cómo se llamarían.

Solución en lenguaje C

12.10 Soluciones en lenguaje algorítmico

  1. Solución
function fibonacci(n: integer): integer
  var
    i, suma, a, b: integer;
  end var

  if n < 3 then
    suma:= 1;
  else
    a:= 0;
    b:= 1;
    suma:= 0;

    for i:= 1 to n do
      suma:= a+b;
      b:= a;
      a:= suma;
    end for

  end if

  return sum;

end function

algorithm main
  var
    number: integer;
  end var

  writeString("Introduce un número entero positivo: ");
  number:= readInteger()

  writeString("La suma de los ");
  writeInteger(number);
  writeString(" primeros números de la serie Fibonacci es: ");
  writeInteger(fibonacci(n));

end algorithm
  1. Solución
const
  SALDO_INICIAL: real = 0.0;
end const

action actualizarsaldo(inout saldo: real, in cargo: real)
  saldo:= saldo + cargo;
end action

algorithm main
  var
    saldo, cargo: real;
    continuar: string;
  end var

  saldo:= SALDO_INICIAL;

  writeString("[i] El saldo actual de la cuenta corriente es de ");
  writeReal(saldo);
  writeString(" Petrodolares");

  while continuar = "n" do
    writeString("[+] Introduce el cargo o imposición: ");
    cargo:= readReal();

    actualizarsaldo(saldo, cargo);

    writeString("[i] El saldo actual de la cuenta corriente es de ");
    writeReal(saldo);
    writeString(" Petrodolares");

    writeString("[+] Quiere realizar otra operación? (s/n): ");
    continuar:= readString();

  end while

end algorithm
  1. Solución

  1. Solución

4b. Solución


  1. Solución

5b. Solución


Volver arriba