Clases derivadas

Anuncio
Las clases pueden introducirse de muchas formas, comenzando por la que dice que representan un intento de
abstraer el mundo real. Pero desde el punto de vista del programador clásico, lo mejor es considerarlas como
"entes" que superceden las estructuras C en el sentido de que tanto los datos como los instrumentos para su
manipulación (funciones) se encuentran encapsulados en ellos. La idea es empaquetar juntos los datos y la
funcionalidad, de ahí que tengan dos tipos de componentes (aquí se prefiere llamarlos miembros). Por un lado
las propiedades, también llamadas variables o campos (fields), y de otro los métodos, también llamados
procedimientos o funciones [1]; más formalmente: variables de clase y métodos de clase.
La terminología utilizada en la Programación Orientada a Objetos POO (OOP en inglés), no es demasiado
consistente, y a veces induce a cierto error a los programadores que se acercan por primera vez con una
cultura de programación procedural. De hecho, estas cuestiones semánticas suponen una dificultad adicional
en el proceso de entender los conceptos subyacentes en la POO, sus ventajas y su potencial como herramienta.
Las clases C++ ofrecen la posibilidad de extender los tipos predefinidos en el lenguaje (básico y derivado).
Cada clase representa un nuevo tipo; un nuevo conjunto de objetos caracterizado por ciertos valores
(propiedades) y las operaciones (métodos) disponibles para crearlos, manipularlos y destruirlos. Más tarde se
podrán declarar objetos pertenecientes a dicho tipo (clase) del mismo modo que se hace para las variables
simples tradicionales.
Considerando que son vehículos para manejo y manipulación de información, las clases han sido comparadas
en ocasiones con los sistemas tradicionales de manejo de datos DBMS ("DataBase Management System");
aunque de un tipo muy especial, ya que sus características les permiten operaciones que están absolutamente
prohibidas a los sistemas DBMS clásicos.
La mejor manera de entender las clases es considerar que se trata simplemente de tipos de datos cuya única
peculiaridad es que pueden ser definidos por el usuario. Generalmente se trata de tipos complejos,
constituidos a su vez por elementos de cualquier tipo (incluso otras clases). La definición que puede hacerse
de ellos no se reduce a diseñar su "contenido"; también pueden definirse su álgebra y su interfaz. Es decir:
como se opera con estos tipos y como los ve el usuario (que puede hacer con ellos). El propio inventor del
lenguaje señala que la principal razón para definir un nuevo tipo es separar los detalles poco relevantes de la
implementación de las propiedades que son verdaderamente esenciales para utilizarlos correctament
CLASES DERIVADAS.
En C++, la herencia simple se realiza tomando una clase existente y derivando nuevas clases de ella. La clase
derivada hereda las estructuras de datos y funciones de la clase original. Además, se pueden añadir nuevos
miembros a las clases derivadas y los miembros heredados pueden ser modificados.
Una clase utilizada para derivar nuevas clases se denomina clase base, clase padre, superclase o ascendiente.
Una clase creada de otra clase se denomina clase derivada o subclase.
Se pueden construir jerarquías de clases, en las que cada clase sirve como padre o raíz de una nueva clase.
Conceptos fundamentales de derivación
C++ utiliza un sistema de herencia jerárquica. Es decir, se hereda una clase de otra, creando nuevas clases a
partir de las clases ya existentes. Sólo se pueden heredar clases, no funciones ordinarias n variables, en C++.
Una clase derivada hereda todos los miembros dato excepto, miembros dato estático, de cada una de sus
clases base. Una clase derivada hereda la función miembro de su clase base. Esto significa que se hereda la
capacidad para llamar a funciones miembro de la clase base en los objetos de la clase derivada.
Los siguientes elementos de la clase no se heredan:
1
− Constructores
− Destructores
− Funciones amigas
− Funciones estáticas de la clase
− Datos estáticos de la clase
− Operador de asignación sobrecargado
Las clases base diseñadas con el objetivo principal de ser heredadas por otras se denominan clases abstractas.
Normalmente, no se crean instancias a partir de clases abstractas, aunque sea posible.
La herencia en C++
En C++ existen dos tipos de herencia: simple y múltiple. La herencia simple es aquella en la que cada clase
derivada hereda de una única clase. En herencia simple, cada clase tiene un solo ascendiente. Cada clase
puede tener, sin embargo, muchos descendientes. La herencia múltiple es aquella en la cual una clase derivada
tiene más de una clase base. Aunque el concepto de herencia múltiple es muy útil, el diseño de clases suele ser
más complejo.
Creación de una clase derivada
Cada clase derivada se debe referir a una clase base declarada anteriormente. La declaración de una clase
derivada tiene la siguiente sintaxis:
Class clase_derivada:<especificadores_de_acceso> clase_base {...};
Los especificadotes de acceso pueden ser: public, protected o private.
Clases de derivación
Los especificadores de acceso a las clases base definen los posibles tipos de derivación: public, protected y
private. El tipo de acceso a la clase base especifica cómo recibirá la clase derivada a los miembros de la clase
base. Si no se especifica un acceso a la clase base, C++ supone que su tipo de herencia es privado.
− Derivación pública (public). Todos los miembros public y protected de la clase base son accesibles en la
clase derivada, mientras que los miembros private de la clase base son siempre inaccesibles en la clase
derivada.
− Derivación privada (private). Todos los miembros de la clase base se comportan como miembros privados
de la clase derivada. Esto significa que los miembros public y protected de la clase base no son accesibles más
que por las funciones miembro de la clase derivada. Los miembros privados de la clase siguen siendo
inaccesibles desde la clase derivada.
− Derivación protegida (protected). Todos los miembros public y protected de la clase base se comportan
como miembros protected de la clase derivada. Estos miembros no son, pues, accesibles al programa exterior,
pero las clases que se deriven a continuación podrán acceder normalmente a estos miembros (datos o
funciones).
CREACIÓN DE UNA CLASE.
Sintaxis
2
La construcción de una clase partiendo desde cero, es decir, cuando no deriva de una clase previa, tiene la
siguiente sintaxis (que es un caso particular de la sintaxis general:
Class−key <info> nomb−clase {<lista−miembros>};
Ejemplo
class Hotel { int habitd; int habits; char stars[5]; };
Es significativo que la declaración (y definición) de una clase puede efectuarse en cualquier punto del
programa, incluso en el cuerpo de otra clase.
Salvo que se trate de una declaración adelantada, el bloque <lista−miembros>, también denominado cuerpo de
la clase, debe existir, y declarar en su interior los miembros que constituirán la nueva clase, incluyendo
especificadotes de acceso (explícitos o por defecto) que especifican aspectos de la accesibilidad actual y
futura (en los descendientes) de los miembros de la clase.
la cuestión de la accesibilidad de los miembros está estrechamente relacionada con la herencia, por lo que
hemos preferido trasladar la explicación de esta importante propiedad al capítulo dedicado a la herencia.
Quién puede ser miembro
La lista de miembros es una secuencia de declaraciones de propiedades de cualquier tipo, incluyendo
enumeraciones, campos de bits etc.; así como declaración y definición de métodos, todos ellos con
especificadotes opcionales de acceso y de tipo de almacenamiento. Auto, extern y register no son permitidos;
si en cambio static y const. Los elementos así definidos se denominan miembros de la clase. Hemos dicho
que son de dos tipo: propiedades de clase y métodos de clase.
Es importante advertir que los elementos constitutivos de la clase deben ser completamente definidos para el
compilador en el momento de su utilización. Esta advertencia solo tiene sentido cuando se refiere a
utilización de tipos abstractos como miembros de clases, ya que los tipos simples (preconstruidos en el
lenguaje) quedan perfectamente definidos con su declaración. Ver a continuación una aclaración sobre este
punto.
Ejemplo:
Class Vuelo {
Char nombre [30];
// Vuelo es la clase
// nombre es una propiedad
Int. capacidad;
Enum modelo {B747, DC10};
Char origen[8];
Char destino [8];
Char fecha [8];
3
Void despegue (&operación}; // despegue es un método
Void crucero (&operación);
};
Los miembros pueden ser de cualquier tipo con una excepción: No pueden ser la misma clase que se está
definiendo (lo que daría lugar a una definición circular), por ejemplo:
Class Vuelo {
Char nombre [30];
Class Vuelo;
// Ilegal
...
Sin embargo, si es lícito que un miembro sea puntero al tipo de la propia clase que se está declarando:
class Vuelo {
Char nombre [30];
Vuelo* ptr;
...
En la práctica esto significa que un miembro ptr de un objeto c1 de una clase C, es un puntero que puede
señalar a otro objeto c2 de la misma clase.
Nota: Esta posibilidad es muy utilizada, pues permite construir árboles y listas de objetos (unos enlazan con
otros). Precisamente en el capítulo dedicado a las estructuras auto referenciadas, se muestra la construcción
de un árbol binario utilizando una estructura que tiene dos elementos que son punteros a objetos del tipo de la
propia estructura (recuerde que las estructuras C++ son un caso particular de clases.
También es lícito que se utilicen referencias a la propia clase:
class X {
int i;
char c;
public:
X(const X& ref, int x = 0); { // Ok. correcto
i = ref.i;
c = ref.c;
};
4
De hecho, un grupo importante de funciones miembro, los constructores−copia, se caracterizan precisamente
por aceptar una referencia a la clase como primer argumento.
Las clases pueden ser miembros de otras clases, clases anidadas. Por ejemplo:
class X {
// clase contenedora (exterior)
public:
int x;
class Xa {
// clase dentro de clase (anidada)
public:
int x;
};
};
Ver aspectos generales en: clases dentro de clases.
Las clases pueden ser declaradas dentro de funciones, en cuyo caso se denominan clases locales, aunque
presentan algunas limitaciones. Ejemplo:
void foo() {
// función contenedora
...
int x;
class C {
// clase local
public:
int x;
};
}
También pueden ser miembros las instancias de otras clases (objetos):
class Vertice {
public: int x, y;
};
5
class Triangulo {
// Clase contenedora
public:
Vertice va, vb, vc;
// Objetos dentro de una clase
};
Es pertinente recordar lo señalado al principio: que los miembros de la clase deben ser perfectamente
conocidos por el compilador en el momento de su utilización. Por ejemplo:
class Triangulo {
...
Vertice v;
// Error: Vertice no definido
};
class Vertice {...};
En estos casos no es suficiente realizar una declaración adelantada de Vértice:
class Vertice;
class Triangulo {
public:
Vértice v;
// Error: Información insuficiente de Vértice
};
Class Vértice {...};
Ya que el compilador necesita una definición completa del objeto v para insertarlo como miembro de la clase
Triangulo.
La consecuencia es que importa el orden de declaración de las clases en el fuente. Debe comenzarse
definiendo los tipos más simples (que no tienen dependencia de otros) y seguir en orden creciente de
complejidad (clases que dependen de otras clases para su definición). También se colige que deben evitarse
definiciones de clases mutuamente dependientes:
class A {
...
B b1;
};
class B {
6
...
A a1;
};
ya que conducirían a definiciones circulares como las señaladas antes
Los miembros de la clase deben ser completamente declarados dentro del cuerpo, sin posibilidad de que
puedan se añadidos fuera de él. Las definiciones de las propiedades se efectúan generalmente en los
constructores (un tipo de función−miembro), aunque existen otros recursos inicialización de miembros. La
definición de los métodos puede realizarse dentro o fuera del cuerpo ( funciones inline. Ejemplo:
class C {
int x;
char c;
void foo();
};
int C::y;
// Error!! declaración off−line
void C::foo() { ++x; } // Ok. definición off−line
Las funciones−miembro, denominadas métodos, pueden ser declaradas inline, static virtual, const y explicit si
son constructores. Por defecto tienen enlazado externo.
Clases vacías
Los miembros pueden faltar completamente, en cuyo caso tendremos una clase vacía. Ejemplo:
Class empty {};
La clase vacía es una definición completa y sus objetos son de tamaño distinto de cero, por lo que cada una de
sus instancias tiene existencia independiente. Suelen utilizarse como clases−base durante el proceso de
desarrollo de aplicaciones. Cuando se sospecha que dos clases pueden tener algo en común, pero de momento
no se sabe exactamente que.
Inicialización de miembros
Lo mismo que ocurre con las estructuras, que a fin de cuentas son un tipo de clase en su declaración solo está
permitido señalar tipo y nombre de los miembros, sin que se pueda efectuar ninguna asignación, ni aún en el
caso de que se trate de una constante. Así pues, en el bloque <lista−miembros> no pueden existir
asignaciones. Por ejemplo:
class C {
7
...
int x = 33;
// Asignación ilegal !!
...
};
Las únicas excepciones permitidas son la asignación a constantes estáticas enteras y los enumeradores (ver a
continuación), ya que los miembros estáticos tienen unas características muy especiales. Ejemplo:
class C {
...
static const int kte = 33;
// Ok:
static const kt1 = 33.0
// Error: No entero
cont int kt2 = 33;
static kt3 = 33;
// Error: No estática
// Error: No constante
static const int kt4 = f(33); // Error: inicializador no constante
};
El sitio idóneo para situar las asignaciones a miembros es en el cuerpo de las funciones de clase (métodos).
En especial las asignaciones iniciales (que deben efectuarse al instanciar un objeto de la clase) tienen un sitio
específico en el cuerpo de ciertos métodos especiales denominados constructores. En el epígrafe "Inicializar
miembros" se ahonda en esta cuestión.
Si es posible utilizar y definir un enumerador (que es una constante simbólica dentro de una clase. Por
ejemplo:
class C {
...
enum En { E1 = 3, E2 = 1, E3, E4 = 0};
...
};
En ocasiones es posible utilizar un enumerador para no tener que definir una constante estática.
Ejemplo−1 Las tres formas siguientes serían aceptables:
8
class C {
static const int k1 = 10;
char v1[k1];
enum e {E1 = 10};
char v2[E1];
enum {KT = 20};
char v3[KT];
...
};
Ejemplo−2:
class CAboutDlg : public CDialog {
...
enum { IDD = IDD_ABOUTBOX };
...
};
La definición de la clase CAboutDlg pertenece a un caso real tomado de MS VC++. El enumerador anónimo
es utilizado aquí como un recurso para inicializar la propiedad IDD con el valor IDD_ABOUTBOX que es a
su vez una constante simbólica para el compilador. De no haberse hecho así, se tendría que haber declarado
IDD como constante estática, en cambio la forma adoptada la convierte en una variable enumerada anónima
que solo puede adoptar un valor (otra forma de designar al mismo concepto).
Ejemplo−3:
Class C {
...
enum {CERO = 0, UNO = 1, DOS = 2, TRES = 3};
};
...
void foo(C& c1) {
std::cout << c1.CERO;
// −> 0
Std::cout << c1.TRES;
// −> 3
}
Téngase en cuenta que las clases son tipos de datos que posteriormente tienen su concreción en objetos
determinados. Precisamente una de las razones de ser de las variables de clase, es que pueden adoptar valores
9
distintos en cada instancia concreta de la clase. Por esta razón, a excepción de las constantes y los miembros
estáticos, no tiene mucho sentido asignar valores concretos a las variables de clase, ya que los valores
concretos los reciben las instancias, bien por asignación directa, o a través de los constructores.
En el apartado dedicado a Inicialización de miembros volvemos sobre la cuestión, exponiendo con detalle la
forma de realizar estas asignaciones, en especial cuando se trata de constantes.
NATURALEZA: TIPO U OBJETOS.
Gestión dinámica de tipos.
GLib incluye un sistema dinámico de tipos, que no es más que una base de datos en la que se van registrando
las distintas clases. En esa base de datos, se almacenan todas las propiedades asociadas a cada tipo registrado,
información tal como las funciones de inicialización del tipo, el tipo base del que deriva, el nombre del tipo,
etc. Todo ello identificado por un identificador único, conocido como GType.
Tipos basados en clases (objetos).
Para crear instancias de una clase, es necesario que el tipo haya sido registrado anteriormente, de forma que
esté presente en la base de datos de tipos de GLib!. El registro de tipos se hace mediante una estructura
llamada GTypeInfo, que tiene la siguiente forma:
Tipos no instanciables (fundamentales).
Muchos de los tipos que se registran no son directamente instanciables (no se pueden crear nuevas instancias)
y no están basados en una clase. Estos tipos se denominan tipos fundamentales en la terminología de GLib! y
son tipos que no están basados en ningún otro tipo, tal y como sí ocurre con los tipos instanciables (u objetos).
Entre estos tipos fundamentales se encuentran algunos viejos conocidos, como por ejemplo gchar y otros tipos
básicos, que son automáticamente registrados cada vez que se inicia una aplicación que use GLib!.
Como en el caso de los tipos instanciables, para registrar un tipo fundamental es necesaria una estructura de
tipo GTypeInfo, con la diferencia que para los tipos fundamentales bastará con rellenar con ceros toda la
estructura.
La mayor parte de los tipos no instanciables están diseñados para usarse junto con GValue, que permite
asociar fácilmente un tipo GType con una posición de memoria. Se usan principalmente como simples
contenedores genéricos para tipos sencillos (números, cadenas, estructuras, etc.).
Implementación de nuevos tipos.
En el apartado anterior se mostraba la forma de registrar una nueva clase en el sistema de objetos de GLib!, y
se hacía referencia a distintas estructuras y funciones de inicialización. En este apartado, se va a indagar con
más detalle en ellos.
Tanto los tipos fundamentales como los no fundamentales quedan definidos por la siguiente información:
• Tamaño de la clase.
• Funciones de inicialización (constructores en C++!).
• Funciones de destrucción (destructores en C++!), llamadas de finalización en la jerga de GLib!.
• Tamaño de las instancias.
• Normas de instanciación de objetos (uso del operador new en C++!).
10
• Funciones de copia.
Toda esta información queda almacenada, como se comentaba anteriormente, en una estructura de tipo
GTypeInfo. Todas las clases deben implementar, aparte de la función de registro de la clase
(my_object_get_type), al menos dos funciones que se especificaban en la estructura GTypeInfo a la hora de
registrar la clase. Estas funciones son:
• my_object_class_init: función de inicialización de la clase, donde se inicializará todo lo relacionado
con la clase en sí, tal como las señales que tendrá la clase, los manejadores de los métodos virtuales si
los hubiera, etc.
• my_object_init: función de inicialización de las instancias de la clase, que será llamada cada vez que
se solicite la creación de una nueva instancia de la clase. En esta función, las tareas a desempeñar son
todas aquellas relacionadas con la inicialización de una nueva instancia de la clase, tales como los
valores iniciales de las variables internas de la instancia.
11
Descargar