Una función es un bloque de código que tiene un nombre y un conjunto de instrucciones que son ejecutadas cuando se llama a la función. Son funciones setup() y loop() de las que ya se ha hablado.
Las funciones de usuario pueden ser escritas para realizar tareas repetitivas y para reducir el tamaño de un programa. Segmentar el código en funciones permite crear piezas de código que hacen una determinada tarea y volver al área del código desde la que han sido llamadas.
Las funciones se declaran asociadas a un tipo de valor. Este valor será el que devolverá la función, por ejemplo ‘int’ se utilizará cuando la función devuelva un dato numérico de tipo entero. Si la función no devuelve ningún valor entonces se colocará delante la palabra “void”, que significa “función vacía”
Sintaxis:
tipo nombreFunción (parámetros) {
instrucciones;
}
Para llamar a una función, simplemente:
nombreFunción(parámetros);
En una función que devuelve un valor siempre debe tener la instrucción Return, este termina una función y devuelve un valor a quien ha llamado a la función: http://arduino.cc/en/Reference/Return
Ventajas del uso de funciones:
- Ayuda a tener organizado el código.
- Una función codifica una tarea en un lugar de nuestro sketch, así que la función solo debe ser pensada una sola vez.
- Reduce la probabilidad de errores al modificar el código.
- Hacen que el tamaño del sketch sea menor porque el código de la función es reutilizado.
- Facilita la lectura del código.
- Hace más sencillo reutilizar código en otros sketches.
Más información: http://arduino.cc/en/Reference/FunctionDeclaration
Nombres de funciones
Generalmente los nombres de las funciones deben ser en minúscula, con las palabras separadas por un guión bajo, aplicándose éstos tanto como sea necesario para mejorar la legibilidad.
“mixedCase” (primera palabra en minúscula) es aceptado únicamente en contextos en donde éste es el estilo predominante con el objetivo de mantener la compatibilidad con versiones anteriores.
En el caso de las clases, los nombres deben utilizar la convención “CapWords” (palabras que comienzan con mayúsculas).
Las funciones en Arduino pueden estar dentro del mismo fichero .ino o en otro fichero con extensión .ino dentro del directorio del sketch.
Paso por Valor y Paso por Referencia
Hasta ahora siempre hemos declarado los parámetros de nuestras funciones del mismo modo. Sin embargo, éste no es el único modo que existe para pasar parámetros.
La forma en que hemos declarado y pasado los parámetros de las funciones hasta ahora es la que normalmente se conoce como «por valor». Esto quiere decir que cuando el control pasa a la función, los valores de los parámetros en la llamada se copian a «objetos» locales de la función, estos «objetos» son de hecho los propios parámetros.
int funcion(int n, int m) {
n = n + 2;
m = m - 5;
return n+m;
}
int a = 10;
int b = 20;
Serial.println(funcion(a,b));
Serial.println(funcion(10,20));
Empezamos haciendo a = 10 y b = 20, después llamamos a la función «funcion» con las objetos a y b como parámetros. Dentro de «funcion» esos parámetros se llaman n y m, y sus valores son modificados. Sin embargo al retornar al programa que lo llama, a y b conservan sus valores originales. Lo que pasamos no son los objetos a y b, sino que copiamos sus valores a los objetos n y m. Es lo mismo que hacer funcion(10,20), cuando llamamos a la función con parámetros constantes. Si los parámetros por valor no funcionasen así, no sería posible llamar a una función con valores constantes o literales.
Las referencias sirven para definir «alias» o nombres alternativos para un mismo objeto. Para ello se usa el operador de referencia (&).
Por ejemplo:
int a;
int &r = a;
a = 10;
Serial.println(r);
En este ejemplo los identificadores a y r se refieren al mismo objeto, cualquier cambio en una de ellos se produce en el otro, ya que son, de hecho, el mismo objeto. El compilador mantiene una tabla en la que se hace corresponder una dirección de memoria para cada identificador de objeto. A cada nuevo objeto declarado se le reserva un espacio de memoria y se almacena su dirección. En el caso de las referencias, se omite ese paso, y se asigna la dirección de otro objeto que ya existía previamente. De ese modo, podemos tener varios identificadores que hacen referencia al mismo objeto, pero sin usar punteros.
Si queremos que los cambios realizados en los parámetros dentro de la función se conserven al retornar de la llamada, deberemos pasarlos por referencia. Esto se hace declarando los parámetros de la función como referencias a objetos. Por ejemplo:
int funcion(int &n, int &m) {
n = n + 2;
m = m - 5;
return n+m;
}
int a = 10;
int b = 20;
Serial.println(funcion(a,b));
Serial.println("a = " + String(a) + " b = " + String(b));
//es ilegal pasar constantes como parámetros cuando estos son referencias
Serial.println(funcion(10,20));
En este caso, los objetos «a» y «b» tendrán valores distintos después de llamar a la función. Cualquier cambio de valor que realicemos en los parámetros dentro de la función, se hará también en los objetos referenciadas. Esto quiere decir que no podremos llamar a la función con parámetros constantes, ya que aunque es posible definir referencias a constantes, en este ejemplo, la función tiene como parámetros referencias a objetos variables. Y si bien es posible hacer un casting implícito de un objeto variable a uno constante, no es posible hacerlo en el sentido inverso. Un objeto constante no puede tratarse como objeto variable.
Una const reference es una referencia a que no permite cambiar la variable a través de esa referencia. Por ejemplo const int &r = a; en r tengo el valor de a pero no puedo cambiar el valor de a usando r.
No confundir este concepto con el modificador de variable static, que es utilizado para crear variables que solo son visibles dentro de una función, sin embargo, al contrario que las variables locales que se crean y destruyen cada vez que se llama a la función, las variables estáticas mantienen sus valores entre las llamadas a las funciones.
Más información:
Sobrecarga de Funciones
Hay operadores que tienen varios usos, como por ejemplo *, &, << o >>. Esto es lo que se conoce en C++ como sobrecarga de operadores. Con las funciones existe un mecanismo análogo, de hecho, en C++, los operadores no son sino un tipo especial de funciones, aunque eso sí, algo peculiares.
Así que en C++ podemos definir varias funciones con el mismo nombre, con la única condición de que el número y/o el tipo de los argumentos sean distintos. El compilador decide cuál de las versiones de la función usará después de analizar el número y el tipo de los parámetros. Si ninguna de las funciones se adapta a los parámetros indicados, se aplicarán las reglas implícitas de conversión de tipos.
Las ventajas son más evidentes cuando debemos hacer las mismas operaciones con objetos de diferentes tipos o con distinto número de objetos. También pueden usarse macros para esto, pero no siempre es posible usarlas, y además las macros tienen la desventaja de que se expanden siempre, y son difíciles de diseñar para funciones complejas. Sin embargo las funciones serán ejecutadas mediante llamadas, y por lo tanto sólo habrá una copia de cada una.
Ejemplo:
int mayor(int a, int b);
char mayor(char a, char b);
float mayor(float a, float b);
int mayor(int a, int b, int c, int d);
int mayor(int a, int b) {
if(a > b) return a; else return b;
}
char mayor(char a, char b) {
if(a > b) return a; else return b;
}
float mayor(float a, float b) {
if(a > b) return a; else return b;
}
int mayor(int a, int b, int c, int d) {
return mayor(mayor(a, b), mayor(c, d));
}
Las llamadas a funciones sobrecargadas se resuelven en la fase de compilación. Es el compilador el que decide qué versión de la función debe ser invocada, después de analizar, y en ciertos casos, tratar los argumentos pasados en la llamadas. A este proceso se le llama resolución de sobrecarga.
Tener en cuenta que el tipo de retorno de la función no se considera en la sobrecarga de funciones. Consideremos el caso en el que desea escribir una función que devuelve un número aleatorio, pero se necesita una versión que devolverá un entero, y otra versión que devolverá un doble.
int getRandomValue();
double getRandomValue();
Pero el compilador toma esto como un error. Estas dos funciones tienen los mismos parámetros (ninguno) y en consecuencia, la segunda getRandomValue () serán tratada como una redeclaración errónea de la primera. En consecuencia, tendrán que ser dado diferentes nombres a estas funciones.
Más información:
Sobrecarga de operadores:
Ámbito de las variables
Una variable puede ser declarada al inicio del programa antes de la parte de configuración setup(), a nivel local dentro de las funciones, y, a veces, dentro de un bloque, como para los bucles del tipo if.. for.., etc. En función del lugar de declaración de la variable así se determinará el ámbito de aplicación, o la capacidad de ciertas partes de un programa para hacer uso de ella.
Una variable global es aquella que puede ser vista y utilizada por cualquier función y estamento de un programa. Esta variable se declara al comienzo del programa, antes de setup().
Recordad que al declarar una variable global, está un espacio en memoria permanente en la zona de static data y el abuso de variables globales supone un uso ineficiente de la memoria.
Una variable local es aquella que se define dentro de una función o como parte de un bucle. Sólo es visible y sólo puede utilizarse dentro de la función en la que se declaró. Por lo tanto, es posible tener dos o más variables del mismo nombre en diferentes partes del mismo programa que pueden contener valores diferentes, pero no es una práctica aconsejable porque complica la lectura de código.
En el reference de Arduino hay una muy buena explicación del ámbito de las variables:http://arduino.cc/en/Reference/Scope
La variables estáticas solo se crean e inicializan la primera vez que la función es llamada. Ver ejemplo en: http://arduino.cc/en/Reference/Static
Más información:
Inline
Cuando usamos el nombre de una función, indicando valores para sus argumentos, dentro de un programa, decimos que llamamos o invocamos a esa función. Esto quiere decir que el procesador guarda la dirección actual, «salta» a la dirección donde comienza el código de la función, la ejecuta, recupera la dirección guardada previamente, y retorna al punto desde el que fue llamada.
Esto es cierto para las funciones que hemos usado hasta ahora, pero hay un tipo especial de funciones que trabajan de otro modo. En lugar de existir una única copia de la función dentro del código, si se declara una función como inline, lo que se hace es insertar el código de la función, en el lugar (y cada vez) que sea llamada. Esta indica al compilador que cada llamada a la función inline deberá ser reemplazado por el cuerpo de esta función. En la práctica la función inline es utilizado sólo cuando las funciones son pequeñas para evitar generar un ejecutable de tamaño considerable.
La palabra reservada inline tiene la ventaja de acelerar un programa si éste invoca regularmente a la función inline. Permite reducir considerablemente el código, en particular para los accesadores de una clase. Un accesador de clase es típicamente una función de una línea.
Ejemplo:
inline int mayor(int a, int b) {
if(a > b) return a;
return b;
}
Más información de inline:
Prototipos de Funciones
Primero recordar que en el lenguaje de Arduino al contrario que en estandar C, no es necesario declarar los prototipos de las funciones, puesto que de eso se encarga el de incluirlo el arduino builder, al igual que de añadir el main..
En C++ es obligatorio usar prototipos. Un prototipo es una declaración de una función. Consiste en una presentación de la función, exactamente con la misma estructura que la definición, pero sin cuerpo y terminada con un «;».
En general, el prototipo de una función se compone de las siguientes secciones:
- Opcionalmente, una palabra que especifique el tipo de almacenamiento, puede ser extern o static. Si no se especifica ninguna, por defecto será extern.
- El tipo del valor de retorno, que puede ser void, si no necesitamos valor de retorno.
- Modificadores opcionales.
- El identificador de la función. Es costumbre, muy útil y muy recomendable, poner nombres que indiquen, lo más claramente posible, qué es lo que hace la función, y que permitan interpretar qué hace el programa con sólo leerlos.
- Una lista de declaraciones de parámetros entre paréntesis. Los parámetros de una función son los valores de entrada (y en ocasiones también de salida).
Un prototipo sirve para indicar al compilador los tipos de retorno y los de los parámetros de una función, de modo que compruebe si son del tipo correcto cada vez que se use esta función dentro del programa, o para hacer las conversiones de tipo cuando sea necesario.
Normalmente, los prototipos de las funciones se declaran dentro del fichero del programa, o bien se incluyen desde un fichero externo, llamado fichero de cabecera, (para esto se usa la directiva #include).
Ya lo hemos dicho más arriba, pero las funciones son extern por defecto. Esto quiere decir que son accesibles desde cualquier punto del programa, aunque se encuentren en otros ficheros fuente del mismo programa. En contraposición las funciones declaradas static sólo son accesibles dentro del fichero fuente donde se definen.
Más información:
Bibliotecas/Librerías en C++
Junto con los compiladores de C y C++, se incluyen ciertos archivos llamados bibliotecas más comúnmente librerías. Las bibliotecas contienen el código objeto de muchos programas que permiten hacer cosas comunes, como leer el teclado, escribir en la pantalla, manejar números, realizar funciones matemáticas, etc.
Las bibliotecas están clasificadas por el tipo de trabajos que hacen, hay bibliotecas de entrada y salida, matemáticas, de manejo de memoria, de manejo de textos y como imaginarás existen muchísimas librerías disponibles y todas con una función específica.
La declaración de librerías, tanto en C como en C++, se debe hacer al principio de todo nuestro código, antes de la declaración de cualquier función o línea de código, debemos indicarle al compilador que librerías usar, para el saber qué términos están correctos en la escritura de nuestro código y cuáles no. La sintaxis es la siguiente: #include <nombre de la librería> o alternativamente #include «nombre de la librería». En tu código puedes declarar todas las librerías que quieras aunque en realidad no tienen sentido declarar una librería que no vas a usar en tu programa, sin embargo no existe límite para esto.
La directiva de preprocesador #include se usa en los lenguajes C y C++ para “incluir” las declaraciones de otro fichero en la compilación. Esta directiva no tiene más misterio para proyectos pequeños. En cambio, puede ayudar aprovechar bien esta directiva en proyectos con un gran número de subdirectorios.
Ejemplo:
#include «iostream»
#include «string»
#include <math.h>
using namespace std;
Lo único adicional, es la línea que dice using namespace std; esta línea nos ayuda a declarar un espacio de nombre que evita tener que usarlo cada que accedemos a alguna función específica de una librería. Teniendo este namespace declarado podemos llamar por ejemplo el comando cout >>, que pertenece a la librería iostream, sin embargo sin este namespace sería std::cout >>, imagina tener que hacer esto cada vez que uses algún comando o función de las librerías, sería bastante tedioso.
A continuación pondré algunas de las librerías de uso más común de C++ y que forman parte de las librerías estándar de este lenguaje.
- fstream: Flujos hacia/desde ficheros. Permite la manipulación de archivos desde el programar, tanto leer como escribir en ellos.
- iosfwd: Contiene declaraciones adelantadas de todas las plantillas de flujos y sus typedefs estándar. Por ejemplo ostream.
- iostream: Parte del a STL que contiene los algoritmos estándar, es quizá la más usada e importante (aunque no indispensable).
- math: Contiene los prototipos de las funciones y otras definiciones para el uso y manipulación de funciones matemáticas.
- memory: Utilidades relativas a la gestión de memoria, incluyendo asignadores y punteros inteligentes (auto_ptr). «auto_ptr» es una clase que conforma la librería memory y permite un fácil manejo de punteros y su destrucción automáticamente.
- ostream: Algoritmos estándar para los flujos de salida.
- Librería stdio: Contiene los prototipos de las funciones, macros, y tipos para manipular datos de entrada y salida.
- Librería stdlib: Contiene los prototipos de las funciones, macros, y tipos para utilidades de uso general.
- string: Parte de la STL relativa a contenedores tipo string; una generalización de las cadenas alfanuméricas para albergar cadenas de objetos.
- vector: Parte de la STL relativa a los contenedores tipo vector; una generalización de las matrices unidimensionales C/C++
- list: Permite implementar listas doblemente enlazadas (listas enlazadas dobles) fácilmente.
- iterator: Proporciona un conjunto de clases para iterar elementos.
- regex: Proporciona fácil acceso al uso de expresiones regulares para la comparación de patrones.
- thread: Útil para trabajar programación multihilos y crear múltiples hilos en nuestra aplicación.
Más información:
Ejercicios Funciones
Ver ejemplo sencillo de uso de funciones: https://github.com/jecrespo/Aprendiendo-Arduino/tree/master/Ejercicio02-Funciones
Comparar las funciones con librerías: https://github.com/jecrespo/Aprendiendo-Arduino/tree/master/Ejercicio03-Librerias
Ejercicio: Menú interactivo con Arduino. Con todo lo visto de Strings, operadores, estructuras de control y funciones, hacer un ejemplo de un menú interactivo donde se dan varias opciones y pulsando cada una de ellas se ejecuta una acción concreta. Si el valor pulsado no es ninguna de las opciones avisar y volver a mostrar el menú hasta que se pulse una opción correcta.
Solución: https://github.com/jecrespo/Aprendiendo-Arduino/tree/master/Ejercicio46-Estructuras_de_Control
Librerías vs Funciones en Arduino
Como se ha visto anteriormente, las librerías son trozos de código hechas por terceros que usamos en nuestro sketch. Esto nos facilita mucho la programación y hace que nuestro programa sea más sencillo de hacer y luego de entender. Más adelante veremos cómo hacer una librería.
Las librerías en Arduino incluyen los siguientes archivos comprimidos en un archivo ZIP o dentro de un directorio. Estas siempre contienen:
- Un archivo .cpp (código de C++)
- Un archivo .h o encabezado de C, que contiene las propiedades y métodos o funciones de la librería.
- Un archivo Keywords.txt, que contiene las palabras clave que se resaltan en el IDE (opcional).
- Muy posiblemente la librería incluye un archivo readme con información adicional de lo que hace y con instrucciones de como usarla.
- Directorio denominado examples con varios sketchs de ejemplo que nos ayudará a entender cómo usar la librería (opcional).
Más información: https://aprendiendoarduino.wordpress.com/2016/06/27/librerias-3/
Una librería a diferencia de las funciones debe estar al menos en un fichero diferente con extensión .h y opcionalmente en otro .cpp y además debe ser llamada con #include desde el sketch de arduino y estar en una ruta accesible desde el IDE de Arduino, ya sea el mismo directorio del sketch o en algunas de las rutas configuradas para librerías.
La ventaja de usar librerías frente a las funciones es que no es necesario incluir el código cada vez que se va a reutilizar sino que con tener la librería instalada en el IDE y llamarla mediante #include ya la puedo usar en mi código.
Al llamar a una librería desde un sketch, la librería completa es cargada a la placa de Arduino incrementando el tamaño del espacio usado en el microcontrolador, tanto en la memoria flash como en la RAM.
NOTA: Como instalar una librería de github: http://scidle.com/install-github-libraries-on-arduino-ide/ habla de quitar caracteres no ASCII, pero comprueba también que no funcione porque haya subdirectorios.
Las librerías que usamos para los sketches tienen una versión, que se suelen actualizar con frecuencia. También tenemos un control de versiones en el nuevo IDE a partir de 1.6.4 que nos facilita la gestión de la versión de las librerías usadas. Este aspecto es importante porque un sketch que funciona con una versión de una librería es posible que al compilarlo con otra versión en otro IDE no funcione. Por ello es importante documentar con que versión de librería está hecho o distribuir el sketch con la librería con la que se ha creado. Generalmente las librerías tienen compatibilidad hacia atrás, pero puede que no ocurra o que el comportamiento de la librería sea diferente.
Al cambiar el IDE también nos podemos encontrar que nuestro sketch no es compatible con la versión de la librería que estemos usando, que es diferente con la que se diseñó originalmente el sketch.
Listado de librerías: http://playground.arduino.cc/Main/LibraryList
Ejercicio Librerías vs Funciones
Ya hemos visto lo que son las librerías y las funciones definidas por usuario, el uso de unas u otras dependerá de varios factores como la posibilidad de reutilización del código o la experiencia del programador con el uso de librerías.
Ejemplos simples de librerías: