2.4 Archivos de encabezado (Opcional)

(*) Los contenidos desarrollados en este artículo no son fundamentales para comprender el contenido posterior del curso, y pueden saltearse o leerse más adelante.
 

Un tipo de datos que veremos más adelante, pero que utilizaremos para este ejemplo es el tipo struct. Una variable de tipo struct puede contener varios valores simultáneamente, en diferentes campos. Así por ejemplo, podemos definir el tipo de datos fecha que contiente tres datos: año, mes y día; todos de tipo int:
 

struct fecha
{
       int dia;
       int mes;
       int anio;
};


Una variable puede definirse del tipo fecha, y permite acceder a los tres valores indicados. Las variables de tipo struct, las veremos luego en la sección 3.7
Utilizando esta estructura, podemos crear la función pascua, que recibe como parámetro una fecha y devuelve la fecha de pascua correspondiente al año de la fecha ingresada.



fecha pascua(fecha);


Para realizar el cálculo de la fecha, seguiremos el algoritmo de Butcher:




fecha pascua(fecha dp)
{
       int a,b,c,d,e,f,g,h,i,k,l,m,n;
       fecha p;
       if (dp.anio>1583)
       {
             a = dp.anio%19;
             b = dp.anio/100;
             c = dp.anio%100;
             d = b/4;
             e = b%4;
             f = (b+8)/25;
             g = (b-f+1) / 3;
             h = (19*a + b - d - g + 15 )% 30;
             i = c/4;
             k = c%4;
             l = (32 + 2*e + 2 * i - h - k) %7;
             m = (a + 11*h + 22*l) /451;
             n = h + l - 7*m + 114;
             p.mes = n/31;
             p.dia = 1 + n%31;
             p.anio = dp.anio;
       }
       else
       {
             p.dia = 1;
             p.mes = 1;
             p.anio = dp.anio;
       }
       return p;
}



Una vez definida esta función, podemos utilizarla en cualquier programa que la necesite. Un programa muy sencillo podría ser el siguiente:



void main()
{
       fecha dia, diapascua;
       cout<<"ingrese dia mes y año:";
       cin >>dia.dia;
       cin >>dia.mes;
       cin >>dia.anio;

       diapascua = pascua(dia);

       cout <<"pascua es el día: ";
       cout << diapascua.dia <<" del mes "<<diapascua.mes<<endl;

}


Este programa pide al usuario que ingrese una fecha, y le indica en qué fecha se festeja la Pascua en ese año. El programa utiliza la función, pero no tiene porqué saber cómo realiza el cálculo de dicha fecha, le basta conocer qué parámetros recibe y qué valores devuelve la función. Si quien elabora la función quiere que esta sirva luego para otros programamadores, deberá crearla en archivos separados, que pueden luego ser llamados por los programas.

El primer archivo es el archivo de encabezados, que debe incluir el prototipo de la función a compartir. (En este caso, también la definición de la estructura.) Este archivo es el que deberá ser incluido en todos los programas que quieran utilizar la función. Para identificarlo, se le agrega la extensión .h (por header, encabezado en inglés)

Archivo fechas.h:



struct fecha
{
       int dia;
       int mes;
       int anio;
};

fecha pascua(fecha);



Al archivo con la definición de la función, se rcomienda asignarle el mismo nombre que al encabezado, pero con la extensión habitual .cpp.

Archivo fechas.cpp:




#include "stdafx.h"
#include"fechas.h"

fecha pascua(fecha dp)
{
       int a,b,c,d,e,f,g,h,i,k,l,m,n;
       fecha p;
       if (dp.anio>1583)
       {
             a = dp.anio%19;
             b = dp.anio/100;
             c = dp.anio%100;
             d = b/4;
             e = b%4;
             f = (b+8)/25;
             g = (b-f+1) / 3;
             h = (19*a + b - d - g + 15 )% 30;
             i = c/4;
             k = c%4;
             l = (32 + 2*e + 2 * i - h - k) %7;
             m = (a + 11*h + 22*l) /451;
             n = h + l - 7*m + 114;
             p.mes = n/31;
             p.dia = 1 + n%31;
             p.anio = dp.anio;
       }
       else
       {
             p.dia = 1;
             p.mes = 1;
             p.anio = dp.anio;
       }
       return p;
}



Como dijimos, este archivo debe incluir la línea

   #include"fechas.h"

En esta línea se le indica al compilador que incluya el archivo anterior, imprescindible para entender la función y la estructura utilizadas. Cuando se trata de un archivo creado por el usuario, el nombre del archivo va entre comillas ("  ") y el archivo debe estar en la misma carpeta que el archivo del programa principal. Si en lugar de comillas, se lo marca con los signos menor y mayor (<    >), el compilador entiende que se trata de archivos estándar de las bibliotecas de C++ y los busca en las carpetas correspondientes.

Archivo prueba.cpp: 
 

#include "stdafx.h"
#include <iostream>
using namespace std;
#include "fechas.h"
void main()
{
       fecha dia, diapascua;
       cout<<"ingrese dia mes y anio:";
       cin >>dia.dia;
       cin >>dia.mes;
       cin >>dia.anio;
       diapascua = pascua(dia);
       cout <<"pascua es el día: " <<diapascua.dia <<" del mes "<<diapascua.mes<<endl;
}



 Al escribir el programa en el archivo prueba.cpp, se debe agregar también el #include correspondiente al archivo de encabezado, para que el compilador pueda interpretar correctamente la función y la estructura utilizadas.

También se debe incluir el archivo fechas.cpp en el proyecto, o de lo contrario no se podrá ejecutar:


En el proyecto se deben agregar los archivos de encabezados (fechas.h) y de código fuente con la definición de funciones (fechas.cpp). 

 Esta posibilidad de crear las funciones en archivos separados es fundamental para permitir la reutilización de software sin tener que estar copiando y pegando el código en todos los programas. Al estar en un único archivo, cualquier modificación posterior a fechas.cpp, se deberá hacer en este único archivo.
Incluso se puede compilar el archivo de definición de funciones y compartirlo ya compilado. De esta manera cualquier programador que quiera utilizar la función, podrá hacerlo pero no podrá ver ni modificar su código fuente.

2.5 Parámetros por valor y por referencia

Dijimos antes que cuando se pasa una variable como parámetro a una función, se le pasa una copia del valor de esta variable y su valor en el programa principal no se modificará.
Si uno quisiera que la función sí modifique el valor de la variable en el programa principal, lo que se debe hacer es pasar la variable como parámetro por referencia en vez de pasarla como parámetro por valor, que es la habitual.
Para pasar parámetros por referencia, se debe colocar el signo & luego del tipo de dato en el prototipo y en el encabezado de la función.
Veamos el siguiente ejemplo:
 

#include "stdafx.h"
#include <iostream>
using namespace std;
int doble1 (int);
int doble2 (int&);

void main()
{
       int n, d1, d2;
       n = 7;
       cout<<"n vale "<<n<<endl;
       d1 = doble1(n);
       cout<<"Despues de doble1, n vale "<<n<<endl;
       d2 = doble2(n);
       cout<<"Después de doble2, n vale "<<n<<endl;
       cout<<"d1 vale "<<d1<<endl;
       cout<<"d2 vale "<<d2<<endl;
}
 
int doble1 (int n)
{
       int f;
       n = n*2;
       cout<<"En doble1, n vale "<<n<<endl;
       return n;
}
      
int doble2 (int& n)
{
       int f;
       n = n*2;
       cout<<"En doble2, n vale "<<n<<endl;
       return n;
}
      

 La variable n recibe un valor inicial de 7. Cuando se llama a doble1, la copia de n dentro de la función vale 14, pero la variable n del principal continúa valiendo 7.
Cuando se llama a la función doble2, que recibe el parámetro n por referencia, por lo tanto su valor en el programa principal será modificado.



2.6 Funciones recursivas

En una función se puede ejecutar cualquier instrucción, e incluso llamar a cualquier otra función. También es posible, con ciertas restricciones, que una función se llame a sí misma. Una función que se invoca a sí misma se llama función recursiva.
Veamos una vez más el ejemplo de la función factorial que vimos en el capítulo anterior.  




#include "stdafx.h"
#include <iostream>
using namespace std;
int factorial (int);
void main()
{
       int n, fact, i;
       cout<<"Ingrese un número:";
       cin>>n;
       fact = factorial(n);
       cout<<"El factorial de "<<n<<" es "<<fact<<endl;
}
 
int factorial (int n)
{
       int f;
       if (n==1) return 1;
       else
       {
             f = factorial(n-1);
             return n*f;
       }
}


El programa principal es el mismo, pero la función es distinta. Sólo tiene un if que pregunta si n es igual a 1. Si es así, devuelve 1 (el factorial de 1 es 1). Si n es mayor que 1, podemos calcular su factorial como n multiplicado por el factorial de n-1. Eso es lo que hacen las instrucciones en la rama else del if.
Cuando llamamos a la función con, por ejemplo, n = 5, la función se llama a sí misma pasando por parámetro n-1, o sea, 4. La instancia de la función en la que n era igual a 5, se queda esperando el resultado de esta nueva ejecución. Ésta a su vez se llama a sí misma otra vez con n = 3, y se queda esperando el resultado. La función se llama una cuarta vez con n = 2, y una quinta vez con n=1.
En este momento, tenemos al programa esperando el resultado de la función, y tenemos cuatro ejecuciones de la función detenidas, esperando el resultado de la instancia siguiente.  Cuando se ejecuta la quinta instancia con n = 1, la pregunta del if da verdadero y la función devuelve el valor de 1 a la función que la llamó. Allí, donde n = 2, multiplica el resultado que devolvió la función (1) por el valor de n (2) y devuelve este valor a quien la llamó. Así se va retrocediendo en la cadena de funciones abiertas, hasta devolver el factorial de 5 al programa principal.

Es importante tener una condición que evite que la función continúe llamándose a sí misma hasta el infinito. La función puede llamarse a sí misma, pero no siempre: Tiene que haber alguna instancia que termine la función sin llamarse a sí misma. Esta condición es llamada condición de corte. En este ejemplo, la condición de corte es n=1: Cuando n es igual a 1, la función no se llama una vez más.

Al igual que en el caso del factorial, cualquier función que se pueda escribir en forma recursiva, puede escribirse también en forma no recursiva, con un ciclo. No hay una regla clara que diga cuándo una función debe ser recursiva y cuándo no. Si bien una función recursiva ocupa más memoria y es más lenta que una iteración, en muchos casos puede ser más simple de programar e interpretar.


Como dijimos al principio, el programa principal que llama a la función es el mismo que el del capítulo anterior: Al llamar a una función, el programa no sabe si la función es recursiva o no: sólo sabe que le devuelve un entero, y que ese entero es el factorial del parámetro enviado. La forma de resolverlo, es interna de la función y está oculta al programa que la invoca.