sábado, 5 de abril de 2008

Capítulo 3

.
Funciones en C

En lenguaje C solo podemos utilizar funciones. No existe el concepto de "procedimiento" que estudiamos en Pascal. Pero claro, si definimos una función que "no retorne nada" entonces sería una especie de procedimiento.

Como ya estudiamos, la función es un "subprograma" que retorna un valor al momento de su invocación. En C existe el tipo de datos void. Si una función retorna un valor de este tipo entonces es una función que no retorna nada, como un procedimiento.

Los parámetros de las funciones siempre se reciben por valor. No existe un modificador var como el de Pascal para indicar que el parámetro se recibe por referencia. La única manera de que una función modifique el valor de sus parámetros es que reciba sus direcciones de memoria, es decir: punteros.

Veamos el desarrollo de la función factorial y un programa principal que la pruebe.

testFactorial.c
   1:
2:#include<stdio.h>
3:
4:long factorial(int p)
5:{
6: int i=1;
7: long fact=1;
8:
9: while( i<=p )
10: {
11: // multiplica fact*i y luego incrementa i
12: fact=fact*i++;
13: }
14: return fact;
15:}
16:
17:int main()
18:{
19: int n;
20: long result;
21:
22: printf("Ingrese un valor: ");
23: scanf("%d",&n);
24:
25: result = factorial(n);
26:
27: printf("Factorial de %d = %d\n", n,result);
28:}
29:


Librerías y Funciones Externas

Podemos separar nuestro código C en diferentes archivos de código fuente de forma tal que las funciones que desarrollemos puedan ser reutilizadas por diferentes programas.

El ejemplo anterior (testFactorial.c) definimos en un mismo archivo la función factorial y una función main que la utiliza. Pero obviamente, ya que desarrollamos la función factorial sería interesante poder mantenerla al margen del programa principal para no tener que "copiarla y pegarla" en cada programa que necesitemos calcular un factorial.

En principio podemos tener dos archivos separados: libreria.c y testLibreria.c.

libreria.c
   1:
2:#include<stdio.h>
3:
4:long factorial(int p)
5:{
6: int i=1;
7: long fact=1;
8:
9: while( i<=p )
10: {
11: fact=fact*i++;
12: }
13: return fact;
14:}
15:

testLibreria.c
   1:
2:#include<stdio.h>
3:
4:int main()
5:{
6: int n;
7: long result;
8:
9: printf("Ingrese un valor: ");
10: scanf("%d",&n);
11:
12: result = factorial(n);
13:
14: printf("Factorial de %d = %d\n", n,result);
15:}
16:

Para compilar tendremos que realizar dos pasos:
  1. Compilar los archivos .c (generando archivos .o)
  2. Enlazar los .o que para generar el .exe (ejecutable)
Utilizando la distribución de EditPad PRO+GCC con CTRL+F8 abrimos una ventana de comandos como vemos en la siguiente imagen.



Tipeamos los siguientes comandos:
  1. (compilacion) - gcc -c libreria.c testLibreria.c
  2. (linkedición) - gcc libreria.o testLibreria.o -o prgEjecutable
Esto generará el archivo prgEjecutable.exe, nuestro programa.

El problema de compilar y linkeditar por separado es que el compilador solo validará que el programa que le mandamos a compilar sea sintacticamente correcto. No validará que las funciones que invocamos existan ni que los tipos de datos de los argumentos que le pasamos sean los que realmente la función espera recibir.

Esto lo podemos probar modificando la línea 12 de testLibreria.c de la siguiente manera:

//estaba asi...
result = factorial(n);

// modificarla para que quede asi...
result = factorial("recibia un int pero le paso un string");

Veremos que al compilar con gcc -c el compilador no tira ningún error de compilación ya que sintacticamente la línea de código es correcta pero, obviamente, cuando ejecutemos el programa el resultado será cualquier cosa.

Para evitar este problema C permite definir archivos de cabecera (.h) en los que podemos especificar los header o prototipos de las funciones. El compilador validará la invocación a las funciones contra los prototipos definidos en el .h y así, si corresponde, podrá tirar el error.


Archivos de Prototipos de Funciones (.h)

Los archivos .h (header) permiten exponer los prototipos (encabezados) de las funciones desarrolladas en nuestras librerías. Así podremos validar en tiempo de compilación que la invocación a dichas funciones sea correcta.

libreria.h
   1:
2:// este archivo contiene los prototipos
3:// de las funciones.
4:// Notemos que el prototipo no especifica
5:// el nombre del parametro que recibe, solo su tipo
6:// Se especifica el valor de retorno, nombre de la
7:// funcion y los tipos de parametros que recibe
8:long factorial(int);
9:

libreria.c
   1:
2:// incluimos el libreria.h
3:// con comillas indicamos que el .h esta en el
4:// mismo directorio que este archivo .c
5:#include "libreria.h"
6:
7:// con <mayor y menor> indicamo que el .h
8:// esta en otro directorio que debera especificarse
9:// seteando la variable de entorno INCLUDE
10:#include<stdio.h>
11:
12:
13:long factorial(int p)
14:{
15: int i=1;
16: long fact=1;
17:
18: while( i<=p )
19: {
20: fact=fact*i++;
21: }
22: return fact;
23:}
24:

testLibreria.c
   1:
2:#include<stdio.h>
3:#include "libreria.h" // incluimos libreria.h
4:
5:int main()
6:{
7: int n;
8: long result;
9:
10: printf("Ingrese un valor: ");
11: scanf("%d",&n);
12:
13: result = factorial(n);
14:
15: printf("Factorial de %d = %d\n", n,result);
16:}
17:

Si ahora reemplazamos en la línea 13 del programa principal y en lugar de pasar n (que es un int) pasamos un string veremos el siguiente error de compilación.




Parámetros por valor y por referencia

En C no existe el concepto de "parámetro por referencia". Los parámetros siempre son pasados por valor. La única manera de que una función pueda modificar el valor de sus parámetros es que reciba su puntero.

testPermutar.c
   1:
2:#include<stdio.h>
3:
4:void permutar(int a, int b)
5:{
6: int aux=a;
7: a=b;
8: b=aux;
9:}
10:
11:int main()
12:{
13: int x,y;
14: printf("Ingrese dos valores: ");
15: scanf("%d %d",&x,&y);
16:
17: // intenta permutar los valores de x,y pero no
18: // lo logra porque los recibe por valor
19: permutar(x,y);
20:
21: printf("x vale %d, y vale %d\n",x,y);
22:}
23:

La función no podrá permuar los valores de los parámetros porque los recibe por valor. Para que pueda modificarlos debe recibir punteros a sus contenidos como vemos a continuación.
   1:
2:#include<stdio.h>
3:
4:// ahora recibe dos punteros a int
5:void permutar(int *a, int *b)
6:{
7: // a aux le asigno "el contenido de a"
8: int aux = *a;
9:
10: // al "contenido de a" le asigno "el contenido de b"
11: *a = *b;
12:
13: // al "contenido de b" le asigno el valor de aux
14: *b = aux;
15:}
16:
17:int main()
18:{
19: int x,y;
20: printf("Ingrese dos valores: ");
21: scanf("%d %d",&x,&y);
22:
23: // paso las direcciones de x,y (sus punteros)
24: permutar(&x,&y);
25: printf("x vale %d, y vale %d\n",x,y);
26:}
27:

Ahora la función recibe dos punteros a entero con lo cual puede permutar sus contenidos. En el programa principal definimos dos enteros x, y. No podemos pasarlos directamente como parámetros a la función porque la función espera dos punteros a int, no dos int. Pero si x es un int entonces &x es "la dirección de x": es el puntero.

Así como en la línea 21 la función scanf recibe &x, &y para poder asignar en estas variables los valores ingresados por el usuario, con el mismo critero en la línea 24 la función permutar recibe &x, &y para poder permutar sus contenidos.


Problema 3.2
Considere dos circunferencias en un plano y un conjunto de 10 puntos. Realizar un programa que indique:
  1. Cuantos puntos caen dentro de la circunferencia 1
  2. Cuantos puntos caen dentro de la circunferencia 2
  3. Cuantos puntos están afuera de la circunferencia 1
  4. Cuantos puntos están afuera de la circunferencia 2
  5. Cuantos puntos caen en el centro de la circunferencia 1
  6. Cuantos puntos caen en el centro de la circunferencia 2
  7. Cuantos puntos caen dentro de la intersección de las dos circunferencias
  8. Cuantos puntos caen afuera de las dos circunferencias
Para describir una circunferencia se ingresan tres valores reales: el radio y sus coordenadas (x, y). Para describir un punto se ingresan sus coordenadas (xp, yp).
   1:
2:#include<stdio.h>
3:#include<math.h>
4:
5:float procesaCirc(float r
6: , float cx
7: , float cy
8: , float px
9: , float py
10: , int *cDentro
11: , int *cCentro
12: , int *cFuera)
13:{
14: // sqrt y pow definidas en math.h
15: float d = sqrt( pow( px-cx,2) + pow(py-cy,2) );
16: if( d<=r )
17: {
18: // incremento el contenido de cDentro
19: (*cDentro)++;
20: if( d==0)
21: {
22: (*cCentro)++;
23: }
24: }
25: else
26: {
27: (*cFuera)++;
28: }
29:
30: return d-r;
31:}
32:
33:// programa principal
34:int main()
35:{
36: int cCirc1=0,cCirc2=0,cCentro1=0,cCentro2=0
37: ,cFue1=0,cFue2=0,cInter=0,cNing=0;
38: float d1,d2;
39: float r1,x1,y1,r2,x2,y2;
40: float xp,yp;
41: int i;
42:
43: printf("Ingrese circunferencia 1 (r1,x1,y1): ");
44: scanf("%f %f %f",&r1,&x1,&y1);
45:
46: printf("Ingrese circunferencia 1 (r2,x2,y2): ");
47: scanf("%f %f %f",&r2,&x2,&y2);
48:
49: for( i=1; i<=10; i++ )
50: {
51: printf("Ingrese punto %d (xp,yp): ",i);
52: scanf("%f %f",&xp,&yp);
53:
54: d1 = procesaCirc(r1,x1,y1,xp,yp
55: ,&cCirc1,&cCentro1,&cFue1);
56:
57: d2 = procesaCirc(r2,x2,y2,xp,yp
58: ,&cCirc2,&cCentro2,&cFue2);
59:
60: if( (d1<0) && (d2<0) )
61: {
62: cInter++;
63: }
64: else
65: {
66: if( (d1>0) && (d2>0) )
67: {
68: cNing++;
69: }
70: }
71: }
72:
73: // datos circunferencia 1
74: printf("Circunferencia 1: Adentro: %d, ",cCirc1);
75: printf("Centros %d, ",cCentro1);
76: printf("Afuera %d\n",cFue1);
77:
78: // datos circunferencia 2
79: printf("Circunferencia 2: Adentro %d, ",cCirc2);
80: printf("Centros %d, ",cCentro2);
81: printf("Afuera %d\n", cFue2);
82:
83: // datos interseccion y afuera
84: printf("Interseccion: %d, ",cInter);
85: printf("Afuera de las dos: %d\n", cNing);
86:}
87:


Directivas de Preprocesador

C permite utilizar lo que se llama directivas de preprocesador. Son instrucciones que definimos con el prefijo " # " y nos permiten incluir archivos (directiva #include) y definir macros y constantes (directiva #define). Existen otras directivas pero no las estudiaremos en este trabajo.

Estas instrucciones se procesan antes de compilar el código C.


Directiva #include

Con esta directiva podemos incluir en nuestro código el contenido de otro/s archivo/s. Lo correcto es incluir archivos header (.h).

Muchos programadores utilizan esta directiva para incluir en un archivo .c otros archivos .c, pero esto está mal, es una pésima práctica de programación.

Lo que veremos a continuación está mal. Funciona pero está mal. Solo les traerá problemas y frustraciones además de depresión crónica, insomnio y alteraciones gastrointestinales.

incluido.c
   1:
2:void f()
3:{
4: printf("Soy la funcion f()\n");
5:}
6:

incluye.c
   1:
2:#include<stdio.h>
3:
4:// esto es apestoso, horrible, UN ASCO !
5:#include "incluido.c" // no hagan esto, por favor !
6:
7:int main()
8:{
9: // esta funcion esta disponible porque incluimos
10: // el archivo incluido.c, puaj!
11: f();
12:}
13:

Solo debemos utilizar la directiva #include para incluir archivos .h. Si los archivos que vamos a incluir están en el mismo directorio que el archivo .c entonces el nombre del archivo a incluir lo encerramos entre "comillas dobles". Si tenemos varios archivos .h y los queremos mantener en un directorio separado tendremos que definir una variable de entorno llamada INCLUDE y asignarle la ruta del directorio que los contiene. En este caso utilizamos <mayor y menor> para encerrar el nombre del .h que vamos a incluir.


Directiva #define

Podemos utilizar esta directiva para definir constantes y macros. Veamos el siguiente ejemplo.

testDefine.c
   1:
2:#include<stdio.h>
3:
4:// defino la constante PI
5:#define PI 3.141592
6:
7:// defino una macro MAX
8:#define MAX(a, b) a>b?a:b
9:
10:// defino una macro MIN
11:#define MIN(a,b) a<b?a:b
12:
13:int main()
14:{
15: int a=10, b=5;
16:
17: // usamos la macro MAX
18: printf("El maximo entre %d y %d es: %d\n"
19: ,a,b,MAX(a,b));
20:
21: // usamos la macro MIN
22: printf("El minimo entre %d y %d es: %d\n"
23: ,a,b,MIN(a,b));
24:
25: // usamos la constante PI
26: printf("PI vale: %f\n",PI);
27:}
28:


Argumentos en Línea de Comandos

En C la función main puede recibir los argumentos que se pasan en línea de comandos. El siguiente ejemplo muestra como hacerlo.

testArgs1.c
   1:
2:#include<stdio.h>
3:
4:// argc, entero que indica cuantos argumentos recibe
5:// argv, "array de strings" con los argumenos
6:int main(int argc, char **argv)
7:{
8: int i;
9:
10: // muestro la cantidad de argumentos recibidos
11: printf("argc: %d\n",argc);
12:
13: // ahora muestro cada uno de los argumentos
14: for( i=0; i<argc; i++)
15: {
16: printf("%s\n",argv[i]);
17: }
18:}
19:

Por convención se utilizan las variables argc (argument count) y argv (argument vector).

Si compilamos y corremos el programa con los mismos argumentos que utilizamos para probar esto en Pascal veremos el siguente resultado.


Si bien le pasamos 4 argumentos argc contiene el valor 5 y luego, cuando iteramos el array de argumentos vemos que argv[0] (el primer argumento) contiene el mismo nombre del programa.

Por lo tanto, el primer argumento verdadero está en la posición 1 del array argv.

No debemos exaltarnos por escribir char **argv. En realidad esto representa un array de strings que podemos interpretar de la siguiente manera: un string es un array de caracteres, y un array es un puntero al primer elemento así que con char* podemos representar un string. Ahora bien, si queremos hablar de un array de strings entonces simplemente agregamos un segundo asterisco.

De cualquier manera, en la línea 16 vemos que manejamos argv[i] como ubiéramos manejado cualquier array de strings de Pascal.





.

No hay comentarios: