Tabla de fichas Indice del Tema 0007
0001 0002 0003 0004 0005 0006 0007 0008

CONCEPTO DE SUBPROGRAMA.







Funciones y subprogramas
Según lo visto anteriormente, los procesadores disponen de un juego de instrucciones adaptado al uso de funciones. Las funciones o rutinas, en el nivel del lenguaje máquina, tienen total acceso al espacio de memoria asignado al proceso por el sistema operativo: pueden acceder, en principio, a todos los bytes de su bloque, incluyendo aquellos en que reside el código. Esto supone, por cierto, una divertida posibilidad: la de un código que se modifica a sí mismo al ejecutarse. No hablaremos aquí de semejantes prácticas heréticas, aunque confesamos haber pecado en nuestra juventud.
Volviendo a las funciones "de bajo nivel", los lenguajes de programación "de alto nivel" se basan en ellas, ofreciendo otras "funciones" de características sofisticadas que facilitan la programación, evitan la comisión de errores, mejoran la legibilidad del código, etc. Entre estas características adicionales cabe mencionar los espacios de nombres, parámetros y la duración. Aun cuando no estén relacionadas directamente con las funciones propias del procesador, también son de gran utilidad otros dos aditamentos: los módulos de compilación y las bibliotecas. Pasaremos seguidamente a examinar estos conceptos. Para quienes gusten de la lectura anticipada, véase también el tema dedicado a Funciones.

Concepto de variable global y local
El concepto de variable, ya examinado desde un punto de vista de bajo nivel, se puede complementar en la forma siguiente: para cada función, el lenguaje crea una tabla de variables que es exclusiva de esa función e independiente de las tablas de variables de las demás funciones. Se dice que las variables de cada función (declaradas en ella) son variables locales de la función, o dicho de otro modo, que pertenecen al espacio de nombres de esa función. Además, las variables que se declaran fuera de toda función se anotan en una tabla de variables especial, caracterizada porque sus variables son visibles y utilizables en todas las funciones del programa. Estas variables reciben el nombre de variables globales. Se dice que estas variable pertenecen al espacio de nombres global. De esta manera, las variables de todo programa son o bien locales o bien globales; las variables globales son visibles y modificables desde cualquier función, y las variables locales son visibles y modificables únicamente desde el interior de la función en la cual están declaradas. En este sentido, las variables declaradas en la función principal son tan locales como las demás; sólamente son globales las variables declaradas fuera de toda función. Estos conceptos, desde luego, son aplicables a los lenguajes basados en la metodología estructurado, o en lenguajes de programación parcialmente orientados a objetos. Los lenguajes orientados a objetos (Java, por ejemplo) imponen un nuevo paradigma, en el cual no tiene sentido el concepto de variable global: todas las variables pasan a ser atributos de alguna clase. Se mantiene, sin embargo (como era lógico) el concepto de programa principal: el programa tiene que arrancar en un punto determinado.
Cabe preguntarse, desde luego, lo que ocurre en caso de colisión de nombres, esto es, cuando se declara una variable local y otra global de igual nombre. La regla seguida en tales casos es bien clara: en caso de colisión, prima la variable local. Esto es, toda referencia hecha a la variable dentro de la función lo será a la variable local, y no a la global, que será invisible dentro de la función. Algunos lenguajes permiten suplementar esta regla con una sintaxis especial, y hacer alusión a la variable global.

Concepto de parámetro
La independencia de tablas de variables impuesta por los lenguajes de alto nivel supone un problema de comunicación que no existe a bajo nivel: los subprogramas no pueden acceder a otra cosa que sus propias variables locales, o las variables declaradas globalmente. La comunicación entre subprogramas o funciones a través de variables globles no está reglamentada: cualquiera puede crear o utilizar variables globales. Esto supone un grave peligro: una función podría cambiar el valor de una variable global usada por otras como medio de comunicación; el error sería indetectable y muy difícil de corregir. Sería de desear un mecanismo reglado de comunicación entre subprogramas, y esa es precisamente la idea de los parámetros.
Un parámetro formal es una variable local que se declara con una sintaxis particular, y que posee una característica especial, que sirve para intercambiar información entre la función que hace la llamada y la función que la recibe. Un parámetro real es una expresión (formada por variables locales o globales) que se especifica en la llamada a una función. En el proceso de llamada a la función o subprograma, se evalúan los parámetros reales y se copia su valor en los parámetros formales. Esto hace que la función a la que se llama reciba información procedente del exterior, información que se almacena en una de sus variables locales (el parámetro formal) y que por tanto puede utilizarse en ella. Este paso de parámetros (reales a formales) recibe el nombre de paso por valor, ya que el valor de la expresión empleada en el parámetro real se copia en el parámetro formal. Este tipo de paso se caracteriza porque la función que recibe la información puede procesarla, pero no puede (salvo que use el mecanismo que se explica a continuación) modificar el valor de la variable empleada como paràmetro real.
También existe la posibilidad de paso por referencia, consistente en emplear como parámetro real una expresión que, al ser evaluada, produzca como valor la dirección de una variable (local o global). La función que reciba esta dirección podrá modificar el valor de la variable correspondiente a esta dirección, a diferencia ele caso anterior, en que sólo se recibía una copia del valor. Todo esto se explicará con detalle, aplicándolo al caso concreto del lenguaje que nos ocupa, en el tema dedicado a Funciones.

Concepto de variable automática, estática y dinámica.
El conjunto de aditamentos ofrecidos por los lenguajes de alto nivel no se limita a lo expuesto anteriormente. Las variables declaradas en un programa pueden durar tanto como el programa, y se dirá que tienen duración estática. Concretamente, las variables globales son de duración estática; su ventaja (desde el punto de vista temporal) consiste en que mantienen su valor entre ejecuciones (activaciones, en la jerga informática) de todas las funciones del programa.
Otra posibilidad es que las variables declaradas en un programa duren tanto como la función en que han sido declaradas: empiezan a existir en el momento en que comienza la ejecución de la función (en su activación, en la jerga informática) y dejan de existir cuando la función concluye. Desde el punto de vista del lenguaje máquina, esto significa que se reserva el espacio de las variables en la pila del programa, de forma muy rápida, y se libera ese espacio cuando concluye la función. Las variables automáticas, por tanto, suponen una reducción del espacio de memoria necesario, pero no perduran entre ejecuciones sucesivas de la función, a diferencia de las estáticas.
Por último, es posible (en ciertos lenguajes de alto nivel) crear y destruir variables, bajo control del programador. Estas variables, denominadas dinámicas, se utilizan a través de un tipo especial de variables, llamadas referencias o punteros. Son las que permiten un uso más flexible de la memoria de la computadora, aunque exigen un mayor esfuerzo por parte del programador, sobre todo desde el punto de vista conceptual. Desde el nivel del lenguaje máquina, por otra parte, no hay diferencia entre el acceso a variables "normales" y el acceso a variables dinámicas. Los lenguajes de alto nivel descargan al programador de las tareas de mantenimiento que debe efectuar el programador de bajo nivel.

Concepto de activación de un subprograma.
El mecanismo de salto a función en bajo nivel tiene su contrapartida en el mecanismo de activación de funciones en alto nivel. Cuando se activa una función, como consecuencia de una llamada, el procesador ejecuta el código necesario para realizar las tareas siguientes:



Cuando finaliza la ejecución del código correspondiente a la función, se produce su desactivación. Esto conlleva varias actividades:

Evidentemente, el proceso de activación y desactivación de la función requiere un cierto tiempo, que no está dedicado al cálculo y por tanto ralentiza la consecución de resultados. Esto implica que una función dotada de una cantidad muy pequeña de código puede ser ineficiente si se ejecuta muchas veces, ya que la mayor parte del tiempo se invertirá en activar y desactivar la función. Ciertos compiladores ofrecen una posibilidad de optimización, denominada inlining, consistente en sustituir las llamadas a función por copias de su código. De este modo se ahorra el tiempo asociado a las activaciones y desactivaciones, a costa de alargar el programa. Se produce así la consabida contraposición de objetivos, tan frecuente en ingeniería del software: lo que mejora un aspecto del rendimiento (velocidad) redunda en perjuicio de otro (espacio ocupado).

Concepto de compilación por separado.
En la práctica, los programas crecen de forma sorprendente: no es imposible hallar ciertos casos en los que el número de líneas de código asciende a cifras de millones. Esto supone que los tiempos de compilación se vuelven muy elevados, de horas cuando no de días. Además, los procesos industriales de desarrollo exigen la creación de módulos por separado, esto es, la posibilidad de fragmentar el código fuente de tal modo que puedan estar escribiéndolo múltiples personas simultáneamente. Esto se consigue el programa en funciones individuales, que se almacenarán (bien de una en una o por grupos) en ficheros diferentes. De este modo, cada programador puede modificar un fichero y compilar el conjunto de ficheros de que consta el programa.
La dificultad que esto conlleva es patente: no se puede crear un programa viable si uno de los ficheros de que consta no está en un estado viable (compilable). La solución es compleja, y exige disponer siempre de un juego de ficheros en estado estable (no decimos completo o acabado, pero si compilable y ejecutable sin errores). Cada programador "saca" de un depósito su fichero, dejando a disposición de los demás una versión viable; cuando llega a otra fase del desarrollo en que su fragmento de código (mejorado) vuelve a ser viable, lo devuelve al depósito y ésta pasa a ser la nueva copia que queda a disposición de los demás componentes del equipo.
Desde el punto de vista más modesto de un programador individual, sigue siendo necesario fragmentar el código para optimizar la velocidad de compilación: no tiene sentido recompilar un programa entero cuando los cambios efectuados afectan a una sola función. Entonces se recurre a distintas estrategias, que van desde la creación de un archivo de "proyecto" que contiene todo lo referente al programa y automatiza las compilaciones, hasta la creación (manual o no) de un makefile, que es un conjunto de especificaciones que permiten a la herramienta make decidir cuáles son los archivos que deben recompilarse, y la forma de enlazarlos. El objetivo final es que el programador pueda pulsar una tecla y estar seguro de que el programa se compilará correctamente, sin preocuparse de los detalles de la compilación, una vez especificados.
Un aspecto interesante de la compilación es la creación de archivos de código objeto, no ejecutables, pero formados ya por código compilado.

Concepto de biblioteca.
Los archivos de código objeto descritos en el apartado anterior pueden contene el código correspondiente a una o más funciones. Este código se puede enlazar con el de otros archivos sin necesidad de recompilarlo, y puede recibir un formato especial con este fin, recibiendo el nombre de biblioteca. Una biblioteca es, por tanto, una colección de funciones precompiladas, que pueden emplearse sin necesidad de disponer de su código fuente.
Una ventaja del uso de bibliotecas es la rapidez, porque al estar ya hecha la compilación no es preciso volver a realizarla. Es una forma de reutilización de código, uno de los principios básicos de la ingeniería del software de todos los tiempos. Además, se obtiene otra ventaja menos evidente: las funciones de estas bibliotecas suelen ser los descendientes de otras funciones que, habiendo sido empleadas en otros proyectos, se han refinado en sucesivas aplicaciones. Lo que fueran sencillas funciones (inmaduras) en un principio se convierte con el paso del tiempo en funciones muy generales, estables, eficientes y sofisticadas. El programador hace uso de estas bibliotecas y, sin esfuerzo, percibe el beneficio de una ejecución fiable y eficiente. El uso de bibliotecas (sean de funciones o de clases) es un aspecto clave del desarrollo de software en la actualidad.