Publicado el 6 de Febrero del 2019
772 visualizaciones desde el 6 de Febrero del 2019
109,1 KB
20 paginas
Creado hace 18a (27/04/2006)
Tema 4
Clases y objetos en C++
4.1.
Introducción
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úmeros racionales, naipes en un juego de cartas, fichas
de clientes, listas de nombres, etc. La solución 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ón presenta varios inconvenientes
que ilustraremos a continuación.
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ás de definir la estructura TFecha, debemos definir funciones y procedimientos que
soporten operaciones básicas 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ón
Laboratorio de Programación 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ásicas,
este nuevo tipo se pudiera emplear como si fuera un tipo predefinido de C++. Por
desgracia, esto no es así. La solución 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ás 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ón como la siguiente:
void pasado_manyana(TFecha& f)
{
}
f.dia= f.dia+2;
Es fácil ver que la función pasado_manyana puede dar lugar a fechas inconsistentes como
el 30 de febrero de 2002. El programador ha olvidado que “pasado mañana” puede ser “el
mes que viene” o incluso “el año que viene”. Si todos los accesos directos a los campos
de TFecha los realiza el programador que definió el tipo y nos encontramos con una fecha
inconsistente, el error debe estar necesariamente localizado en alguna de las operaciones
básicas del tipo.
Otro problema que se deriva de permitir el acceso directo a la estructura es que los
programas se vuelven más difíciles de modificar. Supongamos que decidimos alterar la
estructura interna del tipo TFecha modificando el tipo del campo mes, añadiendo 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ón como la siguiente:
2
Clases y objetos en C++
void mes_que_viene(TFecha& f)
{
}
f.mes= (f.mes % 12 ) + 1;
ésta dejará de compilar. Si todos los accesos directos a TFecha se han realizado en las
operaciones básicas, sólo éstas necesitan ser modificadas.
Finalmente, otro inconveniente de definir un nuevo tipo mediante una estructura y
una serie de operaciones básicas es la falta de cohesión. No hay forma de ver el tipo
TFecha como un todo, como un conjunto de valores y una serie de operaciones básicas
asociadas. En concreto, no hay forma de establecer explícitamente la relación entre el tipo
TFecha y sus operaciones básicas. Suponemos que la función festivo es una operación
básica del tipo TFecha simplemente porque tiene un argumento de este tipo. Pero, ¿cómo
sabemos si pasado_manyana es o no una operación básica? Y si definimos una función
que toma argumentos de diferentes tipos... ¿a cuál de esos tipos pertenece la función?,
¿de cuál de ellos es una operación básica?
El propósito 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ón interna de un tipo, así como indicar claramente cuáles son
las operaciones básicas definidas para el tipo.
4.2. Revisión de conceptos básicos
4.2.1.
Interfaz vs. Implementación
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ódigo distinguiendo entre el interfaz y la implementación de la clase:
INTERFAZ CLASE NombreClase
METODOS
...
FIN NombreClase
IMPLEMENTACION CLASE NombreClase
ATRIBUTOS
3
E.T.S.I. Telecomunicación
Laboratorio de Programación 2
...
METODOS
...
FIN NombreClase
El interfaz puede entenderse como las instrucciones de uso de la clase, mientras que la
implementación contiene (y oculta) los detalles de funcionamiento.
4.2.2.
Implementador vs. Usuario
Es muy importante recordar que un programador puede desempeñar 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étodos) y de desarro-
llar los detalles internos de su implementación (atributos y cuerpo de los métodos). El
implementador de una clase tiene acceso total a los objetos de esa clase.
Por otro lado, el programador usuario sólo puede utilizar los objetos de una clase
aplicándoles los métodos definidos en su interfaz. El usuario no tiene acceso directo a los
detalles internos de la implementación.
En las siguientes secciones, veremos cómo definir e implementar clases en C++ (punto
de vista del implementador) y cómo usar una clase C++ (punto de vista del usuario).
4.3. Definición de clases en C++
Desgraciadamente, la división entre interfaz e implementación no es tan limpia en
C++ como en el pseudo-código. Las clases se definen en C++ mediante una construcción
class dividida en dos partes: una parte privada (private) que contiene algunos detalles
de la implementación, y una parte pública (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ón class aparecen sólo los atributos de la clase y
algunos tipos intermedios que puedan ser necesarios. En C++, la implementación de los
4
Clases y objetos en C++
métodos de la clase se facilita aparte. En la parte pública, suelen aparecer solamente las
declaraciones (cabeceras) de los métodos de la clase. Por ejemplo, la siguiente es una
definición de la clase CComplejo que representa números 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án declarados en la parte privada de
la clase, forman parte de la implementación y no es posible acceder a ellos desde fuera
de la clase. Su acceso está restringido: sólo se puede acceder a ellos en la implementación
de los métodos de la clase.
Los métodos que aparecen en la parte pública 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étodos podemos asignar valores a las partes real e
imaginaria, leer las partes real e imaginaria, y sumar dos numeros complejos.
4.4.
Implementación de métodos en C++
Como comentamos anteriormente, la implementación de los métodos de una clase en
C++ se realiza fuera de la construcción class {...}. La sintaxis de la definición de un
método es similar a la de la definición de una función (o procedimiento), excepto que el
nombre del método 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ón
Laboratorio de Programación 2
Como puede apreciarse, el método asignar_real no recibe ningún argumento de tipo
CComplejo. ¿Cómo es posible entonces que este método sepa qué número complejo tiene
que modificar? La respuesta es que todos los métodos de la clase CComplejo reciben
como argumento de entrada/salida implícito el complejo al que se va a aplicar el método.
Surge entonces la siguiente pregunta: si este argumento es implícito y no le hemos dado
ningún nombre, ¿cómo accedemos a sus atributos? La respuesta en este caso es que
podemos referirnos a los atributos de este parámetro implícito simplemente escribiendo
los nombres de los atributos, sin referirnos a qué objeto pertenecen. C++ sobreentiende
que nos referimos a los atributos del argumento implícito. Así, el método 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ón es el atributo del
argumento implícito. Incluso un método como parte_imaginaria, que aparentemente
no tiene argumentos, recibe este argumento implícito que representa el objeto al que se
aplica el método:
double CComplejo::parte_imaginaria()
{
}
return imag; // atributo imag del argumento implicito
Por otro lado, un método 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