0% encontró este documento útil (0 votos)
35 vistas9 páginas

Capitulo 5

Este documento trata sobre los conceptos básicos de subprogramas en programación estructurada, incluyendo características, tipos de parámetros y ámbito de variables. Explica los conceptos a través de un ejemplo de cálculo de tangente descompuesto en subprogramas en C++.

Cargado por

victorfuster2001
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
35 vistas9 páginas

Capitulo 5

Este documento trata sobre los conceptos básicos de subprogramas en programación estructurada, incluyendo características, tipos de parámetros y ámbito de variables. Explica los conceptos a través de un ejemplo de cálculo de tangente descompuesto en subprogramas en C++.

Cargado por

victorfuster2001
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Tema 5. Concepto de subprograma.

Ambito y paso de variables en subprogramas

3.1Subprogramas, características básicas


Los lenguajes estructurados ofrecen las herramientas para codificar los programas según el diseño
descendente en el que se basa la programación estructurada. Estas herramientas consisten en la
posibilidad de organizar (a veces oiremos la palabra “encapsular”) el código en procedimientos y
funciones. Estos son, por tanto, trozos de código, que, según el diseño descendente aplicado a la
resolución del problema, realizan un proceso diferenciado y con significado que contribuye a la
resolución del problema abordado por el algoritmo.
Vamos a ver un ejemplo, de un problema sencillo para ver estos conceptos. Consideremos el
problema de calcular la tangente de un ángulo descompuesto en:
-Leer el dato de entrada en grados (más cómodo para el usuario) y transformarlo a radianes
-Calcular la tangente por su aproximación en serie de McLaurin
-Escribir su valor

Para esta sencilla descomposición descendente, vamos a definir estos subprogramas en C++:

#include <iostream>
#include <cmath>

using namespace std;

float const PI = 3.14159;

/*prototipos*/
void conversionGs_Rd(float &angulo);
float calcula_tan(float angulo);

/*Codigo de los subprogramas*/


void conversionGr_Rd(float &angulo){
float aux;

cout << “Introduce un angulo en grados entre 0 y 360” << endl;


cin >> aux;
angulo =(aux*2*PI) / 360;
}

float calcula_tan(float angulo) //el subprograma devuelve un real


float aux;

if((angulo > -PI/2) && (angulo < PI/2))


aux = angulo + 1.0/3*pow(angulo,3) + 2.0/15*pow(angulo, 5) + 17.0/315
*pow(angulo, 7);
else{
cout << “el valor no esta entre –PI/2 y PI/2”
aux = NaN; /*valor que indica un error de operación*/
} /*del else*/
return(aux);
}
El programa principal se limitaría a las llamadas de los subprogramas y a la salida de datos:
void main(){
float ang;
float resultado;

conversionGr_Rd(ang);
resultado = calcula_tan(ang);
if(resultado != NaN)
cout << “La tangente de” << ang << “ es “ <<resultado << endl;
}

Vamos a hacer unas cuantas puntualizaciones:


 Cada subprograma es en sí mismo un programa, ya que sigue la misma estructura que éstos
(Declaración de constantes, Declaración de variables, cuerpo de enunciados y sentencias) y en sí
mismos son verdaderos algoritmos.
 Hay dos estados o momentos diferentes del subprograma dentro del programa:
1. cuando se le declara y se enuncia su código explícitamente
2. cuando es llamado como enunciado dentro de otro subprograma o el programa principal con
unos valores de entrada concretos.
 Los subprogramas son anidables. Es decir, podemos, dentro de un subprograma, llamar a otro
subprograma, hasta el orden de anidación que nos permita la capacidad de la pila del programa
 Hay dos tipos de subprogramas diferentes:
1. -aquellos que son equivalentes a una sentencia, y su llamada es como tal. Se llaman
procedimientos y su objetivo es realizar tareas
2. –Aquellos que son equivalentes a expresiones y forman parte de un enunciado de
asignación. Se llaman funciones y devuelven un valor y pueden ser utilizados en
cualquier lugar donde aparezca un valor. Por ejemplo una expresión válida sería:
Escribir(calcula_tan(angulo)) {}
Esta división no es tajante ya que lo más corriente es encontrarnos con subprogramas que hacen
alguna tarea y además devuelven un valor como parte del trabajo.
 Llamamos argumentos o parámetros de un subprograma a las variables que se declaran en una
parte especial del subprograma y que realizan la función de entrada (y a veces también salida)
de datos al subprograma. Estas variables siguen el convenio en casi todos los lenguajes
estructurados de incluirse dentro de un paréntesis al lado del nombre de la función, tanto para su
declaración como para su llamada
conversionGr_Rd(float &angulo)
float calcula_tan(float angulo)

No es necesario que un subprograma tenga parámetros. Podemos definirlos sin ellos:


int sin_parametros( ){...}

Distinguiremos dos momentos en el uso de parámetros. Cuando estamos definiendo el código del
subprograma y trabajamos con el parámetro como una variable que contendrá un dato, llamamos en
ese momento al parámetro parámetro formal.
Cuando estamos en el momento de llamar a la función, el parámetro se carga con un valor concreto
recibiendo el nombre de parámetro real. Por ejemplo, en el programa anterior, en la definición de
la función calcula_tan(float angulo) utilizamos el argumento angulo como parámetro
formal y éste es usado en la definición del código de la función. En cambio, cuando es llamada, el
valor que usa lo da la variable ang que es el parámetro real.
El elemento que distingue a los parámetros del resto de variables es que poseen dos modos de
recibir el valor que discutiremos en breve. Además la inicialización de estas variables se produce
únicamente en la llamada a la función, no antes. Por lo demás, los parámetros de un subprograma
son como el resto de variables definidas dentro del subprograma en cuanto a lugar de alojamiento y
ámbito.

3.2 Pasos de parámetros.


Hay dos maneras de definir la relación entre un parámetro y la variable (si esta existe) que le cede
su valor en la llamada a la función.
Paso por valor: El parámetro involucrado en un paso por valor es independiente de la variable que
hace de parámetro real. Este valor a partir de su asignación es independiente de la variable que
hizo de parámetro real, en el sentido de que podemos modificarlo dentro del cuerpo del
subprograma y esto no afectará nada a la variable que hizo de parámetro real. En el ejemplo de la
cálculo de la tangente, el parámetro angulo de calcula_tan esta pasado por valor ya que sólo
vamos a usar su valor en radianes para sustituirlo en la serie de McLaurin

Paso por referencia: La variable que hace de parámetro real permanece ligada al subprograma, no
es independiente del valor con el que el subprograma realiza la computación. Así cualquier
modificación al valor del parámetro dentro del subprograma se produce igualmente en la variable
que hace de parámetro real. En nuestro ejemplo de calculo de tangente, cualquier modificación que
se le haga al parámetro formal angulo en conversionGr_Rd, también se producirá en la
variable ang .

En conclusión podemos pensar que en el paso por valor, el parámetro real cede su valor a una
variable local al subprograma en la cual se copia. Y seguidamente el subprograma hace uso de esta
variable hasta que el subprograma acaba, eliminándose entonces. En cambio, en el paso por
referencia, el parámetro real es el que realmente pasa al subprograma y se modifíca cuando varían
su valor. Al final, cuando acaba el subprograma, la variable que hizo de parámetro real permanece,
habiéndose modificado su valor. Por tanto el criterio para elegir un paso u otro a la hora de
implementar un subprograma será el siguiente (dejando consideraciones de eficiencia para más
adelante): Si necesitamos un parámetro para recibir un valor como dato de entrada para realizar
algún proceso, pero dichos datos no van a ser modificados o si lo son, esta modificación no
concierne al resto del programa, el paso adecuado es por valor. En cambio, si lo que realmente
interesa es modificar el valor de alguna variable mediante los cálculos del subprograma para ser
empleado en otras partes del programa, entonces el paso de ser por referencia.
Esta decisión es individual para cada parámetro de un subprograma, de tal manera que puede haber
parámetros por valor y por referencia en el mismo subprograma.

3.2.1 Implementación en C++ de los conceptos vistos


En C++ no hay distinción formal entre procedimiento o función. Todos los subprogramas son de
tipo función. Un procedimiento en una función que devuelve void. La sintaxis de las funciones en
C++ es:
<tipo del dato devuelto> <nombre funcion> (<tipo dato> >nombre param1>,…){

declaración de variables locales

Cuerpo del subprograma

}
Pasos de parámetros por valor y por referencia:

Declarar parámetros por valor se realiza de la siguiente manera:


void procedimiento1(int a, float b, char c){

}

Un ejemplo de paso por valor:


void procedimiento2( double num){

num = 5.67;{aquí cambimos el valor a la copia del dato}

cout << num << endl; //sacará por pantalla 5.67


}
void main(){
double dato=6.78;
procedimiento2(dato);
cout << dato << endl; //sacará por pantalla 6.78

}
En cambio, el mismo ejemplo en un paso por referencia:

void procedimiento2( double &num){



num = 5.67;{aquí cambimos el valor}

cout << num << endl; //sacará por pantalla 5.67


}
void main(){
double dato=6.78;
procedimiento2(dato);
cout << dato << endl; //sacará por pantalla 5.67

}
Podemos mezclar en un mismo subprograma pasos por valor y referencia:
float funcion1(int num, float &valor_real)

3.3 Ambito de las variables de los subprogramas


Se dice que los parámetros y las variables definidas en un subprograma son locales a dicho
subprograma. Esto quiere decir que el ámbito de dichas variables es únicamente el código definido
dentro del subprograma. Dichas variables no son accesibles por código (sentencias) que no esté
definido dentro del subprograma que las declara. Sin embargo hay variables que el código de
cualquier subprograma puede manejar y que están definidas fuera de su propio código. Son las
variables globales del programa. En C++ estas variables se definen fuera de cualquier función
(incluida main) normalmente en la cabecera del programa.
Un ejemplo sencillo:

int numero_global=9;
void procedimiento3(){
int x=5
numero_global = x * numero_global;
cout << numero_global << endl; /* imprime un 45 */
}
main(){
cout << numero global << endl; /*imprime un 9 */
procedimiento3();
cout << numero global << endl; /* imprime un 45 */
}

En este ejemplo la variable x es local a procedimiento3() y sería incorrecto que la usara la


función main ya que estaría fuera de su ámbito y el compilador nos daría un error. El uso de
variables globales se restringe mucho en la programación estructurada. La razón es conocida, el
uso intensivo de variables globales hace oscuro, y a veces ilegible, el código ya que al ser
modificadas en cualquier parte del código de un subprograma sin haber tenido una “entrada oficial”
a través de los parámetros de éste provoca que sea difícil seguir su valor a lo largo del programa. Lo
recomendable es usar las mínimas variables globales posibles, ninguna en la mayoría de los casos,
declarando argumentos en los subprogramas que reciban los valores con los que trabaja el
subprograma. Es decir, la filosofía es tratar de ver a los parámetros de los subprogramas como la
interface de comunicación de ese subprograma con el resto del código, pasando cualquier dato que
se quiera utilizar dentro del subprograma usando un parámetro.

MAL EJEMPLO BUEN EJEMPLO


int DIA, MES, AÑO; void pregunta_fecha( int &dia, int &mes, int
&año){
void pregunta_fecha(){ cout << “Introduce el dia del mes”;
cout <<“Introduce el dia del mes”; cin >> dia;
cin >>DIA; cout <<“\nIntroduce el numero de mes”;
cout << “\nIntroduce el numero de mes”); cin >>mes;
cin >>MES; cout <<“\nIntroduce el año”;
cout <<”Introduce el año”; cin >>año;
cin >>AÑO; }
}
void main(){
void main(){ int DIA, MES, AÑO;
pregunta_fecha(); pregunta_fecha(DIA,MES,AÑO);
} }

En el programa de la derecha las variables son locales a la función main, por tanto no se utilizan
como globales sino que se pasan a la función a través de los parámetros de ésta. Podemos ver a
estos parámetros como la interface. De esta manera, viendo la función con sus parámetros ya
sabemos que vamos a manejar tres valores externos a la función e importantes para el desarrollo del
programa. Sin embargo viendo la función de la izquierda no sabemos nada de ella si no nos
introducimos en su código. Si pensamos que estas funciones tuvieran miles de líneas, el uso de las
tres variables quedaría muy oculto entre el código. En la versión de la derecha, el incluir como
locales de la función main a las variables restringe su ámbito al de dicha función. Si hubiera otros
subprogramas, éstos no podrían acceder a las variables en cuestión.

3.4 El modelo de ejecución de programas compilados. Relación con el concepto de


encapsulamiento.

El concepto de ámbito de las variables definidas en una función y el de las variables globales queda
mucho más claro si se explica la organización en memoria de un programa que se está ejecutando.
Tras haber compilado el programa, (si el lenguaje no es interpretado), o una vez escrito el código, si
el lenguaje es interpretado, para ejecutar el programa, el S.O. debe cargar a éste en una porción de
la memoria RAM, y colocar en el registro PC de la CPU la dirección de la primera instrucción de
este programa. La manera de organizar en memoria los datos y el código depende del lenguaje
empleado y del compilador, no del SO ni de la arquitectura de la máquina, por eso es
importante aclarar que este modelo de organización que ahora vamos a explicar es el más usual en
lenguajes estructurados imperativos como PASCAL y C pero no es el único (por ejemplo, las
primeras versiones de FORTRAN no tenían lo que ahora conoceremos como pila del programa) y
cambia todavía más para los lenguajes interpretados (por ejemplo PROLOG maneja no una sino tres
pilas para el programa). Lo que es cierto también es que esta manera de organizar el programa en
memoria da al lenguaje un estilo y unas características propias y unas soluciones de implementación
de aspectos como ámbito de variables, recursividad,….) que son distintas de otro lenguaje que use
otra organización.

3.4.1 El tipo abstracto de datos Pila


Antes de entrar en este modelo de organización del programa en memoria necesitamos el concepto
del tipo abstracto Pila. Imaginemos lo que comúnmente llamamos una pila de libros. Para coger el
libro que está más abajo, se necesitan desapilar todos los que están encima. Igualmente el primer
libro que cogemos de la pila es el último que habíamos dejado en ella.
Este mismo modelo va a seguir la estructura que vamos a explicar. Imaginemos que en vez de
libros, los elementos que vamos a apilar son “discos” de memoria, es decir trozos de memoria
formados por bytes consecutivos, y con número de bytes variable de un disco a otro. Así como los
libros podemos pensar el discos “gruesos “ apilados con discos “delgados” dependiendo del
elemento al que haga referencia.
Disponemos de tres elementos para manejar esta pila de discos: una referencia que señale el disco
que está más arriba en la pila, referencia que llamaremos Top. Esta referencia puede ser por
ejemplo un puntero que señale a ese disco. Una función que desapile un disco de la pila y ponga a
Top a señalar al elemento inmediatamente inferior al que hacía de tapa de la pila. Esta función la
llamaremos Pop. La función Pop pone a Top a NULO (NULL) cuando hemos desapilado el último
elemento de la pila y ésta se encuentra vacía. La función Push apila un elemento en la pila y coloca
a Top señalando a dicho elemento. No vamos a entrar en los detalles de la implementación de este
modelo de datos. Simplemente hay que tener en cuenta que este tipo abstracto de datos con sus
funciones y su elemento de referencia Top modela un modo de almacenamiento (de elementos de
cualquier naturaleza) basado en la idea “el último dato en almacenarse es el primero en salir del
almacenamiento” y “el dato que se almacenó después, sale antes”.
3.4.2 El modelo de ejecución en programas compilados
La estructura de un programa cargado en memoria dentro de este contexto es la siguiente:
Las variables globales se almacenan en el Segmento de datos (S.D.).

Las variables locales se crean y se destruyen dinámicamente en la pila del programa.


Las variables dinámicas reservadas con alguna función de reserva de memoria (malloc, calloc en C)
se almacenan dinámicamente en el montículo (Heap en inglés).
Las instrucciones en las que el compilador ha convertido las sentencias del código fuente se
almacenan en el Segmento de Código (S.C.). El código compilado se almacena ordenadamente aquí
salvo las variables . En lugar de variables, en el código compilado hay referencias a las mismas. Si
son variables globales, las referencias son directas a esas variables en el S.D., si son variables
locales (porque es código de una función en particular) son etiquetas que harán referencia a dichas
variables en la pila cuando la función esté activa (una función está activa cuando el programa en
ejecución llama a la función).
La dinámica de funcionamiento es la siguiente:

El PC de la CPU se carga con la dirección de la primera instrucción del programa, que será parte de
la primera sentencia de la función main de nuestro código fuente. A partir de aquí las instrucciones
del SC se irán ejecutando normalmente, hasta que se encuentre una instrucción de salto que vaya al
código de una función. En ese momento entra en acción la gestión de la pila. Los
microprocesadores tienen instrucciones de gestión de pila (normalmente las operaciones PUSH
<direccion de memoria> y POP PUSH <dirección de memoria>) con las que manejar la pila del
programa. Cuando se llama a una función, lo primero que se hace es guardar en la pila (apilar) toda
la información necesaria para volver a recuperar el estado del programa anterior a la llamada de la
pila. Por eso se almacenan la dirección del PC y los contenidos de los distintos registros del
microprocesador. El PC se carga con la dirección de comienzo de la función que ha sido llamada
(que también está almacenada en el SD) y se reserva memoria en la pila para las variables locales
de la función activa. Una vez hecho esto comienza la ejecución normal de código de la función o
procedimiento propiamente dicho. Podemos pensar todo este manejo de la pila de una forma
aproximada pero válida de la siguiente manera. Imagina que la Pila almacena discos de tal manera
que un disco representa todos los datos necesarios para que se ejecute un subprograma. En el disco
se almacena la información que había en el microprocesador en el momento de llamar al
subprograma (estado de los registros) , se crea un espacio para las variables locales del subprograma
y otra información adicional necesaria.
Así si nos encontramos con una función anidada, podemos
DISCO de PILA imaginarnos el proceso como que encima del disco propio del
subprograma activo, se apila (Pop) un nuevo disco
correspondiente al subprograma anidado. El subprograma activo
es el que tiene su disco más alto en la pila y está apuntado por el
Otros
apuntador (Top) de la pila. Todos los demás subprogramas están,
Variables locales al igual que el programa principal, inactivos (latentes) esperando
su turno. Una vez termina la última instrucción del subprograma,
Estado Registros se desapila el disco de dicho subprograma en la

pila. Si el subprograma es una función se carga el valor devuelto en una variable temporal para que
sustituya a la llamada de la función en la expresión correspondiente. Se cargan el PC y los demás
registros con los valores almacenados en el disco, con lo cual el flujo del programa retorna a la
instrucción inmediatamente posterior a la llamada de la función y el micro procesador se configura
con los valores de los registros igual que antes de la llamada. Una vez hecho esto, se desapila (Pop)
el disco de la pila y toda la información del disco (incluída las variables locales al
subprograma) se destruye. De esta manera, el estado natural de la pila es vacía o bien con un
disco correspondiente a la función principal (si el lenguaje es C++). La pila se va llenando y
vaciando a demanda de tal manera que crece más cuanto más sea el grado de anidamiento del
subprograma llamado. Cuando el programa acaba, el control es devuelto al SO, el cual se encarga
de eliminar toda la estructura del programa descrita de la memoria.

3.4.3 Tipos de variables según su almacenamiento en el programa en ejecución


Ahora que ya conocemos la estructura de un programa en memoria, podemos fijarnos en que hay
una gran diferencia en la propiedad de la persistencia entre las variables que hemos definido
como globales y las variables que son locales a un subprograma. Las variables globales tienen su
espacio reservado de memoria en el segmento de datos y permanecen desde que el programa es
cargado hasta que finaliza, su persistencia es igual a todo el tiempo de ejecución del programa. A
este tipo de variables se les conoce como estáticas. En cambio, las variables locales a una función
aparecen en la pila en el momento en que se activa esta función y son eliminadas en el momento en
que finaliza la función. Así decimos que la persistencia de una variable local es el tiempo que dura
la ejecución de la función que la declara1.

3.4.4 Encapsulamiento

1
Existen en C++ otras variables locales que tienen una persistencia igual al de las globales. Son las declaradas en un
subprograma con el calificativo de static. Estas variables son útiles en algunos casos concretos ya que
conservan el valor de la variable local entre las llamadas de la función que las declara, pero están restringidas al
ámbito de la función que las declara, con lo que el dato está encapsulado y sólo es accesible por las instrucciones
que forman parte del código de la función que la declaró.
Habitualmente, los programas se presentaran descompuestos en subprogramas encargados de una
tarea concreta de tal manera que el programa total es el ensamblaje de los diferentes módulos,
desempeñando cada uno una tarea concreta. El modelo de programa en memoria que acabamos de
describir refuerza más la idea de subprograma = modulo independiente al encapsular los datos de
cada subprograma. El encapsulamiento es la propiedad por la cual ni los datos ni el código de un
subprograma son accesibles directamente por el resto de código que forma el programa ni por otros
programas. Según este modelo tenemos :
-encapsulamiento del código: No viene dado directamente por el modelo de
memoria aunque sí indirectamente ya que si saltáramos voluntariamente dentro del código
de un subprograma A al código de otro subprograma B usando la instrucción GOTO,
estaríamos violando las reglas de la estructura de datos Pila, ya que tendríamos que acceder
al disco correspondiente al subprograma B sin haber desapilado el subprograma A. Este
encapsulamiento se previene prohibiendo en la programación estructurada el uso de la
instrucción GOTO. (Hay una explicación más detallada de este uso en el famoso y clásico
artículo de Dijkstra, E.W. “GOTO Statements considered Harmful” Communications of the
ACM, 11,1968.
-encapsulamiento de los datos. Este sí que viene directamente dado por el uso de la
pila, ya que los datos accesibles en cada instante son los correspondientes al último disco
apilado en ésta que son los del subprograma activo en ese momento. Como el código que
se está ejecutando es el de dicho subprograma, es imposible que código de otro
subprograma o del programa principal haga uso de las variables de dicho subprograma ya
que sencillamente son eliminadas cuando acaba de ejecutarase el código del subprograma.

El lenguaje C++ favorece el encapsulamiento usando este modelo de programa en memoria. El


estilo de programación de Orientación a Objetos (POO) impone la encapsulación de los datos y el
código de los objetos, de tal manera que el objeto puede verse como un conjunto de datos y código
encapsulados que se relaciona con el resto del programa a través de una interface que el objeto
ofrece al resto de código. La encapsulación ha resultado ser una propiedad muy útil para los
programadores cuando se enfrentan a proyectos informáticos grandes, ya que hace muy fácil que
distintos grupos de trabajo puedan implementar independientemente código que luego debe formar
parte del proyecto y previene y facilita enormemente la detección de errores en el código al estar
sus componentes aislados.

También podría gustarte