Autor Tema: Proyecto de Curso (Dictado - Notas): Programación en C (2013, por Argentinator)

0 Usuarios y 1 Visitante están viendo este tema.

04 Marzo, 2014, 07:02 pm
Respuesta #60

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,739
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
60. Funciones

1. Introducción a las funciones en C

   Una función es, a grandes rasgos, un bloque de código que tiene un nombre.
   Constituyen uno de los elementos más importantes del lenguaje C, y es realmente una lástima que yo no haya sabido introducir este tema mucho antes en este curso.
   Una función se escribe para definir una tarea (o serie de tareas) que vamos a utilizar repetidamente en un programa o proyecto dado.
   Cada vez que tengamos que llevar a cabo una misma tarea, conviene pues invocar una función que contiene los pasos a seguir, lo cual es más eficiente que estar repitiendo una y otra vez el mimsmo código.
   Esto nos da una herramienta de abstracción básica en programación: la modularidad.
   Las funciones pueden tener parámetros que modifiquen su comportamiento.
   Los parámetros se indiquen con un nombre formal local a la función, cada uno con un determinado tipo.
   Las funciones pueden retornar un valor, que es de determinado tipo.



2. Definición de una función (forma básica)

   Hay dos clases de funciones: las que retornan un valor de un determinado tipo y las que no.
   A su vez, hay tres clases de funciones según el número de parámetros:
           (*) sin parámetros,
           (*) con una cantidad determinada de parámetros, y
           (*) con una cantidad indeterminada de parámetros.
   Una función se define con la siguiente sintaxis:

    rettype funcname(T1 V1, T2 V2, . . ., Tn Vn)
    {
         /*  Aquí van las instrucciones que indican       */
         /*  las acciones que va a desempeñar la función. */
         
         return expr;
    }

   Se denomina cabecera de la función al primer renglón:

    rettype funcname(T1 V1, T2 V2, . . ., Tn Vn)

   Con rettype indicamos un tipo de datos, que es el tipo de retorno de la función.
   El tipo de retorno rettype está restringido: No puede ser ningún tipo array, ni tampoco un tipo función. Ver apartado 5, debajo.

   Con T1 V1 indicamos que el 1er parámetro de la función es de tipo T1 y tiene nombre formal V1.
   Asimismo, T2 V2, T3 V3, etc., hasta Tn Vn indican el 2do, 3er, etc., hasta el n-ésimo parámetro de la función. (Los puntos suspensivos que hemos puesto en rojo son "metasintácticos", o sea, no tenemos que ponerlos en un programa real).
   Luego, entre llaves {  } va el cuerpo de la función, en donde se coloca una serie de sentencias que perfilarán las acciones de la misma en cuanto sea invocada.
   En alguna parte del cuerpo de la función normalmente debe ir al menos una sentencia return, acompañada de una expresión, que hemos indicado con expr.
   Una sentencia return realiza una "terminación" de las tareas de la función, devolviendo el control del flujo del programa al proceso que llamó la función.
   Además, la expresión expr se evalúa, produciendo un valor cuyo tipo es rettype. Este valor es utilizado a su vez de alguna expresión del proceso llamante, desde la cual se ha llamado a la función.
   ¿Y qué es un "proceso"? Bueno, en C esto es siempre otra función.
   El "proceso principal" es en realidad una función principal, que en C se llama main(), y que tiene una sintaxis determinada, que en breve explicaremos.

   Sujestivamente podemos decir que en C "todo son funciones", pero esta frase es muy vaga como para que podamos defenderla, así que lo dejamos aquí.

   Observación: Aún no hemos definido formalmente el término expresión (en referencia a expr). Eso lo haremos luego.
   No podemos definirlo antes de haber siquiera haber introducido el tema de funciones, porque las expresiones involucran llamadas a funciones.
   O sea que hay cierta recursividad en las definiciones que estamos dando, tanto de función como de expresión.
   Esto es así, y no es demasiado problemático, mientras que no haya "circularidad".

EJEMPLO 1

   Consideremos la función main(), que es la principal en un programa en C, desde la cual se llama a la función promedio3(), encargada de calcular el promedio de 3 números reales:

    #include <stdio.h>
   
    double promedio3(double x, double y, double z)
    {
         printf("El 1er número es: %f\n", x);
         printf("El 2do número es: %f\n", y);
         printf("El 3er número es: %f\n", z);
         
         return (x + y + z) / 3;
    }
   
    int main(void)
    {
        printf("Cálculo del promedio de 3 números: \n\n");
       
        double resultado;
        resultado = promedio3(1.14, 9.8, 7.7777);
        printf("El promedio es: %f\n", resultado);
       
        return 0;
    }


   El programa contiene tres elementos principales:
       - Se incluye la librería <stdio.h>, con el fin de utilizar la función de biblioteca printf().
       - Se define la función promedio, de manera que formalmente admite 3 parámetros
         de tipo double, y retorna un valor de tipo double también.
       - Se define la función main(), desde la cual se invoca a la función promedio().

   El flujo del programa comienza en la primer llave de la función main().
   Enseguida se imprime en pantalla un mensaje con printf(), indicando que se va a calcular un promedio.
   Debajo se declara la variable resultado, de tipo double, o sea, de punto flotante.
   Luego, se realiza una llamada a la función promedio() pasándole 3 argumentos.
   Esto hace que el flujo del programa ingrese al cuerpo de la función con los 3 argumentos pasados ahora como parámetros a la función.
   Lo primero que ocurre es que los parámetros formales x, y, z son reemplazados por los argumentos: 1.14, 9.8, 7.7777, respectivamente.
   Luego se realizan las tareas estipuladas en la función, que en el ejemplo son, simplemente, impresiones de mensajes en pantalla con los valores de los parámetros.
   Tras terminar esto, se llega a la sentencia return que contiene la expresión (x + y + z) / 3.
   El valor de dicha expresión es devuelto al proceso llamador, es decir, la función main(), y puesto en la expresión de asignación de la variable resultado.
   Ahora la variable resultado ha "recibido" el valor devuelto por la función promedio() y tiene ese valor de allí en más.
   La sentencia que sigue es una llamada a printf() que imprime el valor de resultado en la pantalla, con lo cual visualizamos cuánto es que da el dicho promedio de los 3 números.

   Con el ejemplo pretendemos ilustrar cómo es que el programa evoluciona sentencia a sentencia en el bloque main(),
   hasta que se hace una llamada a la función invocándola con los argumentos que más nos gusten, para ser reemplazados por los parámetros formales de la función,
   interrumpiendo aquí el flujo normal con un "salto" al cuerpo de la función, que cumple con sus tareas,
   hasta que se encuentra con una sentencia return.
   Esto devuelve el flujo del programa al punto exacto en que la función fue llamada, y además, con un valor de retorno, que es usado en alguna expresión.
   El programa continúa hasta terminar.
   Se ve que main() es también una función, y que también retorna un valor.

   ¿Qué sucede si en el cuerpo de la función no hay ninguna sentencia return?
   En ese caso, la llave de cierre } delimita la frontera de acciones de la función y termina allí,
   devolviendo asimismo el control del flujo del programma al proceso que hizo la llamada a la función.
   Esto equivale a haber hecho un return, pero sin haber especificado un valor concreto de retorno.



3. Definición de una función (formas alternativas)

   Variando la cabecera de la función obtenemos otras posibles definiciones de funciones. Veamos:

   (a) Función que retorna un valor de un tipo dado, y que admite
       una cantidad bien determinada de parámetros:

    rettype funcname(T1 V1, T2 V2, . . ., Tn Vn)

   (es el caso típico estudiado en el apartado 2).

   (b) Función que retorna un valor de un tipo dado, y que no admite parámetros:

    rettype funcname(void)

   (aquí la palabra clave void adquiere el significado de: "nada", o sea, "0 parámetros").

   (c) Función que retorna un valor de un tipo dado, y que admite
       una cantidad variable de parámetros:

    rettype funcname(T1 V1, T2 V2, . . ., Tn Vn, ...)

   Los puntos suspensivos ... al final de la lista de parámetros son sintácticos.
   Los ponemos explícitamente en nuestro programa para indicar que esa función tiene una lista variable de parámetros.
   Los primeros n parámetros tienen un tipo predeterminado, pero los parámetros siguientes, los que
   vienen a partir de los puntos suspensivos, no se sabe ni cuántos son ni de qué tipo.
   Sin embargo, esto tiene perfecto sentido, y hay maneras de tratar estos parámetros misteriosos,
   aunque no podemos tratarlo por ahora.

   En la lista de parámetros no puede haber (siguiendo estrictamente el estándar) solamente puntos suspensivos. Tiene que haber al menos un parámetro debidamente declarado antes, con un tipo concreto.
   Así: int find_it(int x, ...) es correcto, pero int find_it(...) no.

   Funciones sin retorno:
   Si en el tipo de retorno rettype ponemos void, significa que la función no retorna valores.
   En tal caso, la función pasa a ser lo que en otros lenguajes de programación se denomina procedimiento o subrutina.
   En este tipo de función la palabra return aún se usa para devolver el control al proceso llamante,
   pero esta vez no se pone ninguna expresión acompañándole, porque no hay valor alguno que retornar.




4. La función main()

   En C existe una función especial, llamada main(), la cual tiene un cometido específico.
   El programa inicia su ejecución en el cuerpo de la función main().
   Termina en la llave de cierre, o bien cuando se encuentra una sentencia return.
   El tipo de retorno de main() tiene que ser siempre int.
   Hay dos opciones para la lista de parámetros:

   Opción 1: main() sin parámetros. La declaración quedaría así en el programa:

int main(void)
{
   /* ... */
}

   Opción 2: main() con dos parámetros. Quedaría así:

int main(int argc, char **argv)

   Es decir, el 1er parámetros es de tipo int y el 2do de tipo puntero a puntero a char.

   Cuando se da esta última declaración, el programa acepta argumentos desde el sistema operativo.
   Lo más común es poner esos argumentos en la línea de comandos separados por espacios en blanco.
   La cantidad de argumentos pasados al programa se cuenta en la variable argc.
   El mismo nombre del programa se considera un argumento, así que su valor es al menos de 1.
   En cuanto a argv es en realidad considerado como un array de cadenas de caracteres.
   Un tal array "decae", como sabemos, a un puntero a char*.
   (El estándar hace una declaración un poco distinta, pero resulta equivalente a la nuestra, que mantenemos por conveniencia).

   Los nombres de los parámetros: argc, argv, se acostumbra ponerlos siempre con ese nombre, pero no es obligatorio.

   El valor de retorno de main() es, como dijimos, de tipo int.
   Sin embargo, no hay otra función a la cual retornar este valor.
   Lo que ocurre al hallar una sentencia return es que el programa termina, y se devuelve al sistema operativo el valor int indicado.
   Este valor de retorno suele tener el significado de un indicador de "error" por parte del programa: indica si todo terminó bien o con errores.
   Normalmente, un valor de retorno de 0 significará "terminación normal del programa", y un valor distinto de 0 indicará "programa terminado con errores".



(continúa en el siguiente post...)



Organización

Comentarios y Consultas

04 Marzo, 2014, 07:03 pm
Respuesta #61

argentinator

  • Consultar la FIRMAPEDIA
  • Administrador
  • Mensajes: 7,739
  • País: ar
  • Karma: +0/-0
  • Sexo: Masculino
  • Vean mis posts activos en mi página personal
    • Mis posts activos (click aquí)
61. Funciones (continuación...)

Este es un post rompe-cráneos :banghead: :banghead:

5. Tipos de datos "función"

   Hasta ahora hemos visto tipos de datos que describen objetos (que residen con un tamaño fijo en bytes en la memoria RAM).
   Sin embargo, también hay tipos de datos que describen funciones. Se llaman tipos función.
   En este caso, tendríamos un identificador denotando una variable, la cual puede adquirir como valor distintas funciones previamente definidas.

   Un tipo función está determinado por:
       - El tipo del valor de retorno.
       - El número de parámetros, y si tiene puntos suspensivos o no en la cabecera.
       - El tipo de cada parámetro (excepto los correspondientes a puntos suspensivos, si los hay, ya que ahí no se conoce el tipo y número de los subsecuentes parámetros).
   El tipo de una función como en el apartado 2 sería: función que retorna rettype, con parámetros de tipos T1, T2, ..., Tn.
   Si al final de la lista de parámetros hubiera unos puntos suspensivos, el tipo de la función sería: función con número variable de parámetros, que retorna rettype, cuyos parámetros fijos son de tipos T1, T2, ..., Tn.

Por ejemplo, dadas las definiciones siguientes:

float trunc_decimal(float x, int dig)
{
    for (int j = 0; j < dig; j++)
       x *= 10.0;
    x = (long int) x;
    for (int j = 0; j < dig; j++)
       x /= 10.0;

    return x;
}

float frac_decimal(float x, int dig)
{
    return trunc_decimal(x, dig) - (long int ) x;
}

   Se trata de dos funciones: la 1era trunca un número de punto flotante a sus primeros dig dígitos decimales. La 2da determina los primeros dig dígitos de la parte fraccionaria de un número.
   A su vez, para realizar su cometido, la 2da función hace una llamada a la 1era.

   Lo importante en este apartado para nosotros es que ambas funciones tienen el mismo tipo.
   Su tipo es: función que retorna float, con 2 parámetros de tipos float e int.

   Si ahora tenemos una variable FF de ese tipo, podríamos hacer algo como esto:

int main(void)
{
   FF = trunc_decimal;  /* La variable FF ahora equivale a la función trunc_decimal. */
   FF(3.14159265, 2);   /* Esto llama a la función FF(), */
                        /* que en realidad ejecuta el cuerpo de la función trunc_decimal. */
                        /* El resultado de esa llamada da el valor 3.14. */
   FF = frac_decimal;   /* Ahora la variable FF cambió para jugar el rol de la función frac_decimal */
   FF(3.14159265, 2);   /* Esto llama a FF(), pero ahora equivale a llamar a frac_decimal(). */
                        /* El resultado de la llamada es 0.14. */
   return 0;
}


   ¿Cómo se declaran identificadores de tipo función?

   Primero digamos algo sobre lo que ocurre al poner en nuestro programa un identificador de función.
   Llamar a la función:
   Se pone el nombre de la función seguida de paréntesis. En los paréntesis van los parámetros que le pasamos a la función:

     trunc_decimal(3.1415, 1);

   Si una función tiene lista de parámetros vacía, entonces la llamada se hace con los paréntesis sin parámetro alguno dentro:

     int say_five(void)
     {
       return 5;
     }
     /*  ...   */
     say_five();    /* Llamada a la función say_five() */

   Usar el identificador de función en una expresión:
   Se pone solamente el identificador de la función.
   Eso, en una expresión, se convierte automáticamente en un valor de tipo puntero a función.
   (Esto es parecido al "decaimiento" de arrays a punteros).
   Excepciones: El "decaimiento" a puntero a función no se produce con los operadores sizeof y el operador unario de dirección &.

   En virtud de estos hechos, para que las asignaciones del ejemplo sean válidas, la variable FF tiene que declararse como un puntero a función.
   Para indicar puntero a a algo, en una declaración, se usa, como siempre, el asterisco: *.
   Y para indicar que se trata de una función de cierto tipo, tenemos que anteponer el tipo de retorno, y a la derecha de la declaración irían, entre paréntesis, la lista con los tipos de los parámetros, así:

    T (*funcvar) (T1, T2, ..., Tn);

   Eso declara una variable de tipo puntero a función que retorna T, cuyos parámetros son de tipo T1, T2, ..., Tn.
   También pueden ponerse puntos suspensivos al final de la lista, para indicar tipos función con número variable de parámetros.

   Volviendo a nuestro ejemplo, tendríamos que declarar esto:

   float (*FF)(float, int);

   Poniendo eso antes de la función main() nuestro programa funcionará correctamente.

   Los paréntesis en (*FF) son necesarios sólo por las reglas de agrupación en las declaraciones.
   Si hubiésemos escrito esto:

   float *FF (float, int);

   hubiésemos obtenido la declaración de un tipo función que retorna "puntero a float", con parámetros de tipos float, int.
   Eso es distinto de nuestras intenciones.

   Reescribamos el ejemplo completo, para que se vea más claro:

float trunc_decimal(float x, int dig)
{
    for (int j = 0; j < dig; j++)
       x *= 10.0;
    x = (long int) x;
    for (int j = 0; j < dig; j++)
       x /= 10.0;

    return x;
}

float frac_decimal(float x, int dig)
{
    return trunc_decimal(x, dig) - (long int ) x;
}

float (*FF)(float, int);

int main(void)
{
   FF = trunc_decimal;  /* FF ahora actuará como trunc_decimal. */
   FF(3.14159265, 2);   /* Da 3.14. */
   FF = frac_decimal;   /* FF ahora actuará como frac_decimal */
   FF(3.14159265, 2);   /* Da 0.14. */
   return 0;
}




6. Prototipos y otros temas sobre funciones

   A veces conviene listar las cabeceras de función al principio del programa, y postergar la escritura del cuerpo de las funciones para después.
   Podemos hacer esto si ponemos todo en un mismo archivo.
   Esto nos da la libertad de poner el cuerpo de la función donde nos quede más cómodo.
   Para esto se utilizan los prototipos de funciones, que tan sólo son una cabecera conteniendo el nombre de la función, sus tipos de retorno y de los parámetros.
   Para la sintaxis de estos prototipos, basta recordar que son declaraciones como cualesquiera otras: definen un identificador, de determinado tipo. En este caso, de tipo función.
   En nuestro ejemplo, nos puede quedar algo como esto:

float trunc_decimal(float x, int dig);
float frac_decimal(float x, int dig);

float (*FF)(float, int);

int main(void)
{
   FF = trunc_decimal;  /* FF ahora actuará como trunc_decimal. */
   FF(3.14159265, 2);   /* Da 3.14. */
   FF = frac_decimal;   /* FF ahora actuará como frac_decimal */
   FF(3.14159265, 2);   /* Da 0.14. */
   return 0;
}

float trunc_decimal(float x, int dig)
{
    for (int j = 0; j < dig; j++)
       x *= 10.0;
    x = (long int) x;
    for (int j = 0; j < dig; j++)
       x /= 10.0;

    return x;
}

float frac_decimal(float x, int dig)
{
    return trunc_decimal(x, dig) - (long int ) x;
}


   Observemos cómo es que hemos podido poner ahora los cuerpos de las funciones debajo del cuerpo de main().
   Antes no podíamos. ¿Por qué?

   Un identificador tiene que ser visible antes de poder ser usado o llamado.

   Ahora la presencia de los prototipos antes de main() hacen visibles las funciones trunc_decimal() y frac_decimal() antes de haber detallado su cuerpo.

   Los prototipos como T func1(T1, ..., Tn) declaran funciones que retonarn T, para un tipo dado T.
   En cambio, una declaración T (*func2)(T1, ..., Tn) declara un identificador de tipo puntero a función que retorna T.
   La diferencia es que func1 es un identificador atado a una función específica, que requiere que en alguna parte del archivo del programa le declaremos un cuerpo de función que defina su comportamiento.
   En cambio, func2 es un identificador que sirve para asignarle distintas funciones, a fin de cambiar su comportamiento según lo requiera la situación.

   Importante: Remarcamos aquí que es crucial no confundir un valor de tipo función con una llamada a función.
   Veamos estas sentencias:

   float val;
   float (*FF)(float, int);

   /* Se llama a la función, se obtiene el valor 3.14, */
   /*  y se asigna 3.14 a la variable val. */
   val = trunc_decimal(3.1415, 2); 

   FF = trunc_decimal;              /* Se asigna un puntero a trunc_decimal a la variable FF. */


   Todos los identificadores pueden definirse en cualquier parte del programa, inclusive dentro del cuerpo de una función.
   Sin embargo, el cuerpo de una función dada no puede aparecer desarrollado "adentro" de otras funciones. Tienen que estar todas "por fuera" en el archivo fuente del programa.
   En particular, como los prototipos son declaraciones de identificadores, pueden ir dentro del cuerpo de una función.
   No obstante, tales prototipos están condenados a designar funciones visibles globalmente, debido a que el cuerpo de la función tiene que tener visibilidad en todo el archivo del programa.

   ¿Se puede prototipar main()?
   Sí. Es una función como cualquier otra, a los fines sintácticos.

   ¿Cómo entender el "tipo función"?[/font][/i]
   Bueno, ellos son tan sólo lo que hemos dicho: un tipo de datos determinado por el tipo de retorno y el tipo y número de parámetros.
   Sin embargo, ese tipo no entra nunca realmente en juego en un programa, porque toda expresión conteniendo un identificador de función automáticamente se convierte a puntero a función.
   Esto se aplica a toda expresión, incluso a aquellas en que sólo se realizan llamadas a funciones.
   Mirando en detalle, una llamada a función f() se realiza en 2 etapas:
      (1) Se convierte f a un puntero a función.
      (2) Luego se llama al (cuerpo de) la función "apuntada".
   Esto explicaría que las siguientes llamadas a función son todas equivalentes:

    f();
    (*f)();
    (*****f)();
    (&f)();

   Es ilustrativo estudiar el caso de *f().
   En la expresión dada, f tiene tipo función, que automáticamente "decae" a puntero a función.
   Esto nos da la dirección de memoria de la función f (o sea , &f).
   El operador de indirección * toma ese puntero y lo desreferencia, designando ahora el objeto apuntado, que es de nuevo f.
   Pero esto genera una expresión de tipo función que, de nuevo, "decae" a puntero a función.
   En cuanto al operador de dirección &, sabemos que en ese caso no se "decae" a puntero a función.
   Entonces f allí se mantiene como de tipo función, pero & obtiene su dirección, dando ahora un puntero a función.

   Apenas luego de haber ocurrido esta conversión a puntero a la función se hace la llamada a función.

   Retornando tipos función:
   Aunque no se permite declarar funciones cuyo tipo de retorno sea función,
   es admitido, sin embargo, que el tipo de retorno sea puntero a tipo función.

EJEMPLO 2

float trunc_decimal(float x, int dig);
float frac_decimal(float x, int dig);

typedef float (FTYPE)(float, int) ;   /* FTYPE denota un tipo "función" */

FTYPE * G(int select)                 /* G es una función que retorna un "puntero a función" */
    {
        if (select == 1)
           return trunc_decimal;      /* El valor de retorno es un puntero a una función */
        if (select == 2)
           return frac_decimal;       /* El valor de retorno es un puntero a una función */
    }

int main(void)
{
    G(1);     /* Expresión cuyo resultado es un puntero a la función trunc_decimal */
    G(2);     /* Expresión cuyo resultado es un puntero a la función frac_decimal */

    /* A continuación obtenemos la función trunc_decimal llamando a la función G con parámetro 1. */
    /* El valor de retorno es un puntero a función, cuyo valor concreto es trunc_decimal.         */
    /* Poniendo parámetros entre paréntesis luego de G(1), equivale a hacer */
    /* una llamada a la función trunc_decimal.                              */

    return G(1)(3.1415, 2); /* Equivale a trunc_decimal(3.1415, 2), que da 3.14 (convertido a int da 3) */
}


   Parámetros de tipo función:
   Un parámetro de una función puede tener tipo función, y también puntero a función.
   Si bien son tipos distintos de parámetro, en la práctica dan resultados idénticos en el programa ejecutable.
   Al tener tipos distintos, determinan tipos función diferentes. Sin embargo, C los considera tipos compatibles,
   así que podemos trabajar tranquilos sin preocuparnos sobre qué tan exactos fuimos al indicar el tipo función de un parámetro.

EJEMPLO 3

float trunc_decimal(float x, int dig);
float frac_decimal(float x, int dig);

typedef float (FTYPE)(float, int) ;

float H(FTYPE FF)    /* H es una función que retorna float, y admite un parámetro de tipo "función". */
{
   return FF(3.1415, 2);
}

float HH(FTYPE *FF)   /* HH es una función que retorna float, y admite un parámetro de tipo "puntero a función". */
{
   return FF(3.1415, 2);
}

int main(void)
{
    H(trunc_decimal);    /* Equivale a trunc_decimal(3.1415, 2), que da 3.14 */
    HH(trunc_decimal);   /* Equivale a (&trunc_decimal)(3.1415, 2), que da 3.14 */
    return 0;
}




Organización

Comentarios y Consultas