======= Funciones =======
===== Motivación =====
Veremos en esta lección el concepto de función. Empezaremos tratando de entender cuál es el **problema** o **dificultad** que las funciones nos pueden ayudar a aliviar, es decir, empezaremos dándonos una mínima idea de la respuesta a la pregunta de: "Funciones, ¿para qué?".
Si bien es posible resolver cualquier problema sin usar funciones, hay 3 excelentes motivos para utilizar funciones:
- Para ordenar mejor el código fuente, y hacerlo más fácil de **leer y entender**.
- Para facilitar un enfoque de programación **top-down**, en el cuál vamos **descomponiendo** el problema en partes, y luego nos concentramos en cómo resolver cada parte.
- Para evitar tener **código repetido**, que es algo que debemos evitar a toda costa si queremos programar mejor y con menos chances de errores.
==== Ordenar el código fuente ====
Supongamos que tenemos el siguiente código (que iría dentro de ''main'' en un programa):
int a,b;
cout << "Ingrese el rango de numeros a explorar" << endl;
cin >> a >> b;
int sumaPrimosAlCuadrado = 0;
for (int i=a; i<= b; i++)
if (i >= 2)
{
bool esPrimo = true;
for (int d = 2; d*d <= i; d++)
if (i % d == 0)
esPrimo = false;
if (esPrimo)
sumaPrimosAlCuadrado += i*i;
}
cout << "La primera suma pedida es:" << sumaPrimosAlCuadrado << endl;
int sumaMultiplosEspeciales = 0;
for (int i=a; i<= b; i++)
{
if ((i % 3 == 0 || i % 10 == 0) && i % 30 != 0)
sumaMultiplosEspeciales += i;
}
cout << "La segunda suma pedida es:" << sumaMultiplosEspeciales << endl;
Es muy difícil de entender qué hace este código a simple vista, y el motivo principal es que **hace muchas cosas mezcladas todas a la vez**. Veremos que una función permite **separar** una porción de programa definida, y luego **usarla** cuando la necesitemos. Es bueno comparar el tener que leer y entender el programa anterior, con leer algo como lo siguiente:
int a,b;
leerRangoAExplorar(a,b);
mostrarResultados(sumaDePrimosAlCuadradoEnRango(a,b), sumaDeMultiplosEspecialesEnRango(a,b));
Aquí, ''leerRangoAExplorar(a,b);'' corresponderá al siguiente fragmento del código anterior:
cout << "Ingrese el rango de numeros a explorar" << endl;
cin >> a >> b;
Mientras que ''sumaDePrimosAlCuadradoEnRango(a,b)'' representa:
int sumaPrimosAlCuadrado = 0;
for (int i=a; i<= b; i++)
if (i >= 2)
{
bool esPrimo = true;
for (int d = 2; d*d <= i; d++)
if (i % d == 0)
esPrimo = false;
if (esPrimo)
sumaPrimosAlCuadrado += i*i;
}
''sumaDeMultiplosEspecialesEnRango(a,b)'' corresponde al:
int sumaMultiplosEspeciales = 0;
for (int i=a; i<= b; i++)
{
if ((i % 3 == 0 || i % 10 == 0) && i % 30 != 0)
sumaMultiplosEspeciales += i;
}
Y el ''mostrarResultados'' corresponde al:
cout << "La primera suma pedida es:" << sumaPrimosAlCuadrado << endl;
cout << "La segunda suma pedida es:" << sumaMultiplosEspeciales << endl;
Separando estas operaciones que son **independientes** entre sí, y dándoles un nombre claro, el código es mucho más fácil de entender, ya que podemos analizar cada parte independientemente por un lado, y por otro lado, su combinación para formar el programa completo. ''leerRangoAExplorar'' , ''sumaDePrimosAlCuadradoEnRango'', ''sumaDeMultiplosEspecialesEnRango'' y ''mostrarResultados'' son ejemplos de //funciones//.
==== Facilitar un enfoque top-down ====
Esta ventaja está estrechamente relacionada con la anterior. Supongamos que nos dieran la siguiente consigna:
"Crear un programa que lea dos números ''a'' y ''b'', que indican un rango de números (inclusive), y calcule y muestre en la pantalla dos valores: La suma de los cuadrados de todos los números primos entre a y b, y además, la suma de todos los números entre a y b que son múltiplos de 3 y de 10, pero no de 30."
El programa anterior (el que tenía todo junto) sería un ejemplo de resolución de esta tarea. Ahora bien, escribir y pensar todo ese código junto en un solo paso, a partir de esta descripción, es muy complicado. Pero podríamos planear **descomponer** este problema en tareas más chicas, y escribir nuestra solución **suponiendo que tenemos resueltas esas tareas**.
Por ejemplo, en este caso podríamos identificar que tenemos que hacer 4 cosas:
- Leer a y b de la entrada
- Calcular la suma de los cuadrados de los primos pedidos
- Calcular la suma de los números que son "múltiplos especiales" (explicado en la consigna)
- Escribir los resultados a la salida
Identificamos entonces que para 1), necesitaremos dos variables ''a'',''b'' en las cuales guardaremos los números leídos. Podemos denominar ''leerRangoAExplorar(a,b);'' al proceso de leer esas variables: **Sin preocuparnos por ahora sobre cómo lo haremos**. Eso lo dejamos para después.
Para realizar los cálculos de 2) y 3), **necesitaremos los valores a y b**, que obtuvimos en 1). Llamaremos (de vuelta, sin preocuparnos todavía por **cómo** lograremos hacer los cálculos) ''sumaDePrimosAlCuadradoEnRango(a,b)'' al resultado de hacer los cálculos que indica el paso 2), y llamaremos ''sumaDeMultiplosEspecialesEnRango(a,b)'' al resultado del paso 3).
Finalmente, llamaremos ''mostrarResultados'' al proceso del paso 4), que **usa los resultados obtenidos en los pasos 2 y 3**. Con estas ideas, podemos planear un ''esqueleto'' de nuestro programa, que quedaría más o menos así:
int a,b;
leerRangoAExplorar(a,b);
mostrarResultados(sumaDePrimosAlCuadradoEnRango(a,b), sumaDeMultiplosEspecialesEnRango(a,b));
Ahora que tenemos el esqueleto del programa listo, podemos concentrarnos en detalle en ver **cómo** resolvemos cada una de esas 4 partes. Usar funciones nos permitirá **escribir este esqueleto directamente en el main**, y luego aclarar en funciones separadas cómo realizar cada una de las 4 partes.
==== Evitar código repetido ====
Llamamos **código repetido** a un conjunto de **operaciones** completamente análogas, que aparece repetido en el programa más de una vez. Esto es algo que queremos evitar a toda costa porque es muy propenso a errores.
Una ejemplo podría ser, si tenemos un programa que tiene que analizar si un número dado es primo, luego realizar un montón de cálculos y operaciones, y al final del programa antes de terminar debe analizar si **otro** número es primo. Si bien se analizan dos números distintos, la serie de **operaciones** que hacemos es la misma en los dos casos, y **solamente cambia el número** (o la variable donde está guardado). Esto nos llevaría a tener dos fragmentos del programa (por ejemplo, dos ''for'' con cálculos) esencialmente iguales, uno para cada número. Esto lleva fácilmente a errores, porque si en algún momento queremos cambiar algo de este código (o corregir un error encontrado) tenemos que cambiarlo **en los dos lugares**, y es muy fácil **olvidarse** de cambiar uno, o equivocarse **en uno** de los cambios.
Mediante funciones, podremos escribir el código para determinar si un número es primo **una sola vez**, y luego **reutilizar ese código** todas las veces que queramos, sin necesidad de escribir todo el código de nuevo.
===== La idea de función =====
Una función en C++((Si bien tiene ciertas similitudes con una función matemática, la palabra función en C++ y en la matemática significan cosas bien diferentes)) será un **fragmento de programa bien definido, que puede utilizarse como parte de otros programas o funciones**.
La sintaxis para definir una función es la siguiente:
tipo_de_la_respuesta nombreDeLaFuncion(lista_de_parametros)
{
// Cuerpo de la funcion, con las instrucciones correspondientes a la misma
return resultado; // Con return se termina la función y se indica el resultado final
}
Esto debe escribirse **antes del main**, y no adentro. Eso es porque **main es una función como las demás**, y no se permite en C++ escribir una función dentro de otra. La particularidad que tiene la función ''main'' es que es allí donde comienza a ejecutarse el programa: la computadora comienza a leer instrucciones por el ''main''.
Una función, al ser un fragmento de programa, puede realizar cálculos y tareas, y puede eventualmente producir un **resultado**. Ese resultado se llama el **valor de retorno** de la función, y se dice que la función **lo devuelve** al usar la instrucción **return**. En el código anterior, la parte de ''tipo_de_la_respuesta'' se usa para indicar el tipo que tendrá el **resultado** de la función.
Veamos un ejemplo de función con ''lista_de_parametros'' vacía, lo cual es válido en C++:
int leerUnNumeroYElevarloAlCuadrado()
{
int x;
cin >> x;
return x*x;
}
Este código corresponde a un fragmento de programa, que lee con ''cin'' un número ''x'', y devuelve con ''return'' el valor ''x*x'', es decir el número al cuadrado: Si se lee 3, se devuelve 9, si se lee 5 se devuelve 25, si se lee -2 se devuelve 4, etc. Cuando se ejecuta una instrucción ''return'', se devuelve el valor indicado y la función **termina inmediatamente**, sin importar que pudiera haber más instrucciones además del ''return''.
Como lo que devuelve es un entero, hemos colocado ''int'' justo al comienzo, antes del nombre de la función. Los paréntesis luego del nombre de la función **son obligatorios** siempre que escribamos una función, incluso cuando dentro de ellos no pongamos nada, como en el ejemplo.
Si en nuestro programa colocamos este código antes del ''main'', podremos utilizar esta función en el programa principal: para ello basta con escribir la instrucción ''leerUnNumeroYElevarloAlCuadrado();'', y eso automáticamente ejecutará todo el código correspondiente a esa función. Nuevamente, al **utilizar** (también denominado **llamar** o **invocar**) una función, los paréntesis son obligatorios.
Veamos a continuación un ejemplo de programa completo que usa esa función:
#include
using namespace std;
int leerUnNumeroYElevarloAlCuadrado()
{
int x;
cin >> x;
return x*x;
}
int main()
{
int a = leerUnNumeroYElevarloAlCuadrado();
int b = leerUnNumeroYElevarloAlCuadrado();
int c = leerUnNumeroYElevarloAlCuadrado();
cout << "El gran total es: " << a+b+c << endl;
}
Este programa lee **tres** números, y al final muestra el gran total: La suma de los cuadrados de los números leídos. Notar que la operación de leer un número con ''cin'' y elevarlo al cuadrado se realiza 3 veces, porque 3 veces escribimos ''leerUnNumeroYElevarloAlCuadrado()'' en el programa principal: Pero una sola vez tuvimos que escribir **las instrucciones completas** para realizar esa tarea, al definir la función antes del main. Luego podemos usarla libremente como si fuera una operación más.
De este programa podemos destacar que cuando tenemos una función que devuelve un resultado, al llamar a la función podemos directamente escribir la llamada y usar el resultado en una expresión, **como si fuera directamente el valor**. Es decir, cuando ponemos ''leerUnNumeroYElevarloAlCuadrado()'', podemos pensar para nuestro razonamiento que eso se va a reemplazar directamente por el resultado final de los cálculos de la función.
Así, si al ejecutar el programa anterior ingresáramos los valores 3, 1, y 10, sería como si en el main se ejecutase lo siguiente:
int main()
{
int a = 9;
int b = 1;
int c = 100;
cout << "El gran total es: " << a+b+c << endl;
}
Obteniendo el resultado 110. A modo de ejemplo, damos una versión distinta del programa que calcula la suma del **triple** de cada número al cuadrado, para que quede claro que las llamadas a funciones se pueden usar en el medio de expresiones más complejas si así nos conviene (damos solo el main, pues la función es igual que antes):
int main()
{
int a = 3 * leerUnNumeroYElevarloAlCuadrado();
int b = 3 * leerUnNumeroYElevarloAlCuadrado();
int c = 3 * leerUnNumeroYElevarloAlCuadrado();
cout << "El gran total es: " << a+b+c << endl;
}
Incluso sería válido (aunque es más difícil de leer) escribir el programa original con todas las llamadas en la misma línea:
int main()
{
cout << "El gran total es: " << leerUnNumeroYElevarloAlCuadrado() +
leerUnNumeroYElevarloAlCuadrado() +
leerUnNumeroYElevarloAlCuadrado() << endl;
}
Este último ejemplo muestra una característica importante de las funciones: **Cada vez** que se escribe una llamada a función en el código, **se ejecutan las instrucciones de la función**: Si se escribe 3 veces, se ejecutan tres veces. En nuestro ejemplo, eso quiere decir que el fragmento anterior no lee un número y luego lo "triplica" al sumarlo consigo mismo 3 veces, sino que lee 3 números distintos, porque lee uno nuevo en cada llamada.
Si queremos reutilizar el valor que se obtuvo al ejecutar una función sin volver a ejecutarla, conviene **guardar el resultado en una variable**, como hicimos en los primeros ejemplos, donde teníamos algo como ''int a = leerUnNumeroYElevarloAlCuadrado();''.
===== Parámetros =====
No siempre queremos que una función haga exactamente lo mismo cada vez que se usa. A veces, queremos que haga **casi** lo mismo, pero cambiando algún **dato** entre usos. Por ejemplo, podríamos querer una función que eleve un número al cuadrado, es decir, que permita calcular ''x*x'' si ya tenemos un entero ''x''. Así, cuando usamos la función con 3, queremos que devuelva ''3*3 == 9'', y cuando la usamos con -4 queremos que devuelva ''(-4)*(-4) == 16''.
En el ejemplo anterior la función no hace siempre lo mismo, porque a veces hace 3*3 y a veces (-4)*(-4), pero más allá del número que vamos a elevar, las **operaciones** que hace la función son siempre las mismas, y solo cambia este **dato** inicial. A ese **dato que cambia**, lo llamamos en programación un **parámetro** de la función. Una función puede tener 1 o más parámetros, o incluso cero: Las funciones que vimos antes tenían cero parámetros. La función de elevar al cuadrado tendría un único parámetro: El número entero que vamos a querer elevar.
Los parámetros que tendrá una función se indican al escribir su código, entre paréntesis, luego del nombre: se debe dar una lista separada por comas, en la cual se indique **el tipo y el nombre** de cada parámetro. Es por eso que en los ejemplos anteriores necesitamos siempre un par de paréntesis ''()'' vacíos: las funciones que estábamos utilizando tenían cero parámetros.
Veamos a continuación el ejemplo de la función para elevar un entero al cuadrado:
int alCuadrado(int x)
{
return x*x;
}
Esta función es muy simple, pues lo único que hace es devolver ''x*x''. Notemos que ''x'' es el único parámetro de esta función: entre paréntesis hemos indicado su tipo, ''int'', y le hemos dado el nombre ''x'', lo cual permite **usar este parámetro en el código de la función**.
Cuando una función tiene parámetros, al usarla hay que aclarar **qué valores tomar** para esos parámetros. En nuestro ejemplo de función que eleva al cuadrado un número, no podemos en el programa simplemente usar ''alCuadrado()'' como en los ejemplos anteriores: ¿Cuál sería el resultado si hiciéramos eso? ¿**Qué número** se estaría elevando al cuadrado? La función no puede saberlo si no se lo indicamos. Por este motivo, cuando se usa una función con parámetros hay que indicar entre paréntesis los **valores** que queremos usar para esos parámetros, en el mismo orden en que se dieron al escribir el código de la función.
Veamos un ejemplo completo de programa que usa la función ''alCuadrado'':
#include
using namespace std;
int alCuadrado(int x)
{
return x*x;
}
int main()
{
int num;
cout << "Ingrese un numero" << endl;
cin >> num;
cout << "El numero " << num << " al cuadrado es " << alCuadrado(num) << endl;
cout << "De paso, te cuento que 7 al cuadrado es " << alCuadrado(2+5) << endl;
return 0;
}
El ejemplo muestra que la llamada a una función (es decir, usarla) cuenta como una "operación", igual que la suma o la resta, y puede usarse en expresiones más complicadas (por ejemplo como parte de una cuenta y operaciones aritméticas).
==== Funciones con varios parámetros ====
Una función puede necesitar varios datos para realizar su tarea. De ser así, se trabaja de idéntica manera pero separando los parámetros con comas, y dándolos siempre en el mismo orden. Por ejemplo, a continuación se muestra un ejemplo de código que usa una función que pega dos palabras usando un guión entre ellas:
#include
using namespace std;
string pegarConGuion(string palabra1, string palabra2)
{
return palabra1 + "-" + palabra2;
}
int main()
{
string a,b;
cout << "Ingrese dos palabras" << endl;
cin >> a >> b;
cout << pegarConGuion(a,b) << endl;
cout << pegarConGuion("super", "sonico") << endl;
return 0;
}
Alentamos al lector a que lea este código y prediga qué es lo que va a mostrar por pantalla, y luego verifique que efectivamente así sea.
===== Procedimientos =====
Veremos ahora ejemplos de funciones **que no devuelven nada**, lo que a veces (sobre todo en otros lenguajes) se denomina procedimiento o subrutina (En C++, lo usual es llamar a todos función, aunque no devuelvan nada).
¿Por qué podríamos querer que una función no devuelva nada? Porque podría importarnos solamente lo que la función **hace**, es decir, las instrucciones que ejecuta, sin necesidad de que nos devuelva un valor final. Un ejemplo sencillo de esto podría ser alguna función que escriba en pantalla:
void mostrarConEspacios(string palabra)
{
for (int i=0;i 0)
cout << " ";
cout << palabra[i];
}
}
Esta función recibe una palabra, y la escribe en pantalla pero con un espacio insertado entre cada par de letras consecutivas. En este caso, no hay nada que devolver: Al que llama no le importa obtener ningún resultado, sino simplemente lograr que se escriba a pantalla lo que queremos. Una vez que la función **hace** lo que queríamos (en este caso, imprimir a pantalla), el que la usa no espera ningún resultado adicional. Es por eso que por ejemplo no pusimos return en esta función, pues no hay nada que devolver.
Cuando una función no devuelve nada, se indica ''void'' como su tipo de retorno. ''void'' significa "vacío" en inglés, y es un tipo muy especial que se usa en el valor de retorno de las funciones para indicar que no devuelven nada.
En una función que devuelve ''void'' (que es lo mismo que decir que no devuelve nada), no es posible utilizar ''return x;'', pues no se puede devolver nada. Sin embargo, está permitido utilizar ''return;'' a secas, sin indicar ningún valor de retorno: Esto tiene el efecto de **terminar inmediatamente la ejecución de la función**, en el momento en que se ejecuta el ''return''.
==== Pasaje por copia y por referencia ====
Supongamos que escribimos la siguiente función, con la idea de que aumente en uno la variable indicada:
void incrementar(int x)
{
x = x + 1;
}
Con lo que vimos hasta ahora, podría parecer que esta función hará lo que queremos. Podemos intentar utilizarla en un programa:
#include
using namespace std;
void incrementar(int x)
{
x = x + 1;
}
int main()
{
int x = 20;
cout << x << endl;
incrementar(x);
cout << x << endl;
incrementar(x);
cout << x << endl;
return 0;
}
Querríamos que este programa muestre 20, 21 y 22, pues utiliza nuestra función para ir aumentando la variable. Sin embargo si lo ejecutamos, veremos por pantalla 20, 20 y 20: No se ha incrementado nada. ¿Por qué resulta ser así?
Para ayudar a entender esto agregaremos al programa un par de instrucciones con ''cout'' dentro de la función, para ver si se incrementa o no.
#include
using namespace std;
void incrementar(int x)
{
cout << "Recibo x con " << x << endl;
x = x + 1;
cout << "Al terminar x tiene " << x << endl;
}
int main()
{
int x = 20;
cout << x << endl;
incrementar(x);
cout << x << endl;
incrementar(x);
cout << x << endl;
return 0;
}
Este programa muestra por pantalla:
20
Recibo x con 20
Al terminar x tiene 21
20
Recibo x con 20
Al terminar x tiene 21
20
Podemos ver que dentro de la función se está produciendo el incremento, como queremos, pues se recibe 20 y luego se tiene 21. Pero este cambio **no se observa fuera de la función**: La llamada a la función parece no estar teniendo ningún efecto sobre el ''x'' del main.
El motivo por el que esto ocurre es que cuando se ejecuta una función, al comenzar se hace una **copia** de los parámetros: En la primera llamada a incrementar, El ''x'' del main vale 20. Pero la función **no trabaja con el x del main**: trabaja todo el tiempo con una **copia** de ese ''x''. Así, la función tiene **su propia copia** del parámetro ''x'', que al comenzar toma el valor 20 que tenía la variable con la cual fue llamada.
Lo que observamos es que la función incrementa esta copia, y dentro de la función siempre estamos usando la copia, pero al terminar la función esta copia deja de existir y la variable original del ''main'' queda inalterada sin cambios. Si bien en muchos casos esto es exactamente lo que queremos, para evitar que una función "nos cambie accidentalmente" nuestras variables, en este ejemplo queremos intencionalmente cambiar una variable del main. Es decir, en este ejemplo queremos intencionalmente que **no se haga ninguna copia** para la función, sino que se trabaje **directamente con el dato original**, para que todos los cambios que haga la función se hagan sobre el original.
La manera de hacer esto en C++ es agregando un ampersand ''&'' justo antes del nombre del parámetro en el momento en que escribimos el código de la función: dicho ampersand indica a C++ que no queremos trabajar con una copia, sino con la variable original directamente.
Nuestro ejemplo quedaría entonces (solamente le agregamos un ''&'' en el parámetro ''x''):
#include
using namespace std;
void incrementar(int &x)
{
cout << "Recibo x con " << x << endl;
x = x + 1;
cout << "Al terminar x tiene " << x << endl;
}
int main()
{
int x = 20;
cout << x << endl;
incrementar(x);
cout << x << endl;
incrementar(x);
cout << x << endl;
return 0;
}
Que produce el resultado esperado:
20
Recibo x con 20
Al terminar x tiene 21
21
Recibo x con 21
Al terminar x tiene 22
22
Notar que el ampersand se debe agregar en la **declaración** (donde escribimos el código) de la función, y no en la invocación (donde la usamos / llamamos).
A la forma normal de pasar los parámetros, que ocurre cuando no especificamos ningún ampersand, se la llama //pasar por copia// o //pasar por valor//, pues en la función directamente se copia el valor de los parámetros y se trabaja siempre con las copias, sin cambiar los originales.
En cambio, cuando se usa el ampersand, se dice que ese parámetro se pasa //por referencia// o //por variable//, pues en todo momento se hace referencia a la variable original, y nunca existe una copia diferente para la función. Cualquier cambio que haga la función, impactará en la variable original.
La mayoría de la veces pasaremos los parámetros por valor, y solo cuando tengamos alguna razón específica para hacerlo los pasaremos por referencia. Uno de los ejemplos más comunes es el que acabamos de ver, donde queremos modificar una variable del main como parte de las operaciones de la función.
==== Procedimientos + Pasaje por referencia ====
El uso de procedimientos es bastante usual cuando se utilizan parámetros pasados por referencia. Esto es porque uno de los usos principales de pasar un parámetro por referencia es para que la función **lo pueda modificar**, y entonces, si queremos que la función modifique el parámetro que le pasamos, es posible (pero no necesariamente cierto) que no necesitemos ningún resultado adicional, sino que solamente nos importe esta modificación.
Un ejemplo podría ser una función que recibe un ''vector'' y duplica todos sus elementos:
#include
#include
using namespace std;
void duplicarElementos(vector &v)
{
for (int i=0;i w = {1,2,3,8};
duplicarElementos(w);
for (int x : w)
cout << x << endl;
return 0;
}
Que muestra por pantalla:
2
4
6
16
Notar que si cambiamos la línea ''void duplicarElementos(vector &v)'' por ''void duplicarElementos(vector v)'' (quitando el ''&''), el programa ya no hará lo qué queremos. ¿Qué mostrará? ¿Y por qué es así?
===== Variables locales y globales =====
Todas las variables que hemos utilizado hasta ahora han sido siempre //variables locales//. Esto significa que fueron definidas **dentro de una función** (''main'' es también una función: por lo tanto, las variables que hemos definido en ''main'' son también variables locales, de la función main).
Una variable local solamente puede utilizarse dentro de la función en que fue definida, y no desde otras funciones (para eso existe la idea de usar los parámetros, para pasar información útil a una función).
Existe otro tipo de variables que pueden accederse desde **cualquier lugar del programa**. Estas variables se denominan //variables globales//. Basta declararlas en el programa directamente **fuera de cualquier función** para obtener una variable global. Desde ese punto del programa en adelante, se podrá utilizar esa variable, que está "compartida" entre todas las funciones del programa.
En general, utilizar demasiadas variables globales en lugar de parámetros es considerado una mala práctica en C++, y hacerlo puede llevar fácilmente a tener programas difíciles de leer. Se utilizan principalmente en competencias de programación, como forma práctica de tener accesibles datos importantes que son utilizados a lo largo de todo el programa, y evitar así tener que pasar los mismos parámetros todo el tiempo entre funciones. Existen otras técnicas para lograr esto mismo, pero son más avanzadas (por ejemplo, usar struct/class y métodos, y/o enfoques de programación orientada a objetos).
Además, existe un peligro adicional a tener en cuenta las variables globales, que es la posibilidad de **ocultar** una variable global con una variable local. Esto ocurre cuando tenemos una variable local con el mismo nombre que una variable global: Estas dos serán variables distintas, pero al tener el mismo nombre, no es posible utilizar ambas a la vez. Dentro de la función, solamente será posible utilizar la variable local. Por eso se dice que la variable local //oculta// a la global.
Por ejemplo, el siguiente código:
int mivar = 32;
int foo()
{
int mivar = 12;
mivar++;
return 2*mivar;
}
int main()
{
cout << foo() << endl;
cout << mivar << endl;
return 0;
}
Mostrará 26 y 32. El valor de la variable global ''mivar'' nunca es modificado, ya que dentro de ''foo'' se trabaja con la variable **local** del mismo nombre, que oculta a la variable global correspondiente. Por estos motivos, en general es muy mala idea usar el mismo nombre para una variable global y una local, pues podemos tener problemas y usar la variable que no queríamos sin darnos cuenta.
En inglés esto se denomina //shadow//: Si [[curso-cpp:ambiente:oiax#configuracion_de_compilacion_con_geany|se activa la opción]] ''-Wshadow'', el compilador nos advierte si ocultamos una variable de esta forma.
===== Observaciones adicionales =====
* En las funciones se puede llamar (utilizar) otras funciones: esto está totalmente permitido:
int multiplicar(int a, int b)
{
return a*b;
}
int elevar(int base, int exponente)
{
int resultado = 1;
for (int i=0;i
* Variables con el mismo nombre pero definidas en funciones distintas, representan variables **diferentes** (como el ''x'' del main y el ''x'' de la función en el ejemplo anterior de ''incrementar'')
===== Ejemplos de implementación de funciones =====
* Nuestro primer ejemplo es la función ''main'', que ya venimos usando en todos nuestros programas: Es una función que devuelve un ''int'', que usa la computadora para saber si hubo errores. Por convención, se debe devolver cero si todo salió bien, y por eso es buena costumbre terminar todos los programas con ''return 0''. La función ''main'' es importante porque tiene la característica especial de que allí comienzan a ejecutarse todos nuestros programas, aunque tengan otras funciones.
* Como ejemplo de pensamiento top-down, supongamos que debemos realizar un programa que lea una secuencia de números, y luego calcule y muestre por pantalla la suma, el máximo y el mínimo de todos estos números. Podemos programar primero que anda el main de la siguiente manera:
int main()
{
vector v;
v = leerNumeros();
imprimirResultados(suma(v), maximo(v), minimo(v));
return 0;
}
Donde nos hemos ordenado y hemos logrado **descomponer** el problema entero en tareas más pequeñas. Luego podríamos agregarle las funciones que faltan al programa, para completarlo, dejando inalterado el mismo main que ya escribimos:
vector leerNumeros()
{
// instrucciones...
}
int suma(vector v)
{
// instrucciones...
}
int maximo(vector v)
{
// instrucciones...
}
int minimo(vector v)
{
// instrucciones...
}
void imprimirResultados(int laSuma,int elMaximo, int elMinimo)
{
// instrucciones...
}
* El siguiente es un ejemplo con funciones que calculan áreas de figuras geométricas, que muestra como podemos reutilizar ciertas funciones dentro de otras:
int areaParalelogramo(int base, int altura)
{
return base * altura;
}
int areaCuadrado(int lado)
{
return areaParalelogramo(lado, lado);
}
int areaTriangulo(int base, int altura) // Trabaja con enteros: Redondea hacia abajo
{
return areaParalelogramo(base, altura) / 2;
}
* El siguiente es un ejemplo de función que recibe dos variables enteras, e intercambia sus valores. ¡Notar el uso del ampersand!
void intercambiar(int &variable1, int &variable2)
{
int auxiliar = variable1; // Es necesario un auxiliar: ¿Por qué?
variable1 = variable2;
variable2 = auxiliar;
}
Similarmente, el siguiente ejemplo permite "rotar" los valores de tres variables dadas: Es decir, transforma [a,b,c] en [b,c,a]:
void rotar3(int &a, int &b, int &c)
{
int auxiliar = a; // Nuevamente, ¿Por qué es necesario el auxiliar?
a = b;
b = c;
c = auxiliar;
}
===== Algunas funciones predefinidas =====
En C++, existen algunas funciones predefinidas que ya existen, y conocerlas puede simplificarnos la tarea de programar ya que nos ahorramos tener que escribirlas nosotros mismos. Mencionamos algunas a continuación:
* ''max'': Dados dos números, devuelve el máximo. Por ejemplo ''max(2,9) == 9'' y ''max(5,3) == 5''. Similarmente tenemos ''min'' para el mínimo.
* ''swap'': Intercambia los valores de las dos variables que se le indica. Por ejemplo si ''x'' tiene un ''3'', y ''q[i]'' tiene un ''8'', luego de hacer ''swap(x,q[i])'' quedará ''q[i]'' con un 3 y ''x'' con un 8.
* ''abs'': Devuelve el valor absoluto (módulo) de un entero. Por ejemplo ''abs(-3) == 3'', ''abs(0) == 0'' y ''abs(15) == 15''.
Todas estas funciones requieren utilizar ''#include '' para tenerlas disponibles.
===== Algunos errores comunes =====
* Pasar a la función una cantidad de parámetros diferente de las que la función necesita, o con el tipo incorrecto. Por ejemplo si tenemos la función
int mayor(int num1, int num2)
{
if (num1 > num2)
return num1;
else
return num2;
}
serían incorrectas las siguientes llamadas:
mayor(k,m,n) // Pasa 3 parámetros, pero la función toma solamente 2
mayor(23, "miliwatt") // Pasa 2 parámetros, pero el segundo es una cadena y debería ser un int
* Diseñar una función con la idea de que **modifique** uno de sus parámetros, pero trabajar con una copia por no utilizar el ampersand ''&''.
* Intentar utilizar un parámetro (con su nombre) **fuera** de una función: Los parámetros solamente están definidos dentro de la función, y no tiene sentido utilizarlos fuera de ella (son variables locales).
* Utilizar una función que todavía no se definió. Se debe programar el código de una función, antes de utilizarla.
===== Ejercicios =====
Recomendamos revisar los ejercicios pasados del curso, y volver a programarlos aprovechando la idea de funciones: ¿En cuáles es apropiado utilizar funciones para estructurar mejor el programa?
Por ejemplo, en los ejercicios en los que se hablaba de números primos, se podría escribir una función que toma un ''int N'', y devuelve un ''bool'' indicando si es primo. O por ejemplo, escribir una función ''sumaDeDivisores'' puede ser útil para escribir de forma más fácil y clara programas que buscan números perfectos.
Otros ejercicios:
* Escribir una función ''string escribirEnBase(int numero, int base)'', que tome un número y una base (Entre 2 y 16 inclusive) y devuelva una cadena con la escritura de ese número en la base indicada.
* Escribir una función ''int leerNumeroEnBase(string escritura, int base)'', que tome la escritura de un cierto número en la base indicada (Entre 2 y 16 inclusive) y devuelva el número en cuestión.
Puede ver aquí [[algoritmos-oia:enteros:cambio-de-base|cómo realizar cambios de base]].
===== Los #define =====
En C++, además de las funciones que ya vimos, existe un mecanismo relacionado para evitar repetir código, y es la directiva ''#define''.
Mediante un ''#define'', es posible definir lo que se denomina una //macro//, que es una regla para **reemplazar textualmente un fragmento de código en el programa**. Por ejemplo, supongamos que colocamos en cualquier línea de un programa, el siguiente ''#define'':
#define declaraEInicializaEnUnValor(nombreVariable, valor) int nombreVariable = valor
Luego de haber escrito este ''#define'', si en **cualquier** lugar del programa aparece un ''declaraEInicializaEnUnValor(x, 33)'', se **reemplazará textualmente** en ese mismo lugar por un ''int x = 33''.
Por ejemplo,el siguiente sería un programa válido:
#include
using namespace std;
#define declaraEInicializaEnUnValor(nombreVariable, valor) int nombreVariable = valor
int main()
{
declaraEInicializaEnUnValor(x, 40);
declaraEInicializaEnUnValor(y, 100);
cout << x + y << endl;
return 0;
}
Que muestra por pantalla 140. Notar que luego de los reemplazos, el programa es absolutamente equivalente a si se hubiera escrito directamente:
#include
using namespace std;
int main()
{
int x = 40;
int y = 100;
cout << x + y << endl;
return 0;
}
En general, siempre que podamos conviene utilizar funciones en lugar de ''#defines'', y dejaremos los ''#defines'' únicamente para los casos en que no podamos hacer lo mismo con una función. Por ejemplo, en el ejemplo anterior creamos una macro que permite **declarar** una variable y empezar a usarla directamente, que es algo que no podríamos haber hecho con una función. A diferencia de una función, una macro hace un **reemplazo totalmente mecánico** de los valores indicados entre paréntesis, exactamente igual que si se hiciera copy paste mecánicamente en el código.
==== Algunos ejemplos de macros muy útiles ====
Especialmente en competencias de programación, es muy común tener un fragmento de código como el siguiente:
for (int numero = 0; numero < valorMaximo; numero++)
// instrucciones
Donde recorremos todos los números desde ''0'' hasta ''valorMaximo - 1'' inclusive. Notar que al escribir ese fragmento tenemos que escribir la variable ''numero'' **tres veces**, lo cual aumenta las chances de equivocarnos (especialmente, si hacemos copy + paste de otro for similar, ya que entonces es muy fácil olvidarnos de cambiar **alguna** de las 3 apariciones).
Otra variante muy similar sería cuando queremos recorrer todos los índices de un cierto vector:
for (int var = 0; var < int(vector.size()); var++)
// instrucciones
En este caso se agrega la conversión con ''int()'', que debe usarse al comparar con ''.size()''.
Para programar más fácilmente y con menor chances de errores este tipo de for comunes, podemos utilizar un ''#define'' para definir una forma compacta de indicar los elementos importantes que cambian de caso en caso, y que el resto se reemplace mecánicamente siempre igual:
#define forn(i,n) for(int i=0;i
En este caso, indicamos en el ''#define'' el **nombre de la variable** que vamos a declarar, y el **valor tope** hasta el cual vamos a iterar (iteraremos desde ''0'' hasta ''n-1'' inclusive). Esto es lo único que cambia en estos ejemplos, y el resto es siempre igual.
De esta forma, una vez que tenemos este #define el primer for que mostramos nos quedaría simplemente:
forn(numero, valorMaximo)
// instrucciones
Y el segundo quedaría:
forn(var, vector.size())
// instrucciones
Vemos que ahora solamente hace falta especificar **una vez** el nombre de la variable, y todo lo demás es copiado automáticamente en forma mecánica por el ''#define''.
Se puede consultar [[cpp-avanzado:macros |aquí]] otros ejemplos de macros más avanzadas, muy útiles para programación competitiva, además del ''forn'' ya mostrado.
==== Por qué es mejor usar funciones ====
Hemos mencionado que siempre que podamos hacer algo con funciones en lugar de ''#define'', es conveniente hacerlo con funciones. Esto es porque las funciones son "más seguras" de utilizar. Veamos un ejemplo para ver por qué esto es así.
Supongamos que queremos tener un fragmento de código para elevar un número al cuadrado. La forma más simple de hacerlo es multiplicar al número con sí mismo, ya que multiplicar es una operación atómica disponible. Podemos entonces pensar en hacer un ''#define'' para ello:
#define cuadrado(x) x*x
De esta forma, cuando escribamos ''cuadrado(2)'', por ejemplo, se reemplaza por ''2*2'', que es el número al cuadrado como queremos. Sin embargo, el ''#define'' que acabamos de definir es muy peligroso de utilizar: imaginemos por ejemplo que lo usamos en la siguiente línea:
cout << cuadrado(2+3) << endl;
Esperamos obtener 25... pero la salida de este programa produce 11. ¿Por qué ha ocurrido esto?
El motivo es que, como ya hemos mencionado, los ''#define'' realizan un reemplazo **mecánico y textual** de los elementos que les indicamos, igual que si hiciéramos copy paste, sin entender el significado de su contenido. Por lo tanto, como el ''#define'' indica que debemos cambiar ''cuadrado(x)'' por ''x*x'', tenemos que fijarnos quién es ''x'', copiarlo dos veces y colocar un asterisco en el medio, textualmente, pues eso es lo que hacen los ''#define''. En nuestro ejemplo, ''x'' es ''2+3'', pues se ha escrito ''cuadrado(2+3)'': Entonces, el fragmento ''cuadrado(2+3)'' es reemplazado por ''2+3*2+3'', ya que el ''#define'' reemplaza textualmente cada copia de ''x'' por el texto indicado. Esta expresión, al no tener paréntesis, da por resultado ''2+6+3=11'', pero nosotros queríamos realizar ''(2+3)*(2+3)=25''.
Esto podría resolverse si utilizamos **paréntesis en todos lados**:
#define cuadrado(x) ((x)*(x))
Este ''#define'' funciona correctamente, pero ahora quedó bastante más feo y difícil de leer. Una función como
int cuadrado(int x)
{
return x*x;
}
nos hubiera evitado todos estos problemas, pues en las funciones **no se hace un reemplazo textual mecánico del código**, sino que se **calcula el valor indicado antes de comenzar a ejecutar la función**. En el ejemplo con la función cuadrado, se calcularía el valor ''2+3=5'' antes de iniciar la función, de forma que cuando se comienza a ejecutar la función cuadrado, ya se tiene ''x=5''. En este sentido, **las funciones son más inteligentes que el #define**.
En resumen, utilizaremos ''#define'' solamente cuando nos permite hacer algo que con una función no podríamos hacer. Por ejemplo, escribir algún tipo de ''for'' común, o resumir alguna instrucción que declare una variable, es algo que no podríamos reemplazar fácilmente por una función. En cambio, un simple cálculo como elevar al cuadrado sí es algo que podríamos hacer fácilmente con una función, y entonces generalmente conviene hacerlo así para evitar posibles errores, y no tener que llenar todo de paréntesis.