Está en la página 1de 4

Introducción a clases en programas imperativos

Algoritmos y Estructuras de Datos I

Segundo cuatrimestre de 2004

1. Introducción

Como dijimos, al igual que en programación funcional, en la mayorı́a de los lenguajes


imperativos también es posible crear tipos complejos a partir de otros más simples. Ya
vimos una de las formas más básicas de hacer esto: los arreglos. Pero C++ también
brinda un mecanismo de construcción de tipos más complejo: las clases1 . Las clases nos
permiten construir tipos comparables a los que podemos construir con la cláusula data
y exportar a través de módulos en Haskell.
Veamos cómo trabajaremos con clases a través de un ejemplo. Vamos a implementar
el tipo Conjunto de enteros, partiendo de lo que hemos especificado en la práctica de
tipos compuestos. Concentrémonos aquı́ en un conjunto reducido de operaciones:
∆ : ConjuntohZi = ConjuntoInt
P ≡ {True}
Q ≡ {|∆| = 0}
C : ConjuntohZi = agregar(e : Z, C : ConjuntohZi)
P ≡ {C = C 0 ∧ e = E 0 }
Q ≡ {(∀n : Z)(n ∈ C ↔ (n ∈ C 0 ∨ n = E 0 ))}
∆ : Z = cardinal(C : ConjuntohZi)
P ≡ {C = C 0 }
Q ≡ {∆ = |C 0 |}
∆ : B = pertenece(e : Z, C : ConjuntohZi)
P ≡ {C = C 0 ∧ e = E 0 }
Q ≡ {∆ = E 0 ∈ C 0 }
(∆ : Z, C : ConjuntohZi) = tomarU no(C : ConjuntohZi)
P ≡ {C = C 0 ∧ |C 0 | > 0}
Q ≡ {∆ ∈ C 0 ∧ (∀n : Z)(n ∈ C ↔ (n ∈ C 0 ∧ n 6= ∆))}

2. Definición de clases en C++

La declaración de una clase ConjuntoInt que implemente el tipo y las operaciones


especificadas arriba tiene esta forma:

1
En realidad, las clases no son meramente un “mecanismo de construcción de tipos”, sino que repre-
sentan un concepto más complejo, que forma parte del paradigma de programación con objetos. No las
veremos desde ese punto de vista en la materia.

1
#define MAXTAM 20

class ConjuntoInt{
public:
ConjuntoInt();
void agregar(int elem);
int cardinal() const;
bool pertenece(int elem) const;
int tomarUno();
private:
int elems[MAXTAM];
int card;
};

Desde fuera de esta declaración, para usar algo de tipo ConjuntoInt, lo declaramos
de esta manera:

int main(){
ConjuntoInt miConj;
...
}

3. Un poco de vocabulario

En esta definición deberı́a haber bastantes elementos que les llamen la atención.
Empecemos por notar que la declaración de esta clase tiene tres partes: primero que
nada, su nombre, y luego, entre corchetes, una lista de declaraciones bajo la palabra
public y otra bajo la palabra private.
Estas dos palabras clave son las que proveen el ocultamiento, uno de los componentes
esenciales de los tipos abstractos de datos. Todas las declaraciones de la primera parte
se llaman públicas y forman la interfaz de la clase: son las operaciones visibles desde
fuera, que constituyen la única manera de acceder y manipular elementos de la clase.
Las declaraciones de la segunda parte son privadas: forman parte de la representación
interna, que permanece oculta cuando se utiliza la clase desde fuera. La comparación
con Haskell es directa: en la parte pública están los elementos que exportarı́amos en un
módulo de Haskell, y en la privada, los restantes.
En este caso, nuestra interfaz está compuesta de las cinco operaciones que especifi-
camos. Las funciones provistas por una clase se llaman métodos. En la parte privada,
mantenemos oculta la representación interna del conjunto: en este caso, lo implemen-
tarı́amos con un arreglo que contenga sus elementos, y un entero que represente su
tamaño. Las variables de una clase se llaman propiedades. Los métodos y propiedades
pueden ir tanto en la parte pública como en la privada, aunque en general, en la parte
pública sólo aparecen métodos (los de la interfaz), mientras que la parte privada contiene
las propiedades y eventualmente algunos métodos auxiliares.

2
Una vez definida, la clase funciona como un tipo abstracto de datos, y podemos
utilizarla creando elementos del tipo. Estos se llaman instancias de la clase. En nuestro
ejemplo, miConj es una instancia de la clase ConjuntoInt.

4. El parámetro implı́cito

Comparemos ahora las declaraciones de la interfaz de la clase con las operaciones


especificadas al principio. En la primera de ellas, parece faltar un valor de retorno; en
todas las demás, falta un parámetro. Pero todavı́a podemos decir algo más: lo que falta
es siempre del tipo ConjuntoInt.
En Haskell, lo que vinculaba un tipo abstracto de datos con sus operaciones, era
que las funciones aparecı́an dentro del mismo módulo. Naturalmente, la mayorı́a de
estas funciones recibı́a o devolvı́a por lo menos un elemento del tipo, pero esto no
debı́a ser necesariamente ası́. En cambio, en C++, todos los métodos declarados en una
clase reciben tácitamente una instancia de dicha clase, que denominamos el parámetro
implı́cito de la función. Además, existen métodos especiales, llamados constructores,
que llevan el mismo nombre que la clase, y se utilizan para crear instancias. En nuestro
ejemplo, al declarar ConjuntoInt miConj, internamente se está llamando al constructor
de la clase, declarado como ConjuntoInt();.
Vamos a incorporar esta noción a la especificación. Llamaremos this al parámetro
implı́cito, y lo utlizaremos como un argumento de entrada, salida o entrada/salida, según
corresponda. Veamos cómo queda la especificación para los primeros métodos:

this : ConjuntohZi = ConjuntoInt


P ≡ {True}
Q ≡ {|this| = 0}
Aquı́ estamos diciendo que ConjuntoInt crea una instancia de la clase,
de cardinal 0, o sea, vacı́a.

this : ConjuntohZi = agregar(e : Z, this : ConjuntohZi)


P ≡ {this = T his0 ∧ e = E 0 }
Q ≡ {(∀n : Z)(n ∈ this ↔ (n ∈ T his0 ∨ n = E 0 ))}
Este método modifica el parámetro implı́cito de manera tal de agregar
el elemento recibido.

Y de la misma forma, para las demás operaciones:

∆ : Z = cardinal(this : ConjuntohZi)
P ≡ {this = T his0 }
Q ≡ {∆ = |T his0 |}

∆ : B = pertenece(e : Z, this : ConjuntohZi)


P ≡ {this = T his0 ∧ e = E 0 }
Q ≡ {∆ = E 0 ∈ T his0 }

3
(∆ : Z, this : ConjuntohZi) = tomarU no(this : ConjuntohZi)
P ≡ {this = T his0 ∧ |T his0 | > 0}
Q ≡ {∆ ∈ T his0 ∧ (∀n : Z)(n ∈ this ↔ (n ∈ T his0 ∧ n 6= ∆))}

Cuando llamamos a un método de la clase, el parámetro implı́cito también tiene un


tratamiento especial: en lugar de ir entre paréntesis, como el resto de los argumentos,
va a la izquierda del nombre de la función, y separado de un punto. Continuando con el
ejemplo:

int main(){
ConjuntoInt miConj; (construimos una instancia de
ConjuntoInt)

miConj.agregar(7); (miConj es el parámetro


miConj.agregar(25); implı́cito de este y los
próximos llamados)

int tam = miConj.cardinal(); ¿Qué valor toma tam aquı́?


bool estaEl7 = miConj.pertenece(7); ¿Y estaEl7?
bool estaEl25 = miConj.pertenece(25); ¿Y estaEl25?

int unElemento = miConj.tomarUno();

tam = miConj.cardinal(); ¿Qué valor toma tam aquı́?


estaEl7 = miConj.pertenece(7); ¿Y estaEl7?
estaEl25 = miConj.pertenece(25); ¿Y estaEl25?
}

5. Otros detalles

Para indicar que un método no modifica su parámetro implı́cito, podemos agregar


en C++ la palabra clave const a su declaración. En nuestro ejemplo, lo hicimos con
las operaciones cardinal y pertenece. Aunque esto no es obligatorio, es un control
extra que realiza el compilador, verificando que en la implementación del método, no se
modifique nunca el parámetro implı́cito, ni se llame a una función que pueda modificarlo.

6. Implementación

Esto lo vemos en otra clase...

También podría gustarte