FUNCIONES - PASO DE PARÁMETROS







Parámetros reales .- Son las expresiones que aparecen en la llamada a una función. Por ejemplo, dada la sentencia:
printf("Hola, Mundo\n");
la cadena "Hola, Mundo\n" es un parámetro real.

Parámetros formales .- Son variables locales que se declaran en el encabezado de la función. Los parámetros formales reciben el valor de los parámetros reales empleados en la llamada a la función.

Paso de parámetros por valor.
¿Qué sucede internamente cuando se produce una llamada a una función? El proceso consta de varias fases sucesivas:
  1. Evaluación de las expresiones utilizadas como parámetros reales.
  2. Activación de la función. A su vez, esta fase posee otras:
    1. Reserva de espacio para los valores proporcionados por la función, si los hay.
    2. Almacenamiento de la dirección de retorno.
    3. Reserva de espacio para parámetros formales.
    4. Reserva de espacio para las demás variables locales, si las hubiere.
    5. Copia de los valores de los parámetros reales (ya evaluados al principio) en el espacio reservado para parámetros formales.
  3. Ejecución. Se ejecutan las distintas sentencias de que consta el cuerpo de la función, haciendo uso de la información aportada por los parámetros reales y almacenada previamente en los parámetros formales.
  4. Retorno. También esta fase posee otras:
    1. Almacenamiento del resultado de evaluar la expresión que acompaña a return en el espacio reservado en 2(a).
    2. Liberación del espacio reservado para los parámetros formales y otras variables locales, si las hay
    3. Salto a la dirección de retorno, almacenada en 2(b). Esta activación de la función ha concluido. Obsérvese que posiblemente hayan quedado en la pila los valores para los cuales se reservó espacio en 2(a).
Este proceso, relativamente complejo, se realiza con notable velocidad en el procesador, que cuenta con instrucciones especializadas para muchas de las fases que se han mencionado. Pero ciertamente la fase de activación y retorno supone un tiempo "muerto", en el sentido de que sólo se efectúan cálculos en la fase de ejecución propiamente dicha. Es bueno, entonces reducir el número de llamadas a función cuando el tiempo necesario para las fases 1, 2 y 4 es comparable con el tiempo dedicado a la fase 3. Los compiladores, consciente de esto, admiten como opción la creación de funciones inline .
Veamos un ejemplo de llamada a una función en el cual se pueden ver todas las fases, aunque solo sea a través de sus efectos. Evidentemente, la activación real de una función sólo puede seguirse "en directo" mediante un depurador que nos permita seguir la ejecución desde el nivel del lenguaje ensamblador , ejecutando una tras otra las instrucciones del lenguaje máquina. Esta tarea sobrepasa los límites de este curso, pero puede realizarse con relativa facilidad desde un IDE. Veamos el programa:

#include<stdio.h>

void f(int, float);

int main(int argc, char * argv)
{
 int entero;
 float real;

 entero = 22;
 real = 33.3;

 printf("Antes de llamar a f, entero vale %d y real vale %6.2f\n", entero, real);
 f(entero, real);
 printf("Después de volver de f, entero vale %d y real vale %6.2f\n", entero, real);

 return 0;
}


void f(int p, float q)
{
 printf("Al entrar en f, p (= entero) vale %d y q (= real) vale %6.2f\n", p, q);

 p = 7777;
 q = 12345.67;


 printf("Al salir de f, p vale %d y q vale %6.2f\n", p, q);
 return;
}
/*
Antes de llamar a f, entero vale 22 y real vale  33.30
Al entrar en f, p (= entero) vale 22 y q (= real) vale  33.30
Al salir de f, p vale 7777 y q vale 12345.67
Después de volver de f, entero vale 22 y real vale  33.30
*/

Comenterios .- Los resultados del programa confirman la descripción efectuada más arriba más arriba. Obsérvese que los cambios efectuados en los parámetros formales p y q (que son variables locales de la función f() ) no afectan -no pueden afectar- a los valores de los parámetros reales entero y real , que son variables locales de la función main() . Para ser exactos parámetro real entero tiene el valor 22 y el parámetro real real tiene el valor 33.3. Al efectuar la llamada a la función, es como si se ejecutase el código siguiente:
p = entero;
q = real;

y por tanto p toma el valor 22 y q toma el valor 33.3. A continuación, dentro de la función se ejecuta el segmento de código:
p = 7777;
q = 12345.67;

y cambian los valores de p y q , según se comprueba mediante la sentencia printf() que contiene la función f() . Pero al volver a la función main() y escribir de nuevo (tras volver de f() , según se indica) los valores las variables locales de main() , se observa que no han cambiado los valores de entero y real . Esto significa, desde un punto de vista "macroscópico", que en C el paso de parámetros está construido de tal manera que el paso de información es unidireccional: la información pasa de la función que hace la llamada (la que aporta parámetros reales) a la función que recibe la llamada (la que tiene los parámetros formales) pero no en el sentido inverso . Esto se denomina paso de parámetros por valor.
En cierto sentido, esto es un problema, porque el objeto de los programas es procesar información, y es lógico pensar que la función que hace la llamada desea recibir la información procesada. Ciertamente se puede devolver información a través del valor proporcionado por la función, pero esto es un cuello de botella y sería interesante disponer de alguna forma de alterar el valor de los parámetros reales a través de los paràmetros formales. Este procedimiento existe, y consiste en pasar punteros como parámetros formales. Véase la sección siguiente.

Paso de parámetros por referencia .
El paso de parámetros en C está construido de tal modo que los valores resultantes de evaluar los parámetros formales se copian en los parámetros formales. Como los parámetros formales son variables distintas de los parámetros reales, los cambios efectuados en los parámetros foramales no afectan para nada a los parámetros reales. Esto es cierto e insalvable, pero disponemos de un auxiliar sumamente interesante: los punteros. En efecto, considérese el siguiente fragmento de código:
int p;
int * q = &p;
*q = 33;

Como puede verse, hemos modificado el valor del entero p a través de un puntero. A todos los efectos, *q equivale a p , esto es, se puede poner *q en cualquier lugar en que aparezca p . Esto puede aplicarse al paso de parámetros: si en lugar de pasar el valor de un parámetro real se pasa la dirección del parámetro real en cuestión, entonces será posible modificar el valor del parámetro real deshaciendo la indirección (anteponiendo un asterisco al puntero) desde el interior de la función . Considérese el programa siguiente, que es una modificación del empleado en la sección anterior:

#include<stdio.h>

void f(int *, float *);

int main(int argc, char * argv)
{
 int entero;
 float real;

 entero = 22;
 real = 33.3;

 printf("Antes de llamar a f, entero vale %d y real vale %6.2f\n", entero, real);
 f(&entero, &real);
 printf("Después de volver de f, entero vale %d y real vale %6.2f\n", entero, real);

 return 0;
}


void f(int *p, float *q)
{
 printf("Al entrar en f, *p (= entero) vale %d y *q (= real) vale %6.2f\n", *p, *q);

 *p = 7777;
 *q = 12345.67;


 printf("Al salir de f, *p vale %d y *q vale %6.2f\n", *p, *q);
 return;
}
/*
El resultado de ejecutar este programa es el siguiente:

Antes de llamar a f, entero vale 22 y real vale 33.30 Al entrar en f, *p (= entero) vale 22 y *q (= real) vale 33.30 Al salir de f, p vale 7777 y q vale 12345.67 Después de volver de f, entero vale 7777 y real vale 12345.67 */


Comentarios .- Este programa tiene un comportamiento sutilmente distinto del anterior. Al hacer la llamada a la función, se evalúan los parámetros reales, que son &entero y &real , respectivamente. Los valores de estos parámetros son las direcciones de entero y real , respectivamente. Luego al llamar a esta función es como si se ejecutase el siguiente fragmento de código:
p = &entero;
q = &real;

de modo que p pasa a contener la dirección de entero y q pasa a contener la dirección de real . Entonces *p equivale por completo a entero y *q equivale por completo a real , puesto que el operador asterisco antepuesto a un puntero deshace la indirección y proporciona la variable señalada por el puntero al que se antepone. Como *p equivale a entero , poner *p = 7777 es exactamente lo mismo que poner entero = 7777 , y análogamente poner *q = 12345.67 equivale a poner real = 12345.67 . En resumidas cuentas, al pasar como parámetros punteros en lugar de valores, se pueden modificar los valores señalados por los punteros (los parámetros reales) . Esto se denomina paso por referencia .

Se ve entonces que la comunicación entre funciones puede ser monodireccional o bidirecional, a nuestro arbitrio:

Y desde luego, tanto las variables estándar como los punteros pasan por valor, esto es, las modificaciones efectuadas en los parámetros (variables estándar o punteros) dentro de la función no afectan a las variables estándar o punteros empleados como parámetros reales. Lo único que puede resultar afectado son las variables señaladas por los punteros pasados como parámetros.
Podría pensarse, entonces, que no se puede modificar el valor de un puntero cuando éste pasa como parámetro, y así es. Ahora bien, en C existe el concepto de doble indirección , o puntero de puntero. Nada impide utilizar un puntero de puntero como parámetro de una función, luego dentro de la función se dispone de la dirección del puntero, así que es posible modificar su valor. Considérese el programa siguiente:

#include<stdio.h>

#define MAX 10

void crear(int **q);

int main(int argc, char * argv[])
{

 int * p;
 int i;
 crear(&p);
 if (p != NULL)
  {
   for(i=0;i<MAX;i++) p[i] = i;
   for(i=0;i<MAX;i++) printf("p[%d] vale %d\n", i, p[i]);
  }
 else
  printf("\nNo hay memoria suficiente\n");
 return 0;
}

void crear(int **q)
{
 *q = (int *)malloc(MAX*sizeof(int));
}

/*
p[1] vale 1
p[2] vale 2
p[3] vale 3
p[4] vale 4
p[5] vale 5
p[6] vale 6
p[7] vale 7
p[8] vale 8
p[9] vale 9
*/

En la llamada a la función crear() , el parámetro real es &p ; este valor se copia en el parámetro formal, q . Por tanto, en la llamada a la función es como si se ejecutase el código:
q = &p;
que almacena en q la dirección de p . Entonces, si se antepone un asterisco a q , se obtiene precisamente p . Luego podemos asignar a *q el resultado de una llamada a malloc() , que reserva memoria para toda una lista. Los punteros tienen la posibilidad de admitir un índice, y eso es precisamente lo que se hace en el programa principal, una vez asignado a p un valor que es la dirección del bloque reservado por malloc() .
En resumen, hemos pasado p a crear() por referencia; esto nos ha permitido modificar el valor de p desde dentro de crear() , y utilizar entonces el nuevo valor de p desde el programa principal.

Un caso especial: paso de listas como parámetros .
El compilador de C trata las listas monodimensionales como si fuesen punteros del tipo base de la lista en cuestión. A todos los efectos, una lista monodimensional se trata como un puntero cuyo valor es la dirección del primer elemento de la lista. Dicho formalmente, si se tiene la declaración:
Tipo_base lista[MAX];

entonces es como si lista fuera equivalente a &lista[0] . Esto tiene consecuencias inmediatas cuando se pasa una lista como parámetro a una función: todas las listas en C pasan por referencia , porque su nombre equivale automáticamente a la dirección de su primer elemento. Considérese el ejemplo siguiente:

#include<stdio.h>

#define MAX 20
void leer(char * p);
void leer2(char * p);

int main(int argc, char * argv[]){

 char nombre[MAX];
 printf("Escriba un nombre: ");
 leer(nombre);
 printf("El nombre que ha escrito es %s\n", nombre);
 return 0;
}

void leer(char * p)
{
 fgets(p, MAX, stdin);
 fpurge(stdin);
}

void leer2(char * p)
{
 size_t n;
 fgets(p, MAX, stdin);
 fgetln(stdin, &n);
 if (n != 0)
  {
   printf("Limpieza...\n\n");
   fpurge(stdin);
  }

}

/*
	Escriba un nombre: Miguel de Unamuno y Jugo
El nombre que ha escrito es Miguel de Unamuno y
*/


Comentarios .- En este programa se pasa una lista de char llamada nombre a la función leer() . Como las listas equivalen a la dirección de su primer elemento, se admite el parámetro real nombre. Al efectuar la llamada a la función, es como si se ejecutase el código siguiente:

p = &nombre[0];


de modo que se carga en p la dirección del primer elemento de nombre. Entonces p se puede emplear, a su vez, como argumento de fgets() y de este modo leemos tantos caracteres como indica MAX . La lectura no tiene peligro. Por si acaso, hemos llamado a la función fpurge() para eliminar posibles restos del búfer de teclado. La función leer2() emite un aviso si realmente había necesidad de "limpieza".

Ejercicios propuestos



  1. Ejercicio 0803r01.- Considérese la declaración
    struct Registro {
    char n[40];
    float f;
    int n[10];
    };
    
    Se pide construir una función que admita argumentos de tipo Registro, y que muestre sus campos en pantalla, con formato en columnado.

  2. Ejercicio 0803r02.- Considérese la declaración
    struct Registro {
    char n[40];
    float f;
    int n[10];
    };
    
    Se pide construir una función que admita parámetros de este tipo y pida los datos de sus campos al usuario, para devolver la estructura, con sus datos ya cumplimentados, a la función que haya hecho la llamada. ¿Cómo pasan las estructuras, por valor o por referencia?

  3. Ejercicio 0803r03.- Considérese la declaración
    struct Registro {
    char n[40];
    float f;
    int n[10];
    };
    
    Se dispone de una lista de estructuras de este tipo, formada por MAX elementos. Se pide construir una función que admita la lista y rellene los campos de todas sus estructuras con unos valores iniciales preestablecidos.

  4. Ejercicio 0803r04.- Se dispone de una matriz (una tabla) de dimensiones MxN, formada por elementos de tipo double. Se pide construir una función que admita la matriz como argumento y determine qué fila y qué columna contiene más veces el valor 0.0.

  5. Ejercicio 0803r05.- Se proporciona a un programa una colección de valores de tipo float, escribiéndolos en la línea de órdenes. Se pide construir una función que proporcione como resultado una estructura a través de un puntero. La estructura tendrá como campos una lista de float (los pasados a través de la línea de órdenes) y un campo que indique el número de elementos de la lista.

  6. Ejercicio 0803r06.- Construir una función que admita como argumento el puntero de una lista de punteros de estructuras análogas a la empleada en el Ejercicio 1. La función debe proporcionar un puntero de la estructura que contenga el valor más grande en el campo f .

  7. Ejercicio 0803r07.- Construir una función que admita como argumento una cadena de formato encolumnado y proporcione, mediante un puntero, el valor de cualquiera de sus campos de tipo float . Construir otras funciones análogas para campos de tipo cadena y de tipo int . Si hay errores manifiestos, la función lo comunicará a través de un puntero de int .

  8. Ejercicio 0803r08.- onstruir una función que admita como argumento una cadena de formato delimitado y proporcione, mediante un puntero, el valor de cualquiera de sus campos de tipo double . Construir otras funciones análogas para campos de tipo cadena y de tipo int . Si hay errores manifiestos, la función lo comunicará a través de un puntero de int .