Clases

Programación en C++. Computación. Recursos. Objetos. Autorreferencia. Constructores. Clases amigas. Implementación

  • Enviado por: María
  • Idioma: castellano
  • País: España España
  • 28 páginas
publicidad

CLASES

Introducción

  • Permite crear objetos que pueden ser empleados del mismo modo que los tipos básicos

  • Permite definir un objeto de forma concreta y precisa que no tiene contraparte obvia con los tipos básicos

  • Permite diseñar los objetos de una aplicación de acuerdo a lo que se está desarrollando con lo que se puede entender y modificar de mejor manera la aplicación

Ejemplo : Implementación del concepto de fecha por medio de una estructura (struct) junto con el conjunto de funciones para manipularlas :

En C :

struct fecha { int día, mes, año };

fecha hoy ;

void fijar_fecha (fecha*, int, int, int);

void siguiente_fecha (fecha*);

void imprimir_fecha(const fecha*);

// ...

No existen conexiones explícitas entre las funciones y el tipo de datos.

C++ nos permite hacer lo siguiente :

struct fecha {

int día, mes, año;

void fijar(int, int, int);

void obtener(int*, int*, int*);

void siguiente();

void imprimir();

};

Las funciones miembros se invocan de la siguiente manera :

fecha hoy;

fecha mi_aniversario;

void f ()

{

mi_aniversario.fijar(30, 12,1975);

hoy.fijar(22,9,1997);

mi_aniversario.imprmir();

hoy.siguiente();

}

Diferentes estructuras pueden tener funciones miembro con el mismo nombre, es preciso especificar el nombre de la estructura al definir una función miembro :

void fecha::siguiente()

{

if(++día > 28) {

// resolver esta parte

}

}

En una función miembro, los nombres de los miembros se pueden emplear sin hacer una referencia explícita a un objeto, en ese caso se refieren al miembro del objeto que efectivamente llamó a la función.

DEFINICIÓN DE CLASES

La definición de tipo struct no poseen miembros privados y no restringen el acceso de otra funciones a los objetos. La definición de una clase nos permite implementar esta posibilidad .

class fecha {

int día, mes, año;

public :

void fijar(int, int, int);

void obtener(int*, int*, int*);

void siguiente();

void imprimir();

};

La palabra reservada public nos separa la definición de la clase en dos partes. Sólo las funciones miembro pueden acceder a los nombres definidos en la primera parte, que se denomina privada.

La parte pública consiste es la interfase de los objetos de la clase, esto le permite “comunicarse” con el resto de los componentes de un programa.

Las funciones que no son miembros no podrán utilizar los miembros privados de la clase fecha.

Ventajas de limitar el acceso :

  • cualquier problema que exista con una fecha que tome un valor ilegal habrá que buscarlo en el código de alguna función miembro

  • un cambio de comportamiento de un objeto habrá que efectuarlo a través de las funciones miembro

  • examinando la definición de la funciones miembros un usuario podrá aprender su manejo

AUTOREFERENCIA

Una función miembro puede hacer referencia directa a miembros del objeto al cual invoca.

class X {

int m;

public :

int leerm() { return m };

};

void f( X aa, X bb)

{

int a = aa.leerm();

int b = bb.leerm();

//.....

}

El puntero this se define de la siguiente forma :

X * const this;

this se inicializa de modo que siempre tenga la dirección del objeto al cual se llamó a la función miembro. This tiene una posición constante, varía el objeto al que apunta.

class X {

int m;

public :

int leerm() { return this->m };

};

La utilización de this puede ser útil en la implementación de listas doblemente vinculada :

class double_link_list {

double_link_list* prev; // puntero al nodo anterior

double_link_list* next; // puntero al siguiente nodo

public :

void agrega(double_link_list*);

};

void double_link_list*:: anexar(double_link_list* p)

{

p->next = next; // o sea p-> next = this-> next

p->prev = this; // uso explícito de this

next->pre = p; // o sea this->next->pre = p

next = p; // this->next = p

}

double_link_list* cabeza_lista;

void f(double_link_list* a, double_link_list* b)

{

//...

cabeza_lista->agregar(a);

cabeza_lista->agregar(b);

}

Inicialización

Constructores : funciones que se emplean para inicializar objetos de una clase.

Si no se declara existe un por omisión (default). Tienen el mismo nombre de la clase.

class fecha {

// ...

fecha(int, int, int);

};

Si el constructor tiene argumentos habrás que pasarselos :

fecha hoy = fecha(22, 9, 1997);

fecha navidad(25,12,0) // forma abreviada de llamar al

// constructor

fecha mi_aniversario; // error se deben pasar los

// argumentos

Se pueden declarar varios constructores empleando la sobrecarga de nombre de funciones :

class fecha {

int día, mes, año;

public :

// ....

fecha(int, int, int); // día, mes, año

fecha(int, int); // día, mes; año actual fecha(int); // día; mes y año actuales

fecha(); // fecha por omisión : hoy

fecha (const char*) // fecha como cadena de

}; // caracteres

Cuando los constructores difieren lo suficiente en cuanto a sus tipos de argumentos, el compilador selecciona el correcto :

fecha hoy(4);

fecha primavera(“21 de setiembre”);

fecha alguien(5,11);

fecha ahora; // inicializado por omisión

Hay que cuidar de poner los constructores que sean necesarios realmente para no producir un programa poco legible. Utilizar constructores por omisión puede ser muy importante.

class fecha {

int día, mes ,año;

public:

// ...

fecha(int d=0; int m=0; int y=0);

// ...

};

fecha::fecha(int d, int m, int a)

{

día = d ? d : hoy.día;

mes = m ? m : hoy.mes;

año = a ? a : hoy.año;

// verificar que la fecha sea válida

// ...

}

Un objeto de una clase sin constructores se puede inicializar asignándole otro objeto de esa clase. Esto se puede hacer aún cuando se hayan declarado constructores. Por ejemplo:

fecha f = hoy; // inicialización por asignación

Constructor por omisión: copia de otro objeto de la misma clase. La copia se efectúa miembro por miembro.

CARACTERÍSTICAS Y REGLAS DE LOS CONSTRUCTORES

  • El nombre del constructor debe ser el mismo que el de su clase

  • No debe tener ningun tipo de retorno ni siquiera void

  • Una clase puede tener distintos tipos de constructores o ninguno, el compilador asigna uno automáticamente a esa clase

  • Un constructor predeterminado es aquel que no tiene ningún tipo de parámetro o posee una lista de parámetros donde todos ellos son predeterminados

// constructor sin parámetros

class punto {

double x;

double y;

public:

punto();

……

};

// la misma clase con constructor con argumentos

// predeterminados

class punto {

double x;

double y;

public:

punto(double xval=0, double yval=0);

……

};

  • El constructor de copia permite crear una instancia de clase usando una instancia existente

class punto {

double x;

double y;

public:

punto();

punto(double xval, double yval);

punto(const punto& pt);

……

};

Ejemplo de construcciones :

punto p1; // llama al constructor predeterminado

punto p2(1.1, 1.3); // llama al constructor con dos

// argumentos

punto p3(p2);

Construcción y destrucción

Los objetos se pueden crear como :

  • Objetos automáticos : se crean cuando se llega a su declaración durante la ejecución de un programa, se destruyen cuando se sale del bloque en que aparecen

  • Objetos estáticos : se crean al principio del programa y se destruyen cuando se sale del mismo

  • Objetos dinámicos : se crean a partir del almacenamiento disponible, se crean con new se eliminan con delete

  • Objetos miembro : se crean como miembros de otra clase o como elemento de un arreglo

Variables locales :

  • constructor se ejecuta cuando se pasa por la declaración de la variable

  • destructor se ejecuta cuando se sale del bloque de esa variable

Los destructores se ejecutan en el orden inverso al de su construcción

void f(int i) Construcción Destrucción

{

tabla aa;

tabla bb;

if(i>0) {

tabla cc;

//…

}

//…

}

void h()

{

tabla t1(100); // constructor

tabla t2 = t1; // posibles problemas

tabla t3(200); // constructor

t3 = t2; // problemas

} // En este ejemplo el constructor se llama 2 veces y el

// destructor 3 veces

Almacenamiento disponible

main()

{

tabla *p= new tabla(100);

tabla *q= new tabla(200);

delete p;

delete p; // posible error de ejecución

}// nunca se eliminó a q y se destruyó a p dos veces

No existen garantías que un destructor llamado con new se llame a su destructor alguna vez

No eliminar no significa nada excepto desperdicio de espacios

Arreglo de objetos de clase

Para declarar un arreglo de objetos de una clase con un constructor debe tener un constructor por omisión tal que pueda llamarse con una lista de argumentos vacía

tabla tab[10];

Cuando se destruye se debe llamar al constructor para cada elemento de un arreglo cuando este se destruye

void f()

{

tabla* t1= new tabla;

tabla* t2= new tabla[10];

delete t1; // una tabla no hay problemas

delete t2; // problemas 10 tablas delete[] t2;

}

CLASES AMIGAS

En algunos casos es necesario que una función que no es miembro de una clase pueda acceder a los miembros privados de la clase en estos casos se denomina a la función amiga (friend).

vector multiplicar(const matriz& m, const vector &v)

{

vector r;

for(int i=0; i<3; i++) {// r[i]=m[i]*v;

r.elem(i)=0;

for (int j=0; j<3; j++)

r.elem(i)+=m.elem(i,j)*v.elem(j);

}

return r

}

class matriz;

class vector {

float v[4];

//…

friend vector multiplicar(const matriz&,const vector&);

};

class matriz {

vector v[4];

//…

friend vector multiplicar(const matriz&,const vector&);

};

La función friend no tiene nada particular excepto el derecho de acceder a la parte privada de una clase.

Una función friend se puede declarar tanto en la parte privada como en la pública de una clase.

La función amiga se declara explícitamente en la clase en la cual es amiga forma parte de la interfase de esa clase tanto como una función miembro. Se debe tener en cuenta que en la función friend no se puede emplear el puntero this, por lo tanto se debe referenciar explícitamente el miembro del objeto con el que se está trabajando.

vector multiplicar(const matriz& m, const vector &v)

{

vector(int i=0; i<3; i++) {

r.v[i]=0;

for (int j=0; j<3; j++)

r.v[i]+=m.v[i][j]*v.v[j];

}

return r

}

Una función miembro de una clase puede ser amiga de otra

class X {

//…

void f();

};

class Y {

//…

friend void X::f();// la función miembro f de X es

// amiga de Y

};

Para declarar que todas las funciones miembros de una clase son amigas de otras tenemos :

class X {

friend class Y; // todas las funciones miembros de Y

… // son amigas de X

};

Distinción de Nombres de Miembros

El operador determinación de alcance :: sirve para distinguir los nombres de los miembros de una clase y otros nombres.

class X{

int m;

public :

int leem() const{return m;}

void fijam (int m) {X::m=m;}

};

El argumento m oculta al miembro m nos referimos a él con el nombre calificado X::m.

Si se emplea un nombre precedido por el operador :: nos referimos a un nombre global.

class mi_archivo {

//…

public:

int open(const char*, const char*)

};

int mi_archivo::open(const char*nombre, const char*espec)

{

//…

if(::open(nombre, bandera)) { open del S.O.

//…

}

}

Limpieza

Está relacionado con los destructores, estos garantizan la liberación de memoria al desechar objetos del tipo.

Destructor de la clase X : ~X (complemento del destructor)

class pila_caract {

int tam;

char* superior;

char* s;

public:

pila_caract(int tm) {superior=s=new char[tam=tm];}

~pila-caract() {delete s;} //destructor

void meter(char c) {*superior++ = c;}

char sacar() {return *--superior}

};

Clases anidadas

class conjunto {

struct miembro_conj{

int miembro;

miembro_conj* siguiente;

miembro_conj(int m, miembro_conj* n) {

miembro=m; siguiente=n; }

};

miembro-conj* primero;

public:

conjunto() { primero=0; }

insertar(int m) {

primero=new miembro_conj(m,primero);}

};

Una clase anidada queda oculta dentro de una clase

Miembro_conj m1(,0) //error!! miembro_conj no está en el

// alcance global

Este recurso no es útil cuando la clase anidada no es simple

Otra alternativa es :

class miembro_conj{

friend class conjunto;// solo tienen acceso los miembros

// de conjunto

int miembro;

miembro_conj* siguiente;

miembro_conj(int m, miembro_conj* n)

{miembro=m, siguiente=n}

// muchos otros miembros útiles

};

class conjunto {

miembro_conj * primero;

public:

conjunto() {primero=0;}

insertar(int m) {

primero=new miembro_conj(m,primero);

}

};

Es posible acceder al nombre de una clase miembro desde afuera de la clase que encierra. Por ejemplo :

class X{

struct M1{int m;};

public:

struct M2 {int m;};

M1 f(M2);

};

void f()

{

M1 a; // error!! M1 no está en el alcance

M2 b; //error!! M2 no está en el alcance

X::M1 a; // error!! X::M1 es privado

X::M2 d; // correcto

}

Otros modos de hacerlo :

M1 X::f(M2 a) // error el nombre M1 está fuera del alcance

X::M1 X::f(M2 a) // correcto

X::M1 X::f(X::M2 a)// correcto pero sobra el tercer X::

Miembros estáticos

Una clase es un tipo, no un objeto de datos, cada objeto de datos tiene su propia copia de los miembros de datos de la clase. Por medio de la declaración static un miembro de la clase tendrá un sola copia y no una por cada objeto.

class tarea {

// …

static tarea* cadena_tareas;

static void planificar(int);

// …

};

static es solo una declaración el objeto debe estar definido más adelante, en algún lugar del programa :

tarea* tarea::cadena_tareas = 0;

void tarea::planificar(int p) { /* … */}

La palabra static no es necesaria ni está permitida en la definición de un miembro estático

Punteros a miembros

Para obtener la dirección de un miembro de una clase :

&nombre_clase::nombre_miembro

Para tener un puntero a un miembro de una clase tenemos :

X::*

#include <iostream.h>

struct cl

{

char *val

void imprimir(int x) { cout << val<<x<<'\n'}

cl(char* v) {val = v;}

};

typedef void(cl::*PMFI) (int);

int main()

{

cl z1(“z1”);

cl z2(“z2”);

cl *p=&z2;

PMFI pf=&cl::imprimir;

z1.imprimir(1);

(z1.*pf)(2);

z2.imprimir(3);

(p->*pf)(4);

}

EJEMPLO DE UNA IMPLEMENTACIÓN

Supongamos una tabla de símbolos de un programa (generalmente compuesta por los nombres de variables), supongamos una primera forma como estructura :

struct nombre {

char* cadena;

nombre* sig;

double valor;

};

y ahora implementada como clase :

// archivo tabla.h

class tabla {

nombre* tab;

public:

tabla() { tab = 0; }

nombre* buscar{char*, int = 0);

nombre* insertar(char*s) { return buscar(s,1); }

};

ahora estamos en condiciones de declarar más de una tabla, tener un apuntador a una tabla. Por ejemplo :

#include “tabla.h”

tabla globales;

tabla palabra_clave;

tabla* locales;

main()

{

locales = new tabla;

//...

}

Y ahora vamos a ver una implementación de tabla::buscar() que emplea una búsqueda lineal a través de una lista vinculada de nombres de la tabla :

#include <string.h>

nombre* tabla::buscar(char* p, int ins)

{

for (nombre* n=tab; n; n=n->siguiente)

if(strcmp(p,n->cadena) == 0) return n;

if(ins == 0) error (“no se encuentra el nombre”);

nombre* nn = new nombre;

nn->cadena = new char(strlen(p)+1);

strcpy(nn->cadena, p);

nn->valor = 1;

nn->siguiente = tab;

tab = nn;

return nn;

}

Supongamos ahora que queremos mejorar la clase tabla para utilizar una búsqueda por dispersión :

clase tabla {

nombre** tab;

int tam;

public:

tabla(int tam = 15);

~tabla();

nombre* buscar (char*, int = 0);

nombre* insertar(char *s) { return buscar(s,1); }

};

La estructura de datos y el constructor fueron modificados para reflejar las nuevas necesidades. La inclusión de un tamaño específico en el constructor es porque la búsqueda por dispersión necesita tener un tamaño. La asignación de un tamaño por omisión es necesaria para que el código anterior siga siendo válido.

tabla::tabla(int tm)

{

if(tm < 0) error(“tabla de tamaño negativo”);

tab = new nombre*[tam=tm];

for (int i=0; i<tm; i++) tab[i]= 0;

}

tabla::~tabla()

{

for (int i=0; i<tam; i++) {

nombre* sig;

for (nombre* n = tab[i]; n; n=sig) {

sig = n->siguiente;

delete n->cadena;

delete n;

}

}

delete tab;

}

Ahora especificamos la función buscar :

nombre* tabla::buscar(const char* p, int ins)

{

int ii = 0;

char* pp = p;

while (*pp) ii = ii<< 1^*pp++;

if( ii<0 ) ii = -ii;

ii %=tam;

for (nombre* n=tab[ii]; n; n= n->siguiente) if(strcmp(p,n->cadena) == 0) return n;

if (ins == 0) error(“no se encuentra el nombre”); nombre* nn= new nombre;

nn->cadena = new char{strlen(p)+1};

strcpy(nn->cadena,p);

nn->valor = 1;

nn->siguiente = tab[ii];

tab [ii] = nn;

return nn;

}

Objetos de clase como miembros

class tabla{

nombre ** tab;

int tam;

public:

tabla(int tm=15);

~tabla();

nombre* buscar(char*, int=0);

nombre* insertar(char* s) { return buscar(s,1); }

};

class defclase{

tabla miembros;

int num_miembros;

....

public:

defclase(int tamaño);

~defclase();

};

Defclase debe contener una tabla de miembros de tamaño tamaño. Como se llama al constructor de tablas?

defclase::defclase(int tamaño): miembros(tamaño)

{

num_miembros= tamaño;

}

El constructor de miembros se llama antes del cuerpo del constructor especificando su lista de parámetros.

Si tenemos más miembros como en el siguiente caso:

class defclase{

tabla miembros;

tabla amigas;

int num_miembros;

....

public:

defclase(int tamaño);

~defclase();

};

defclase::defclase(int tamaño): miembros(tamaño),

amigas(tamaño), num_miembros (tamaño)

{

...

}

Si un constructor de un miembro no necesita argumentos no es necesario especificar el constructor de miembros en la lista de argumentos, por ejemplo:

defclase::defclase(int tamaño): miembros(tamaño),

num_miembros (tamaño)

{

...

}

Aquí se llama al constructor por omisión (sin argumentos) por lo tanto el miembro amigas tendrá un tamaño de 15

Universidad Tecnológica Nacional - Santa Fe - Departamento Sistemas -

Curso : Desarrollos de Programación en C++