Objetos y clases en Arduino


Clase.

La clase es uno de los conceptos y herramientas más grandes e importantes del lenguaje C++, es él quien hace que el lenguaje esté orientado a objetos y sea muy poderoso. Usamos mucho los objetos y métodos, ¡porque el 99% de las bibliotecas son solo clases! Objetos, métodos, ¿qué son? Puedo darte algunas definiciones oficiales (aunque puedes buscarlas en Google tú mismo), pero no lo haré, porque son muy abstractas y te confundirán aún más. Veamos todo usando el ejemplo de una biblioteca, sea la biblioteca de Servo estándar y familiar. Tomemos el ejemplo de Knob y averigüemos a quién se llama y cómo.

#include <Servo.h>   // incluye el archivo de encabezado de la biblioteca, Servo.h
Servo myservo;  // crear un OBJETO myservo de clase Servo  
int potpin = 0;  // pin analógico utilizado para conectar el potenciómetro
int val;   	 // variable para leer el valor del pin analógico
void setup() {
  myservo.attach(9); // aplica el MÉTODO adjunto al OBJETO myservo
}
void loop() {
  val = analogRead(potpin);
  val = map(val, 0, 1023, 0, 180);
  myservo.write(val);      // aplica el MÉTODO de escritura al OBJETO myservo
  delay(15);
}

Entonces, podemos crear un objeto de una clase y aplicarle métodos. ¿Cuál es el primer pensamiento al utilizar un objeto? Así es, ¡ oh, qué bien! Después de todo, podemos crear varios objetos Servo y administrar cada uno por separado usando los mismos métodos, pero cada objeto tendrá un conjunto individual de configuraciones que se almacenan en algún lugar dentro de él. Este es un enfoque orientado a objetos, que le permite crear programas multinivel muy complejos sin experimentar demasiada dificultad.

Las clases son muy similares a las estructuras, tanto en declaración como en uso, pero una clase es una unidad mucho más poderosa del lenguaje: si en una estructura almacenamos variables de diferentes tipos bajo el mismo nombre, entonces en la clase almacenamos no solo variables, sino también funciones propias de la clase. Las funciones dentro de una clase, por cierto, se llaman métodos.

Dentro de la clase

Bueno, ¡echemos un vistazo dentro del clase y veamos cómo funciona esto! Comencemos con cómo se declara la clase: usando la palabra clave class.

class / * nombre de la clase * / // el nombre de la clase generalmente se escribe con mayúscula  
{
  private :
  // lista de propiedades y métodos para usar dentro de la clase
  public :
  // lista de métodos disponibles para otras funciones y objetos de programa
  protected :
  // lista de herramientas disponibles para herencia
} ;

Muy similar a la estructura (struct), ¿verdad? Así es, y el objeto se crea de la misma manera:

nombre_clase nombre_objeto;     // crea un objeto
nombre_clase nombre_objeto [ 10 ] ; // crea una matriz de objetos

Veamos ahora un ejemplo de clase:

#define true 1
#define false 0

class OtraClase
{
	bool privateVar; //Acceso privado por defecto
	
	public: //Miembros públicos
	void setPrivateVar(bool newval); //Método Set
	bool getPrivateVar(void);	 //Método Get
};

void OtraClase::setPrivateVar(bool newval)
{
	privateVar = newval;
}

bool OtraClase::getPrivateVar(void)
{
	return privateVar;
}

int main()
{
	OtraClase obj;
	
	obj.setPrivateVar(true);
	obj.setPrivateVar(false);
		
	return 0;
}

Qué público y privado? Estos son especificadores de acceso de miembros de clase. Miembros públicos  (public) son miembros de una estructura o clase a la que se puede acceder desde fuera de la misma estructura o clase (desde una instancia, por ejemplo). Miembros privados  (private), son miembros de una clase a la que solo tienen acceso otros miembros de la misma clase (solo dentro de la clase). Hay algo mas protegido, pero no lo consideraremos, ya que es poco probable que le resulte útil. Si domina el resto, lea sobre la herencia de clases, el tema es vasto y muy complejo.

En realidad, vemos en el «acceso público» todos aquellos métodos que se pueden utilizar cuando se trabaja con una clase. Esto es muy conveniente, porque no necesita buscar en Google la documentación; todo está escrito en la descripción de la clase. Los métodos se declaran de la misma manera que las funciones ordinarias, lo discutimos en detalle en la lección anterior. Los miembros privados en la clase son variables, por sus nombres puedes entender lo que almacenan en sí mismos. No tenemos acceso a estas variables “desde el boceto”, solo los métodos de clase pueden leer estas variables.

También en una clase se puede ver la palabra constructor; esta es otra cosa que puede estar en la clase. El constructor te permite inicializar parámetros privados al crear un objeto y, en general, en esencia: un constructor es una función llamada al momento de crear un objeto .

Creemos nuestra propia clase y, en su ejemplo, analizaremos algunas de las características.

Escribir una clase

Intento seguir la secuencia de presentación del material, por eso en esta lección intentaremos no usar algo que aún no haya salido en las anteriores. Un ejemplo completo de la creación de una clase de biblioteca se publicará por separado, y ya no nos limitaremos. Así que hagamos una clase que almacene el color y el brillo como variables privadas y le permita obtener o cambiar este color / brillo usando métodos.

class Color { // class Color    
  public:
    Color () ;     // constructor
  private:
    byte _color; // variable de color
    byte _bright; // variable brillo 
} ;

Hemos creado una clase llamada Color que tiene un constructor y variables _color y _brillante. Qué es importante saber:

  • Se acostumbra escribir el nombre de la clase con mayúscula inicial para separarlo de las variables (que, como recordatorio, se suelen escribir con minúscula)
  • El nombre del constructor debe coincidir con el nombre de su clase (¡es importante!)
  • Las variables privadas generalmente se nombran comenzando con un guion bajo, _color.
  • Puede que no haya ningún constructor, entonces el compilador lo creará él mismo, el nombre coincidirá con el nombre de la clase

Continuemos. Hagamos que al crear un objeto, pueda especificar un valor _color. Para esto necesitamos un constructor que tome parámetros. Es decir, como una función normal:

class Color { // clase Color  
  public:
    Color ( byte color ) { // constructor 
      _color = color;  // recuerda
    }
  private:
    byte _color; // variable de color
    byte _bright; // brillo variable
} ;
Color myColor ( 10 ) ; // crea un objeto myColor con un valor

Hicimos del constructor una función que toma un parámetro de tipo byte y lo asigna a una variable de clase _color. Escribimos la implementación de la función dentro de la clase, esto es importante, porque se puede hacer afuera. Después de llamar al constructor (creando el objeto) la variable color eliminada de la memoria (era local), pero su valor permanece en nuestro _color. Bueno. Dejemos que el usuario también establezca el brillo al crear el objeto. El conocimiento sobre las funciones sobrecargadas de la lección anterior nos ayudará aquí.

class Color { // clase Color  
  public:
    Color ( byte color ) { // constructor 
      _color = color;  // recuerda
    }
    Color ( byte color, byte bright ) { // constructor 
      _color = color;  // recuerda
      _bright = bright;
    }
  private:
    byte _color; // variable de color
    byte _bright; // variable brillo 
} ;
Color myColor ( 10 ) ; // crea un objeto myColor con un valor
Color myColor2 ( 10, 20 ) ; // ¡establece color y brillo!

Ahora tenemos dos constructores, y al crear un objeto, el programa elegirá cuál usar. Regresemos nuestro constructor vacío para que podamos crear un objeto sin inicializar parámetros:

class Color { // clase Color  
  public:
    Color () ;
    Color ( byte color ) { // constructor 
      _color = color;  // recuerda
    }
    Color ( byte color, byte bright ) { // constructor 
      _color = color;  // recuerda
      _bright = bright;
    }
  private:
    byte _color; // variable de color
    byte _bright; //variable brillo 
} ;
Color myColor ( 10 ) ; // crea un objeto myColor con un valor
Color myColor2 ( 10, 20 ) ; // ¡establece color y brillo!
Color myColor3 () ; // sin inicialización (¡se necesitan paréntesis!)

Y creemos un objeto de inmediato, aquí nos ayudará algo como la inicialización dentro del constructor. Mira como funciona:

class Color { // clase Color  
  public:
    Color ( byte color = 5, byte bright = 30 ) { // constructor 
      _color = color;  // recuerda
      _bright = bright;
    }
  private:
    byte _color; // variable de color
    byte _bright; //variable brillo 
} ;
Color myColor ( 10 ) ; // crea un objeto myColor con _color (obtén 10, 30)
Color myColor2 ( 10, 20 ) ; // ¡establece color y brillo! (obtenemos 10, 20)
Color myColor3 ; // sin inicialización (obtener 5, 30)

Un constructor en el que a través del operador = la inicialización del valor de la variable local se proporciona si no se pasa como parámetro. Es decir, llamando al constructor al crear myColor3 () no pasamos ningún parámetro, y el constructor tomó los parámetros predeterminados, 5 y 30, y los asigno en color y brillo. También tenga en cuenta que al crear myColor3 no ponemos paréntesis, porque ¡nuestro constructor es universal! Mientras creaba myColor ( 10 ) pasamos solo el color, 10, y el brillo se estableció automáticamente en 30. ¿Y cómo no especificar el color, y si especificar el brillo? Pues no hay forma =) Simplemente crea un nuevo constructor.

Agreguemos dos métodos más: para configurar y leer los valores actuales. Aquí todo es simple: la instalación es similar al constructor y la lectura es simple return:

class Color { // clase Color  
  public:
    Color ( byte color = 5, byte bright = 30 ) { // constructor 
      _color = color;  // recuerda
      _bright = bright;
    }
    void setColor ( byte color ) { _color = color; }  
    void setBright ( byte bright ) { _bright = bright; }  
    byte getColor () { return _color; } 
    byte getBright () { return _bright; } 
  private:
    byte _color; // variable de color
    byte _bright; //variable brillo 
} ;
Color myColor ( 10 ) ; // crea un objeto myColor con _color (return 10, 30)
Color myColor2 ( 10, 20 ) ; // ¡establece color y brillo! (return 10, 20)
Color myColor3; // sin inicialización (return 5, 30)

Ahora al llamar por ejemplo myColor2.getColor() obtenemos el valor 10, como lo configuramos durante la inicialización. Si llamamos myColor2.setColor( 50 ), asigna a la variable privada _color objeto myColor2 el valor 50. En otra llamada myColor2.getColor() ya obtenemos 50. Y funciona de la misma manera con el resto de los objetos el método set / get Bright que escribimos. 

Qué más me gustaría agregar: no siempre es conveniente escribir la implementación de un método dentro de una clase, resulta muy engorroso y la clase deja de estar documentada por sí misma. Recuerde la biblioteca Servo: los métodos están declarados, ¡pero están escritos en otro lugar! Esto se hace en el archivo .cpp, hablaremos de ello en el tutorial sobre creación de bibliotecas. Ahora escribamos la implementación de métodos fuera de la clase; dejaremos la descripción de los métodos dentro de la clase y escribiremos la implementación «debajo», como resultado, la clase permanecerá limpia y clara:

// descripción de la clase
class Color { // clase Color  
  public:
    Color ( byte color = 5, byte bright = 30 ) ;
    void setColor (byte color ) ; 
    void setBright ( byte bright ) ; 
    byte getColor () ;
    byte getBright () ;
  private:
    byte _color; // variable de color
    byte _bright; //variable brillo 
} ;
// implementación de métodos
Color :: Color ( byte color, byte bright ) { // constructor 
  _color = color;  // recuerda
  _bright = bright;
}
void Color :: setColor ( byte color ) { _color = color; } 
color vacío :: setBright ( byte bright ) { _bright = bright; } 
byte Color :: getColor () { return _color; } 
byte Color :: getBright () { return _bright; } 
Color myColor ( 10 ) ; // crea un objeto myColor con _color (obtén 10, 30)
Color myColor2 ( 10, 20 ) ; // ¡establece color y brillo! (obtenemos 10, 20)
Color myColor3; // sin inicialización (obtenemos 5, 30)

Oh, ¿qué es ese doble colon? Doble colon :: es un operador que califica el alcance del nombre al que se aplica. Escribiendo color void :: setColor. le dijimos al compilador que esta función (método) en particular setColor pertenece a la clase Color, y es en realidad la implementación del método allí descrito. Esto significa que puede tener otra función con el mismo nombre, pero no relacionada con la clase. Color, es muy conveniente. Por ejemplo, tal finalización no dará lugar a un error, porque le explicamos al compilador a qué se refiere: la primera función pertenece a la clase Color, el segundo es inútil, solo una función en este documento: void Color :: setColor ( byte color ) { _color = color; } // pertenece a la clase Color void setColor ( byte color ) { color de retorno ; }


Miembros de la clase státic.

Una característica muy interesante son los miembros estáticos de la clase: variables y objetos. Si convierte un miembro de la clase en estático, existirá solo en una instancia para todos los objetos de la clase.

  • Variable: se volverá global para todos los objetos creados. Además, se puede acceder a esta variable directamente en el nombre de la clase, sin la participación de objetos en absoluto. Una variable de clase estática debe declararse por separado de la clase (es decir, creada como un objeto).
  • Función (método): puede acceder a él directamente en nombre de la clase, sin la participación de objetos en absoluto. Importante: un método estático solo puede cambiar variables estáticas, porque no se vincula a un objeto, es decir, ¡no sabe con qué variable de objeto interactuar!
class myClass {
  public:
    void setVal(byte val) { Sval = val; }
    byte getVal() { return Sval; }
    static byte getValStatic() { return Sval; }
    static byte Sval;
};
// asegúrese de crear una variable de clase estática separada
byte myClass::Sval;
// crear dos objetos
myClass myObj1, myObj2;
void setup() {
  Serial.begin(9600);
  // podemos trabajar con un miembro estático sin vincularnos a un objeto
  // indica la pertenencia a la clase a través de ::
  myClass::Sval = 10;
   // imprime 10 en todos los casos
  // a través del método del primer objeto
  Serial.println(myObj1.getVal());
 // a través del método del segundo objeto
  Serial.println(myObj2.getVal());
   // a través de un método estático sin vincularse a un objeto
  // indica la pertenencia a la clase a través de ::
  Serial.println(myClass::getValStatic());
   // cambia Sval mediante un método de cualquier objeto
  myObj2.setVal(50);
  // imprimirá 50 en todos los casos
  Serial.println(myObj1.getVal());
  Serial.println(myObj2.getVal());
  Serial.println(myClass::getValStatic());
}
void loop() {}

Incinerador de basura.

Junto con el constructor de la clase, también hay un destructor (del inglés destruct – destruir), que realiza la acción opuesta: destruye un objeto, lo elimina de la memoria dinámica. Como un constructor, un destructor se crea automáticamente a menos que lo especifiques explícitamente. El destructor también se puede declarar de forma independiente para realizar algunas acciones cuando se destruye la clase, por ejemplo, para liberar memoria dinámica. Un destructor se declara exactamente de la misma manera que un constructor, es decir el nombre es el mismo que el nombre de la clase, sin tipo de datos de retorno. La única diferencia es la tilde ~ antes del nombre. Considere nuestra clase de esta lección, su destructor será ~ Color () ;

Consideremos un ejemplo, al mismo tiempo recordemos el alcance de las variables. Si crea un objeto fuera de las funciones, se creará globalmente y existirá durante toda la duración del programa. Si lo crea dentro de una función o bloque de código, existirá solo dentro de este bloque, es decir, las variables de clase ocuparán memoria durante la ejecución de este bloque de código. Considere esta clase:

class Color {   // clase Color
  public:
    Color() {}; // constructor
    void printHello() {
      Serial.println("Hello");
    };
    ~Color() {    // destructor
      Serial.println("destruct");
    };
    byte someVar;   // algún tipo de variable
  private:
};

Tiene un constructor vacío que imprime el método hello y un destructor. Ejecutemos el siguiente código:

Color myColor3;
void setup() {
  Serial.begin(9600);  
  myColor3.printHello();
}

En la salida del puerto, veremos Hello y listo, porque el objeto es global y no se llamó al destructor durante la operación, porque el objeto no fue destruido.

Agreguemos la creación de un objeto al bloque de funciones. setup() y mira lo que pasa:

void setup() {
  Serial.begin(9600);
  Color myColor3;
  myColor3.printHello();
}
// myColor3 se destruye aquí

El objeto se crea dentro de una función, y al salir de esa función, es decir, inmediatamente después de pasar por la llave de cierre. {, el objeto será destruido, se llamará al destructor y se enviará al puerto destruct.

Cómo y por qué aplicar esto en la práctica: lea la lección sobre la memoria dinámica, en la vida difícilmente le será útil, pero sin ella el ciclo de lecciones no estaría completo. Si se asigna memoria dentro del objeto para algunas acciones, sería bueno liberar esta memoria con el destructor. Como ejemplo, considere la clase estándar string, cuyos objetos son cadenas con caracteres, se ubican en la memoria dinámica, y si creas una cadena localmente, se destruye después de salir del bloque de su función, porque esta está escrita en el destructor:

String::~String()
{
 free(buffer);
}

Entonces, hemos creado una clase paso a paso y hemos estudiado la mayoría de las características de trabajar con clases. Esto completa la sección de programación y comienza la sección de tutoriales básicos de Arduino. ¡Volveremos a clases cuando escribamos nuestra propia biblioteca !


Deja un comentario