Ads 468x60px

Perfil

lunes, 17 de octubre de 2011

Punteros


El tipo de datos más característico del C son los punteros. Un puntero contiene un valor que es la dirección en memoria de un dato de cierto tipo. Los punteros se emplean en C para muchas cosas, por ejemplo recorrer vectores, manipular estructuras creadas dinámicamente, pasar parámetros por referencia a funciones, etc.

Cuando se declara una variable, se reserva un espacio en la memoria para almacenar el valor de la variable.
Ese espacio en memoria tiene una dirección.
Un puntero es una dirección dentro de la memoria, o sea, un apuntador a donde se encuentra una variable.

7.1 Operaciones básicas

Declaración

Los punteros se declaran con un asterisco, de esta forma:

tipo * variable;

Por ejemplo:

         int* puntero;

Se dice que la variable puntero es un puntero a enteros (apunta a un entero).

Asignación

El valor que puede adquirir un puntero es, por ejemplo, la dirección de una variable.
El operador & devuelve la dirección de una variable:

         puntero = &variable;

Desreferencia de un puntero

Se puede alterar la variable a la que apunta un puntero.
Para ello se emplea el operador de desreferencia, que es el asterisco:

*puntero = 45;

En este caso, se está introduciendo un 45 en la posición de memoria a la que apunta puntero.

7.2 Ejemplo de uso


         {
            int* puntero;
            int variable;
            
            puntero = &variable;
            *puntero = 33;  /* mismo efecto que variable=33 */
         }


Varios punteros pueden apuntar a la misma variable:

int* puntero1;
int* puntero2;
int var;
 
puntero1 = &var;
puntero2 = &var;
*puntero1 = 50;           /* mismo efecto que var=50  */
var = *puntero2 + 13;     /* var=50+13 */

7.3 Otros usos

Declaración múltiple de punteros


Si en una misma declaración de variables aparecen varios punteros, hay que escribir el asterisco a la izquierda de cada uno de ellos:

         int *puntero1, var, *puntero2;

Se declaran dos punteros a enteros ( puntero1 y puntero2) y un entero ( var).

El puntero nulo


El puntero nulo es un valor especial de puntero que no apunta a ninguna parte. Su valor es cero.
En <stdio.h> se define la constante NULL para representar el puntero nulo.

7.4 Parámetros por referencia a funciones


En C todos los parámetros se pasan por valor. Esto tiene en principio dos inconvenientes:
  • No se pueden modificar variables pasadas como argumentos
  • Si se pasa como parámetro una estructura, se realiza un duplicado de ella, con lo que se pierde tiempo y memoria

Sin embargo, se puede pasar un puntero como argumento a una función. El puntero no se puede alterar, pero sí el valor al que apunta:

void incrementa_variable (int* var)
{
   (*var)++;
}
 
main()
{
   int x = 1;
   incrementa_variable (&x);       /* x pasa a valer 2 */
}

En el ejemplo anterior, habia que poner paréntesis en (*var)++ porque el operador ++ tiene más precedencia que la desreferencia (el asterisco). Entonces *var++ sería como escribir *(var++), que no sería lo que queremos.

7.5 Precauciones con los punteros

Punteros no inicializados

Si se altera el valor al que apunta un puntero no inicializado, se estará modificando cualquier posición de la memoria.

main()
{
         int* puntero;
         *puntero = 1200; /* Se machaca una zona
                                               cualquiera de la memoria */
}


Confusión de tipos

Un puntero a un tipo determinado puede apuntar a una variable de cualquier otro tipo. Aunque el compilador lo puede advertir, no es un error.
(Afortunadamente, esto no ocurre en C++).

main()
{
         int p;
         double numero;
         int* puntero;
         
         p = &numero;     /* incorrecto, 
   pero el compilador no aborta */
         *p = 33;         /* Un desastre */
}

Punteros a variables locales fuera de ámbito

Si un puntero apunta a una variable local, cuando la variable desaparezca el puntero apuntará a una zona de memoria que se estará usando para otros fines. Si se desreferencia el puntero, los resultados son imprevisibles y a menudo catastróficos.

main()
{
         int* puntero;
 
         while (...)
         {
           int local;
           puntero = &local;       
  /* ‘puntero’ apunta a una variable local */
           ...
           *puntero = 33;          /* correcto */
         }
         ...
 
/* ahora ‘puntero’ apunta a una zona de memoria
   inválida */
         puntero = 50; /* catástrofe */
}

7.6 Aritmética de punteros

Un puntero apunta a una dirección de memoria.
El lenguaje C permite sumar o restar cantidades enteras al puntero, para que apunte a una dirección diferente: aritmética de punteros .
Consideremos un puntero a enteros:

         int* ptr;
ptr apuntará a cierta dirección de memoria:








Pero también tendrán sentido las expresiones ptr+1, ptr+2, etc. La expresión ptr+k es un puntero que apunta a la dirección de ptr sumándole k veces el espacio ocupado por un elemento del tipo al que apunta (en este caso un int):




















Ejemplo de aritmética de punteros


int vector [100]; /* un vector de enteros */
int *ptr;                          /* un puntero a enteros */
...
ptr = &vector[0]; /* ptr apunta al principio del vector */
*ptr = 33;                /* igual que vector[0] = 33 */
*(ptr+1) = 44;            /* igual que vector[1] = 44 */
*(ptr+2) = 90;            /* igual que vector[2] = 90 */


La expresión que se suma al puntero ha de ser entera y no tiene por qué ser constante. Obsérvese que ptr es lo mismo que ptr+0.

La expresión sumada NO es el número de bytes que se suman a la dirección, es el número de elementos del tipo al que apunta el puntero:

/* Supongamos que un "char" ocupa 1 byte */
/* y que un "double" ocupa 8 bytes */
 
char* ptrchar;
double* ptrdouble;
 
...
 
*(ptrchar+3) = 33; /* la dirección es ptrchar + 3 bytes */
 
*(ptrdouble+3) = 33.0;    
                                   /* la dirección es ptrdouble + 24 bytes,
   ya que cada double ocupa 8 bytes */

El compilador "sabe" cómo calcular la dirección según el tipo.

Aritmética de punteros (más)


A un puntero se le puede aplicar cualquier clase de operación de suma o resta (incluyendo los incrementos y decrementos).

/* Rellenar de unos los elementos del 10 al 20 */
 
int* ptr;                          /* el puntero */
int vector [100]; /* el vector */
int i;                    /* variable contadora */
 
ptr = &vector[0]; /* ptr apunta al origen del vector */
ptr+=10;                  /* ptr apunta a vector[10] */
 
for ( i=0; i<=10; i++ )
{
  *ptr = 1;      /*       asigna 1 a la posición de memoria apuntada                            por "ptr" */
  ptr++;         /*       ptr pasa al siguiente elemento */
}

7.7 Punteros y vectores


Si ptr es un puntero, la expresión

ptr[k]

es equivalente a

*(ptr+k)

con lo que se puede trabajar con un puntero como si se tratara de un vector:

int* ptr;
int vector [100];
 
ptr = &vector[10];
 
for ( i=0; i<=10; i++ )
  ptr[i] = 1;    /* equivalente a *(ptr+i) = 1 */


Un vector es en realidad un puntero constante. El nombre de un vector se puede utilizar como un puntero, al que se le puede aplicar la aritmética de punteros (salvo que no se puede alterar).

Por ello la expresión

ptr = vector;

es equivalente a

ptr = &vector[0];

7.8 Paso de vectores como parámetros a funciones


La aritmética de punteros permite trabajar con vectores pasados como parámetros a funciones en forma de punteros:


/*       Rellena de ceros los "n_elem" 
         primeros elementos de "vector"
*/
 
void rellena_de_ceros ( int n_elem, int* vector )
{
  int i;
  for ( i=0; i<n_elem; i++ )
         *(vector++) = 0;    /* operador de post-incremento */
}
 
main()
{
  int ejemplo [300];
  int otro_vector [100];
 
  /* pasa la dirección del vector "ejemplo" */
  rellena_de_ceros ( 300, ejemplo );
 
  /* rellena los elems. del 150 al 199 */
  rellena_de_ceros ( 50, otro_vector+150 );
}

7.9 Punteros y estructuras


Un puntero puede apuntar a una estructura y acceder a sus campos:

struct Dato
{
  int campo1, campo2;
  char campo3 [30];
};
 
struct Dato x;
struct Dato *ptr;
...
ptr = &x;
(*ptr).campo1 = 33;
strcpy ( (*ptr).campo3, "hola" );

El operador ->


Para hacer menos incómodo el trabajo con punteros a estructuras, el C tiene el operador flecha -> que se utiliza de esta forma:

ptr->campo

que es equivalente a escribir

(*ptr).campo

Así, el ejemplo anterior quedaría de esta forma:

ptr = &x;
ptr->campo1 = 33;
strcpy ( ptr->campo3, "hola" );


7.10 Memoria dinámica: malloc y free


Los variables y vectores en C ocupan un tamaño prefijado, no pueden variarlo durante la ejecución del programa.
Por medio de punteros se puede reservar o liberar memoria dinámicamente, es decir, según se necesite. Para ello existen varias funciones estándares, de la biblioteca <stdlib.h>

La función malloc sirve para solicitar un bloque de memoria del tamaño suministrado como parámetro. Devuelve un puntero a la zona de memoria concedida:

void* malloc ( unsigned numero_de_bytes );

El tamaño se especifica en bytes. Se garantiza que la zona de memoria concedida no está ocupada por ninguna otra variable ni otra zona devuelta por malloc.
Si malloc es incapaz de conceder el bloque (p.ej. no hay memoria suficiente), devuelve un puntero nulo.

Punteros void*

La función malloc devuelve un puntero inespecífico, que no apunta a un tipo de datos determinado. En C, estos punteros sin tipo se declaran como void*
Muchas funciones que devuelven direcciones de memoria utilizan los punteros void*. Un puntero void* puede convertirse a cualquier otra clase de puntero:

         char* ptr = (char*)malloc(1000);

Operador sizeof

El problema de malloc es conocer cuántos bytes se quieren reservar. Si se quiere reservar una zona para diez enteros, habrá que multiplicar diez por el tamaño de un entero.
El tamaño en bytes de un elemento de tipo T se obtiene con la expresión

sizeof ( T)

El tamaño de un char siempre es 1 (uno).

Función free

Cuando una zona de memoria reservada con malloc ya no se necesita, puede ser liberada mediante la función free.

void free (void* ptr);

ptr es un puntero de cualquier tipo que apunta a un área de memoria reservada previamente con malloc.
Si ptr apunta a una zona de memoria indebida, los efectos pueden ser desastrosos, igual que si se libera dos veces la misma zona.


Ejemplo de uso de malloc, free y sizeof


#include <stdlib.h>
 
int* ptr;        /* puntero a enteros */
int* ptr2;       /* otro puntero */
 
...
 
/* reserva hueco para 300 enteros */
ptr = (int*)malloc ( 300*sizeof(int) );
...
  ptr[33] = 15;           /* trabaja con el área de memoria */
 
  rellena_de_ceros (10,ptr);       /* otro ejemplo */
 
  ptr2 = ptr + 15;                 /* asignación a otro puntero */
 
/* finalmente, libera la zona de memoria */
free(ptr);       

Obsérvese que hay que convertir el puntero void* devuelto por malloc a un int*, para que no haya incompatibilidad de tipos.

7.11 Precauciones con la memoria dinámica


Si una zona de memoria reservada con malloc se pierde, no se puede recuperar ni se libera automáticamente (no se hace recolección de basura ). El programador es el responsable de liberar las áreas de memoria cuando sea debido.
Si una zona de memoria liberada con free estaba siendo apuntada por otros punteros, esos otros punteros apuntarán a una zona ahora incorrecta.


7.12 Otras funciones de manejo de memoria dinámica


void* calloc ( unsigned nbytes );

Como malloc, pero rellena de ceros la zona de memoria.


char* strdup ( char* cadena );

Crea un duplicado de la cadena. Se reservan strlen(cadena)+1 bytes.

7.13 Punteros a funciones

Un puntero puede apuntar a código. La declaración es similar a esta:

void (*ptr_fun) (int,int);

En este caso se está declarando una variable, ptr_fun, que apunta a una función “void” que admite dos parámetros enteros.
Para tomar la dirección de una función, se escribe su nombre sin paréntesis ni parámetros.
Para llamar a una función apuntada por un puntero, se usa el nombre del puntero como si fuera una función.

int suma (int a, int b)
{
  return a+b;
}
 
int resta (int a, int b)
{
  return a-b;
}
 
// declaramos un puntero a funciones con dos parámetros
// enteros que devuelven un entero
int (*funcion) (int,int);
 
 
{
  ...
  funcion = suma;    // ‘funcion’ apunta a ‘suma’
  x = funcion(4,3);  // x=suma(4,3)
  funcion = resta;   // ‘funcion’ apunta a ‘resta’
  x = funcion(4,3);  // x=resta(4,3)
}

Tipos de datos

En este apartado se verán estructuras de datos más complejas que las descritas hasta ahora. Se hablará de:
  • Cadenas de caracteres ( strings)
  • Tipos estructurados
  • Tipos enumerados
  • Uniones

También se presentarán algunas conceptos sobre el uso de identificadores, como el ámbito y la existencia.

6.1 Cadenas de caracteres


En C no existe un tipo predefinido para manipular cadenas de caracteres ( strings). Sin embargo, el estándar de C define algunas funciones de biblioteca para tratamiento de cadenas.

La forma de declarar una cadena en C es mediante un vector de caracteres:

         char hola [5];


Toda cadena ha de terminar con el carácter especial 0 (cero).
El C no tiene otra manera de detectar el final de una cadena.


6.2 Literales e inicialización de cadenas


Los literales tipo cadena son de la forma

          "texto entre comillas"

Al declarar una vector de caracteres, se le puede inicializar con un literal:

         char texto [4] = "abc";


Pero NO se puede hacer una asignación de ese tipo en una sentencia:

         texto = "xyz";   /* ERROR */

En su lugar, hay que emplear ciertas funciones de biblioteca.


Tres formas equivalentes de inicializar una cadena:

char hola [5] = { 'h', 'o', 'l', 'a', 0 };

char hola [5] = "hola";

main()
{
         char hola [5];
         hola[0] = ‘h’;
         hola[1] = ‘o’;
         hola[2] = ‘l’;
         hola[3] = ‘a’;
         hola[4] = 0;
}


Obsérvese que una cadena de N elementos es capaz de almacenar un texto de N-1 caracteres (el último siempre ha de ser un cero).

No importa que un vector de caracteres contenga una cadena de menos letras, el carácter cero marca el final de la cadena.

Lo que sí es un error peligroso (y además no lo detecta siempre el compilador) es intentar asignarle a un vector de caracteres una cadena de mayor tamaño.
Hay que cuidar mucho que las cadenas queden dentro del espacio reservado para ellas.

6.3 Visualización de cadenas


La función printf admite formato de cadenas, con %s

         char cadena[80];
         ...
         printf ( "El texto es %s\n", cadena );

Para usar printf, debería incluirse la cabecera <stdio.h>

El formato %s admite modificadores. Por ejemplo:
%20s Texto a la derecha, ocupando siempre 20 caracteres
%-15s Texto alineado a la izquierda, ocupando 15 caracteres

Para imprimir sólo la cadena, seguida de un salto de línea, se puede emplear puts (también pertenece a <stdio.h>):

         puts (cadena);



6.4 Biblioteca de manejo de cadenas (string.h)


La biblioteca <string.h> contiene un conjunto de funciones para manipular cadenas: copiar, cambiar caracteres, comparar cadenas, etc.
Las funciones más elementales son:

strcpy ( c1, c2 ); Copia c2 en c1
strcat ( c1, c2 ); Añade c2 al final de c1
int strlen ( cadena ); Devuelve la longitud de la cadena
int strcmp ( c1, c2 ); Devuelve cero si c1 es igual a c2;
no-cero en caso contrario

Para trabajar con estas funciones, al comienzo del programa hay que escribir

         #include <string.h>

Ejemplo:


#include <stdio.h>
#include <string.h>
 
char completo [80];
 
char nombre[32] = "Pedro";
char apellidos [32] = "Medario Arenas";
 
main()
{
  /* Construye el nombre completo */
 
  strcpy ( completo, nombre );     /* completo <- "Pedro" */
  strcat ( completo, " ");                  /* completo <- "Pedro " */
  strcat ( completo, apellidos );  /* completo <- "Pedro 
                                                                Medario Arenas" */
 
  printf ( "El nombre completo es %s\n", completo );
 
}

6.5 Lectura de cadenas


En teoría, podría utilizarse la opción %s de scanf, pero
CASI NUNCA FUNCIONA.

Una mejor alternativa es emplear gets, que también viene en stdio.h

#include <stdio.h>
 
main ()
{
         char nombre [80];
         printf ( "¿Cuál es su nombre? " );
         gets ( nombre );
         printf ( "Parece que su nombre es %s\n", nombre );
}

NOTA: gets no comprueba el tamaño de la cadena. Si el texto tecleado tuviera más de 80 caracteres, se destruirían posiciones de memoria incorrectas.
Para leer caracteres hasta un máximo, hay que usar fgets:

         fgets ( nombre, 80, stdin );

(el identificador stdin se refiere a la entrada estándar , normalmente el teclado. Está definido en <stdio.h>)



6.6 Tipos estructurados


Se pueden definir tipos compuestos de varios elementos o campos de tipos más simples.

La sintaxis es:

struct nombre_del_tipo
{
         campo1;
         campo2;
         ...
         campoN;
};


Las variables de ese tipo se declaran así:

struct nombre_de_tipo variable;

y para acceder a un campo de una variable estructurada,
se utiliza esta sintaxis:

variable.campo


6.7 Ejemplo de tipo estructurado

struct Persona
{
         char nombre [40];
         char apellidos [80];
         long telefono;
         long dni;
         char sexo;       
};
 
 
struct Persona usuario;
 
main ()
{
         usuario.dni = 43742809;
         strcpy ( usuario.apellidos, "Santana Melián" );
         strcpy ( usuario.nombre, "Jacinto Lerante" );
         usuario.telefono = 908330033;
         usuario.sexo = 'V';
}

Si hay campos del mismo tipo, se pueden declarar en la misma línea, separándolos por comas.
Se pueden declarar variables de tipo estructurado a la vez que se declara el tipo.

struct T
{
 
   int campo1, campo2, campo3; /* varios campos */
 
} v1, v2;        /* declaración de variables de tipo struct T */
 
...
 
v1.campo1 = 33;
v2.campo3 = 45;


También se permite omitir el tipo de la estructura ( estructura anónima ):

struct {
  char nombre [8];
  char extension [3];
} nombre_fichero;

En este caso, no se puede crear nuevas variables de ese tipo. No se recomienda usar este tipo de declaraciones.

Inicialización de estructuras


Una variable de tipo struct se puede inicializar en su declaración, escribiendo entre llaves los valores de sus campos en el mismo orden en que se declararon estos.

struct Persona
{
         char nombre [32];
         char apellidos [64];
         unsigned edad;
};
 
struct Persona variable = 
{ "Javier",  "Tocerrado", 32 };



6.8 Definición de tipos: typedef


Se puede dar un nombre nuevo a cualquier tipo de datos mediante typedef.
La sintaxis es

                 typedef declaración;

donde declaración tiene la forma de una declaración de variable,
sólo que se está definiendo un tipo de datos.

typedef long pareja [2];

define un tipo pareja que se puede usar en declaraciones de variables:

pareja p;

es equivalente a

long p [2];

Ejemplos de typedef con estructuras

 
 
typedef struct Persona PERSONA;
PERSONA dato;    /* igual que struct Persona dato; */


Un uso típico es la redefinición de tipos estructurados:

typedef struct   /* estructura anónima */
{ 
  char nombre[80]; 
  char sexo;
  int edad;
} Persona;                /* se declara el tipo Persona */
 
...
 
Persona p;
 
...
 
p.edad = 44;

6.9 Tipos enumerados: enum


Con la construcción enum se pueden definir tipos de datos enteros que tengan un rango limitado de valores, y darle un nombre a cada uno de los posibles valores.

enum dia_de_la_semana
{
         lunes, martes, miercoles, jueves, viernes,  sabado, domingo
};

...

enum dia_de_la_semana hoy;

...

hoy = sabado;


Los valores definidos en enum son constantes enteras que se pueden usar en cualquier punto del programa, usando un operador de moldeo (ver ejemplo).
Se empiezan a numerar de cero en adelante (en el ejemplo, lunes vale cero, martes vale uno, etc.)

int dia = (int)sabado;    /* dia = 5 */

6.10 Valores de la lista en enum


Se puede dar un valor inicial a la lista de valores dados en enum:

enum dia
{
         lunes=1, martes, miercoles, jueves, viernes,        sabado, domingo
};

En este caso los valores van del 1 al 7.

También se pueden dar valores individuales:

enum codigo_postal
{
         LasPalmas=35, Madrid=28, Barcelona=8
};

6.11 Uniones


El tipo estructurado union es similar al struct, salvo que en las mismas posiciones de memoria almacena todos sus campos.


struct rectangulo
{

   int ancho;
   int largo;
};
 
 
union todo_en_uno


{
   char cadena [8];
   int entero;
   double real;
};


En el caso del struct, cada campo ocupa unas posiciones de memoria diferentes. En la union los tres campos comparten las mismas posiciones de memoria.
Eso significa que si se altera un campo de una union se afecta a todos los demás campos.
La union sólo tiene sentido si se sabe que sólo se va a emplear un campo en cada ocasión.


6.12 Combinaciones de tipos


Los tipos estructurados y los vectores pueden combinarse entre sí: dentro de un struct se pueden declarar campos que sean struct, vectores, enum, etc.; se puede declarar un vector de struct, etc.

struct Producto
{
   unsigned identificador;
   char nombre [40];
   long disponible;
};
 
 
struct Producto catalogo [1000];
...
 
catalogo[133].disponible = 2467;
 
 
/* Ejemplo de inicialización */

struct Producto dos_productos [2] =
{
   { 12, "Brutopía", 28000 },
   { 33, "Papas el Canario", 35000 }
};


6.13 Ámbitos y existencia de variables y tipos


Las variables y los tipos de datos se pueden declarar en dos puntos:

  • Al principio de un bloque ( después del "abre llave" { )
  • Fuera de bloques

Las variables declaradas fuera de bloques se llaman globales.
Las variables en el interior de un bloque son locales a ese bloque.

Ámbito


Una variable global se puede utilizar en todo el programa, desde el punto en que se declara.

Una variable local se puede utilizar desde el punto en que se declara hasta el final del bloque donde se declaró.

Si se declara una variable local con el mismo nombre de otra variable accesible en el bloque actual, la nueva variable oculta a la otra variable.


Existencia


Una variable global existe permanentemente mientras el programa se ejecuta.

Una variable local sólo existe mientras se está ejecutando código del bloque donde se ha declarado.
Así, una variable local a una función se crea cuando se llama a la función y desaparece cuando la función retorna.

Ejemplo de ámbitos


int global;
 
double area ( double base, double altura )
{
         double temp = base*altura;
         return temp;
}
 
main()
{
         int local;
         ...
         while (1)
         {
            int mas_local;
            ...
            {
                 int mas_local_todavia;
                 int local;
                 ...
            }
         }
}

Ámbitos y existencia de tipos de datos


Los ámbitos y existencia de los tipos son similares a los de las variables.
Los tipos declarados fuera de bloques son globales al programa.
Los tipos declarados dentro de bloques son locales a ese bloque.

struct pepe { int a,b,c; }; /* ámbito global */

int funcion (void)
{
struct local { int x1,x2; }; /* ámbito local */
struct pepe una;
struct local otra;
}
 
struct pepe variable;              /* correcto */
struct local otra;        /* incorrecto: no estamos
 en el ámbito de “local” */

6.14 Variables static


Se pueden definir variables que tengan ámbito local pero existencia permanente.
Para declararlas se utiliza la palabra static.

static declaración;

Por ejemplo, si se declara una variable static dentro de una función, aunque la función retorne la variable permanece con el último valor que se asignó:

int contador (void)
{
         static int cuenta = 0;
         return cuenta++;
}

Esta función devuelve un número que se va incrementando cada vez que se la llama. La variable cuenta es local a la función contador, pero no desaparece con la salida de la función.
NOTA: la inicialización de una variable static se realiza una sola vez, al comienzo de la ejecución del programa. Por eso el ejemplo anterior funciona ( cuenta se inicializa a cero una sola vez).

6.15 Declaraciones de funciones


Las funciones son siempre globales, esto es, no se permite declarar una función dentro de otra.
Las funciones son visibles sólo después de que se han declarado.

Se pueden declarar funciones, especificando sólo su formato, pero no su cuerpo:

         int suma ( int a, int b );

lo anterior es una declaración de la función suma, que queda disponible para su uso, a pesar de no haber sido definido su cuerpo.

La declaración de una función de esta forma se llama prototipo.

Es buena práctica declarar al comienzo del programa los prototipos de las funciones que vamos a definir, incluyendo comentarios sobre su finalidad.