Tema 5
Programación Orientada a Objetos
Curso 2010/2011
Contenido
Introducción
Clases y objetos
Herencia
Herencia múltiple
Genericidad
Colecciones
Definición de estrategias. Punteros a función.
Corrección y robustez en C++
Asertos
Introducción
Creado por Bjarne Stroustrup en los 80.
Diseñado como una extensión de C que incluye
características orientadas a objetos.
Es un lenguaje híbrido.
Ha sido inspiración de lenguajes de programación
posteriores como Java y C#.
A finales de los 90 fue estandarizado: ISO C++
Las librerías del lenguaje son escasas. La librería más
notable es STL (Standard Template Library).
Actualmente, sigue siendo un lenguaje de programación
Clases y Objetos en C++
Este apartado trata:
Definición de clases.
Visibilidad y clases amigas. Espacios de nombres.
Tipos del lenguaje.
Constructores y destructores. Semántica de los objetos.
Semántica de los operadores de asignación e igualdad. Instancia actual.
Métodos y mensajes. Paso de parámetros. Función main.
Clases
Los elementos que definen una clase son:
Atributos, Métodos y Constructores.
A diferencia de Java:
Se separa:
Definición de una clase (fichero cabecera .h) Implementación (fichero código fuente .cpp).
No es obligatorio que los ficheros fuente correspondan
con las clases.
Un fichero puede implementar varias clases.
Para poder utilizar o implementar una clase hay que
Clase Cuenta - Especificación
Cuenta.h Definición de la clase cuenta Banco.cpp #include “Cuenta.h” utiliza Cuenta Cuenta.cpp #include “Cuenta.h” implementa CuentaCuenta.h (1/2)
#include “Persona.h”
class Cuenta {
public:
void reintegro(double cantidad);
void ingreso(double cantidad);
double getSaldo() const;
Persona* getTitular() const;
double* getUltimasOperaciones(int n) const;
static int getNumeroCtas();
Cuenta.h (2/2)
private:const static int MAX_OPERACIONES = 20;
const static double SALDO_MINIMO = 100;
Persona* const titular;
double saldo;
int codigo;
static int ultimoCodigo;
double* ultimasOperaciones;
bool puedoSacar(double cantidad); };
Cuenta.cpp
int Cuenta::ultimoCodigo = 0;
void Cuenta::reintegro(double cantidad){
if (puedoSacar(cantidad)) {
saldo = saldo - cantidad; }
}
void Cuenta::ingreso(double cantidad){
saldo = saldo + cantidad; }
bool Cuenta::puedoSacar(double cantidad){
return (saldo >= cantidad); }
Clases
Los miembros de una clase pueden ser de
instancia
(por defecto) o de
clase
, utilizando el modificador
static
( = Java).
Las
constantes
se declaran
const static
.
Método de consulta
que no va a modificar el estado
se marca utilizando también el modificador const.
Ejemplo: double getSaldo() const;
En la implementación, los nombres de los miembros
son calificados con la clase:
Modificador const en los atributos
El modificador const se utiliza para proteger los
atributos, el estado de los objetos o ambos.
Persona
*
const
titular
;
const protege el atributo.
titular es un atributo de sólo lectura (= final de Java)
const
Persona
*
titular
;
const protege el objeto apuntado por el atributo. El objeto Persona no puede cambiar de estado.
const
Persona
*
const
titular
;
Modificador const en los atributos
Persona
*
const
titular
;
“
Pepito
”
23456789
20
600.0
saldo ultimasOperaciones codigo titularModificador const en los atributos
const
Persona
*
titular
;
“
Pepito
”
23456789
20
600.0
saldo ultimasOperaciones codigo titularModificador const en los atributos
const
Persona
*
const
titular
;
“
Pepito
”
23456789
20
600.0
saldo ultimasOperaciones codigo titularVisibilidad de las declaraciones
El nivel de visibilidad se especifica para un grupo
de miembros (declaraciones):
public: visible para todo el código. private: visible sólo para la clase.
Clases amigas:
En C++ no se existe el concepto de paquete.
En la declaración de una clase podemos indicar el
nombre de otras clases con acceso total a la clase.
El privilegio de amistad de una clase no es transitivo ni
Clases amigas
class Arbol{ private: NodoArbol *raiz; ... … raiz->valor = 50; ... }; class NodoArbol {friend class Arbol;
private: int valor; NodoArbol decha; NodoArbol izda; … };
Estructuras
Unidad modular heredada de C.
En C++ equivale al concepto de clase salvo:
Cambia la palabra class por struct
Por defecto todos los miembros son públicos excepto que se
diga lo contrario.
struct Cuenta{
void ingreso (double cantidad);
void reintrego (double cantidad);
private:
int codigo;
Persona* const titular;
Espacios de nombres
Un espacio de nombres (namespace) es un mecanismo para
agrupar un conjunto de elementos (clases, enumerados, funciones, etc.) relacionados.
Evita la colisión de los nombres de identificadores. El orden de declaración de elementos es relevante. Puede estar definido en varios ficheros.
Es un concepto diferente a los paquetes de Java: No hay relación entre la estructura lógica y física.
Espacios de nombres
Para utilizar un elemento definido en un espacio
de nombres:
Se utiliza el nombre calificado del elemento:
gestionCuentas::Cuenta
Se declara el uso del espacio de nombres:
using namespace gestionCuentas;
No existen elementos privados en un espacio de
Espacios de nombres
namespace gestionCuentas{ class Cuenta { … }; } namespace gestionCuentas{ class Banco { … }; }En Cuenta.h
En Banco.h
Tipos del lenguaje
Tipos de datos primitivos:
byte, short, int, long,float, double,char, bool,
etc.
Enumerados:
enum Estado {OPERATIVA, INMOVILIZADA, NUM_ROJOS}; Objetos.
Punteros:
Cuenta* es el tipo “puntero a Cuenta”
Una variable de tipo Cuenta* puede contener la dirección de
un objeto de tipo Cuenta.
Un puntero a un objeto es un concepto similar a una
Arrays
En la declaración de un array se indica su tamaño ( = C): int array[10];
Si el tipo es una clase, es necesario que la clase tenga un
constructor sin parámetros:
Posicion posiciones[4];
La declaración y uso de un array al estilo Java sería: Cuenta** cuentas = new Cuenta*[10]; Importante: los punteros del array no están
inicializados.
Enumerados
Es un tipo que puede almacenar un conjunto de valores.
El enumerado define un conjunto de constantes de tipo
entero.
Por defecto los valores se asignan de forma creciente
desde 0.
Enumerados – Definición
namespace banco{
enum EstadoCuenta{
OPERATIVA, INMOVILIZADA, NUM_ROJOS
//OPERATIVA == 0, INMOVILIZADA == 1, NUM_ROJOS == 2
}; class Cuenta { … private: … EstadoCuenta estado; }; }
Enumerados – Uso
En Cuenta.cpp
Para referenciar un valor del enumerado no tiene que ir
precedido por el nombre del enumerado. #include "Cuenta.h"
using namespace banco;
…
void Cuenta::reintegro(double cantidad){
if (estado!=INMOVILIZADA && puedoSacar(cantidad)){
saldo = saldo - cantidad; }
Constructores
No se puede asignar un valor inicial a los atributos en el
momento de la declaración a menos que sean const y static (constantes).
Por tanto, no podemos asegurar que los atributos tengan
un valor inicial.
Constructores
Método especial con el mismo nombre que la clase y sin
valor de retorno ( = Java).
Está permitida la sobrecarga (= Java).
Si no existe ningún constructor en la clase, el compilador
proporciona el constructor por defecto (= Java).
Los atributos const se tienen que inicializar en línea.
class Cuenta {
public:
Cuenta (Persona *titular);
Cuenta (Persona *titular, double saldoInicial); …
Constructores - Clase Cuenta
Cuenta::Cuenta (Persona *persona, double saldoInicial) :titular(persona) {
codigo = ++ultimoCodigo;
saldo = saldoInicial;
ultimasOperaciones = new double[Cuenta::MAX_OPERACIONES];
numOperaciones = 0;
estado = OPERATIVA; }
Cuenta::Cuenta (Persona *persona) :titular(persona) {
…
Constructores
A diferencia de Java, no se utiliza la palabra clave this
para la reutilización de constructores.
Soluciones:
Utilizar un método privado que sea invocado desde los
constructores.
Reutilización de constructores
Cuenta::Cuenta (Persona *persona): titular(persona) { inicializa(Cuenta::SALDO_MINIMO);
}
Cuenta::Cuenta (Persona *persona, double saldoInicial)
:titular(persona) { inicializa(saldoInicial);
}
void Cuenta::inicializa(double saldoInicial) {
codigo = ++ultimoCodigo;
saldo = saldoInicial;
ultimasOperaciones = new double[Cuenta::MAX_OPERACIONES];
estado = OPERATIVA; }
Reutilización de constructores
En cualquier función, método o constructor podemos
declarar argumentos con valores por defecto.
Los argumentos por defecto deben aparecer juntos al final
de la lista de argumentos.
Los valores por defecto de los argumentos se indican en la
declaración de la función (fichero .h)
Ejemplo: Cuenta
Definimos un único constructor.
Para el argumento saldoInicial establecemos un valor por
Reutilización de constructores
Fichero cabecera:
Las siguientes creaciones son equivalentes: Cuenta(titular);
Toma como valor del saldo el valor por defecto (100) Cuenta (titular, 200);
El parámetro establece el valor del saldo a 200
class Cuenta {
public:
Cuenta (Persona *titular, double saldoInicial = SALDO_MINIMO); …
Destructores
A diferencia de Java, no existe un mecanismo automático
de liberación de memoria dinámica.
Hay que definir destructores de objetos.
Un destructor es un método sin argumentos con el mismo
nombre de la clase, precedido por ~.
Se invoca automáticamente cada vez que se libera la
memoria del objeto.
Libera memoria y recursos adquiridos por el objeto:
El uso habitual es liberar la memoria adquirida en el
Destructor – Clase Cuenta
Cuenta.h:
Cuenta.cpp:
class Cuenta {
public:
Cuenta (Persona *titular, double saldoInicial= SALDO_MINIMO);
~Cuenta(); … }; Cuenta::~Cuenta(){ delete[] ultimasOperaciones; }
Semántica de los objetos
Los objetos pueden declararse de dos modos:
Por referencia
: se declara un puntero a un objeto.
Los objetos se crean con el operador new (= Java).
Cuenta* cuenta;
Por valor
: la declaración del objeto reserva la
memoria y lo inicializa (llama a un constructor).
Semántica de los objetos
Semántica referencia:
Cuenta* cuenta;
cuenta = new Cuenta(persona, 1000);
El valor null de Java está definido como una constante en
C++: NULL.
El objeto se destruye con el operador delete:
delete cuenta;
Semántica valor:
Cuenta cuenta(persona, 1000);
En la declaración de la variable también se construye el
objeto.
Objetos embebidos
La semántica valor permite definir
objetos embebidos
dentro de otros objetos.
“
Pepito
”
23456789
“
María
”
34546789
20
600.0
saldo autorizado ultimasOperaciones codigo titularObjetos embebidos
La declaración del atributo autorizado es un objeto
embebido. class Cuenta { public: … private: Persona autorizado;
Persona* const titular;
double saldo;
int codigo;
EstadoCuenta estado;
…
Objetos embebidos
La declaración de objetos embebidos sólo es posible si
se cumple una de las siguiente condiciones:
La clase del objeto embebido tiene un constructor sin
argumentos.
Se realiza una inicialización en línea en el
Objetos embebidos
El objeto se inicializa en línea:
La inicialización en línea es obligatoria si la clase del
objeto embebido no tiene constructor sin argumentos.
Cuenta::Cuenta (Persona* persona,
string nombreAut, string dniAut,
double saldoInicial):autorizado(nombreAut, dniAut),
titular(persona) {
codigo = ++ultimoCodigo;
…
Operador de asignación (=)
Cuenta* cta1;
Cuenta cta3;
Cuenta* cta2;
Cuenta cta4;
Semántica:
cta1 = cta2;
copia de punteros (= Java)
cta3 = cta4;
copia campo a campo de los
valores de cta4 en cta3
(copia superficial)
Asignación con semántica valor
“
Pepito
”
23456789
20
600.0
saldo codigo titular cuenta1“
Juanito
”
23456780
45
300.0
saldo codigo titular cuenta2 cuenta1 = cuenta2Asignación con semántica valor
“
Pepito
”
23456789
45
300.0
saldo codigo titular cuenta1“
Juanito
”
23456780
45
300.0
saldo codigo titular cuenta2 cuenta1 = cuenta2 cuenta1.titular = cuenta2.titular cuenta1.codigo = cuenta2.codigoOperador de asignación y atributos const
Problema
:
Cuenta cuenta1(persona1);
Cuenta cuenta2(persona2);
cuenta1 = cuenta2;
En la clase Cuenta el atributo titular se ha declarado
de sólo lectura (const).
Al hacer una asignación entre objetos cuenta con
semántica valor se hace una copia superficial de los atributos.
Se intentaría hacer una copia de los titulares. Esto es,
cambiar el valor del atributo titular de cuenta1 por el valor del atributo titular de cuenta2.
Asignación con semántica valor y atributo const
“
Pepito
”
23456789
45
300.0
saldo codigo titular cuenta1“
Juanito
”
23456780
45
300.0
saldo codigo titular cuenta1 = cuenta2 ERROR!! cuenta1.titular = cuenta2.titular cuenta1.titular está protegido.Operador de igualdad (==)
Cuenta* cta1;
Cuenta cta3;
Cuenta* cta2;
Cuenta cta4;
Semántica:
cta1 == cta2;
igualdad de punteros, identidad
(= Java)
Instancia actual
Al igual que en Java, se define la palabra reservada this
que es un puntero a la instancia actual. void Cuenta::ingreso(double cantidad) {
this->saldo = this->saldo + cantidad; }
Métodos y mensajes
Al igual que en Java, los métodos definidos en una clase son
los mensajes aplicables sobre los objetos de la clase.
Está permitida la sobrecarga de métodos declarados dentro
de la misma clase.
La aplicación de un mensaje varía según si el objeto es
manejado por valor o referencia:
‘.’ (notación punto) el receptor es un objeto por valor ‘->’ (notación flecha) el receptor es un puntero a objeto
Si no se indica el objeto receptor, la llamada se realiza sobre
Mensajes
Cuenta objCta;
Cuenta* ptroCta;
Notación punto para objetos
objCta.reintegro(1000);
(*ptrCta).reintegro(1000);
Notación “flecha” para punteros:
ptroCta->reintegro(1000); (&objCta)->reintegro(1000);
Paso de parámetros
Paso por valor el parámetro formal es una copia del
parámetro real ( = Java ).
Paso por referencia el parámetro formal referencia al
parámetro real.
Además, hay que tener en cuenta que los objetos pueden tener
semántica valor o semántica referencia.
void Cuenta::reintegro(double cantidad) { … } double v = 3000.0; cuenta.reintegro(6000.0); cuenta.reintegro(v);
Parámetro
formal
Parámetro
real
Paso por valor con semántica referencia
Paso por valor del puntero ( = Java).
El estado de cuenta1 y cuenta2 cambia.
El nuevo puntero de emisor no afecta al parámetro real
void Banco::transferencia1(Cuenta* emisor, Cuenta* receptor,
double cantidad){
emisor->reintegro(cantidad);
receptor->ingreso(cantidad);
emisor = new Cuenta(emisor->getTitular()); }
…
Paso por valor con semántica valor
El estado de los parámetros reales cuenta1 y cuenta2
no cambia.
emisor y receptor son una copia de los objetos con
semántica valor que se pasan como parámetro.
void Banco::transferencia2(Cuenta emisor, Cuenta receptor,
double cantidad){ emisor.reintegro(cantidad); receptor.ingreso(cantidad); } … banco.transferencia(cuenta1, cuenta2, 3000.0);
Paso por referencia con semántica referencia
Paso por referencia del puntero.
El cambio del puntero en el parámetro formal (receptor)
afecta al parámetro real (cuenta2).
void Banco::transferencia3(Cuenta* emisor, Cuenta*& receptor,
double cantidad){
emisor->reintegro(cantidad);
receptor->ingreso(cantidad);
receptor = new Cuenta(receptor->getTitular()); }
…
Paso por referencia con semántica valor
Paso por referencia del objeto con semántica valor
cuenta2.
El cambio en receptor (estado y parámetro) afecta a
cuenta2.
void Banco::transferencia4(Cuenta* emisor, Cuenta& receptor,
double cantidad){ emisor->reintegro(cantidad); receptor.ingreso(cantidad); Cuenta otraCuenta; receptor = otraCuenta; } … banco.transferencia(cuenta1, cuenta2, 3000.0);
Parámetros const
Pasar un objeto grande por referencia es más eficiente
que pasarlo por valor, pero se corre el riesgo de modificar el objeto.
Solución: utilizar la palabra reservada const para indicar
que no se puede modificar el estado del objeto.
const se puede utilizar en otras situaciones diferentes. Existen 8 combinaciones de paso de parámetros por
valor o por referencia de un objeto con semántica valor o referencia en el que el modificador const protege el objeto o el parámetro.
Parámetros const en paso por valor con
semántica referencia
void transferencia11(Cuenta* emisor,
const Cuenta* receptor, double cantidad);
Se protege el estado del objeto cuenta apuntado por
receptor.
void transferencia12(Cuenta* emisor,
Cuenta* const receptor, double cantidad);
Equivale a los parámetros final de Java. Se protege el parámetro formal.
No sería válida la sentencia receptor = NULL; en el cuerpo
Parámetros const en paso por valor con
semántica valor
void transferencia21(Cuenta* emisor,
const Cuenta receptor, double cantidad);
void transferencia22(Cuenta* emisor,
Cuenta const receptor, double cantidad);
Ambas declaraciones son equivalentes puesto que con
semántica valor el estado del objeto es equivalente al valor del parámetro.
Parámetros const en paso por referencia con
semántica referencia
void transferencia31(Cuenta* emisor,
const Cuenta*& receptor, double cantidad);
Se protege el estado del objeto cuenta apuntado por
receptor.
void transferencia32(Cuenta* emisor,
Cuenta*& const receptor, double cantidad);
No compila!!
No tiene sentido proteger un parámetro que se ha declarado
Parámetros const en paso por referencia con
semántica valor
void transferencia41(Cuenta* emisor,
const Cuenta& receptor, double cantidad);
Se protege el estado del objeto cuenta receptor.
Desde el punto de vista del paso de parámetros, tiene poco sentido
pasar un objeto con semántica valor por referencia y utilizar const para evitar la modificación. No obstante, se utiliza por motivos de eficiencia.
void transferencia42(Cuenta* emisor,
Cuenta& const receptor, double cantidad);
No compila!!
Función main
Sólo puede haber un punto de entrada en el espacio raíz. Sólo exige declarar una función con nombre main, y valor
de retorno int.
Opcionalmente puede tener un parámetro con los
argumentos del programa.
Ejemplos:
int main(char* args[]) int main()
Herencia en C++
Este apartado trata:
Diferencias entre herencia C++ y Java. Tipos de herencia: pública y privada. Herencia y constructores.
Redefinición y métodos virtuales.
Polimorfismo y ligadura (dinámica y estática). Conversión de tipos.
Compatibilidad de tipos.
Redefinición de la copia e igualdad de objetos. Operadores. Clases abstractas.
Herencia
La herencia en C++ es diferente a Java en varios aspectos:
Herencia múltiple: una clase puede heredar de varias clases.
Herencia privada: heredar de una clase sólo el código, pero
no el tipo.
Ligadura de métodos: los métodos pueden ser invocados por
ligadura dinámica y estática.
Clase Deposito
class Deposito { private: Persona* titular; double capital; int plazoDias; double tipoInteres; public: Deposito(…); double liquidar(); double getIntereses(); double getCapital(); int getPlazoDias(); double getTipoInteres(); Persona* getTitular();Clase Deposito Estructurado
class DepositoEstructurado : public Deposito {private:
double tipoInteresVariable;
double capitalVariable;
public:
DepositoEstructurado(Persona titular, …,
double capitalVariable);
double getInteresesVariable();
void setTipoInteresVariable(double interesVariable);
double getTipoInteresVariable();
double getCapitalVariable();
Tipos de herencia
Herencia pública (= Java): class B: public A
La clase hereda los atributos y métodos de la clase padre.
La visibilidad de las declaraciones heredadas puede aumentar
y disminuir al declararlas en el fichero cabecera.
Herencia privada: class B: private A
Las declaraciones visibles para la clase hija cambian su
visibilidad a privada.
Se puede aumentar la visibilidad en el fichero cabecera.
No se hereda el tipo: los objetos de la clase no pueden ser
asignados a variables polimórficas cuyo tipo estático sea la clase padre.
Niveles de visibilidad
Al igual que en Java, los niveles de visibilidad son
incrementales
private
protected
public
public todo el códigoprotected clase + subclases private clase
Herencia y constructores
Los constructores no se heredan ( = Java )
El constructor de la clase hija tiene que invocar al de la
clase padre.
DepositoEstructurado::DepositoEstructurado(Persona titular,
double capital, int plazoDias,
double tipoInteres, double tipoInteresVariable,
double capitalVariable): Deposito(titular, capital,
plazoDias, tipoInteres) {
this.tipoInteresVariable = tipoInteresVariable;
this.capitalVariable = capitalVariable; }
Redefinición de métodos
Cualquier método que sea visible puede redefinirse en una
subclase.
Método virtual: método que al redefinirse en las
subclases puede resolverse por ligadura dinámica (modificador virtual).
Para redefinir un método (virtual o normal):
Hay que declararlo en la subclase (fichero cabecera) con la
misma signatura e implementarlo en el fichero de implementación.
Para refinar un método en el que es necesario llamar a la
versión del padre:
NombreClase::NombreMetodo
Redefinición de métodos
class Deposito { private: Persona* titular; double capital; int plazoDias; double tipoInteres; public: Deposito(…); double liquidar();virtual double getIntereses();
virtual double getCapital();
int getPlazoDias();
double getTipoInteres();
Redefinición de métodos
Métodos redefinidos en DepositoEstructurado:
Llaman a las versiones de la clase Deposito.
//Override
double DepositoEstructurado::getIntereses() {
return Deposito::getIntereses() + getInteresesVariable();
}
//Override
double DepositoEstructurado::getCapital() {
return Deposito::getCapital() + getCapitalVariable();
Polimorfismo y ligadura
El polimorfismo de asignación está permitido para
entidades (variables) con semántica por valor y
referencia.
Para objetos por valor, se pierde información en la
asignación.
Ligadura dinámica:
Sólo es posible para métodos virtuales.
La entidad polimórfica debe ser de tipo referencia.
Ligadura estática
:
Polimorfismo y ligadura con semántica valor
deposito
tiene semántica valor
se aplica
ligadura estática
Se ejecuta la versión de getCapital disponible en
el tipo estático Deposito::getCapital
Deposito deposito(…);DepositoEstructurado de(…);
//Asignación polimórfica entre objetos valor
deposito = de;
Polimorfismo y ligadura con semántica referencia
Deposito* ptrDeposito = new Deposito(…);
DepositoEstructurado* ptrDe = new DepositoEstructurado(…); ptrDeposito = ptrDe;
ptrDeposito->getCapital(); // Ligadura dinámica
ptrDeposito->liquidar(); // Ligadura estática
ptrDeposito tiene semántica referencia
se aplica ligadura estática o dinámica
getCapital() método virtual ligadura dinámica
Se ejecuta DepositoEstruturado::getCapital
liquidar() no es virtual ligadura estática
Conversión de tipos
Operador
dynamic_cast<Tipo*>(ptro)
Convierte el ptro en el puntero a Tipo
ptro
debe ser una entidad polimórfica (su clase
debe tener algún método virtual)
Al igual que en Java, el tipo de la conversión debe
ser compatible.
Conversión de tipos
Ejemplo:
Establecer interés variable a depósitos estructurados.
Deposito** depositos;
depositos = new Deposito*[MAX_DEPOSITOS];
…
DepositoEstructurado* depEst;
for (int i =0; i < MAX_DEPOSITOS; i++){
depEst = dynamic_cast<DepositoEstructurado*>(depositos[i]);
if (depEst != NULL)
Compatibilidad de tipos
Ejemplo:
Calcula el número de depósitos garantizados.
Equivalente al instanceof de Java.
int numGarantizados = 0;
for (int i =0; i < MAX_DEPOSITOS; i++){
if (dynamic_cast<DepositoGarantizado*>(depositos[i])) ++numGarantizados;
Copia e igualdad de objetos
No existe una clase raíz como la clase Object de Java.
No existen métodos como clone o equals.
Los operadores de asignación y copia entre objetos con
semántica referencia se comportan igual que en Java.
No se debe cambiar la semántica.
El operador de asignación entre objetos con semántica valor
realiza una copia superficial.
¿Cómo se puede definir una copia diferente?
La igualdad entre objetos con semántica valor no está
definida.
Operadores
Gran parte de los operadores del lenguaje (=, ==, !=, <, <=, etc.)
pueden ser definidos para su aplicación sobre objetos.
Ejemplo: utilizar + para sumar matrices.
Se definen utilizando como nombre de método operator
seguido del operador. Ejemplos:
operator= operator== operator<<
Un operador puede definirse dentro o fuera de una clase. En
Operador = en Cuenta
Cuenta& Cuenta::operator=(const Cuenta& otraCuenta){
if (this != &otraCuenta){ //¿son el mismo objeto?
codigo = ++ultimoCodigo;
titular = otraCuenta.titular;
saldo = otraCuenta.saldo;
delete [] ultimasOperaciones;
ultimasOperaciones = new double[Cuenta::MAX_OPERACIONES]; }
return (*this); }
Operador == en Cuenta
Implementar != a partir de == para evitar
inconsistencias.
bool Cuenta::operator==(const Cuenta& otraCuenta){
return (codigo == otraCuenta.codigo); }
bool Cuenta::operator!=(const Cuenta& otraCta){
return !(*this == otraCta); }
Operador externo << en Cuenta
ostream& banco::operator<<(ostream& salida,
const Cuenta& cuenta){ salida <<"Cuenta [codigo = "<<cuenta.getCodigo()
<<", titular = "<<(cuenta.getTitular())->getNombre() <<", estado = "<<cuenta.getEstado() <<", saldo = "<<cuenta.getSaldo() <<"]"<<endl; return salida; }
ostream& banco::operator<<(ostream& salida, const Cuenta* cta){ salida<<(*cta);
Operador externo << para el Enumerado
ostream& banco::operator <<(ostream& salida,
const EstadoCuenta& estado){
switch(estado){
case OPERATIVA: salida<<"OPERATIVA"; return salida;
case INMOVILIZADA: salida<<"INMOVILIZADA"; return salida;
case NUM_ROJOS: salida<<"NUMEROS ROJOS"; return salida; }
return salida;
Declaración de operadores
namespace banco{ … class Cuenta { public: …bool operator==(const Cuenta& otraCta); bool operator!=(const Cuenta& otraCta); Cuenta& operator=(const Cuenta& otraCta); };
//Externos
ostream& operator<<(ostream& salida, const EstadoCuenta& estado); ostream& operator<<(ostream& salida, const Cuenta& cuenta);
Clases abstractas
En C++ una clase es abstracta si tiene algún método
virtual “puro”: sin implementación.
No existe una palabra clave para definir una clase
abstracta.
class ProductoFinanciero{
private:
Persona* titular;
public:
ProductoFinanciero(Persona* titular);
virtual double getBeneficio()=0;
double getImpuestos();
Interfaces
C++ no define el concepto de interfaz de Java.
No es necesario, ya que el lenguaje ofrece herencia
múltiple.
Si una clase quiere ser compatible con varias clases, basta
con que herede públicamente de esas clases.
El equivalente a las interfaces de Java sería una clase
totalmente abstracta sólo con métodos virtuales puros.
Herencia múltiple
En C++ es posible que una clase tenga más de una clase
padre.
Problemas:
Colisión de nombres: la clase hija hereda dos métodos con
la misma signatura.
Herencia repetida: una clase se hereda dos veces,
Herencia múltiple – problemas
La clase CuentaRemunerada
hereda dos veces de la clase ProductoFinanciero.
Por defecto, se duplican todos los
atributos heredados. titular atributos Deposito titular atributos Cuenta at. CtaRemunerada
Herencia múltiple – problemas
El método getTitular() se hereda dos veces: colisión
de nombres.
Al aplicarlo sobre un objeto CuentaRemunerada existe
ambigüedad.
Se resuelve la ambigüedad calificando la versión que
queremos aplicar:
CuentaRemunerada* cr = new CuentaRemunerada(…); cr->Cuenta::getTitular();
Herencia repetida – ambigüedad
La asignación polimórfica a una referencia de tipo
ProductoFinanciero es ambigua:
Habría que indicar la ruta de herencia:
ProductoFinanciero* pf;
CuentaRemunerada* cr = new CuentaRemunerada(…); pf = cr; // Error
ProductoFinanciero* pf;
CuentaRemunerada* cr = new CuentaRemunerada(…);
Herencia repetida – ambigüedad
La aplicación del método getBeneficio() es
ambigua si se aplica sobre una variable de tipo CuentaRemunerada:
ProductoFinanciero* pf;
CuentaRemunerada* cr = new CuentaRemunerada(…);
pf = (Cuenta*)cr;
pf->getTitular();
pf->getBeneficio(); // Versión clase Cuenta cr->getBeneficio(); // Error
Herencia virtual
Parte de los problemas de ambigüedad de la herencia
repetida pueden resolverse con herencia virtual.
Evita que la clase ProductoFinanciero se herede dos
veces en la clase CuentaRemunerada.
Las clases Cuenta y Deposito aplican herencia virtual de
ProductoFinanciero:
La herencia virtual indica que si una subclase hereda de
ProductoFinanciero por otra rama, los atributos y métodos de esa clase no deben repetirse.
Herencia virtual
class ProductoFinanciero{ …};
class Deposito: virtual public ProductoFinanciero { …
};
class Cuenta: virtual public ProductoFinanciero { …
};
class CuentaRemunerada: public Cuenta, public Deposito{ …
Herencia virtual – Constructores
El constructor de la clase CuentaRemunerada tiene que
llamar al constructor de ProductoFinanciero aunque no sea una clase de la que hereda directamente.
CuentaRemunerada::CuentaRemunerada(Persona* p, double s, int
plazoDias,double tipoInteres) : ProductoFinanciero(p),
Cuenta(p,s),
Deposito(p, s, plazoDias, tipoInteres)
{ …
Herencia múltiple – Colisión de nombres
El método getBeneficio() es definido por Cuenta yDeposito: colisión de implementaciones.
Se evita al redefinir el método eligiendo una de las
versiones:
class CuentaRemunerada: public Cuenta, public Deposito{
public:
CuentaRemunerada(…);
double getBeneficio(); // Redefine el método
};
double CuentaRemunerada::getBeneficio(){
Herencia múltiple – Método dominante
Si un método de la clase ProductoFinanciero se defineo redefine sólo en una de las clases hijas, no existe ambigüedad.
Se dice que la versión redefinida domina sobre la versión
original.
En el caso de una asignación polimórfica se ejecutará la
Genericidad en C++
La clases genéricas en C++ se conocen como templates. Ejemplo: Contenedor
template <class T> class Contenedor{
private:
T contenido;
public:
T getContenido();
void setContenido (T elem); };
Genericidad – Implementación
La implementación de la clase genérica debe realizarse en
el fichero de cabecera:
template<class T> T Contenedor<T>::getContenido(){
return contenido; }
template<class T> void Contenedor<T>::setContenido(T elem){ contenido = elem;
Genericidad – Uso
Se indica el tipo de la clase genérica en su declaración. Puede ser aplicada a tipos primitivos.
Persona* titular = new Persona("juan", "34914680");
Cuenta* cuenta = new Cuenta(titular);
Contenedor<Cuenta*> contenedor; contenedor.setContenido(cuenta);
Cuenta* cta = contenedor.getContenido(); Contenedor<int> cEntero;
Genericidad – Críticas
C++ no implementa un auténtico sistema de
genericidad.
Cuando se usa una clase genérica, se realiza un
reemplazo del texto del parámetro en la declaración de la clase.
Por tanto, se genera código para cada uno de los tipos
Genericidad restringida
No se puede restringir la genericidad.
Está permitido aplicar cualquier método y operador
sobre las entidades del tipo genérico (tipo T en el ejemplo).
Es problemático determinar la sintaxis de los mensajes,
ya que se puede instanciar a tipos por valor o referencia.
Si la operación no está disponible sobre el tipo genérico, el
Colecciones en C++
Todas las entidades de la Librería Estándar C++ se han
incluido en un único espacio de nombres denominado
std.
La librería estándar de plantillas, o STL (Standard Template Library), implementa un conjunto de
estructuras de datos y algoritmos que conforman una parte sustancial de la Librería Estándar C++.
Las estructuras de datos genéricas se denominan
contenedores.
Los contenedores son clases genéricas (template)
pensadas para contener cualquier tipo de objeto.
Tipos de colecciones
Existen dos tipos de contenedores:
Secuencias:
Almacenan los elementos en orden secuencial. Adecuadas para accesos directos y secuenciales. vector, list, deque (cola de doble terminación),
stack
Asociaciones:
Almacenan pares de valores <clave, valor>.
Podría verse como una array en el que el índice no tiene
por qué ser un entero.
Adecuados para accesos aleatorios mediante claves.
Cuenta.h
#include <list> using namespace std; class Cuenta { public: …list<double>* getOperaciones() const;
private: …
list<double>* operaciones;
void anotarOperacion(double cantidad); };
Cuenta.cpp
Cuenta::Cuenta (Persona* persona, double saldoInicial){ …
operaciones = new list<double>(); }
list<double>* Cuenta::getOperaciones() const{ return new list<double>(*operaciones);
}
void Cuenta::anotarOperacion(double cantidad){
operaciones->push_back(cantidad); }
Prueba de listas
int main () {…
Cuenta* cuenta = new Cuenta(titular); cuenta->ingreso(100);
cuenta->reintegro(300); cuenta->ingreso(1000);
list<double>* movimientos;
movimientos = cuenta->getOperaciones();
list<double>::const_iterator it; it = movimientos->begin();
for (; it != movimientos->end() ;++it) cout << (*it) << endl;
exit(0); }
Prueba de listas
La implementación de las listas no ofrece métodos
para accesos aleatorios porque serían ineficientes.
Sólo dispone de métodos de acceso al principio y al
final
Se puede utilizar de forma efectiva como una pila
Prueba con vectores
int main () {…
Cuenta* cuenta = new Cuenta(titular); cuenta->ingreso(100);
cuenta->ingreso(300); cuenta->reintegro(300); cuenta->ingreso(1000);
vector<double>* movimientos;
movimientos = cuenta->getOperaciones();
for (int i = 0; i < movimientos->size(); i++) cout << (*movimientos)[i] <<endl;
exit(0);
Prueba con vectores
Los vectores se utilizan como arrays de tamaño
fijo.
Aunque tiene operaciones para aumentar el
tamaño del vector se desaconseja su uso por ser
ineficientes.
Acceso fácil y eficiente a los elementos.
size()
no devuelve el número de elementos
Estrategias
Patrón estrategia: parametrizar una rutina con un
algoritmo.
C++ permite definir punteros a funciones.
Un puntero a función es una variable que guarda la
dirección de comienzo de una función.
X (*fptr) (A);
fptr es un puntero a función que recibe A como
Estrategias
Ejemplo:
En la clase Banco, método de búsqueda de productos
financieros. namespace banco{ class Banco { private: ProductoFinanciero** productos; public: Banco(); ProductoFinanciero* buscar(
bool (*condicion) (ProductoFinanciero*)); };
//Condiciones de búsqueda
Estrategias
El parámetro del método buscar() es una función que
recibe como parámetro un puntero a un
ProductoFinanciero y devuelve un valor booleano.
La función depositoAlto podría ser establecida como
parámetro del método buscar() : cumple la definición del puntero a función.
Estrategias
ProductoFinanciero* Banco::buscar(
bool (*condicion)(ProductoFinanciero*)){
bool encontrado = false;
for (int i =0; i < MAX_PRODUCTOS; i++)
if (condicion(productos[i])){ encontrado = true;
return productos[i]; }
if (!encontrado) return NULL; }
Estrategias
// Comprueba si el producto es un depósito // y su saldo es mayor que 10000
bool banco::depositoAlto(ProductoFinanciero* producto){
Deposito* deposito = dynamic_cast<Deposito*>(producto);
if (deposito != NULL)
return (deposito->getCapital() > 10000);
else return false; }
Banco banco; …
Estrategias – Limitaciones
Los punteros a funciones están limitados a:
Funciones globales (funciones de C). Métodos de clase.
No pueden apuntar a métodos de instancia
Sin embargo, en la mayoría de casos el mecanismo de los
punteros a función es suficiente.
Si fuera necesario, se podría simular la solución de Java
Corrección y Robustez
Este apartado trata:
Asertos.
Excepciones.
Excepciones predefinidas. Definición de excepciones.
Declaración de excepciones en métodos. Tratamiento de excepciones.
Asertos
C++ permite el uso de asertos llamando a la macro
assert() definida en <assert.h>
assert() evalúa una expresión y llama a abort() si el
resultado es cero (false).
void Cuenta::ingreso(double cantidad){ assert(cantidad > 0);
assert(estado == OPERATIVA);
saldo = saldo + cantidad; }
Asertos
Antes de abortar, assert escribe en la salida de error el
nombre del archivo fuente y la línea donde se produjo el error.
Si se define la macro NDEBUG, se desactiva la
Asertos – Limitaciones
Los asertos de C++ tienen los mismos problemas que en
Java.
No se recomienda evaluar las precondiciones con asertos,
ya que se pueden desactivar.
La verificación de código se realiza con pruebas
Excepciones
Mecanismo para notificar que se ha producido una
situación excepcional.
A diferencia de Java, una excepción no tiene por qué ser
un objeto de una clase:
Se puede lanzar cualquier dato (un entero, una cadena de
texto, …)
Lanzamiento de excepciones
Se utiliza la palabra reservada throw
Aunque es posible notificar como error cualquier dato, se
recomienda utilizar objetos.
void Cuenta::ingreso(double cantidad) {
if (cantidad < 0)
throw invalid_argument(“Cantidad negativa");
if (estado != OPERATIVA)
throw logic_error("Estado incorrecto");
saldo = saldo + cantidad; }
Excepciones biblioteca estándar
Existe un conjunto de excepciones predefinidas en el
espacio de nombres std (<stdexcept.h>)
Todas ellas heredan de la clase exception.
No es obligatorio que las excepciones del
programador hereden de alguna excepción predefinida.
Uso excepciones estándares
Para el control de precondiciones se lanzan
excepciones compatibles con logic_error void Cuenta::ingreso(double cantidad) {
if (cantidad < 0)
throw invalid_argument(“Cantidad negativa");
if (estado != OPERATIVA)
throw logic_error("Estado incorrecto");
saldo = saldo + cantidad; }
Excepciones de usuario
Para notificar el fallo de una postcondición se recomienda
crear una clase que herede de runtime_error:
Las excepciones que heredan de exception disponen
del método what() que retorna el mensaje de error. class RedNoDisponible: public runtime_error{
public:
RedNoDisponible(const char* msg); };
RedNoDisponible::RedNoDisponible(const char* msg):
Declaración de excepciones
Se utiliza también la palabra reservada throw.
Se puede especificar el conjunto de excepciones que
puede lanzar un método
void f(int a) throw (E1, E2);
f puede lanzar excepciones de tipo E1 y E2, pero no otras
Si no se dice nada, significa que podría lanzar cualquier
excepción
int f();
Se puede indicar que un método no lanzará ninguna
excepción
Declaración de excepciones
A diferencia de Java, el compilador ignora la
declaración de excepciones.
Si en tiempo de ejecución se intenta lanzar una excepción
no declarada, se detecta la violación y termina la ejecución.
En cambio, el compilador controla la redefinición de un
método para que no pueda lanzar más excepciones que las especificadas en el método de la clase padre.
Declaración de excepciones
Declaramos que el método ingreso() sólo puede lanzar
los dos tipos de excepciones.
Si en el cuerpo del método se produjese alguna otra
excepción sería incompatible con la declaración. void Cuenta::ingreso(double cantidad)
throw (invalid_argument, logic_error){
if (cantidad<0)
throw invalid_argument("cantidad negativa");
if (estado!= OPERATIVA)
throw logic_error("Estado incorrecto");
saldo = saldo + cantidad; }
Tratamiento de excepciones
Al igual que en Java, las excepciones pueden ser tratadas en
bloques try-catch.
A diferencia de Java, el compilador no obliga a dar tratamiento
a las excepciones.
Las excepciones son pasadas al manejador (catch) por
referencia.
Cuando ocurre una excepción se evalúan los tipos definidos en
los manejadores y se ejecuta el primero cuyo tipo sea compatible ( = Java).
Se puede definir un manejador para cualquier tipo de
excepción: catch(…)
Tratamiento de excepciones
Si al crear la conexión ocurre la excepción
RecursoNoEncontrado (no manejada) pasará al cliente. void Navegador::visualiza(string url) {
Conexion* conexion;
int intentos = 0;
while (intentos < 20) {
try {
conexion = new Conexion(url);
break;
} catch (RedNoDisponible& e) { intentos++;
if (intentos == 20) throw; //relanza
} …
Seminario de C++
Este tema se completa con el Seminario de C++
que incluye:
La instalación del compilador C++ de GNU en
Windows.
Instalación del entorno de desarrollo Eclipse C++. Creación y ejecución de un proyecto.