Story Transcript
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: 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 nomb−clase {}; 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 , 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 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