Programacion Oop
Programacion Oop
twiter: @rleon1961
TEMA:
Programación OOP
fuente: www.prometec.net
Versión en LATEX:
Ing. Ricardo De León López
Índice
1. Clases y Objetos 2
1.1. Las Clases en Arduino . . . . . . . . . . . . . . . . . . . . . . 2
1.2. Nuestro primer programa con clases . . . . . . . . . . . . . . . 2
1.3. Refinando nuestra Clase: Constructores . . . . . . . . . . . . . 6
1.4. Definiendo fuera las funciones miembros . . . . . . . . . . . . 9
1.5. Clases, Objetos y uso de memoria . . . . . . . . . . . . . . . . 10
1.6. Haciendo resumen . . . . . . . . . . . . . . . . . . . . . . . . . 13
5. La Herencia en C++ 38
5.1. La Herencia en C++ . . . . . . . . . . . . . . . . . . . . . . . 38
5.2. La sintaxis de la Herencia en C++ . . . . . . . . . . . . . . . 39
5.3. Function member Overriding . . . . . . . . . . . . . . . . . . . 44
5.4. Consideraciones finales . . . . . . . . . . . . . . . . . . . . . . 45
5.5. Resumen de la sesión . . . . . . . . . . . . . . . . . . . . . . . 46
1
1. Clases y Objetos
1.1. Las Clases en Arduino
Antes de que empecemos a hablar sobre Clases y Objetos, es importante
insistir en que, la OOP no es tanto un lenguaje de programación diferente,
sino más bien, una manera diferente de organizar tus programas y tus ideas,
de acuerdo con unos principios guı́a que nos permiten modelar nuestro código
de un modo distinto a como lo hemos hecho hasta ahora.
La OOP consiste en organizar tus programas de otra forma, que nos
evite los problemas que mencionábamos en la sesión anterior, pero seguimos
usando C++ con algunos añadidos.
Para definir las Clases, existen una serie de reglas y de nuevas instruccio-
nes, pero por lo demás el lenguaje sigue siendo el de siempre.
La diferencia, es que ahora vamos a empezar definiendo unos entes abs-
tractos que llamamos Clases y que son la base de la OOP.
En esta sesión daremos los primeros pasos con las Clases y su termi-
nologı́a Veremos cómo definir Clases y Objetos y veremos cómo acceder a
las propiedades o variables miembros de la Clase y sus métodos o funciones
miembros.
Escribiremos un par de programas completos que involucren Clases y
veremos cómo usarlas.
Ası́ que poneros cómodos, sujetad el temblor de rodillas y vamos a lı́o.
2
común, y más importante aún, aplicamos el principio de: “Esconder los da-
tos y mostrar los métodos o funciones”.
Iremos hablando más de esto, pero de entrada conviene destacar que si
escondemos los datos, pero proporcionamos las funciones que trabajan con
ellos vamos a reducir drásticamente la posibilidad de que alguien nos la lı́e.
Por eso cuando definimos Clases, veremos que hay partes que son públicas
y otras que son privadas (Y si no se especifica lo contrario son privadas.
Volveremos a esto)
La sintaxis para definir la Clase contador que nos ocupa:
class Contador
{
private:
............
public:
.............
} ;
3
class Contador
{ private:
int N ;
public:
void SetContador( int n)
{ N = n ; }
void Incrementar()
{ N++ ; }
int GetCont()
{ return (N) ;}
} ;
Contador C1,C2 ;
void loop()
{ C1.SetContador(0);
C1.Incrementar() ;
Serial.print("C1 = ") ; Serial.println( C1.GetCont() ) ;
C2.SetContador(0);
C2.Incrementar() ; C2.Incrementar() ; C2.Incrementar() ;
4
Serial.print("C2 = ") ; Serial.println( C2.GetCont() ) ;
No ganaremos premios con este programa, pero a cambio ilustra muy bien
algunos conceptos básicos de la programación con Clases.
El primero es que una cosa es la definición de la Clase y otra distinta la
instanciación. La clase es Contador pero el compilador no asigna memoria
hasta que creamos par de instancias de la misma: C1 y C2. Ahora si que se
crean los objetos.
Una Clase, pero tantas ocurrencias como sean precisas, que no se mezclan,
son distintos objetos.
Hemos escondido las variables miembros, pero proporcionamos las fun-
ciones o métodos necesarios para manejar los objetos, y va a ser difı́cil que
alguien enrede las variables globales porque no existen. ¿Qué te parece?
Podemos crear tantos contadores independientes como queramos, con ab-
soluta certeza de que cada uno está aislado delos demás.
5
1.3. Refinando nuestra Clase: Constructores
La Clase anterior esconde una bomba de relojerı́a, porque el que la use
tiene que ser consciente de que por cada instancia que creemos de Contador,
necesitamos una instrucción que la inicialice:
Contador C1 ;
C1.SetContador(0);
class Contador
{ private:
int N ;
public:
Contador( ) // Constructor
{ N = 0 ; }
void Incrementar()
{ N++ ; }
int GetCont()
{ return (N) ;}
} ;
public:
void Contador( ) // Constructor
6
Usando el constructor, podemos reescribir el programa anterior ası́, sin
problemas:
void loop()
{ Serial.println( C1.GetCont() ) ;
Serial.println("...............");
C1.Incrementar() ;
Serial.print("C1 = ") ;
Serial.println( C1.GetCont() ) ;
Serial.flush(); exit(0);
}
7
C1 = C2 ;
Serial.print("C1 = ") ;
Serial.println( C1.GetCont() ) ;
public:
Contador( ) : N(0) { } // Constructor
public:
Contador( ) : N(0) , M(4) , P(44) // Constructor
{}
Se me escapan las razones por las que algo ası́ ha llegado a ser lo habitual,
pero os hartareis a verlo si revisáis las librerı́as de Arduino, ası́ que yo con
informaros cumplo.
Parece que hay variables que pueden ser inicializadas, pero que son
problemáticas para asignarse por programa (Como por ejemplo las constantes)
y por eso algunos recomiendan seguir este último método siempre que sea
posible.
8
1.4. Definiendo fuera las funciones miembros
Cuando las clases y las funciones miembro son tan pequeñas y sencillas
como en este caso, la forma que hemos visto de definirlas puede valer, pero
en seguida se quedará corta.
Por eso podemos declarar las funciones y variables miembros en la decla-
ración de Clase, y definirlas fuera para mayor comodidad y evitar errores de
sintaxis complicados de detectar.
Vamos a reescribir la clase Contador ası́:
class Contador
{ private:
int N ;
public:
Contador( ) ; // Constructor
void SetContador( int n) ; // Declaracion de funcion externa
void Incrementar() ; // Declaracion de funcion externa
int GetCont() ; // Declaracion de funcion externa
} ;
// ----------------------------------------
void Contador::SetContador( int n)
{ N = n ; }
void Contador::Incrementar()
{ N++ ; }
int Contador::GetCont()
{ return (N) ;}
9
Si editáis cualquiera de las librerı́as de Arduino, encontrareis que ésta es
la forma habitual de programar las clases y librerı́as (Pero mucho ojo, con
cambiar nada por la cuenta que os tiene)
En algún momento tendremos que hablar de cómo se organizan las librerı́as
en diferentes módulos y ficheros, pero aun es un poco pronto.
10
que se ponga inmediatamente de cara a la pared el próximo cuarto de hora,
y haga severo acto de contricción.
Veamos un pequeño ejemplo:
class Contador
{ private:
int N ;
static int Num ;
public:
Contador( ) ; // Constructor
void SetContador( int n) ; // Declaracion de funcion externa
void Incrementar() ; // Declaracion de funcion externa
int GetCont() ; // Declaracion de funcion externa
} ;
Añadimos una variable static llamada Num que llevara la cuenta del nu-
mero de contadores que vamos a crear. He modificado ligeramente las fun-
ciones miembros :
Contador::Contador( ) // Constructor
{ N = 0 ;
++Num ;
}
void Contador::SetContador( int n)
{ N = n ;
++Num ;
}
void Contador::Incrementar()
{ N++ ; }
int Contador::GetCont()
{ return (N) ;}
int Contador::Num_Objetos()
{ return(Num) ; }
Básicamente he modifica el Constructor del objeto para que incremen-
te la variable static Num, incrementándola cada vez que se ejecute (O sea
cada vez que se cree un objeto de esta Clase) y añadido un nuevo método,
Num Objetos(), que nos devuelve el valor de Num.
Si usamos un programa como este:
11
void loop()
{ Serial.println(C1.Num_Objetos());
Serial.flush(); exit(0);
}
int Contador::Num = 0
Hay que tener un poco cuidado cuando definimos una variable static
asociada a una clase, ya que hay que asignarla solo una vez, y fuera de
las funciones miembros porque de lo contrario podemos encontrarnos con
comportamientos extraños.
12
1.6. Haciendo resumen
Bueno yo creo que para esta primera sesión sobre objetos puede valer
ya. He procurado mostrar con el ejemplo más sencillo que se me ha ocurrido,
que programar con objetos es conceptualmente distinto del modo procedural,
pero que tampoco es para tanto.
En lugar de resolver problemas pensando en funciones, buscamos un mo-
delo a partir de objetos a los que vamos definiendo métodos y propiedades,
de una manera muy parecida a como lo harı́amos a base de funciones estruc-
turadas.
La peculiaridad es que encapsulamos esas funciones y propiedades en un
objeto abstracto que las contiene y aı́sla del exterior.
Para quienes podéis pensar que es una manera extraña y más trabajosa
de hacer lo mismo, me gustarı́a haceros alguna consideración.
En primer lugar, ciertamente puede haber algo más de trabajo en plani-
ficar y diseñar las Clases, cuando el programa a desarrollar es pequeño, pero
en cuanto el programa crece la ventaja se invierte, porque defino la clase una
vez y la utilizo las veces que requiera.
En un ejemplo en el que el número de instancias de un objeto crezca,
la ventaja a favor de la OOP es abismal. Menor código, mejor encapsulado,
disminución de errores.
Está también la cuestión de la reutilización del código, que con una clase
es automática, mientras que con una colección de funciones hay que andar
con tiento.
Los objetos se parece mucho a la forma en como pensamos en nuestro
cerebro y eso nos ayuda a desarrollar mejores programas y más seguros.
Para programas muy pequeños quizás no compense, pero a medida que
la complejidad crece, es más seguro dedicar un tiempo a esa planificación a
la que tan reacios somos los amigos del “Tu dispara y pregunta luego”.
13
programar vuestros Arduinos.
¿Qué no? Ya lo creo que sı́, pero no os habéis dado cuenta porque el
concepto es tan natural que ni siquiera solemos percibirlo, salvo haciendo un
esfuerzo mental.
Y para que veáis que no os engaño vamos a empezar con algunos casos
que deberı́an haberos disparado todas las alarmas y que sin embargo, os han
parecido completamente normales desde el minuto uno.
Quizás empezando ası́, os daréis cuenta de que aunque no sabı́as como se
llamaba, me creeréis cuando os digo que habéis estado usando el Polimorfismo
de un modo natural desde que empezasteis con Arduino C++.
Por ejemplo lleváis mucho tiempo usando la función Serial.println(), que
no es nada de sospechosa de veleidades extravagantes y sin embargo tiene un
comportamiento sorprendente . ¿No veis nada raro en estas lı́neas?
Serial.println( 5) ;
Serial.println( 3.1416 ) ;
Serial.println(\ Buenos dı́as") ;
Insisto, ¿No veis nada sospechoso ahı́? Eso es porque estáis tan acostum-
brados a ello que no es fácil ver la trampa.
Según lo que hemos aprendido hasta ahora, una función solo puede acep-
tar un tipo definido de parámetros. ¿Qué demonios es eso, de pasar a una
función un int un float o un String según se me ocurra?
¿Si el parámetro es int. . . Porque me acepta que le pase un float o String?
Aquı́ está pasando algo raro. ¿Por qué nuestro compilador, siempre tan ama-
ble el, no nos devuelve un ladrido diciendo que te den?
Lo lleváis haciendo desde siempre pero es imposible, ¿Por qué funciona?
¿Serias capaz de programar una función ası́? ¿A que no?
Y eso, queridos amigos, en una función que habéis estado usando hasta
hartaros sin pensar ni por un momento que era imposible (¿Creı́as que os
engañaba?)
El misterio está precisamente en una caracterı́stica inherente a C++ y
que no existı́a en C, y no es otra que una caracterı́stica llamada function
overloading.
14
Y el misterio está en que no existe una única función println(), sino que
las lı́neas anteriores invocan 3 funciones completamente diferentes. . . que se
llaman igual.
¡ Queee ¡ ¡Venga ya!
Normalmente aquı́ aparecen frases del tipo: “Todo el mundo sabe que
dos funciones distintas no pueden llamarse igual, lo mismo que dos variables
diferentes no pueden tener el mismo nombre”.
Veamos. Si intento algo ası́:
int var = 0 ;
String var = "Buenos dias" ;
15
Serial.println(Duplica(5));
Serial.println(Duplica(3.1416 )) ;
Serial.println(Duplica("Hola."));
Lo lógico es que el compilador diga que ni de coña se traga esto. “Las
funciones tienen que llamarse distinto y punto”. Pero resulta que no, ya ves.
Siempre consiguen sorprendernos:
16
Y la respuesta es que si, y os habéis hartado a usarlo sin daros cuenta
tampoco. ¿Adivináis que puede ser? Os doy una pista: En el último programa
usamos el Overloading de algo más que las funciones, pero de esto hablaremos
en la próxima sesión.
De momento quiero volver a la clase Contador que definimos en la sesión
previa, para darle más vueltas.
class Contador
{ private:
int N ;
public:
Contador( ) ; // Constructor
void SetContador( int n) ;
void Incrementar() ;
int GetCont() ;
} ;
Contador::Contador( ) // Constructor
{ N = 0 ; }
void Contador::Incrementar()
{ N++ ; }
int Contador::GetCont()
{ return (N) ;}
Bien, a lo nuestro. No está mal para ser nuestra primera Clase, pero es
manifiestamente mejorable. Por ejemplo, todos nuestros contadores se ponen
a cero mediante el Constructor, lo que ha sido una mejora con respecto a
tener que inicializarlo a mano, pero. . . ¿Qué hago si necesito un contador
que empiece en digamos 129 o cualquier otro valor, claro?
17
Puedo usar el método SetContador(), pero nuestros amigos nos mirarán
con desprecio por usar semejante solución, ası́ que hay que discurrir algo más.
La solución elegante y que hará suspirar a los freakys de tus colegas
es hacer un Overloading del Constructor, que lo acepta sin rechistar como
cualquier otra función.
class Contador
{ private:
int N ;
public:
Contador( ) ; // Constructor
Contador( int k ) ; // Constructor
void SetContador( int n) ;
void Incrementar() ;
int GetCont() ;
} ;
Contador::Contador( ) // Constructor
{ N = 0 ; }
void Contador::Incrementar()
{ N++ ; }
int Contador::GetCont()
{ return (N) ;}
Hemos hecho un Overloading del Constructor de la Clase, Que dicho ası́ suena
muy raro, pero que traducido significa, que podemos declarar dos Construc-
tores diferentes siempre y cuando le pasemos diferente firma parámetros (En
numero o tipo). Si hacemos dos constructores, uno sin parametros y otro que
acepte un int:
18
void loop()
{
C1.Incrementar() ;
Serial.print("C1 = "); Serial.println(C1.GetCont());
cradas son sencillas una a una, pero al ir construyendo una idea sobre otra,
puede haber que dar un paso atrás para coger perspectiva (Y aire).
Recapitulemos.
Definimos una clase llamado Contador que nos permite llevar la cuenta
de lo que se nos ocurra.
Pero no queremos tener que inicializar el contador cada vez que instan-
ciamos un nuevo Objeto (Forma pija de decir que creamos un contador)
19
Para ello Hacemos un Constructor Overloading, o segundo constructor
de modo que pueda aceptar un parámetro al instanciar el contador y
poner a ese valor el contador interno.
mpresionamos a los colegas fijo (De ligar nada, no sirve para eso)
La potencia que este tipo de unión entre las Clases y el Overloading nos
proporciona es impresionante, no tanto para quedarnos con los colegas, sino
para hacer programas más sencillos y comprensibles.
En lugar de usar varias funciones que puedan hacer algo que para nosotros
es lo mismo, nos basta con recordar una. Casi parece lo normal
Vale, esto va cogiendo buena pinta, pero vaya asco de contador que hemos
hecho. Solo se incrementa. ¿Y si a mı́ me apetece que decremente porque voy
a hacer una cuenta atrás, que?
Además C++ siempre ha tenido esa chulada del ++ o el – para variar el
valor de una variable. ¿Podrı́a hacer lo mismo con un objeto?
O más aún, Si tengo dos contadores ¿Puedo sumarlos y obtener un con-
tador con la suma neta de los dos contadores? ¿Y podrı́a restarlos?
Creo que ya adivináis la respuesta. Desde luego que sı́, mediante un Ope-
rator Overloading en lugar de un Function Overloading.
Pero esto, queridos amigos, será el tema de la próxima sesión que por hoy
ya nos hemos complicado suficiente y conviene descansar el cerebro.
20
Por eso me contentaré con decir aquı́ simplemente, que el Polimorfismo
es una cualidad abstracta de los objetos que nos permite usar un interface
único, de métodos y propiedades, en una colección de objetos de distintos
tipos o Clases.
Recordad el ejemplo que comentamos en alguna sesión previa, que existe
un concepto abstracto llamado arrancar que nos resulta natural, para un
motor eléctrico, de gasolina o de diésel.
En la forma en que nuestro cerebro procesa el mundo, las tres objetos
comparten ese método común, y para nosotros es de lo más natural consi-
derarlos iguales, por más que comprendemos muy bien que el procedimiento
fı́sico que arranca esos tres motores es completamente diferente.
Polimorfismo es un concepto abstracto que representa precisamente esa
capacidad de modelizar diferentes sistemas fı́sicos u Objetos, mediante méto-
dos y propiedades comunes, en un concepto abstracto (Y jerárquicamente su-
perior) de motor que comparten métodos como Arrancar, Frenar o Acelerar
y propiedades como Potencia o Velocidad.
La Clase Motor en abstracto, es independiente de la tecnologı́a que se
emplea en un caso concreto y sigue siendo válida cuando se desarrollen otros
tipos de motores en el futuro.
Si queréis profundizar en el tema, no tendréis dificultad en hallar docu-
mentación en Internet, pero os recomiendo que si esta es vuestro primera
aproximación a la OOP, evitéis hacerlo hasta que hayáis asentado e interio-
rizado bastante más el asunto.
21
sintaxis y sobre todo nos centramos en el Function Overloading, ya que nos
daba una ventaja importante de cara a usar un nombre único de función,
para varis cosas que en principio serian diferentes.
La ventaja de esto es que resulta mucho más fácil de recordar y más
sencillo de utilizar porque encaja bien con nuestra forma de procesar las
ideas.
Pero una vez que abrimos la caja de Pandora con el Overloading, re-
sulta muy complicado cerrarla, porque en cuanto te acostumbras a la idea,
empiezas a hacerte muchas preguntas raras, del tipo de ¿Y qué más puedo
sobrecargar? Y aquı́ es cuando la cosa se lı́a.
Porque no solo se pueden sobrecargar las funciones, sino también los ope-
radores para que hagan cosas diferentes en función del tipo de los operadores.
No creo que tenga que insistir mucho para que me creáis si os digo que la
suma de dos enteros no se parece (A nivel de procedimiento) a la de dos float,
y lo mismo pasa con +, -, * y / por poner un caso.
Los operadores invocan distintos procedimientos en función del tipo de
los operandos, y nunca es más evidente que cuando hacemos:
22
En la primera categorı́a, Unary Operators, están los operadores de incremen-
tar y decrementar ++ y –, tanto en su versión prefijo como sufijo (++i, i++)
y además la negación y el sı́mbolo negativo – cuando se aplica a un número
para cambiarle el signo. En la categorı́a de Binary Operators tenemos +, -,
*, /,
Esto es importante porque vamos a empezar viendo como se hace el Ope-
rator Overload de los Unary Operators (No corráis cobardes).
class Contador
{ private:
int N ;
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
void SetContador( int n) ;
void Incrementar() ;
int GetCont() ;
} ;
Hemos reescrito los constructores para tener una notación más compacta.
Bien no está mal. Podemos inicializar los objetos de Contador, con y sin
parámetro, lo que es un avance y nos permite escribir tal y como veı́amos en
la última sesión algo ası́:
Lo que resulta bastante fácil de leer, y cómodo de usar, pero ya que estamos
(Ay Dios) nos preguntamos si se podrı́an hacer algunas cosas normales en
C++ como esto:
23
++C2 ;
En lugar de nuestra forma actual:
C2.Incrementar() ;
Que es como un poco raro de leer. ¿Serı́a posible? Intentadlo y veréis lo que
dice el compilador. Recordad que dijimos que crear una Clase es como crear
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
void SetContador( int n) ;
int GetCont() ;
void operator ++ (); // Aqui esta ++
} ;
24
Después hemos definido la función que el operador ++ aplicará y de paso
eliminamos la función Incrementar() que aunque útil, era un asco de usar. Si
ahora hacemos esto:
Contador C1(10) ;
++C1 ;
Serial.println(C1.GetCont());
mos definido como void el resultado del operador ++ y no podemos hacer que
el resultado void, se asigne a un objeto de la Clase Contador, y naturalmente
el compilador se pone atacado en cuanto lo ve.
Para resolver eso, vamos a necesitar que lo que devuelva el operador ++,
sea un objeto de la Clase Contador, y para ello tenemos que definir la función
ası́:
Contador Contador::operator ++()
{ return Contador (++N); }
25
Y ahora si que es posible hacer:
Contador C1, C3(10) ;
C1 = ++C3 ;
Serial.println(C1.GetCont());
Que aunque lo hemos hecho con mucha facilidad, conviene fijarse en un par
de cosas:
De Una función puede devolver un objeto tranquilamente. Algo que
hasta ahora no habı́amos planteado pero que es bastante frecuente. En
este caso es un objeto del tipo Contador.
En este caso el objeto que devolvemos es un objeto temporal que ni
siquiera tiene nombre y que se calcula sobre la marcha, para devolverlo
a quien invoque el operador ++.En este caso se asigna a C1 y el Objeto
temporal se desvanece sin más, sin haber llegado siquiera a bautizarlo.
Para que este método que hemos usado funcione necesitamos haber
hecho un Constructor Overloading que nos permita crear un objeto
tipo y pasarle el valor que deseamos al crearlo.
Vale, es un buen momento para tomar aire y volver a leer despacio lo de
arriba, porque aunque la operación es sencilla y parece sencilla tiene un
fondo importante, y de nuevo, muchos conceptos mezclados.
26
Contador C1, C3(10) ;
C1 = C3++ ;
Serial.println(C1.GetCont());
Serial.println(C3.GetCont());
El resultado es el que cabı́a esperar:
27
Cuando instanciamos C1, cualquier función miembro que reclame el operador
this, recibe un puntero a la dirección de memoria que almacena sus datos,
que por definición es una la dirección del objeto C1 (No de la definición de
la clase ).
Si recordáis como trabajábamos con punteros, podremos escribir la fun-
ción de Overloading del operador ++, de este modo (Coged aire):
const Contador &Contador::operator ++()
{ ++N;
return *this ;
}
Antes de nadie salga corriendo, dejad que me explique.
Definimos la función operator ++ como tipo Contador porque va a
devolver un objeto de este tipo. (Esta parte ya estaba dominada, re-
cordad)
28
3.5. Resumen de la sesión
Vimos la diferencia entre operadores unitarios y binarios.
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
void SetContador( int n) ;
int GetCont() ;
const Contador &operator ++ ();
} ;
29
{ N = n ; }
int Contador::GetCont()
{ return (N) ;}
30
Contador C3 = C1 + C2 ;
class Contador
{ private:
int N ;
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
void SetContador( int n) ;
int GetCont() ;
const Contador &operator ++ ();
Contador operator ++ (int) ;
Contador operator + ( Contador &) ; // Pasamos una referencia a un con
} ;
void loop()
{ Contador C1, C2(10), C3(11) ;
C1 = C2 + C3 ;
Serial.println(C1.GetCont());
Serial.flush(); exit(0);
}
31
Contador C1, C2(12) ;
if ( C1 > C2 )
Serial.println( \Mayor");
else
Serial.println( \Menor");
Contador C1 = 12, C2 = 6 ;
if (C1 > C2 )
Serial.println("SI");
else
Serial.println("NO");
32
bool operator > (Contador Cont)
{ return ( N > Cont.N ) ? true : false ; }
33
la sobrecarga de operadores en un lı́mite sensato y aplica la regla de que si
dudas de si algo es sensato, entonces seguro que no lo es.
Cuando programes ten piedad de quien tenga que leer tu código (Que
probablemente serás tú además) y utiliza la sobrecarga para hacer los pro-
gramas más fáciles de leer, no para impresionar a tus colegas. Se trata de
evitar leerse el manual y que la lectura del código se comprenda de modo
natural.
En el ejemplo de suma que hemos hecho arriba a cualquiera sin leer un
manual se le puede ocurrir intentar la suma del modo que lo hemos hecho.
34
La razón estriba en que ha supuesto que queremos convertir un int a
Contador y es lo bastante astuto para buscar un constructor que requiera un
único int como argumento, y lo ha encontrado. Por eso ha sabido cómo hacer
una conversión de tipo automática.
O dicho de otro modo ha supuesto, mediante la firma de la función so-
brecargada lo que pretendı́amos, y lo ha aceptado sin rechistar, pero tened
cuidado con las conversiones automáticas porque pueden daros más de una
sorpresa.
Y para mayor sorpresa aun, esto otro también va a funcionar (por increı́ble
que parezca) con un float:
Contador C1 = 5.7F ;
Contador C1 = 12 ;
int i = C1 ;
35
Mucho cuidado con estas conversiones que no siempre son tan evidentes
y la cosa se puede complicar, pero aquı́ estamos para presentaros un
ejemplo sencillo de algo que os puede hacer falta.
Para especificarle al compilador que queremos asignar a un int el valor con-
tenido en N, podemos usar la instrucción :
operator unsigned int()
{ return (N) ; }
Donde os conviene fijaros en que la sintaxis es un poco extraña. Las conver-
siones de tipos no se declaran con tipo de retorno, aunque devuelva uno (A
mı́ no me miréis)
Y ahora nuestro programa ha definido al compilador como convertimos
de int a Contador (Mediante un Constructor) y como convertir a int un
Contador, ¿Qué os parece?
Contador C1 = 12 ;
int i = C1 ;
Serial.println(i);
La salida es esta: No puedo resistirme aquı́ a una pequeña maldad, que con-
siste en que probéis algo aparentemente inocente. Con este mismo programa
haced lo siguiente:
36
Contador C1 = 12, C2 = 6 ;
if (C1 > C2 )
Serial.println("SI");
else
Serial.println("NO");
Tal y como vimos en algún programa anterior la respuesta es, tal y como
esperábamos: Nada de particular en esto ¿Noo? El problema radica en que en
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
int GetCont() ;
const Contador &operator ++ ();
Contador operator ++ (int) ;
Contador operator + ( Contador &) ; // Pasamos una referencia a un co
operator unsigned int()
{ return (N) ; }
} ;
¿Pero entonces cómo es posible que funcione?
De nuevo la respuesta está en las suposiciones con las que C++ trata de
ayudarnos que van normalmente en la buena dirección pero que os pueden
jugar malas pasadas si no estáis sobre aviso.
37
Como C++ sabe cómo convertir nuestros Contadores a una variable tipo
int que conoce bien, ha supuesto que podrı́a hacer la comparación ası́, en
tanto en cuanto no le declaremos especı́ficamente otra forma de hacer la
conversión.
Ası́ pues tened un poco de cuidado hasta que os acostumbréis, porque algunas
de estas suposiciones pueden acabar siendo un tiro en el pie.
5. La Herencia en C++
5.1. La Herencia en C++
Hemos ido viendo en las sesiones previas la sintaxis para crear y trabajar
con la OOP y las clases en un entorno de C++. Pero la programación orien-
tada a objetos es mucho más que las clases, y en esta sesión veremos otra de
sus mayores ventajas: La herencia.
Si recordáis hemos ido insistiendo en que una de las ventajas de definir
Clases en nuestros programas era aprovechar una de las caracterı́sticas de
la OOP, llamada data hiding, que significa esconder las variables globales
dentro de las propias clases en la medida de lo posible para evitar que un uso
atolondrado de las mismas cause problemas globales de difı́cil detección.
Mientras que la programación estructurada separa por completo los datos
y las funciones que los manejan, la idea básica detrás de la OOP es unir ambos
conceptos en un único objeto llamado clase,
De ese modo, encapsulamos los datos y los métodos, dentro de las clases
y mediante las instrucciones public y private, podemos limitar el acceso de
ambas a programadores atolondrados y por ende, limitamos su capacidad de
causar daños inadvertidos.
38
Pero la OOP dispone de más medios de limitar esos daños mediante otra
capacidad que se llama Herencia, de no menor importancia, y a la que vamos
a dedicar esta sesión.
Y para ello tenemos que volver a hablar del santo grial de la programación:
La reusabilidad del código.
A medida que el software se iba haciendo más complicado y los proyectos
más descomunales, cualquier sistema que nos permita usar un código que ya
tenı́amos escrito y reusarlo, redunda en una mayor rapidez en el desarrollo y
por tanto en mayores beneficios (La pasta manda, como siempre)
Pero copiar y pegar código no es una buena solución, porque al final
siempre hay que modificarlo un poco para adaptarlo y la ventaja que tenı́amos
de usar un software probado se pierde al modificarlo, ya que a la primera de
cambio aparecen nuevos problemas de depuración con los que no se contaba
y que rápidamente añaden horas y coste a un proyecto que parecı́a chupado
hasta convertirlo en ruinoso.
Por eso, los directores de equipos de programación con cierta experiencia,
son alérgicos a modificar programas probados (Y depurados con sangre) y
acaban forzando a procedimientos de calidad que impidan estas prácticas, lo
que está muy bien, pero al final, lo que te ahorras en errores te lo gastas en
burocracia.
Y por eso la el concepto de herencia de la OOP, es una magnifica solución
a este problema y ha sido adoptada por cualquier departamento de un cierto
tamaño. Porque mejora la reusabilidad del código probado, y a la vez te
impide tocarlo, evitando el interminable circulo sin fin, de modificar, depurar
y vuelta empezar.
Veamos cómo.
39
Pero en el mundo real, los programadores suelen querer cobrar por su
trabajo (Aunque os resulte increı́ble) y no suelen darte el código fuente
de sus programas, con lo que tenemos mal para modificarlos.
DOS. Aun cuando dispongas del código fuente, el jefe que dirige el pro-
yecto en el que trabajas, tiene a otros 45 programadores a su cargo y te
dejará muy claro con un par de ladridos, lo que piensa de que modifiques
programas que ya funcionan y se usan en otros sitios (Normalmente a
gritos).
Motivo por el que si desear seguir cobrando tu cheque a fin de mes, te conviene
buscar una solución alternativa. Y para eso está la herencia.
El método aprobado es derivar una nueva clase de una que ya existe.
Esto hace que la clase derivada herede todas las caracterı́sticas y métodos
de la Clase Base sin tocarla y ahora podamos añadir lo que nos interese,
garantizando que la Clase original permanece inalterada.
Tened en cuenta que tocar una Clase en la que se apoyan otros pro-
gramas, puede suponer un lı́o mayúsculo, ya que cualquier pequeña
diferencia con el original puede suponer una mirı́ada de problemas en
otros programas que ya estaban probados y con los que ahora hay que
volver a empezar.
Vamos a empezar definiendo nuestra clase Contador para después ver como
derivamos una clase CountDown de ella. Empecemos definiendo una Clase
de base sencilla:
class Contador
{ private:
int N ;
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
int GetCont() ;
} ;
int Contador::GetCont()
{ return (N) ;}
const Contador &Contador::operator ++() // Prefix Operator
{ return Contador( ++N) ; }
40
Queremos definir una nueva clase que se llame CountDown derivada de Con-
tador y añadirle una función de decremento. Para ello lo primero es ver como
derivamos una clase de otra. La sintaxis es esta:
Class CountDown : public Contador // Es una clase derivada
{ public:
Counter Operator {()
{ return Counter(--N) ;
}
En la primera lı́nea declaramos una nueva clase CountDown que deriva de
Counter y es de acceso público, y despues definimos un prefix operator para
decrementar la variable interna. Aunque la sintaxis es buena, el compilador
no tragarı́a con esto. ¿Adivináis porque?
Si te fijas en la definición de Contador, hemos definido N, el contador
interno, como private, y eso significa que no permitirá el acceso a ninguna
función externa a la clase Contador (Incluido CountDown), lo que nos hace
imposible acceder desde la nueva clase derivada.
Pero que no cunda el pánico. Para que podamos acceder a propiedades o
métodos internos desde clase derivadas (Pero no desde cualquier otro medio),
necesitamos definirlo no como private, sino como protected:
class Contador
{ protected: // Aqui esta el truco
int N ;
public:
Contador( ) : N(0) {} // Constructor
Contador(int k ) : N(k) {} // Constructor
int GetCont()
{ return (N) ; }
41
No seré yo quien intente mediar en semejante refriega, pero es evidente
que una propiedad como protected es menos segura que como private,
pero las ventajas compensan el riesgo en muchas ocasiones y en la vida
no hay nada perfecto.
void loop()
{ CountDown C1 ;
++C1; ++C1; ++C1;
Serial.println (C1.GetCont()) ;
--C1 ; --C1 ;
Serial.println (C1.GetCont()) ;
}
Obtendremos una salida similar a esta: Que requiere una cierta explicación.
42
usarlas sin problema, lo que le confiere una potencia inusitada para definir
jerarquı́as conceptuales.
Pero hay otra cuestión sorprendente implı́cita en ese mismo programita.
Y es que el compilador ha inicializado a 0 nuestra instancia C1, sin que exista
un constructor en CountDown ¿Por qué?
La respuesta vuelve a ser que el compilador ha interpretado que al no
haber constructor propio sin parámetros en la clase derivada, debe aplicar
el constructor de la clase base, lo cual puede ser mucho suponer y causar
problemas si no estáis advertidos.
¿Significa eso que puedo hacer entonces algo ası́, Para aprovecharme del
segundo constructor de la clase base?
CountDown C1(25) ;
Contador operator -- ()
{ return Contador( --N) ; }
} ;
CountDown C1(20) ;
43
CountDown( ) : Contador() {}
CountDown(int k ) : Contador(k) {}
Donde especificamos al compilador que cuando se cree una instancia de
CountDown, debe invocar el constructor de la clase base que le indicamos.
La primera podrı́amos omitirla porque ya sabemos que el compilador pro-
porcionara un constructor por defecto, pero es buena polı́tica definirlo aquı́
para evitar sobresaltos.
44
Normalmente la función Overriding se utiliza para que una función de
clases derivados se comporten de forma similar a pesar de ser diferentes,
pero soy incapaz de imaginar ningún ejemplo sensato, con el programa
que nos ocupa.
Contador operator -- ()
{ return Contador( --N) ; }
int GetCont()
{ return(2*N) ; }
} ;
45
Por eso nuestra intención no es ser exhaustivos, sino más bien presentar
los conceptos básicos que os permitan desbrozar el camino inicial y permitiros
seguir aprendiendo por vuestra cuenta.
Y naturalmente solo hemos desvelado la punta del iceberg en lo que se
refiere a la programación orientada a objetos y especialmente en las cuestiones
de herencia, porque se podrı́a seguir hablando indefinidamente sobre el tema,
pero creemos que ha llegado el momento de cortar aquı́ el tema OOP por
ahora.
Por supuesto que hay infinidad de cuestiones como la herencia múltiple, el
polimorfismo o las funciones virtuales que darı́an tema para una interminable
y probablemente desierta disquisición que no nos conducirı́a a nada por ahora.
En mi experiencia no suele ser útil, describir soluciones a problemas que
aún no habéis tenido, y antes de entrar en esos temas, deberéis trabajar
y madurar los conceptos que hemos expuesto hasta aquı́ para desarrollar la
experiencia necesaria para seguir avanzando, aquellos que decidáis seguir este
camino.
Recuerdo aquel chiste en el que al salir de una clase de universidad, un
alumno pregunta a otro que le ha parecido el profesor. Y este responde que
debe ser una eminencia porque no ha entendido nada.
Nuestra aspiración es a ayudar a aprender y no a ganar puntos de cara
a no sé muy bien que otros estamentos. Nuestros amigos saben ya, hace
mucho, que no tenemos remedio y a pesar de todo (Increı́blemente) siguen
invitándonos a cañas de vez en cuando.
Pero sı́ que me gustarı́a aseguraros que no hay nada raro en la programa-
ción orientada a objetos que no podáis aprender. No es para tanto. Simple-
mente es otra manera conceptual de organizar tus programas, y con grandes
ventajas si el proyecto crece.
Presentamos su sintaxis.
46