AnteriorPosterior

Tema 8: Procedimientos y funciones.

  Curso: Curso de Pascal, por Nacho Cabanes

Tema 8: Procedimientos y funciones.

Tema 8.1: Procedimientos.

La programación estructurada trata de dividir el programa en bloques más pequeños, buscando una mayor legibilidad, y más comodidad a la hora de corregir o ampliar.

Por ejemplo, en el caso de nuestra maravillosa agenda, podemos empezar a teclear directamente y crear un programa de 2000 líneas que quizás incluso funcione, o dividirlo en partes, de modo que el cuerpo del programa sea


begin
  InicializaVariables;
  PantallaPresentacion;
  Repeat
  PideOpcion;
  case Opcion of
      '1': MasDatos;
      '2': CorregirActual;
      '3': Imprimir;
    ...
    end;
  Until Opcion = OpcionDeSalida;
  GuardaCambios;
  LiberaMemoria
end.

begin
InicializaVariables;
PantallaPresentacion;
Repeat
PideOpcion;
case Opcion of
'1': MasDatos;
'2': CorregirActual;
'3': Imprimir;
...
end;
Until Opcion = OpcionDeSalida;
GuardaCambios;
LiberaMemoria
end.


Bastante más fácil de seguir, ¿verdad?

En nuestro caso (en el lenguaje Pascal), estos bloques serán de dos tipos: procedimientos (procedure) y funciones (function).

La diferencia entre ellos es que un procedimiento ejecuta una serie de acciones que están relacionadas entre sí, y no devuelve ningún valor, mientras que la función sí que va a devolver valores. Veamoslo con un par de ejemplos:


procedure Acceso;
var
    clave: string; (* Esta variable es local *)
begin
    writeln(' Bienvenido a SuperAgenda ');
    writeln('=========================='); (* Para subrayar *)
    writeln; writeln; (* Dos líneas en blanco *)
    writeln('Introduzca su clave de acceso');
    readln( clave ); (* Lee un valor *)
    if clave <> ClaveCorrecta then (* Compara con el correcto *)
        begin (* Si no lo es *)
        writeln('La clave no es correcta!'); (* avisa y *)
        exit (* abandona el programa *)
        end
end;

Primeros comentarios sobre este ejemplo:

  • El cuerpo de un procedimiento se encierra entre "begin" y "end", igual que las sentencias compuestas y que el propio cuerpo del programa.
  • Un procedimiento puede tener sus propias variables, que llamaremos variables locales, frente a las del resto del programa, que son globales. Desde dentro de un procedimiento podemos acceder a las variables globales (como ClaveCorrecta del ejemplo anterior), pero desde fuera de un procedimiento no podemos acceder a las variables locales que hemos definido dentro de él.
  • La orden exit, que no habíamos visto aún, permite interrumpir la ejecución del programa (o de un procedimiento) en un determinado momento.
Ejercicio propuesto: Crear un procedimiento "LimpiarPantalla", que escriba 25 líneas en blanco en la pantalla, de modo que el texto que estaba escrito anteriormente en pantalla deje de estar visible (en una pantalla de texto convencional de 80x25 caracteres).

Tema 8.2: Funciones.

Veamos el segundo ejemplo: una función que eleve un número a otro (esa posibilidad no existe "de forma nativa" en Pascal), se podría hacer así, si ambos son enteros:


function potencia(a,b: integer): integer; (* a elevado a b *)
var
    i: integer;        (* para bucles *)
    temporal: integer; (* para el valor temporal *)
begin
    temporal := 1;             (* incialización *)
    for i := 1 to b do
    temporal := temporal * a;  (* hacemos 'b' veces 'a*a' *)
    potencia := temporal;      (* y finalmente damos el valor *)
end;


Comentemos cosas también:

  • Esta función se llama "potencia".
  • Tiene dos parámetros llamados "a" y "b" que son números enteros (valores que "se le pasan" a la función para que trabaje con ellos).
  • El resultado va a ser también un número entero.
  • "i" y "temporal" son variables locales: una para el bucle "for" y la otra almacena el valor temporal del producto.
  • Antes de salir es cuando asignamos a la función el que será su valor definitivo.
Ejercicio propuesto: Crear una función "triple", que reciba un número real como parámetro, y devuelva como resultado ese número multiplicado por tres.
Ejercicio propuesto: Crear una función "media", que reciba dos números reales como parámetros, y devuelva como resultado su media artimética.

(Nota: al final de este apartado tienes la lista de funciones matemáticas y de manipulación de cadenas que incluye Turbo Pascal)


Tema 8.3: Uso de las funciones.

Pero vamos a ver un programita que use esta función, para que quede un poco más claro:


 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Primer ejemplo de     }
 {    función: potencia     }
 {    POTENCIA.PAS          }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Free Pascal 2.2.0w  }
 {    - Turbo Pascal 7.0    }
 {    - Turbo Pascal 5.0    }
 {    - Surpas 1.00         }
 {--------------------------}

 program PruebaDePotencia;

 var
   numero1, numero2: integer;                (* Variables globales *)

 function potencia(a,b: integer): integer;  (* Definimos la función *)
 var
   i: integer;                      (* Locales: para bucles *)
   temporal: integer;               (* y para el valor temporal *)
 begin
   temporal := 1;                   (* incialización *)
   for i := 1 to b do
     temporal := temporal * a;      (* hacemos "b" veces "a*a" *)
   potencia := temporal;            (* y finalmente damos el valor *)
 end;

 begin                                       (* Cuerpo del programa *)
   writeln('Potencia de un número entero');
   writeln;
   writeln('Introduce el primer número');
   readln( numero1 );
   writeln('Introduce el segundo número');
   readln( numero2 );
   writeln( numero1 ,' elevado a ', numero2 ,' vale ',
     potencia (numero1, numero2) )
 end. 


Tema 8.4: Procedimientos con parámetros.

Un procedimiento también puede tener "parámetros", igual que la función que acabamos de ver:

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Ejemplo de procedi-   }
 {    miento al que se le   }
 {    pasan parámetros      }
 {    PROCPAR.PAS           }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Free Pascal 2.2.0w  }
 {    - Turbo Pascal 7.0    }
 {    - Turbo Pascal 5.0    }
 {--------------------------}

 program ProcConParametros;
 { Para usarlo con SURPAS 1.00 habría    }
 { definir un tipo "str20", por ejemplo, }
 { que usar en vez de "string".          }

 procedure saludo (nombre: string);     (* Nuestro procedimiento *)
 begin
   writeln('Hola ', nombre, ' ¿qué tal estás?');
 end;

 begin                  (* Comienzo del programa *)
   writeln;             (* Linea en blanco *)
   saludo( 'Aurora' );  (* Saludamos a Aurora *)
 end.                   (* Y se acabó *) 

En el próximo apartado veremos la diferencia entre pasar parámetros por valor (lo que hemos estado haciendo) y por referencia (para poder modificarlos), y jugaremos un poco con la recursividad.

Ejercicio propuesto: Crear una función "potenciaReal", que trabaje con números reales, y permita cálculos como 3.2 ^ 1.7 (pista; hay que usar una par de funciones matemáticas: las exponenciales y los logaritmos; si te rindes, puedes mirar la ampliación 1 del curso).
Ejercicio propuesto: Hacer una función que halle la raíz cúbica del número que se le indique (pista: hallar una raíz cúbica es lo mismo que elevar a 1/3).
Ejercicio propuesto: Definir las funciones suma y producto de tres números, y crear un programa que haga una operación u otra según le indiquemos (empleando "case", etc).
Ejercicio propuesto: Un programita que halle la letra (NIF) que corresponde a un cierto DNI (documento nacional de identidad) español.
Ejercicio propuesto: Crea un programa que sume dos números "grandes", de 30 cifras. Esos números deberemos leerlos como "string" y sumarlos "letra a letra". Para ello, deberás crear una función "sumar", que reciba como parámetros dos "strings", y que devuelva su resultado en un nuevo "string".
Ejercicio propuesto: Crea un programa que multiplique dos números "grandes", de entre 30 y 100 cifras, por ejemplo. Para esos números no nos basta con los tipos numéricos que incorpora Pascal, sino que deberemos leerlos como "string" y pensar cómo multiplicar dos strings: ir cifra por cifra en cada uno de los factores y tener en cuenta lo que "me llevo"...


Tema 8.5: Modificación de parámetros.

Ya habíamos visto qué era eso de los procedimientos y las funciones, pero habíamos dejado aparcados dos temas importantes: los tipos de parámetros y la recursividad.

Vamos con el primero. Ya habíamos visto, sin entrar en detalles, qué es eso de los parámetros: una serie de datos extra que indicábamos entre paréntesis en la cabecera de un procedimiento o función.

Es algo que estamos usando, sin saberlo, desde el primer día, cuando empezamos a usar "WriteLn":

writeln( 'Hola' );

Esta línea es una llamada al procedimiento "WriteLn", y como parámetros le estamos indicando lo que queremos que escriba, en este caso, el texto "Hola".

Pero vamos a ver qué ocurre si hacemos cosas como ésta:

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Primer procedimiento  }
 {    que modif. variables  }
 {    PROCMOD1.PAS          }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Free Pascal 2.2.0w  }
 {    - Turbo Pascal 7.0    }
 {    - Turbo Pascal 5.0    }
 {    - Surpas 1.00         }
 {--------------------------}

 program PruebaDeParametros;

 var dato: integer;

 procedure modifica( variable : integer);
 begin
   variable := 3 ;
   writeln( 'Dentro: ', variable );
 end;

 begin
   dato := 2;
   writeln( 'Antes: ', dato );
   modifica( dato );
   writeln( 'Después: ', dato );
 end. 

¿Qué podemos esperar que pase? Vamos a ir siguiendo cada instrucción:

  • Declaramos el nombre del programa. No hay problema.
  • Usaremos la variable "dato", de tipo entero.
  • El procedimiento "modifica" toma una variable de tipo entero, le asigna el valor 3 y la escribe. Lógicamente, siempre escribirá 3.
  • Empieza el cuerpo del programa: damos el valor 2 a "dato".
  • Escribimos el valor de "dato". Claramente, será 2.
  • Llamamos al procedimiento "modifica", que asigna el valor 3 a "dato" y lo escribe.
  • Finalmente volvemos a escribir el valor de "dato"... ¿3?

¡¡¡¡NO!!!! Escribe un 2. Las modificaciones que hagamos a "dato" dentro del procedimiento modifica sólo son válidas mientras estemos dentro de ese procedimiento. Lo que modificamos es la variable genérica que hemos llamado "variable", y que no existe fuera del procedimiento.

Eso es pasar un parámetro por valor. Podemos leer su valor, pero no podemos alterarlo.

Pero, ¿cómo lo hacemos si realmente queremos modificar el parámetro. Pues nada más que añadir la palabra "var" delante de cada parámetro que queremos permitir que se pueda modificar...


Tema 8.6: Parámetros por referencia.

El programa quedaría:

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Segundo proc. que     }
 {    modifica variables    }
 {    PROCMOD2.PAS          }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Free Pascal 2.2.0w  }
 {    - Turbo Pascal 7.0    }
 {    - Turbo Pascal 5.0    }
 {    - Surpas 1.00         }
 {--------------------------}

 program PruebaDeParametros2;

 var dato: integer;

 procedure modifica( var variable : integer);
 begin
   variable := 3 ;
   writeln( 'Dentro: ', variable );
 end;

 begin
   dato := 2;
   writeln( 'Antes: ', dato );
   modifica( dato );
   writeln( 'Después: ', dato );
 end.


Esta vez la última línea del programa sí que escribe un 3 y no un 2, porque hemos permitido que los cambios hechos a la variable salgan del procedimiento (para eso hemos añadido la palabra "var"). Esto es pasar un parámetro por referencia.

El nombre "referencia" alude a que no se pasa realmente al procedimiento o función el valor de la variable, sino la dirección de memoria en la que se encuentra, algo que más adelante llamaremos un "puntero".

Una de las aplicaciones más habituales de pasar parámetros por referencia es cuando una función debe devolver más de un valor. Habíamos visto que una función era como un procedimiento, pero además devolvía un valor (pero sólo uno). Si queremos obtener más de un valor de salida, una de las formas de hacerlo es pasándolos como parámetros, precedidos por la palabra "var".

Ejercicio propuesto: Crear una función "intercambia", que intercambie el valor de los dos números enteros que se le indiquen como parámetro.
Ejercicio propuesto: Crear una función "iniciales", que reciba una cadena como "Nacho Cabanes" y devuelva las letras N y C (primera letra, y letra situada tras el primer espacio), usando parámetros por referencia.


Tema 8.7: Parámetros con el mismo nombre que las variables locales.

Y como ejercicio queda un caso un poco más "enrevesado". Qué ocurre si el primer programa lo modificamos para que sea así:

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Tercer proc. que      }
 {    modifica variables    }
 {    PROCMOD3.PAS          }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Free Pascal 2.2.0w  }
 {    - Turbo Pascal 7.0    }
 {    - Turbo Pascal 5.0    }
 {    - Surpas 1.00         }
 {--------------------------}

 program PruebaDeParametros3;

 var dato: integer;

 procedure modifica( dato : integer);
 begin
   dato := 3 ;
   writeln( 'Dentro: ', dato );
 end;

 begin
   dato := 2;
   writeln( 'Antes: ', dato );
   modifica( dato );
   writeln( 'Después: ', dato );
 end.
  

 

(Puedes consultar la respuesta)


Tema 8.8: Recursividad.

Vamos al segundo apartado de hoy: qué es eso de la "recursividad". Pues la idea en sí es muy sencilla: un procedimiento o función es recursivo si se llama a sí mismo.

¿Y qué utilidad puede tener eso? Habrá muchos problemas que son más fáciles de resolver si se descomponen en pasos cada vez más sencillos.

Vamos a verlo con un ejemplo clásico: el factorial de un número.

Partimos de la definición de factorial de un número n:

n! = n · (n-1) · (n-2) · ... · 3 · 2 · 1

Por otra parte, el factorial del siguiente número más pequeño (n-1) es

(n-1)! = (n-1) · (n-2) · ... · 3 · 2 · 1

Se parecen mucho. Luego podemos escribir cada factorial en función del factorial del siguiente número:

n! = n · (n-1)!

¡Acabamos de dar la definición recursiva del factorial! Así vamos "delegando" para que el problema lo resuelva el siguiente número, y este a su vez se lo pasa al siguiente, y este al otro, y así sucesivamente hasta llegar a algún caso que sea muy fácil.

Pues ahora sólo queda ver cómo se haría eso programando:

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Factorial, ejemplo de }
 {    recursividad          }
 {    FACT.PAS              }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Free Pascal 2.2.0w  }
 {    - Turbo Pascal 7.0    }
 {    - Turbo Pascal 5.0    }
 {    - Surpas 1.00         }
 {--------------------------}

 program PruebaDeFactorial;

 var numero: integer;

 function factorial( num : integer) : integer;
 begin
   if num = 1 then
     factorial := 1        (* Aseguramos que tenga salida siempre *)
   else
     factorial := num * factorial( num-1 );       (* Caso general *)
 end;

 begin
   writeln( 'Introduce un número entero (no muy grande)  ;-)' );
   readln(numero);
   writeln( 'Su factorial es ', factorial(numero) );
 end. 


Un par de comentarios sobre este programilla:

  • Atención a la primera parte de la función recursiva: es muy importante comprobar que hay salida de la función, para que no se quede dando vueltas todo el tiempo y nos cuelgue el ordenador. Normalmente, la condición de salida será que ya hallamos llegado al caso fácil (en este ejemplo: el factorial de 1 es 1).
  • No pongais números demasiado grandes. Recordad que los enteros van desde -32768 hasta 32767, luego si el resultado es mayor que este número, tendremos un desbordamiento y el resultado será erróneo. ¿Qué es "demasiado grande"? Pues el factorial de 8 es cerca de 40.000, luego sólo podremos usar números del 1 al 7.
Si este límite del tamaño de los enteros os parece preocupante, no le deis muchas vueltas, porque en la próxima lección veremos que hay otros tipos de datos que almacenan números más grandes o que nos permiten realizar ciertas cosas con más comodidad.

Ejercicio propuesto: Crear una función recursiva que halle el producto de dos números enteros.
Ejercicio propuesto: Crear otra función que halle la potencia (a elevado a b), también recursiva.
Ejercicio propuesto: Hacer un programa que halle de forma recursiva el factorial de cualquier número, por grande que sea. (Pista: habrá que usar la rutina de multiplicar números grandes, que se propuso como ejemplo en el apartado anterior)
Ejercicio propuesto: Crear un programa que emplee recursividad para calcular un número de la serie Fibonacci (en la que los dos primeros elementos valen 1, y para los restantes, cada elemento es la suma de los dos anteriores).
Ejercicio propuesto: Crea un programa que emplee recursividad para calcular la suma de los elementos de un vector.
Ejercicio propuesto: Crear un programa que encuentre el máximo común divisor de dos números usando el algoritmo de Euclides: Dados dos números enteros positivos m y n, tal que m > n, para encontrar su máximo común divisor, es decir, el mayor entero positivo que divide a ambos: - Dividir m por n para obtener el resto r (0 ? r < n) ; - Si r = 0, el MCD es n.; - Si no, el máximo común divisor es MCD(n,r).

Tema 8.9: La sentencia "forward".

La sentencia "forward" es necesaria sólo en un caso muy concreto, y cada vez menos, gracias a la programación modular, pero aun así vamos a comentar su uso, por si alguien llega a necesitarla:

Cuando desde un procedimiento A se llama a otro B, este otro procedimiento B debe haberse declarado antes, o el compilador "no lo conocerá". Esto nos supone llevar un poco de cuidado, pero no suele ser problemático.

Eso sí, puede llegar a ocurrir (aunque es muy poco probable) que a su vez, el procedimiento B llame a A, de modo que A debería haberse declarado antes que B, o el compilador protestará porque "no conoce" a A.

En este (poco habitual) caso de que cada uno de los procedimientos exigiría que el otro se hubiese detallado antes, la única solución es decirle que uno de los dos (por ejemplo "A") ya lo detallaremos más adelante, pero que existe. Así ya podemos escribir "B" y después dar los detalles sobre cómo es "A", así:

{--------------------------}
{  Ejemplo en Pascal:      }
{                          }
{    Ejemplo de "Forward"  }
{    FORW.PAS              }
{                          }
{  Este fuente procede de  }
{  CUPAS, curso de Pascal  }
{  por Nacho Cabanes       }
{                          }
{  Comprobado con:         }
{    - Turbo Pascal 7.0    }
{--------------------------}

program forw;

procedure A(n: byte); forward;  { No detallamos cómo es "A", porque
                                  "A" necesita a "B", que el compilador
                                  aún no conoce; por eso sólo  "declaramos"
                                  "A", e indicamos con "forward" que
                                  más adelante lo detallaremos }

procedure B (n: byte);          { "B" ya puede llamar a "A" }
begin
  writeLn('Entrando a B, con parámetro ', n);
  if n>0 then A (n-1);
end;

procedure A;                   { Y aquí detallamos "A"; ya no hace falta }
begin                          { volver a indicar los parámetros }
  writeLn('Entrando a A, con parámetro ', n);
  if n>0 then B (n-1);
end;

begin
  A(10);
end. 

 

Nota: en los Pascal actuales, más modulares, si usamos "unidades" (que veremos en el tema 12) se evita la necesidad de emplear "forward", porque primero se dan las "cabeceras" de todos los procedimientos (lo que se conocerá como "interface") y más adelante los detalles de todos ellos (lo que será la "implementation").


 

Actualizado el: 16-06-2010 12:56

AnteriorPosterior