Contenu connexe Plus de Aldo Hernán Zanabria Gálvez (20) Pooc 11. Programación Orientada al Objeto con C++
Por Antonio Rojo
Breve recorrido histórico
Antes de comenzar daremos un breve repaso histórico al nacimiento del C++. En un principio se
pretendía que fuera un lenguaje distinto del C pero aprovechando todas sus características.
Pero el tiempo lo tornó como una extensión del C o como un avance de éste.
El caso es que su nacimiento se lo debemos al Sr. Bjarne Stroustrup mientras trabajaba en los
laboratorios Bell de New Jersey, allá por el año 1980. En un principio se llamó C con Clases
para posteriormente, llamarse C++.
El mismo Sr. Bjarne reconoció que se basó en el lenguaje C y en el lenguaje Simula67
aprovechando por entero las características del primero y añadiendo la potencia de la
programación orientada al objeto del segundo.
El lenguaje C++ ha sido revisado en dos ocasiones. La primera en 1985 que determinó las
bases generales del hoy conocido C++. La segunda fue en el año 1990 (bastante reciente) en la
que se le añadieron algunas nuevas características como es la herencia múltiple, de la que
carecía en la revisión 1. Es por ello, que a veces se puede ver en la publicidad de algunos
compiladores de C++ el hecho de hacer referencia a cual de las revisiones soportan. No
obstante la mayoría de los lenguajes C++ de hoy soportan la revisión 2.
No obstante la ampliación que C++ hace al lenguaje C no es exclusivamente en la programación
orientada a objetos, sino en otras facilidades para el programador que pueden ayudarnos en
esta ardua tarea. Bjarne Stroustrup diseñó este lenguaje sin perder la flexibilidad que tenía el
lenguaje C y siempre teniendo en cuenta que es el programador quien debe controlar el
programa no el lenguaje.
Quizás sea por esto que sigue siendo el lenguaje de programación más utilizado para cualquier
tipo de programación, ya sea de gestión, matemática, críticas en velocidad, de control de
aparatos, y un largo etc.
La base OOP en C++
La extensión del lenguaje C++ se construye aprovechando las cualidades de su predecesor, el
C. Si además, tenemos en cuenta que las clases no dejan de ser estructuras de datos
asociadas con programas, no es de extrañar que las propias estructuras de C se conviertan en
clases en C++ (en realidad no es cierto, pero lo vamos a ver desde este punto de vista para que
resulte más cómodo para el lector).
Veamos la definición de las estructuras en el lenguaje C.
struct [<nombre estructura>] {
<tipo variable> <nombre variable> ;
...
} [<lista de variables de la estructura>] ;
Podemos recordar los artículos de la revista ClippeRmanía en su curso de C, para ver este tipo
1
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
2. de datos. Ahora nos vamos a basar en que el lector conoce el lenguaje C y solamente vamos a
explicar la extensión C++.
Ahora en C++ este tipo de datos se ve ampliado por una nueva sintaxis para la construcción de
clases quedando la siguiente sintaxis:
struct [<nombre estructura>] {
[<modificador de ámbito>:]
<tipo variable> <nombre variable> ;
...
[<declaración de función miembro> ;]
...
} [<lista de variables de la estructura>] ;
Veamos la gran ampliación que se ha efectuado a este tipo de dato añadiendo dos nuevos
conceptos, como son el Modificador de ámbito y las Funciones miembro.
Veamos por separados cada uno de ellos. En primer lugar veremos lo que significa el
modificador de ámbito. Este modificador señala el nuevo ámbito de las siguientes variables o
funciones miembro de la estructura. Este ámbito no es otra cosa que la posibilidad de acceder a
dichas variables o funciones desde fuera de la estructura, pudiendo ser o public, private o
protected, seguido de dos puntos.
Hay que señalar dos cosas, la primera es que para el tipo de dato struct el ámbito por defecto
es público, y como segunda cosa es que no existe un orden entre la declaración de variables y
las funciones miembro, pudiéndose mezclar entre ellas. Lo normal es que en la utilización de
estructuras se declaren primero las variables públicas seguidas de las funciones con el mismo
ámbito, para luego utilizar el modificador private: y declarar seguidamente las variables y
funciones locales a la estructura. Veamos un ejemplo muy sencillo:
struct point {
int x, y;
void say( void );
void suma( int a, int b );
} ;
Como ya vimos en C una estructura puede contener en su definición como dato tipo variable,
otra estructura. Ahora el concepto resulta un poco mas complejo ya que las estructuras pueden
contener funciones. Sin embargo, no debe preocuparnos, ya que cada estructura sabe en todo
momento cuales son sus funciones miembro. Pero hasta ahora hemos visto como se declaran
las estructuras y sus funciones miembro, pero no el código de dichas funciones. Para ello
debemos tener en cuenta que el código de una función miembro no tiene por qué estar dentro
del mismo fuente, por lo que hay que definirle de alguna forma a que clase pertenece. Para ello
no hay forma mas sencilla que declarar la función precedida del nombre de la clase y seguida de
cuatro puntos (::). El resto de la definición de la función es similar a la declaración de una función
normal en C.
void point::say( void )
{
2
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
3. ...
}
o
void point::suma( int a, int b )
{
...
}
La forma de acceso a los datos de la estructura dentro de las funciones miembro, se efectúa
directamente en la declaración de variables de dicha estructura, sin ser precedidas del nombre
de la estructura. Si completamos la función miembro suma() el resultado sería el siguiente:
void point::suma( int a, int b )
{
x += a;
y += b;
}
La función suma() ya reconoce a las variables x e y como variables de la estructura por lo que
no necesitan ser declaradas ni referenciadas.
Las clases en C++
Hasta ahora solo hemos visto un acercamiento a las extensiones de C++ para las estructuras,
dotándolas de la potencia de las Clases, sin embargo no dejan de ser estructuras, y es así
como se las debe de ver. Aunque la diferencia solo sea de concepto y no de contenido existe
una forma de declaración de clases en C++ siendo similar a las estructuras, pero utilizando la
palabra Clave Class.
Todo lo dicho anteriormente para las estructuras es aplicable a este modo, siendo incluso su
sintaxis muy similar, hasta el punto que solo existen dos diferencias. Una, el cambio de la
palabra clave struct por class, y la segunda, que el ámbito por defecto para las clases es el
privado. La declaración de las clases quedaría de la siguiente forma:
class [<nombre clase>] {
[<modificador de ámbito>:]
<tipo variable> <nombre variable> ;
...
[<declaración de función miembro> ;]
...
} [<lista de variables de la clase>] ;
3
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
4. La misma definición de clase que se dio para la estructura point vendría a ser la siguiente:
class point {
public:
int x, y;
void say( void );
void suma( int a, int b );
} ;
Hay que hacer constar la utilización del modificador de ámbito public: ya que sin el no se podría
acceder ni a las variables x e y ni a las funciones miembro say() y suma().
Como veis, no existe una diferenciación de concepto entre las estructuras y las clases, ya que su
funcionamiento es similar, salvo el ámbito. Sin embargo es conveniente seguir diferenciando
entre estructuras y clases ya que un programa escrito en C es a su vez C++ pero sin aprovechar
las características de éste. No olvidemos que C++ es una extensión o un avance del lenguaje C.
Hemos visto como se definen las clases pero todavía no hemos visto como se definen los
objetos dentro del programa. Pues bien, se definen como cualquier otro tipo de dato de C. El
acceso a los datos y las funciones miembro del objeto es igual al acceso a los datos de las
estructuras utilizando el operador punto (.) seguido de la variable o función miembro. Veamos un
ejemplo utilizando la clase point:
main()
{
int a, b;
point c, d;
...
c.x = 5;
c.y = 15;
c.say();
c.suma( 23, 57 );
}
Para aquellas personas que ya hayan trabajado en C les resultará muy lógico el funcionamiento,
ya que continúa la línea de acceso a estructuras de este lenguaje. Para aquellas personas que
no conozcan el lenguaje C es conveniente que lo estudien antes, ya que estamos basándonos en
el conocimiento del lenguaje C por parte del lector.
Hasta aquí no hemos visto nada más que la definición de las clases, sin embargo no hemos visto
como C++ contempla las características de la programación orientada a objetos. Veamos pues
este comportamiento.
Polimorfismo
Esta característica se podría definir como aquella que permite que dos o más clases tengan
funciones miembro con el mismo nombre pero con distintos resultados para cada clase. O lo que
es lo mismo que una clase X puede tener una función miembro say() al mismo tiempo que una
4
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
5. clase Y puede tener a su vez, una función miembro say(). La función say() que se ejecute será la
correspondiente a la clase a la que pertenece el objeto.
Esto nos permite utilizar nombres lógicos para diversas operaciones sin tener en cuenta si estos
nombres ya han sido utilizados por otras clases, ya que el lenguaje sabrá discernir cual de las
funciones debe ejecutar partiendo del objeto al que se referencia.
Siguiendo con la clase point podríamos definir una clase Window que tuviera también la función
miembro say(), con lo que los nombres lógicos de las funciones no se complican para evitar
duplicidades como ocurría en C.
En C++ el polimorfismo llega mas allá ya que la diferencia de tipos de datos pude dar lugar a
distintas funciones. Un ejemplo sería la posibilidad de pasar dos números largos como
parámetros a la función suma() de la clase point. En principio deberíamos definir una nueva
función miembro para soportar este tipo de parámetros. Sin embargo C++ incorpora lo que viene
a denominarse la sobrecarga de funciones.
Esta sobrecarga consiste en la utilización del mismo nombre de función para dos o más
procesos con diferentes parámetros o retorno de valores. En C clásico éste problema se tenía
que tratar utilizando funciones diferentes. Tal es el caso de la conversión de datos numéricos a
datos tipo carácter ( itoa(), ltoa(), etc ).
Con la sobrecarga de funciones se puede utilizar el mismo identificador para todas las funciones,
siendo el compilador quien ejecute la opción correcta dependiendo de los parámetros puestos.
Las mayor parte de las características que incorpora C++ para las clases no son exclusivas de
estas, sino que se pueden trasladar a todo el lenguaje. Esto quiere decir que el concepto de
sobrecarga de funciones no es exclusivo de las funciones miembro de una clase o estructura
sino que también es accesible para cualquier función definida en el lenguaje.
La definición de este tipo de funciones es similar al resto, aunque se utilice varias veces el
mismo nombre de función en las definiciones. Tiene que haber al menos alguna diferencia en sus
parámetros para poder discernir posteriormente cual se ejecutará dependiendo de los datos
enviados.
Siguiendo con la clase point vamos a definir una nueva función suma() que tenga como
parámetro un objeto de la clase point. El resultado completo sería el siguiente:
class point {
public:
int x, y;
void say( void );
void suma( int a, int b );
void suma( point p );
} ;
...
void point::suma( int a, int b )
{
x += a;
y += b;
}
void point::suma( point p )
{
x += p.x;
y += p.y;
}
5
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
6. En la utilización de los objetos será cuando se diferencie la ejecución de las funciones sin ser
tarea del programador tener que estar diferenciando cual se ejecutará. Esta ventaja nos ayuda
mucho a los programadores a la hora de utilizar varias funciones que efectúan una lógica similar
pero con diferente tipo de datos.
En estos casos es el propio lenguaje quien se encarga de diferenciar la tarea, mientras que para
el programador la tarea lógica sigue siendo la misma. Sobre todo, teniendo en cuenta que el
lenguaje C es uno de los más estrictos en cuanto a la utilización de los tipos de datos al definir
las funciones.
Este lenguaje exige un uso correcto de los parámetros para su funcionamiento. Un ejemplo de
utilización de esta técnica sería el siguiente:
main()
{
point j, k;
...
j.x = 5;
j.y = 25;
k.x = 12;
k.y = 33;
...
j.suma( 10, 20 );
j.suma( k );
...
}
Otra de las características del polimorfismo en C++ es la incorporación de lo que se denominan
funciones virtuales para ligamiento dinámico. Estas funciones virtuales de definen en una clase
anteponiendo la palabra clave, virtual, a la declaración de la función miembro.
Cuando se efectúa la herencia de la clase donde se definieron las funciones virtuales es cuando
se definen las funciones reales. Aquí es cuando se determina en tiempo de ejecución cual es la
función real a ejecutar dependiendo del objeto que se trate. Esta ejecución en tiempo de
compilación no es real, ya que se sabe en tiempo de compilación cual es en realidad la función
definida, ya que el objeto se define como de una determinada clase y es en la definición de la
clase donde ya se encuentra la función virtual redefinida.
En C++ no existe en realidad la característica de ligamiento dinámico ya que todo se resuelve en
tiempo de compilación. Se puede emular utilizando punteros a funciones pero esta manera es
excesivamente compleja, y precisamente va en contra de la principal característica que Bjarne
quería implementar con el lenguaje C++, que es precisamente la sencillez para el programador y
la posibilidad de controlar mejor el código.
No obstante diremos que en C++, la programación orientada a objetos se reducen a meras
definiciones en tiempo de compilación, con lo que el ligamiento dinámico se puede emular de
forma sencilla sin la necesidad de utilizar las funciones virtuales. Pero esto se queda para un
próximo capitulo, ya que primero deberemos ver todas las ampliaciones que el lenguaje C++
hace al antiguo C antes de adentrarnos en pequeños trucos.
6
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
7. Herencia
Otra de las características importantes de la OOP es la herencia. Quizás sea la característica
más importante ya que gracias a ésta es por la que se puede aprovechar todo el código escrito
de una forma sencilla y eficiente.
Ya habíamos comentado que la revisión 1 sólo soportaba la herencia simple mientras que en la
versión 2 ya se incorporó la herencia múltiple. Nosotros vamos a ver la herencia múltiple, sin
embargo quiero resaltar que en mi opinión, la herencia múltiple genera malas practicas de
programación, ya que conduce a conceptos erróneos. Frente a la herencia múltiple está la
técnica de los objetos contenedores y objetos contenidos, que también veremos en próximos
capítulos.
Antes de entrar en la herencia repasemos el ámbito de las variables y las funciones miembro, ya
que este ámbito juega un papel muy importante en la herencia.
Habíamos comentado tres ámbitos, el publico, por el que se pueden acceder tanto desde dentro
del objeto como desde fuera a cualquier variable o función así declarada. El privado, por el que
no se puede acceder a los datos o funciones nada mas que a trabes de las funciones miembro.
Y protegido, que es similar a la anterior pero que juega un papel muy distinto en la herencia.
Para declarar que una clase va ha heredar de otra se expresa en la declaración de la clase
seguido de dos puntos, y posteriormente se expresan la clase o las clases de las que se
heredará.
Cuando hago referencia a las clases también se interpreta para las estructuras incluso
pudiéndose mezclar entre ellas dando lugar a que una clase herede de una estructura, o de una
estructura y una clase, etc. Puede se cualquier convinación que se nos ocurra.
Precediendo al nombre de la clase o estructura de la que se hereda se especifica el modo de la
herencia. Este modo puede ser o bien publico o bien privado. Aquí es donde interviene la
diferencia entre las declaraciones de ámbito private y protected. La sintaxis de definición de
clase quedaría de la siguiente forma:
class <nombre clase> [: [<ámbito>] <clase>[, ...]] {
...
}
En toda herencia los datos privados se convierten en inaccesibles para la clase que hereda,
mientras los públicos y protegidos permanecen del mismo modo, si la especificación fue pública.
En caso de ser privada tanto los públicos como protegidos pasan a ser privados en la nueva
clase. Podemos ver un ejemplo en la tabla 1.
public protected private
public public protected inaccesible
private private private inaccesible
Tabla 1. Herencia
7
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
8. Estas modificaciones de ámbito hay que tenerlas muy en cuenta en la programación, ya que un
dato o función miembro declarada como privada no será accesible desde ninguna clase que
herede de ésta.
El ámbito puede ser opcional, siendo por defecto público. Un ejemplo sencillo podría ser la clase
coordenadas tridimensionales heredando de la clase point.
class coord: public point {
public:
int z;
...
}
En este nivel es donde se podrán redefinir las funciones miembro para dotarla de las
necesidades que la nueva clase va a tener.
Constructores y destructores
Es muy deseable que la inicialización de los datos de objeto se efectúe de forma automática al
crearse éste. Por esta razón las clases de C++ llevan lo que se denomina constructores.
El constructor no es más que una función miembro con el mismo nombre que la clase en la que
se codifica la inicialización del objeto a nuestra medida.
Este constructor o función miembro automática se ejecuta en el momento de la definición del
objeto en la declaración de variables de la función.
Hay que tener en cuenta que tanto en C como C++ las variables locales a la función se
construyen en la pila del procesador reservando espacio únicamente. Este espacio puede estar
sucio por anteriores utilizaciones del mismo con lo que los datos allí reflejados pueden resultar
muy erróneos durante la ejecución de la aplicación si éstos no se inicializan correctamente. Con
las estructuras y los objetos esto sigue siendo válido.
A parte de este tipo de inicialización, también se puede utilizar para el seguimiento de los objetos
creados y obtener un mejor control.
Para ayudarnos en este control existen también las funciones destructoras, que se ejecutan
cuando el objeto desaparece. Estas funciones se ejecutan justo antes de desaparecer el objeto.
Hay que tener muy presente la vida de las variables dentro del lenguaje C y C++ ya que resulta
determinante para la corrección de errores.
Las funciones destructoras tienen el mismo nombre que el de la clase pero precedido del
carácter '~'.
Continuando con el ejemplo podemos ver como quedaría la clase point con la definición del
constructor.
class point {
public:
int x, y;
void say( void );
8
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
9. void suma( int a, int b );
void suma( point p );
void point( void );
} ;
...
void point::suma( int a, int b )
{
x += a;
y += b;
}
void point::suma( point p )
{
x += p.x;
y += p.y;
}
void point::point( void )
{
x = 0;
y = 0;
}
Su utilización sería automática durante la ejecución del programa al definir los objetos de la
clase.
Por ejemplo:
main()
{
point j, k ;
// En esta definición se ejecuta el constructor
...
}
Desde el punto de vista de automatismo resulta muy eficiente la ejecución automática del
constructor, pero se queda un poco cojo, por si solo si no se permite una inicialización diferente
para cada objeto. Esta inicialización diferente se consigue con la parametrización de los
constructores.
Esta parametrización consiste simplemente en la declaración de diversos parámetros para la
construcción del objeto de diferentes formas. Lo mejor es ver esto con un ejemplo para lo que
ampliaremos el ejemplo del constructor de la clase point.
void point::point( int a, int b )
{
x = a;
y = b;
}
De esta forma resulta muy cómodo la inicialización de los objetos en su creación. Pero veamos
9
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
10. como se utilizan los constructores parametrizados. Se pueden utilizar de dos formas siendo su
efecto similar.
main()
{
point a = point( 5, 10 );
...
}
o bien
main()
{
point a( 5, 10 );
...
}
El efecto es el mismo y el código generado también si el compilador está lo suficientemente
optimizado.
Pero aún hay que ir más allá ya que en la mayoría de las ocasiones la construcción de los
objetos se hará con los mismos valores, por lo que resulta bastante redundante tener que
expresarlos en cada creación de un objeto de la clase.
Esto se evita utilizando la definición del valor por defecto para los parámetros. Esta definición se
hace asignando el valor por defecto en la declaración de parámetros de la función. Veamos
como se haría en el constructor de point.
void point::point( int a = 0, int b = 0 )
{
x = a;
y = b;
}
Ahora la utilización del constructor se puede hacer, bien pasando parámetros en la inicialización,
o bien sin ellos con lo que los valores que tomará serán los definidos por defecto.
main()
{
point a( 5, 10 ); // Los valores son: x=5 e y=10
point b; // Los valores son: x=0 e y=0
...
}
10
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS
11. Los valores por defecto en los parámetros no es exclusivo de los constructores de las clases. Es
válido para cualquier función miembro de una clase, o incluso, para cualquier función genérica
definida. Está definida en C++ como una característica general para las funciones, sin embargo,
donde mayor utilidad se le puede ver es precisamente en los constructores de las clases
11
Algoritmo. La revista para el programador de sistemas de bases de datos. http:/www.eidos.es - © Grupo EIDOS