ESCUELA POLITÉCNICA SUPERIOR. 1º GRADO DE INGENIERÍA ELECTRÓNICA. INFORMÁTICA. PRACTICA 2. FUNCIONES. OBJETIVOS Introducir las funciones, como la implementación del concepto de “caja negra” usual en ingeniería. Al finalizar esta práctica, el alumno debe ser capaz de: Usar las funciones de librería que proporciona el compilador. Escribir funciones sencillas en C, y usarlas desde sus propios programas. RESUMEN TEORICO printf Se usa para imprimir texto y expresiones por la pantalla. Su primer parámetro es una cadena de formato que especifica qué texto ha de imprimirse, y en qué lugar deben aparecer cada una de las expresiones que se pretenden imprimir. También se especifica de qué tipo son las expresiones a imprimir. printf (“He nacido el dia %d de %d de %d. Tengo %d años. Mido %.2f metros. La letra de mi DNI %c\n”, dia, mes, anno, 2014-anno, medida, letra); Variables Espacio de memoria al que se le asigna un nombre, y que guarda un valor. Cada variable pertenece a un “tipo”. Este tipo indica qué tipo de valor puede guardar la variable. Los tipos más comunes son: int: entero de 32 bits, con signo, complemento a 2. char: entero de 8 bits con signo, complemento a 2, también se usa para guardar valores de código ASCII que pueden ser interpretados como letras y símbolos de este código. float y double: números reales de simple y doble precisión, respectivamente, conforme al estándar IEEE754. Han de definirse antes de poder usarse. La definición de las variables se realiza al principio del bloque main, y antes de cualquier orden. Así: #include <stdio.h> int main() { int a,b=3,contador; /* tres variables enteras, una de ellas con un valor inicial de 3 */ float x; /* una variable real de simple precision */ /* aquí vendría el código que usa estas variables */ } Expresiones Cualquier cosa que devuelva un valor. Si una expresión numérica está compuesta únicamente de valores enteros, el resultado de dicha expresión será un entero. Si dentro de ella hay al menos un valor real, el resultado de la expresión, será real. Dentro de una expresión, las operaciones se realizan con una cierta prioridad: las multiplicaciones y divisiones antes que las sumas y restas. (-b+sqrt(b*b-4*a*c))/(2*a) 1 EJERCICIOS PARA RESOLVER EN EL LABORATORIO FUNCIONES DE BIBLIOTECA Cualquier compilador se ofrece con un conjunto de funciones más o menos estándares, para realizar las operaciones más comunes. ¿Cómo se usa una función de biblioteca? Para ello, necesitamos saber, de esa función las siguientes cosas: Cómo se llama Qué argumentos tiene y de qué tipo son Qué tipo de valor de salida genera esa función Dónde se encuentra, dentro del conjunto de funciones de nuestro compilador. Esta información se encuentra en la documentación del compilador. Para las funciones más comunes, también puede usarse cualquier texto o libro que enseñe a programar en C. A menudo, el compilador dispone de una ayuda integrada que permite averiguar toda esta información de una función, sin más que saber el nombre de la función. Por ejemplo, para la función “seno de un ángulo”, el nombre de la misma en C es sin. Si tu compilador tiene instalada la ayuda, prueba a escribir “sin” en el editor de C, pon el cursor de texto dentro de la palabra recién escrita, y pulsa F1. Aparecerá la ayuda contextual en pantalla con la siguiente información: La información que se nos suministra es la siguiente: #include <math.h> : indica el nombre del archivo de cabecera que es necesario incluir en nuestro programa para que éste reconozca la función que vamos a usar. Incluye esa línea completa, al comienzo de tu programa, justo después de los demás “include’s” que hubiera. double sin (double arg); Esta línea muestra el prototipo de la función. El prototipo de una función es algo así como un “resumen” de la función. Nos indica cómo se llama, cuántos argumentos tiene, y de qué tipo son cada uno, y de qué tipo es el resultado de la función. En este caso, se nos indica que esta función necesita un argumento de entrada, de tipo double 1 , y que devuelve un valor, también de tipo double. El argumento se llama “arg” pero para usar esta función, el nombre del argumento que aparece en la ayuda no es relevante. Se incluye para clarificar el rol que tiene ese argumento dentro de la función, cuando la función tiene más de un argumento, o no queda claro para qué sirve cada uno. De hecho, el prototipo de una función como sin podría aparecer también así: double sin (double); Los prototipos de funciones se guardan en los archivos de cabecera. ¿Cómo se usa una función en un programa? Simplemente, en cualquier expresión aritmética en la que se necesite calcular el seno de un ángulo, usaremos sin. Por ejemplo, si en la variable n, que supondremos de tipo float, tenemos guardado un valor de ángulo expresado en radianes, la expresión en C sin(n) nos devuelve el seno de n. EJERCICIO 1 Vamos a comprobar experimentalmente la igualdad sen2α+cos2α=1. Para ello usaremos las funciones sin() y cos(). Carga el programa p2ej1.c, y da un valor a la variable a. Ten en cuenta que las funciones trigonométricas funcionan con radianes, así que obtendrás resultados más precisos si el valor está entre 0 y 2π. ¿Se cumple la igualdad? 1 El tipo double acepta valores con decimales, como float, pero se diferencia de aquél en que double tiene más precisión (guarda más decimales). Se corresponde con el formato de doble precisión del estándar IEEE 754 que se estudia en el TEMA 1 de teoría. 2 EJERCICIO 2 En muchos casos es posible usar el resultado de una función como argumento a otra, sin tener que guardar el resultado de la primera en una variable intermedia. Por ejemplo, la raíz cuadrada del seno de a (expresado en matemáticas como sen ) se escribiría en C así: sqrt(sin(a)) . Vamos ahora a usar la función asin() (arco-seno). Esta función devuelve el ángulo cuyo seno es el argumento dado. Puedes usar el código del ejercicio p2ej1.c como plantilla para hacer éste, guardándolo como p2ej2.c : modifica ese ejercicio para hacer un programa que partiendo de un valor a, le halle el seno, y a ese resultado, le halle el arco-seno, es decir, que halle el arco-seno del seno de ese valor. El resultado lo guardará en la variable res que usábamos también en el anterior ejercicio. Comprueba en la ventana de variables, durante la depuración, que ambas variables toman el mismo valor. Recuerda que las funciones arco- devuelven siempre un resultado comprendido entre 0 y 2π. EJERCICIO 3 Se pueden calcular logaritmos en cualquier base usando únicamente logaritmos naturales. Así, el logaritmo en base b de un número x se define como log b x ln x . En C, el logaritmo natural ln x está implementado con la función log() (busca más información en la ln b ayuda del compilador). Con esto, carga el programa p2ej3.c y complétalo para que calcule el logaritmo en base b del número x, y lo guarde en la variable res. Usa el programa con los siguientes números: 4, 8, 16, 256, 65536 y 1048576. A continuación pasa estos cuatro números a binario (manualmente, o con la calculadora de Windows). ¿Observas alguna relación entre la representación en binario de estos números y el resultado de su logaritmo en base 2? EJERCICIO 4. La función floor() devuelve el número entero más cercano (por defecto) a un valor dado como argumento (busca información sobre esta función en la ayuda del compilador). Partiendo del programa p2ej3.c del ejercicio anterior, cárgalo y guárdalo con el nombre p2ej4.c . Modifícalo para que calcule el valor de la expresión 1 log 2 x (1 + el número entero por defecto más cercano al logaritmo en base 2 de x) y lo guarde en la variable res. Ten presente que aunque floor() devuelve un número sin decimales, lo que devuelve sigue siendo un float, no un int. Simplemente es que la parte decimal de este valor float devuelto será siempre .000000 Para probar el funcionamiento de este programa, escoge algunos números en binario (por ejemplo, 101, 1100 y 101100). Transfórmalos a base 10 bien manualmente, o bien con la calculadora de Windows (los tres números anteriores serían 5, 12 y 44), y por último, usa estos números convertidos como valores para asignar a x. ¿Qué relación observas entre el valor de res, y y la versión en binario del valor de x? FUNCIONES DEFINIDAS POR EL USUARIO Al igual que se pueden usar funciones de librería, ya incorporadas en el compilador, nosotros mismos podemos escribir nuestras propias funciones y usarlas cuántas veces queramos dentro de un programa. Para escribir una función es necesario: Saber qué argumentos y de qué tipo necesita nuestra función. Saber qué debe hacer nuestra función con los argumentos (qué cálculo o proceso debe hacerse con ellos). Saber qué se debe devolver como resultado. Una vez que sabemos estas cosas, una función se compone de dos cosas: Un prototipo. Una implementación, que consta a su vez de: o Una cabecera (muy similar a lo que escribimos en el prototipo) o Un cuerpo (aquello que está encerrado entre dos llaves después de la cabecera) La implementación de una función tiene el siguiente aspecto: Cabecera de la función (es como el prototipo, pero sin el punto y coma al final, y poniendo los nombres a los argumentos) Abrir llave { Variables que use la función (si las necesita) Código de la función (cálculos, etc) Retornar valor con return Cerrar llave } La implementación de una función se escribe detrás del código de main (después de cerrar la llave cuando acaba main). Si hay varias funciones, las escribiremos una debajo de la otra, dejando siempre a main al principio. Dentro del cuerpo de la función, y una vez calculado el valor que debe devolverse, hay que realizar la devolución en sí. Esto se hace con la sentencia return. 3 Por ejemplo, la implementación de una función que suma dos números enteros a y b, y devuelve su suma, también como un entero, sería así: cabecera int suma (int a, int b) { int resultado; resultado = a + b; return resultado; cuerpo } Su prototipo, por cierto, sería éste: int suma (int, int); O bien, este otro (cualquiera de los dos es válido): int suma (int a, int b); ¡O incluso éste! (como ves, los nombres de los argumentos, en el prototipo, son irrelevantes) int suma (int x, int y); Vamos a ver algunos ejemplos de cómo funciona esto, y después trataremos de escribir nuestras propias funciones. EJERCICIO 5. Carga el programa p2ej5.c . Este programa hace en realidad lo mismo que el del ejercicio 1. Aquí main() no está sola, sino que hay otra función con ella, la función cuadrado() que hemos escrito nosotros. Además, la expresión sen2α+cos2α la hemos dividido en varios pasos, usando variables intermedias. Así por ejemplo, la variable sena guarda el valor del seno de a, y cosac guarda el valor del cuadrado del coseno de a. De esta forma, en el paso final se suman los valores de las variables senac y cosac y el resultado debería dar 1. Da un valor adecuado a la variable a y pon un punto de ruptura, como siempre, al principio de main() (en la primera línea donde se ejecuta código). a) Ejecuta paso a paso (con F7) hasta la línea de código donde dice: senac = cuadrado(sena); Cuando llegues allí, fíjate en el valor de la variable sena y fíjate también en lo que aparece a la derecha de Context justo encima de la lista de variables con sus valores. Context indica en qué función estamos y la lista de variables que aparece es de hecho la lista de variables que usa esa función. Ahora pulsa de nuevo F7. ¿Hemos ido un paso más en la función main()? ¿Dónde estamos ahora (qué indica Context)? ¿Qué valor ha tomado la variable n? b) Sigue ejecutando paso a paso en el nuevo contexto, y observa qué pasa al terminar de ejecutarse la función. ¿A qué contexto volvemos? El valor de resultado ¿a dónde ha ido? c) Sigue ejecutando paso a paso hasta llegar a la línea de código donde dice: cosac = cuadrado(cosa); ¿Qué valor toma la variable n? d) Modifica el programa borrando (o poniéndolo como un comentario) la línea 4 del código, donde está escrito el prototipo. Graba el programa modificado y ejecútalo paso a paso de nuevo. ¿Qué ha ocurrido con el comportamiento de la función cuadrado()? e) (PARA CASA) Modifica el programa para que no necesite variables intermedias, sino que todo el cálculo de la variable res lo haga en una sola línea. Para ello, recuerda lo que has hecho en el ejercicio 2, en donde el resultado de una función se puede usar directamente como argumento de otra. EJERCICIO 6. Carga el programa p2ej6.c y ejecútalo paso a paso hasta el final, entrando en la función incrementa() las tres veces. a) ¿Qué pasa con la variable a al final? b) Modifica el cuerpo de la función incrementa() para que el argumento no se llame n, sino a, así: void incrementa (int a) { a = a + 1; return; } ¿Qué ocurre ahora con la variable a cuando se llega al final del programa? c) Prueba ahora con una nueva modificación. En este caso, hay que modificar tanto el prototipo como la implementación (cabecera y cuerpo) de la función. Consiste en que ahora la función devuelve un valor de tipo int en lugar de void. En el cuerpo, hay que cambiar la orden return para que devuelva el valor de a una vez que se ha incrementado. ¿Funciona ahora? d) Termina de modificar el programa para que al final, el valor de a resulte incrementado las tres veces y termine con el valor 6. PISTA: si una función devuelve un valor, lo habitual es que quien llame a esa función haga algo con ese valor que se devuelve. e) Una vez que compruebes que funciona, ¿funciona también si el nombre del argumento de la función incrementa() es el que tenía originalmente, es decir, n, u otro cualquiera? f) Carga el programa p2ej6bis.c y ejecútalo paso a paso atendiendo al valor que toma la variable res. ¿Cómo se realiza la suma si las variables a y b no aparecen cuando la ejecución se traslada al contexto de suma()? ¿Cómo influye el contexto sobre la variable res? Modifica este programa para que funcione como debería. 4 EJERCICIOS PARA RESOLVER EN CASA EJERCICIO 7. Vamos a crear una nueva función que directamente nos proporcione el logaritmo en cualquier base de un número dado usando la fórmula vista en el ejercicio 3, y vamos a usar esa función en el ejercicio 4 reescribiéndolo para que la línea en la que se calcula la expresión contenga únicamente la llamada a nuestra nueva función. Llamaremos a nuestra función “logb”. Dicha función toma como argumento dos números con decimales (float) y devuelve otro número, también float. a) Partimos de una copia de lo que hemos hecho en el ejercicio 4 ¿Cuál sería el prototipo de la función logb? Toma dos argumentos, ambos de tipo float, y el resultado que devuelve también es un float. El prototipo debe escribirse al principio del programa, detrás de todos los “include’s” que tenga, y justo antes de la línea que contiene a main (fíjate dónde estaba puesto en el ejercicio 5). Pongámoslo en su sitio. b) Ahora hay que escribir la implementación de la función. Dentro de ella, el cuerpo de la función es la parte de código C que hace los cálculos o el proceso que se requiere en esa función. En nuestro ejemplo, el cuerpo de la función logb consiste en calcular un valor con ayuda de una fórmula que hemos visto en el ejercicio 3. Dentro de la función se asignará el valor de la fórmula a una variable que tenemos que definir, y a continuación retornamos con el valor de dicha variable, que se convierte en el valor de vuelta de nuestra función. Una vez añadidos el prototipo e implementación de la función logb al programa del ejercicio 4, modifica la línea de código en donde se usa la fórmula que calcula 1 log 2 n para que pase a usar la función logb(), inventada por nosotros. Comprueba que el programa sigue funcionando como lo hacía con el ejercicio 4. EJERCICIO 8. Busca en la ayuda información sobre la función pow(). Con esta función, y sin usar ninguna otra función de math.h, 1 carga el programa p2ej8.c y complétalo para que calcule la raíz cuadrada de un número x de tipo float. Recuerda que x x 2 . El programa hará un cálculo análogo usando la función de librería sqrt() para que puedas comparar los resultados. Usa diferentes valores en x para probar el programa. EJERCICIO 9. La función exponencial ex está implementada en C con la función exp(). La función logaritmo natural ln x es la misma que hemos descrito en el ejercicio 3, log(). Sabiendo que ab=eb·ln a, carga el programa p2ej9.c y complétalo para que calcule el valor ab usando la fórmula dada. Comprueba, dando diferentes valores a a y b en cada ejecución, que el resultado es correcto. Para ello el programa también calcula el mismo valor usando la función de librería pow() con el que puedes comparar. ¿Qué ocurre si a es negativo y b es entero? ¿Y si a es negativo y b tiene decimales? EJERCICIO 10. Carga el programa p2ej10.c. En él, escribe una función a la que llamaremos “potencia”. Esta función tiene un prototipo muy similar (mismos argumentos y mismo tipo, pero diferente nombre) que la función pow() pero calculará la potencia usando la fórmula con la exponencial y el logaritmo del ejercicio anterior. Calcula el resultado de ab usando tanto la función pow() como con nuestra nueva función potencia() y comprueba que los resultados son los mismos que los que obtuviste en el ejercicio 9. EJERCICIO 11. Se denomina inicializar una variable a darle un valor inicial a la misma. Esto se puede hacer de dos formas: o bien se le da el valor inicial justo cuando se declara la variable, o en el código, cuando se usa por primera vez. Vamos a ver dos funciones que inicializan su variable de una forma, para después comprobar qué pasa si se cambia el modo de inicialización. Carga el programa p2ej11.c y ejecútalo paso a paso. a) ¿Qué diferencia hay en el comportamiento de la variable n en cada una de las dos funciones? ¿Qué le hace el modificador static a una variable? b) Cambia en la función incrementa1() la línea int n = 0; por int n; n = 0; Y lo mismo en incrementa2(), pasando a ser: static int n; n = 0; ¿Qué ocurre ahora con el comportamiento de la variable n en cada una de las dos funciones? ¿Se comportan iguales las funciones si se inicializan sus variables de una forma o de otra? EJERCICIO 12. Las variables declaradas con el modificador static se usan frecuentemente para que una función conserve parte de su estado interno cuando se la llama en repetidas ocasiones. Una aplicación para esto son los llamados generadores pseudoaleatorios: funciones que no toman ningún argumento, y devuelven un valor aleatorio diferente cada vez que se las llama. Estos generadores pseudoaleatorios se llaman así porque la secuencia aleatoria que generan no es realmente aleatoria pura, sino que se repite cada N llamadas. Si N es suficientemente grande, y la distribución de los valores aleatorios es uniforme, la secuencia se considera aleatoria. Según cuál sea la aplicación, así tendrá que ser N. Por ejemplo, para un videojuego, basta con que N sea de unos pocos miles. Para una aplicación de seguridad criptográfica, se requiere que N sea del orden de 10307. 5 Los generadores pseudoaleatorios también necesitan de un valor semilla. Es a partir de este valor de donde generan su secuencia aleatoria. Poder cambiar la semilla de un generador posibilita que la secuencia aleatoria arranque desde un valor concreto, lo que es útil para depurar errores en programas que usan estos generadores aleatorios (forzando a que la secuencia aleatoria siempre sea la misma, el comportamiento del programa también lo será, y será más fácil encontrar errores en él. Un ejemplo de generador pseudoaleatorio se puede implementar aplicando las siguientes fórmulas (este generador usa N=65536): semilla 75 ( semilla 1) mod 65537 1 semilla valor pseudoaleatorio 65536 (la operación matemática x mod y se escribe en C de la siguiente forma: x%y . Es el resto de la división entera de x entre y, y se lee “x módulo y”) Cada vez que se usa este generador, se calcula un nuevo valor para semilla, que después sirve para calcular el próximo valor pseudoaleatorio de la secuencia. El valor calculado para semilla se guardará para la próxima vez que se use el generador. Con estos datos, carga y completa el programa p2ej12.c . En él hay que incluir una función aleatorio() que funcione de tal manera que cada vez que es llamada, devuelve un nuevo número pseudoaleatorio, utilizando para ello el generador propuesto. NOTAS: La variable semilla debe poder conservar su valor de una llamada a la siguiente. La variable semilla es entera (tipo int). La variable semilla será inicializada con el valor 1. El valor pseudoaleatorio que se calcule debe ser de tipo float, y así es como será devuelto. MÁS FUNCIONES DE BIBLIOTECA En C-Free 4, hay funciones que no se corresponden a funciones matemáticas, sino que realizan otro tipo de cálculos, o bien modifican algún otro aspecto del comportamiento del programa. Por ejemplo, si incluimos el archivo de cabecera conio.c (no es habitual que un archivo de cabecera tenga extensión .C en lugar de .H, pero esto es correcto), tendremos acceso a funciones que modifican la forma en la que se muestra el texto por pantalla. Estas funciones son diferentes a las vistas hasta ahora en un detalle: no devuelven ningún resultado. Por ello, no pueden aparecer como parte de un cálculo. En lugar de ello, las usaremos en una línea de programa “tal cual”. Esto no es nuevo para nosotros: printf es una función, y también la estamos usando “tal cual”, sin que esté dentro de una expresión. Cuatro de las funciones disponibles en conio.c son: void void void void textbackground (int); textcolor (int); gotoxy (int, int); clrscr (); El “void” significa que no se devuelve nada. La primera función cambia el color de fondo del texto que se escriba a continuación, con printf. La segunda función cambia el color del texto, y la tercera, cambia las coordenadas de pantalla donde aparecerá lo siguiente que se escriba. El primer argumento de gotoxy es la coordenada X, y el segundo argumento, la coordenada Y. Las funciones que no devuelven nada se denominan procedimientos. La cuarta función (procedimiento en realidad) es aún más extraña: no toma ningún argumento, y no devuelve nada a su salida. Cada vez que se usa, borra la pantalla. Aunque una función o procedimiento no use ningún argumento, al usarse, debe escribirse con los paréntesis de apertura y cierre. Por ejemplo, para borrar la pantalla al comienzo de un programa, haremos: #include <stdio.h> #include <conio.c> int main() { clrscr(); /* resto del programa… */ } El uso de estos procedimientos es sencillo: por ejemplo, si queremos que un texto escrito con printf tenga la letra de color amarillo, usa textcolor y como argumento escribe YELLOW (en mayúsculas). EJERCICIO 13. Rescata (o escribe de nuevo) el programa “hola, mundo!” que fue el primer programa que escribimos en C, y consigue que el texto aparezca centrado en pantalla, con letras amarillas sobre fondo azul. Para usar gotoxy correctamente en este ejercicio, ten en cuenta que la pantalla de texto tiene habitualmente 80 caracteres de ancho por 25 de alto. El origen de coordenadas está en la esquina superior izquierda. Las coordenadas comienzan a contarse desde 1, no desde 0. Así, la esquina superior izquierda tiene las coordenadas (1,1) y la esquina inferior derecha, las coordenadas (80,25). ¿Cómo puede evitarse que el mensaje “Pulse una tecla para continuar…” aparezca “pegado” a la frase que hemos puesto? NOTA: a la hora de usar las funciones textcolor()/ textbackground() recuerda fijar primero el color de texto, y después el color de fondo: es decir, usa primero textcolor() y después textbackground(). Si lo haces al contrario, los colores que aparezcan en pantalla no serán correctos. 6