Actualizado el 21 de Marzo del 2018 (Publicado el 2 de Enero del 2018)
821 visualizaciones desde el 2 de Enero del 2018
106,7 KB
20 paginas
Creado hace 18a (27/04/2006)
Tema 4
Clases y objetos en C++
4.1.
Introducci´on
A lo largo del curso nos hemos encontrado con varias situaciones en las que era necesa-
rio trabajar con datos para los que no exist´ıa un tipo predefinido adecuado. Por ejemplo,
programas que deb´ıan procesar n´umeros racionales, naipes en un juego de cartas, fichas
de clientes, listas de nombres, etc. La soluci´on que hemos adoptado hasta el momento es
definir un nuevo tipo, normalmente una estructura (struct), y definir funciones y proce-
dimientos que procesaran tales estructuras. Esta soluci´on presenta varios inconvenientes
que ilustraremos a continuaci´on.
Supongamos que vamos a escribir un programa C++ en el que necesitamos procesar
fechas. C++ carece de un tipo predefinido adecuado para representar las fechas, de
manera que decidimos definir un tipo TFecha de la siguiente manera:
struct TFecha {
int dia;
// 1..31
int mes;
// 1..12
int anyo;
// 2000...2999
};
Adem´as de definir la estructura TFecha, debemos definir funciones y procedimientos que
soporten operaciones b´asicas sobre este tipo. Por ejemplo, podemos incluir en nuestro
programa las siguientes declaraciones:
// operaciones basicas para el tipo TFecha
bool laborable(TFecha f);
bool festivo(TFecha f);
void manyana(TFecha& f);
1
E.T.S.I. Telecomunicaci´on
Laboratorio de Programaci´on 2
void ayer(TFecha& f);
int dias_entre(TFecha f1, TFecha f2);
Ser´ıa deseable que una vez que hemos definido el tipo TFecha y sus operaciones b´asicas,
este nuevo tipo se pudiera emplear como si fuera un tipo predefinido de C++. Por
desgracia, esto no es as´ı. La soluci´on adoptada presenta una serie de inconvenientes.
En primer lugar, no hay forma de prohibir a otros programadores el acceso a los
componentes de la estructura que implementa el tipo TFecha. Cualquier programador
puede acceder de forma directa a cualquier campo y modificar su valor. Esto puede hacer
los programas m´as dif´ıciles de depurar, pues es posible que estos accesos directos a la
estructura no preserven la consistencia de los datos. Por ejemplo, un programador puede
escribir una funci´on como la siguiente:
void pasado_manyana(TFecha& f)
{
}
f.dia= f.dia+2;
Es f´acil ver que la funci´on pasado_manyana puede dar lugar a fechas inconsistentes como
el 30 de febrero de 2002. El programador ha olvidado que “pasado ma˜nana” puede ser “el
mes que viene” o incluso “el a˜no que viene”. Si todos los accesos directos a los campos
de TFecha los realiza el programador que defini´o el tipo y nos encontramos con una fecha
inconsistente, el error debe estar necesariamente localizado en alguna de las operaciones
b´asicas del tipo.
Otro problema que se deriva de permitir el acceso directo a la estructura es que los
programas se vuelven m´as dif´ıciles de modificar. Supongamos que decidimos alterar la
estructura interna del tipo TFecha modificando el tipo del campo mes, a˜nadiendo un tipo
enumerado para los meses:
enum TMes {enero, febrero,..., noviembre, diciembre};
struct TFecha {
int dia;
// 1..31
TMes mes;
// enero...diciembre
int anyo;
// 2000...2999
};
Si otro programador hab´ıa escrito una funci´on como la siguiente:
2
Clases y objetos en C++
void mes_que_viene(TFecha& f)
{
}
f.mes= (f.mes % 12 ) + 1;
´esta dejar´a de compilar. Si todos los accesos directos a TFecha se han realizado en las
operaciones b´asicas, s´olo ´estas necesitan ser modificadas.
Finalmente, otro inconveniente de definir un nuevo tipo mediante una estructura y
una serie de operaciones b´asicas es la falta de cohesi´on. No hay forma de ver el tipo
TFecha como un todo, como un conjunto de valores y una serie de operaciones b´asicas
asociadas. En concreto, no hay forma de establecer expl´ıcitamente la relaci´on entre el tipo
TFecha y sus operaciones b´asicas. Suponemos que la funci´on festivo es una operaci´on
b´asica del tipo TFecha simplemente porque tiene un argumento de este tipo. Pero, ¿c´omo
sabemos si pasado_manyana es o no una operaci´on b´asica? Y si definimos una funci´on
que toma argumentos de diferentes tipos... ¿a cu´al de esos tipos pertenece la funci´on?,
¿de cu´al de ellos es una operaci´on b´asica?
El prop´osito de las clases en C++ es facilitar al programador una herramienta que le
permita definir un nuevo tipo que se pueda usar como un tipo predefinido de C++. En
particular, las clases de C++ facilitan un mecanismo que permite prohibir los accesos
directos a la representaci´on interna de un tipo, as´ı como indicar claramente cu´ales son
las operaciones b´asicas definidas para el tipo.
4.2. Revisi´on de conceptos b´asicos
4.2.1.
Interfaz vs. Implementaci´on
Al definir una clase deben separarse claramente por una parte los detalles del funcio-
namiento interno de la clase, y por otra la forma en que se usa la clase. Esto lo hemos
hecho en pseudo-c´odigo distinguiendo entre el interfaz y la implementaci´on de la clase:
INTERFAZ CLASE NombreClase
METODOS
...
FIN NombreClase
IMPLEMENTACION CLASE NombreClase
ATRIBUTOS
3
E.T.S.I. Telecomunicaci´on
Laboratorio de Programaci´on 2
...
METODOS
...
FIN NombreClase
El interfaz puede entenderse como las instrucciones de uso de la clase, mientras que la
implementaci´on contiene (y oculta) los detalles de funcionamiento.
4.2.2.
Implementador vs. Usuario
Es muy importante recordar que un programador puede desempe˜nar dos papeles di-
ferentes respecto a una clase: implementador y usuario. El programador implementador
de una clase se encarga de definir su interfaz (cabecera de los m´etodos) y de desarro-
llar los detalles internos de su implementaci´on (atributos y cuerpo de los m´etodos). El
implementador de una clase tiene acceso total a los objetos de esa clase.
Por otro lado, el programador usuario s´olo puede utilizar los objetos de una clase
aplic´andoles los m´etodos definidos en su interfaz. El usuario no tiene acceso directo a los
detalles internos de la implementaci´on.
En las siguientes secciones, veremos c´omo definir e implementar clases en C++ (punto
de vista del implementador) y c´omo usar una clase C++ (punto de vista del usuario).
4.3. Definici´on de clases en C++
Desgraciadamente, la divisi´on entre interfaz e implementaci´on no es tan limpia en
C++ como en el pseudo-c´odigo. Las clases se definen en C++ mediante una construcci´on
class dividida en dos partes: una parte privada (private) que contiene algunos detalles
de la implementaci´on, y una parte p´ublica (public) que contiene todo el interfaz.
class NombreClase {
private:
// implementacion de la clase
// solamente los atributos
// interfaz de la clase
public:
};
En la parte privada de la construcci´on class aparecen s´olo los atributos de la clase y
algunos tipos intermedios que puedan ser necesarios. En C++, la implementaci´on de los
4
Clases y objetos en C++
m´etodos de la clase se facilita aparte. En la parte p´ublica, suelen aparecer solamente las
declaraciones (cabeceras) de los m´etodos de la clase. Por ejemplo, la siguiente es una
definici´on de la clase CComplejo que representa n´umeros complejos:
class CComplejo {
private:
// atributos
double real, imag;
// los metodos se implementan aparte
public:
void asigna_real(double r);
void asigna_imag(double i);
double parte_real();
double parte_imag();
void suma(const CComplejo& a, const CComplejo& b);
};
Los campos real e imag son los atributos de la clase y codifican el estado de un objeto
de la clase CComplejo. Puesto que los atributos est´an declarados en la parte privada de
la clase, forman parte de la implementaci´on y no es posible acceder a ellos desde fuera
de la clase. Su acceso est´a restringido: s´olo se puede acceder a ellos en la implementaci´on
de los m´etodos de la clase.
Los m´etodos que aparecen en la parte p´ublica forman el interfaz de la clase y describen
su comportamiento; es decir, las operaciones que podemos aplicar a un objeto del tipo
CComplejo. En particular, con estos m´etodos podemos asignar valores a las partes real e
imaginaria, leer las partes real e imaginaria, y sumar dos numeros complejos.
4.4.
Implementaci´on de m´etodos en C++
Como comentamos anteriormente, la implementaci´on de los m´etodos de una clase en
C++ se realiza fuera de la construcci´on class {...}. La sintaxis de la definici´on de un
m´etodo es similar a la de la definici´on de una funci´on (o procedimiento), excepto que el
nombre del m´etodo debe estar precedido por el nombre de la clase de la que forma parte:
void CComplejo::asigna_real(double r)
{
}
// cuerpo del metodo...
5
E.T.S.I. Telecomunicaci´on
Laboratorio de Programaci´on 2
Como puede apreciarse, el m´etodo asignar_real no recibe ning´un argumento de tipo
CComplejo. ¿C´omo es posible entonces que este m´etodo sepa qu´e n´umero complejo tiene
que modificar? La respuesta es que todos los m´etodos de la clase CComplejo reciben
como argumento de entrada/salida impl´ıcito el complejo al que se va a aplicar el m´etodo.
Surge entonces la siguiente pregunta: si este argumento es impl´ıcito y no le hemos dado
ning´un nombre, ¿c´omo accedemos a sus atributos? La respuesta en este caso es que
podemos referirnos a los atributos de este par´ametro impl´ıcito simplemente escribiendo
los nombres de los atributos, sin referirnos a qu´e objeto pertenecen. C++ sobreentiende
que nos referimos a los atributos del argumento impl´ıcito. As´ı, el m´etodo asigna_real
se implementa como sigue:
void CComplejo::asigna_real(double r)
{
}
real= r;
donde el atributo real que aparece a la izquierda de la asignaci´on es el atributo del
argumento impl´ıcito. Incluso un m´etodo como parte_imaginaria, que aparentemente
no tiene argumentos, recibe este argumento impl´ıcito que representa el objeto al que se
aplica el m´etodo:
double CComplejo::parte_imaginaria()
{
}
return imag; // atributo imag del argumento implicito
Por otro lado, un m´etodo puede recibir argumentos expl´ıcitos de la clase a la que per-
tenece. Por ej
Comentarios de: Tema 4 Clases y objetos en C++ (0)
No hay comentarios