Clases

4.11 Clases §1 Introducción: Las clases pueden introducirse de muchas formas, comenzando por la que dice que representan

Views 201 Downloads 0 File size 993KB

Report DMCA / Copyright

DOWNLOAD FILE

Recommend stories

Citation preview

4.11 Clases §1 Introducción: 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, campos ("fields") o atributos, y de otro los métodos, también llamados procedimientos o funciones [1]; más formalmente: variables de clase y métodos de clase. Nota: 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ásicos y derivados 2.2). 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 [2]. Más tarde se podrán declarar (instanciar) objetos pertenecientes a dicho tipo (clase) del mismo modo que se hace para las variables simples tradicionales. Nota: 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 [5].

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 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 correctamente [6].

§2 En este sentido, podríamos establecer una analogía entre un tipo de dato simple, por ejemplo un int, y una clase a la que llamaremos CL: §2.1 Ambos son "tipos" a1.- El tipo int está preconstruido en el lenguaje; cuando nos referimos a un int, está completamente determinado que significa y que operaciones pueden realizarse con los objetos de su "clase" (los enteros).

[

a2.- El tipo CL no está preconstruido (no existe previamente en el lenguaje) por lo que antes de usado debe ser definido. La definición de la clase es incumbencia del programador, en ella se determina que operaciones se pueden realizar con este tipo de objetos. La definición de una clase puede tener el aspecto siguiente: class CL { /* definición de la clase */ }; §2.2 Pueden declararse objetos de ese tipo: b1.- Puede declararse un objeto x como perteneciente al tipo int: (diríamos que x es un entero): int x;

// declara x perteneciente al tipo int

b2.- Puede declararse un objeto c1 como perteneciente al tipo CL. En este caso la POO tiene su propio vocabulario; prefiere decir que se "instancia" la clase (diríamos que c1 es una instancia u objeto): CL c1;

// declara c1 perteneciente al tipo CL

§2.3 Pueden asignarse valores a los objetos: c1.- Puede asignarse un valor al objeto x: x = y;

// asigna a x el valor de y (que suponemos un objeto tipo int)

c2.- Puede asignarse un valor al objeto c1: c1 = d1;

// asigna a c1 el valor de d1 (que suponemos un objeto tipo CL)

§2.4 Pueden realizarse operaciones con los objetos: d1.- Pueden realizarse operaciones con objetos tipo int. Por ejemplo, en el caso de los enteros, está perfectamente definido en el lenguaje que resultado produce el operador "+" entre dos objetos de dicho tipo. x = y + z;

// suma aritmética de z e y

d2.- Puede realizarse operaciones con objeto tipo CL (suponiendo que estas operaciones hayan sido definidas por el programador de la clase): c1 = d1 + e; clase CL

// el resultado que haya establecido el programador de la

Al llegar a este punto se terminaría la posible analogía entre datos simples y clases porque estas últimas tienen en realidad una "doble personalidad". En efecto, hemos señalado ( 2) que en un programa existen dos tipos de elementos: datos e instrucciones. Los tipos básicos antes aludidos son exclusivamente "datos", y en la programación clásica la funcionalidad está dispersa en el código del programa (generalmente agrupada en unidades lógicas a las que denominamos "funciones"). Los lenguajes orientados a objetos como C++ permiten que las clases contengan también "funcionalidad", además de "datos", de ahí nuestra afirmación de que las clases tienen una doble personalidad. Esta funcionalidad se concreta en funciones alojadas en su interior. De hecho, gran parte del trabajo de las clases en los programas se realiza a través de estas funciones-miembro (métodos).

Nota: generalmente parte de esta "funcionalidad" de los métodos se utiliza para manipular los datos, incluyendo accederlos (verlos) al exterior, por lo que algunos autores [7] insisten en que los métodos son en realidad parte de la información (de los datos).

Por ejemplo, no es infrecuente ver en compiladores C++ o de otros lenguajes de alto nivel orientados a objetos [3], clases predefinidas de tipo "Browse" que son capaces de manejar el contenido de una base de datos de forma absolutamente cómoda para el programador que las usa. Están definidas de tal modo que mediante invocaciones a sus diversos métodos pueden mostrarnos el contenido de la base de datos en forma de filas y columnas; permite definir el tamaño de la ventana o incluso redimensionarla en tiempo de ejecución; desplazarse por sus celdas mediante la acción de las teclas de movimiento de cursor; ir al comienzo, al final, o a cualquier registro que deseemos; hacer "scroll" horizontal y vertical mediante barras de deslizamiento; etc. etc. En la literatura sobre el tema es frecuente encontrar expresiones como: "enviarle un mensaje al objeto". Se refieren a invocar una de estas funciones-miembro que son accesibles desde el exterior; lo que en lenguaje coloquial significa enviar al objeto una petición de que ponga en marcha alguna de las funcionalidades inherentes a "su clase".

§3 Agrupar objetos y funcionalidad El hecho de que las clases encapsulen en una misma entidad datos y funcionalidad (su álgebra y su interfaz), supone una ventaja determinante a la hora de escribir y mantener aplicaciones grandes, y es sin duda una de las razones a las que C++ debe su éxito como lenguaje de opción para grandes proyectos. Los métodos tradicionales de programación exigían que cuando una regla de operación cambiase en un programa (cosa que suele ocurrir con suma frecuencia), el programador debía rastrear todo el código de la aplicación para ir modificando todas las ocurrencias en que apareciese dicha operación, adaptándola a las nuevas circunstancias. Por contra, la POO permite modificar el "álgebra" de la clase sin preocuparse de nada más, ya que toda la operatoria está concentrada en un punto. Nota: como ejemplo, puedo contaros que hace años tuve que escribir una aplicación relativamente grande para un negocio de distribución de libros. Durante el análisis me enteré que debía almacenar un dato denominado Código ISBN. Un código que acompaña a cada libro y que es único en el mundo (no hay dos libros distintos con el mismo ISBN). El cliente me informó que era un código exclusivamente numérico de una longitud máxima de X cifras. Después de casi un año de trabajo, durante las primeras pruebas, en que cogimos varios ejemplares para comprobar el proceso de introducción de datos en condiciones reales, apareció un código ISBN que contenía un carácter alfabético. Ni que decir tiene que la consternación fue mayúscula. El cliente puso cara de sorprendido y me juró que le habían asegurado que bla, bla, bla. Desgraciadamente la aplicación no estaba escrita en un lenguaje orientado a objetos (como era normal en la época), así que su utilización sufrió un retraso de varios meses hasta que el nuevo tipo (alfanumérico) fue corregido en todos los ficheros y líneas de código que lo utilizaban. Actualmente, con un lenguaje orientado a objetos como C++, solo hubiera tenido que modificar unas cuantas líneas de código en la clase Libro.

§4 Ocultar los detalles En realidad las clases C++ son algo más que simple funcionalidad añadida a las estructuras del C clásico. Veremos que además de la posibilidad de contener funciones, las clases C++ disponen de un

mecanismo especial de acceso que a fin de cuentas, se traduce en que algunos de sus miembros permanecen ocultos al exterior. La idea no es solamente encapsular juntos datos y funcionalidad; se trata también de que la clase actúe como un subsistema cerrado dentro del contexto general del programa que la utiliza. De este subsistema solo interesa y es accesible determinada información (datos) y funcionalidad (métodos), sin que importe ni pueda manipularse su interior de ninguna otra forma. La forma concreta de conseguir este control de acceso es haciendo que no todas las variables y funciones-miembro sean accesibles desde el exterior. De hecho, esto ocurre solo con algunas, las denominadas "públicas", que constituyen la parte visible del objeto (su interfaz). Al mismo tiempo, pueden existir una cantidad de otros miembros (los denominados "privados") que no son visibles. La razón de la existencia de los miembros privados es proporcionar cierta funcionalidad interna para soportar la externa. Vendrían a ser como el cuerpo de una función que invocamos pasándole unos argumentos y que nos devuelve un valor. Solo nos interesa el argumento que hay que "enviarle" y el valor devuelto, no interesa en absoluto lo que ocurre "dentro" de la función o "como" es el detalle de la obtención del resultado. Nota: en general, se considera como una buena práctica definir las clases de forma que sus propiedades sean siempre privadas, consiguiendo así que no sean directamente accesibles desde el exterior, y que solo puedan ser vistas y manipuladas a través de métodos públicos específicos diseñados al efecto, que constituyen la interfaz, y garantizan que la manipulación y acceso a las propiedades del objeto se realiza dentro de las condiciones exigidas por la aplicación. Por esta razón, tales métodos se denominan accessors o get methods y mutators o set methods (que podríamos traducir por funciones procuradoras y modificadoras). En caso necesario, los métodos públicos se completan con otros métodos auxiliares privados (helper functions) que aportan funcionalidad complementaria a la interfaz.

A primera vista podría parecer que este sistema de ocultación no tiene demasiado sentido para una clase definida y utilizada por un programador en su propia aplicación; a fin de cuentas, puesto que la define, siempre puede modificarla y accederla como le plazca. Sin embargo, si pensamos que las clases pueden venir empaquetadas en librerías, y que estas librerías son muchas veces confeccionadas por otras personas distintas del programador que las usa, el asunto cobra su verdadero sentido. Por ejemplo, no existe el peligro de que por una manipulación indebida el programador-usuario estropee o corrompa el mecanismo interno de la clase que otros han confeccionado y cuyos detalles de funcionamiento interno él desconoce. Este sistema de protección puede servir incluso para el programador que las diseña y usa en sus propias aplicaciones, puesto que una vez puestas a punto, puede usarlas y rehusarlas (como material-base para construir nuevas clases) sin preocuparse más por su diseño, ya que son subsistemas estables y probados. Una ventaja adicional no menos importante para los fabricantes de clases (en el mundo de la programación también existe un mercado de objetos prefabricados [3]), es que una vez publicada la especificación de una clase y su interfaz (que hace y como se usa), las actualizaciones posteriores no afectan para nada a los usuarios de versiones antiguas que quieran actualizarse. Solamente hay que procurar que la interfaz se mantenga exactamente igual. En todo caso solo es necesario documentar las nuevas funcionalidades si las hubiere. Una tercera ventaja es la que podríamos llamar de protección del "know-how" o secreto industrial. Es posible implementar una clase o conjunto de ellas, con una funcionalidad concreta (por ejemplo, una librería de comunicaciones IP) sin necesidad de desvelar todos sus detalles [4], solo es necesario publicar su interfaz. En la mayoría de los casos los autores de librerías comerciales adoptan todas las

medidas posibles para evitar que incluso los ficheros de cabecera puedan revelar información estratégica a sus competidores. Por ejemplo, la técnica del "Gato de Cheshire" (

4.13.4).

Inicio. [

1] Se prefiere método más que función, para distinguirlos de las funciones de la programación tradicional (procedural). Veremos a continuación que algunas de estas funciones reciben nombres especiales. Por ejemplo, constructores, destructores, accessors y mutators. [2] Este conjunto de operaciones y valores visibles desde el exterior por el "usuario" de la clase (el programador), es lo que se denomina "interfaz" de la clase. [3] Tradicionalmente los fabricantes de "accesorios" para programación, proporcionaban funciones en forma de librerías .LIB o .OBJ, que enlazadas con nuestro código, permitían invocar dichas funciones para conseguir la "funcionalidad" propuesta. Con la popularización de la POO, estos accesorios vienen suministrados en forma de clases. Por lo general, los fabricantes de compiladores las ofrecen bajo la interfaz de las cajas de herramientas o componentes incluidas en las modernas "suites" RAD ( 1.8). Por ejemplo, los entornos de desarrollo C++Builder o Visual C++ incluyen un potente conjunto de ellas. Además, existe un extenso mercado de "componentes" C++ listos para usar con las funcionalidades más variadas. Son lo que en el mundillo de la programación se denominan componentes de terceras partes (3pp), en referencia a que no son las incluidas de forma estándar con los compiladores ni creadas por el programador de la aplicación. [4] Salvo la hipótesis de efectuar ingeniería inversa sobre ellas, las librerías son una caja negra desde el exterior. Además, salvo excepciones, el esfuerzo necesario para desensamblar completamente la ingeniería de una librería es superior al esfuerzo de construirla uno mismo. [5] Al Stevens "Persistent objects in C++" Dr. Dobb's Journal. Dic 1992. [6] Stroustrup [7] Ian Joyner (

TC++PL §5.1 7)

4.11.1 Clases: formas de creación §1 Sinopsis Las clases pueden crearse de tres formas: definición, herencia y composición. §1.1 Por definición (

4.11.2a), construyéndola desde cero.

Se puede considerar la herencia como una forma de cortar y pegar sin trasladar nada en realidad. De hecho, los seguidores fanáticos de C++ y Java se refieren con frecuencia al acto físico de cortar y pegar código como "herencia del editor". David S. Platt. "Así es Microsoft .NET". McGraw-Hill

[

§1.2 Por herencia ( 4.11.2b); partiendo de una clase existente, y perfilando los comportamientos o datos que queremos pulir en una nueva versión que se adapte mejor a nuestro propósito. Por ejemplo, un motor eléctrico deriva de la clase general de los motores, de la cual derivan también los de gasolina, diesel, de vapor, etc. A este tipo de herencia se la denomina también herencia simple, en referencia a que deriva de un único ancestro, y por supuesto una misma clase puede ser antecesora de varias clases derivadas, cada una de ellas comparte rasgos comunes con sus "hermanas" aunque tiene también características propias.

Aunque la herencia es uno de los pilares de la POO y tiene innegables ventajas, como la reutilización de la interfaz, tiene también sus inconvenientes. Por ejemplo, dado que el compilador debe imponer ciertas características en tiempo de compilación sobre las clases creadas por herencia, esto resulta en cierta rigidez posterior. Una característica especial es que, si se cambia el comportamiento de la clase antecesora, cambiará el comportamiento de las clases derivadas de ella. Nota: la nomenclatura utilizada es muy variada: A la clase antecesora se la denomina también clase padre, clase-base o súper-clase. A su vez, a las clases derivadas se las denomina también descendientes y subclases.

Por lo general, derivar una clase de una existente solo tiene sentido si se modifica algo su comportamiento (su interfaz) y/o su contenido (datos). Esto se consigue de tres formas: •

Añadiendo propiedades y/o métodos que no existían en la clase base



Sobrescribiendo métodos con distintos comportamientos (sobrecarga).



Redefiniendo propiedades que existían en la clase base

§1.3 Por agregación, composición o herencia múltiple ( 4.11.2c). Estos son los nombres que se dan al proceso de ensamblar un nuevo tipo, con elementos y piezas (que aquí se llaman "miembros") de otras clases. Es posible declarar clases derivadas de otras haciendo que hereden los miembros de varias clases-padre (antecesoras o ancestros). Es clásico el ejemplo de señalar que un automóvil tiene un motor y cuatro ruedas; elementos estos pertenecientes a la clase de los motores y de las ruedas respectivamente, etc. Este sistema tiene también sus ventajas e inconvenientes, pero es muy flexible, ya que incluso pueden cambiarse los componentes en tiempo de ejecución.

Como el lector se habrá figurado, todo este esquema de herencia, simple o múltiple, que además es recursivo (las clases derivadas pueden ser a su vez clases-base de otras), conduce a un esquema de relaciones parecido a un árbol genealógico, que en este caso recibe el nombre de jerarquía de clases.

4.11.2 Declaración de una nueva clase §1 Sinopsis Como se ha señalado anteriormente, la declaración de una nueva clase puede efectuarse básicamente de tres formas: por definición (partiendo desde cero), por herencia simple (derivando de una clase anterior) y por composición o herencia múltiple (heredando de varias clases-base).

§2 Sintaxis En cualquier caso, la declaración de una nueva clase utiliza una expresión cuya sintaxis general es la siguiente: class-key nomb-clase { }; Significado de los diversos componentes: es alguna de las palabras clave class, struct o union [1]. opcional. Indica una petición de información en tiempo de ejecución sobre la clase. Puede compilarse con la opción –RT (

4.9.14), o puede usarse la palabra clave __rtti.

es el identificador (nombre) de la nueva clase ( menos la inicial del nombre de las clases sea mayúscula.

4.11.3). Es costumbre que al

opcional. En caso que la clase derive de otra/s relaciona la/s clase/s base (ancestros) de las que la nueva hereda propiedades y métodos. En este caso se dice que nomb-clase es una clase derivada. La lista-base puede tener especificadores de acceso (opcionales y por defecto 4.11.2b) que pueden modificar la forma en que los miembros de la clase derivada heredan los privilegios de acceso que tenían en la clase antecesora. opcional. Declara los miembros de la nueva clase (propiedades y métodos) con especificadores de acceso (opcionales y por defecto 4.11.2a), que pueden especificar que métodos y propiedades son accesibles y cómo, desde el exterior. Inicio. [1] Las estructuras y uniones C++ son consideradas clases con ciertas propiedades por defecto. Por ejemplo, todos sus miembros son públicos, y no suelen tener métodos.

4.11.2a Construcción de una clase §1 Sintaxis

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

4.11.2):

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 (§2.5

).

Salvo que se trate de una declaración adelantada ( 4.11.4), 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 especificadores 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. Nota: 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 (

4.11.2b).

§2 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 especificadores opcionales de acceso y de tipo de almacenamiento. auto (

4.1.8a), extern (

4.1.8d)

y register ( 4.1.8b) no son permitidos; sí en cambio static ( 4.11.7) y const ( 3.2.1c). Los elementos así definidos se denominan miembros de la clase. Hemos dicho que son de dos tipo: propiedades de clase (datos) y métodos de clase (algoritmos para manejar los datos). Es importante advertir que los elementos constitutivos de la clase deben estar 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 ( aclaración sobre este punto (§2.6

4.1.2). Ver a continuación una

).

Ejemplo de definición de una clase: class Vuelo { char nombre[30]; int capacidad; enum modelo {B747, DC10}; char origen[8]; char destino[8]; char fecha[8]; void despegue(&operacion}; void crucero(&operacion); };

// Vuelo es la clase // nombre es una propiedad

// despegue es un método

§2.1 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 ... }; §2.2 Sin embargo, sí es lícito que un miembro sea puntero ( está declarando:

4.2.1f) al tipo de la propia clase que se

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 ( 4.5.8), se muestra la construcción de un arbol 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

4.5a1).

§2.3 También es lícito que se utilicen referencias ( class X { int i; char c; public: X(const X& ref, int x = 0); { i = ref.i; c = ref.c; };

4.11.2d4) a la propia clase:

// Ok. correcto

De hecho, un grupo importante de funciones miembro, los constructores-copia, se caracterizan precisamente por aceptar una referencia a la clase como primer argumento (

4.11.2d4).

§2.4 Las clases pueden ser miembros de otras clases, clases anidadas. Por ejemplo: class X { public: int x; class Xa { public: int x; }; };

// clase contenedora (exterior)

// clase dentro de clase (anidada)

Ver aspectos generales en: clases dentro de clases (

4.13.2).

§2.5 Las clases pueden ser declaradas dentro de funciones, en cuyo caso se denominan clases locales, aunque presentan algunas limitaciones ( void foo() { ... int x; class C { public: int x; }; }

4.11.2a3). Ejemplo:

// función contenedora

// clase local

§2.6 También pueden ser miembros las instancias de otras clases (objetos): class Vertice { public: int x, y; }; class Triangulo { public: Vertice va, vb, vc; };

// Clase contenedora // Objetos dentro de una clase

Es pertinente recordar lo señalado al principio (§2 ): 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 (

4.11.4) de Vertice:

class Vertice; class Triangulo { public: Vertice v; // Error: Información insuficiente de Vertice }; class Vertice {...}; 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 { ... A a1; }; ya que conducirían a definiciones circulares como las señaladas antes (§2.2

).

§2.7 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 (§4 inicialización de miembros inline

). La definición de los métodos puede realizarse dentro, o fuera del cuerpo (§5 funciones

). Ejemplo:

class C { int x; char c; void foo(); }; int C::y; void C::foo() { ++x; }

// Error!! declaración off-line // Ok. definición off-line

§2.8 Las funciones-miembro (métodos), pueden ser declaradas inline (§5 (

4.11.8a), const (

externo (

3.2.1c) y explicit (

), static (

4.11.7), virtual

4.11.2d1) si son constructores. Por defecto tienen enlazado

1.4.4)

§3 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 qué.

§4 Inicialización de miembros Lo mismo que ocurre con las estructuras, que a fin de cuentas son un tipo de clase ( 4.5.1), 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 ( no pueden existir asignaciones. Por ejemplo: class C { ... int x = 33; ... };

// Asignación ilegal !!

). Así pues, en el bloque

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 ( características muy especiales. Ejemplo: class C { ... static const static const cont int kt2 static kt3 = static const };

int kte = 33; kt1 = 33.0 = 33; 33; int kt4 = f(33);

// // // // //

Ok: Error: Error: Error: Error:

4.11.7) tienen unas

No entero No estática No constante inicializador no constante

El sitio idónea 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 (

4.11.2d1). En el epígrafe "Inicializar miembros" (

4.11.2d3) se ahonda en esta cuestión.

§4.1 Sí es posible utilizar y definir un enumerador (que es una constante simbólica una clase. Por ejemplo:

3.2.3g), dentro de

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. ( 4.11.7). Ejemplo-1 Las tres formas siguientes serían aceptables: 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 ( 1.4.1a) 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 0 std::cout 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 distintos en cada instancia concreta. Por esta razón, a excepción de las constantes ( 3.2.1c) y los miembros estáticos ( 4.11.7), no tiene mucho sentido asignar valores 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 ( 4.11.2d3) volvemos sobre la cuestión, exponiendo con detalle la forma de realizar estas asignaciones, en especial cuando se trata de constantes.

§4.2 Disposición práctica En proyectos medianos y grandes, es frecuente que las definiciones de clases se coloquen en ficheros de cabecera ( 4.4.1), de forma que sean accesibles a cualquier otro programador (del equipo) que deba utilizarlas. Generalmente estos ficheros tienen el mismo nombre que la clase con la terminación .h. Por ejemplo: MiClase.h, y como en todos los ficheros de este tipo, en su interior deben instalarse las adecuadas directivas de guarda (

4.9.10e).

§5 Funciones inline Las funciones miembro deben declararse dentro del cuerpo de la clase; la definición puede hacerse dentro o fuera. En el primer caso, cuando declaración y definición se realizan dentro del cuerpo de la clase, se denominan funciones inline; la razón es que este tipo de métodos se suponen [1] con el especificador inline implícito (

4.4.6b). Por ejemplo, la definición:

int i; // global class X { public: char* func(void) { return i; } // inline implícito

char* i; }; es equivalente a: int i; // global class X { public: char* func(void); // prototipo (declaración) char* i; }; inline char* X::func(void) { return i; } // definición

En esta segunda sintaxis, func es definida fuera de la clase con el especificador inline explícito. En ambos casos, el valor i devuelto por func es el puntero a carácter i de la clase, no la variable global del mismo nombre. Por esta razón, es frecuente que los miembros definidos fuera del cuerpo de la clase se denominen offline, para distinguirlos de los definidos dentro (inline). Cuando realice este tipo de definiciones (offline) de métodos, no olvide lo indicado respecto a los argumentos por defecto (

4.4.5).

Nota: algunos autores señalan que para facilitar la comprensión y simplicidad del código, debe evitarse siempre la definición de los métodos dentro del cuerpo de la clase. En aquellos casos en que sea deseable una sustitución inline, se debe utilizar explícitamente esta directiva, pero sacando siempre la definición fuera del cuerpo de la clase. Observe que cuando la definición de func se realiza off-line (fuera del cuerpo de la clase), se utiliza el nombre cualificado (

4.1.11c) del método. Para ello se utiliza el nombre de la clase seguido del

especificador de ámbito :: ( 4.9.19) X::func, con objeto que el compilador sepa que se trata de un método de dicha clase y no la declaración de una nueva función. Nota: en el diseño de las clases, es preferible reservar las funciones inline (explícitas o implícitas) para funciones pequeñas y de uso frecuente. Por ejemplo, las funciones operator ( 4.9.18) que implementan operadores sobrecargados. Esta forma de proceder presenta una doble ventaja: al evitar la sustitución inline, da lugar a ejecutables más pequeños; además, es más fácil la lectura e interpretación de la definición de la clase al sacar de ella el cuerpo de sus funciones (especialmente si son grandes y farragosas).

La cuestión de definir los métodos dentro o fuera del cuerpo de la clase, no solo concierne a la relación tamaño/rendimiento propia de las funciones inline. También se presentan ocasiones hay que diferir la definición de un método de una clase hasta que no se han definido otras entidades, lo que obliga a una definición externa (

Ejemplo).

Inicio. [1] En ocasiones el compilador dispone de opciones para que esto no ocurra automáticamente. Por ejemplo: GNU Cpp dispone de la opción -fno-default-inline, que hace que las funciones miembro no sean consideradas inline por el mero hecho de haber sido definidas dentro del cuerpo de la clase.

4.11.2a1 Clases y funciones friend §1 Sinopsis Se ha señalado ( 4.11.2a), que el mecanismo ofrecido por los especificadores de acceso public, private y protected, junto con alguna matización, que veremos al referirnos a los especificadores de acceso en la herencia ( 4.11.2b), permiten mantener una cierta separación (encapsulamiento) entre la implementación de una clase y su interfaz. Lo normal es que las propiedades privadas sean accedidas de forma controlada a través de métodos públicos. Sin embargo, la invocación a funciones tiene su costo ( 4.4.6b), es mucho más eficiente acceder directamente a las propiedades. En ocasiones es necesario acceder a miembros de una clase desde otra o desde muchas otras. Podemos resumir diciendo que el encapsulamiento, pese a sus innegables ventajas, es un artificio demasiado rígido para ciertas situaciones, por lo que se ha previsto un mecanismo que en cierta forma permite "saltárselo". Se establece mediante la palabra-clave friend que puede aplicarse de forma global a clases y de forma individual a funciones; incluyendo entre estas últimas los métodos (funciones dentro de clases).

§2 friend (palabra-clave) La palabra-clave friend es un especificador de acceso. Se usa para declarar que una función o clase (la denominaremos "invitada"), tiene derecho de acceso a los miembros de la clase en que se declara (denominada "anfitriona"), sin que la clase invitada tenga que ser miembro de la anfitriona. En todos los otros aspectos la invitada es una función o clase normal en lo que se refiere a su declaración, definición y ámbito. Dicho en otras palabras: friend es un especificador de ámbito que "alarga" el ámbito del invitado (clase o función) al interior de la clase anfitriona (a todo el interior, incluyendo miembros privados y protegidos !!).

§3 Sintaxis friend ; Si es una función, desde esta puede accederse a los miembros privados y protegidos de la clase anfitriona. Ejemplo: class C { friend void foo(C&); int x; // privada por defecto ... }; void foo(C& c) { cout