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:
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.
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:
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.
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.
Una función en C++1) 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 <iostream> 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();
.
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 <iostream> 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).
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 <iostream> 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.
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<int(palabra.size());i++) { if (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
.
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 <iostream> 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 <iostream> 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 <iostream> 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.
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<int>
y duplica todos sus elementos:
#include <iostream> #include <vector> using namespace std; void duplicarElementos(vector<int> &v) { for (int i=0;i<int(v.size());i++) v[i] *= 2; } int main() { vector<int> 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<int> &v)
por void duplicarElementos(vector<int> v)
(quitando el &
), el programa ya no hará lo qué queremos. ¿Qué mostrará? ¿Y por qué es así?
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 se activa la opción -Wshadow
, el compilador nos advierte si ocultamos una variable de esta forma.
int multiplicar(int a, int b) { return a*b; } int elevar(int base, int exponente) { int resultado = 1; for (int i=0;i<exponente;i++) resultado = multiplicar(resultado, base); return resultado; }
x
del main y el x
de la función en el ejemplo anterior de incrementar
)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.int main() { vector<int> 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<int> leerNumeros() { // instrucciones... } int suma(vector<int> v) { // instrucciones... } int maximo(vector<int> v) { // instrucciones... } int minimo(vector<int> v) { // instrucciones... } void imprimirResultados(int laSuma,int elMaximo, int elMinimo) { // instrucciones... }
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; }
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; }
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 <algorithm>
para tenerlas disponibles.
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
&
.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:
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.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í cómo realizar cambios de base.
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 <iostream> 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 <iostream> 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.
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<int(n);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 aquí otros ejemplos de macros más avanzadas, muy útiles para programación competitiva, además del forn
ya mostrado.
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.