17- 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 鈥渄esde 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