Clases

  • October 2019
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Clases as PDF for free.

More details

  • Words: 58,069
  • Pages: 169
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 <: lista-base> { <lista-miembros> }; 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.

<nomb-clase> 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

<: lista-base> 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. <lista-miembros> 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 { <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 (§2.5

).

Salvo que se trate de una declaración adelantada ( 4.11.4), 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 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 ( <lista-miembros> 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 << 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 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 << c.x << endl; } // Ok.

Si es una clase, los miembros privados y protegidos de la clase anfitriona pueden ser accedidos desde todas las funciones miembro de la invitada. Ejemplo: class C { friend class D; int x; };

// privada por defecto

class D { int y; public: void getx(C c) { cout << c.x << endl; } D(C& c) { y = c.x; } };

// Ok. // Ok.

§4 Ejemplo-1 #include using namespace std; class A { // clase anfitriona char c; // privado por defecto static int sti; char getC(void); // función privada public: friend void finv(A* aptr, char ch ); // L.9: finv es función invitada void func(A* aptr, char ch); // L.10: func es función miembro }; int A::sti = 33; char A::getC(void) { return c; } void finv aptr->c cout << cout << }

// inicia miembro estático // define función privada

(A* aptr, char ch) { // L.16: define función = ch; "valor de c: " << aptr->getC() << endl; "valor de sti: " << A::sti << endl;

void A::func (A* aptr, char ch) { // L.22: define función miembro aptr->c = ch; cout << "valor de c: " << aptr->getC() << endl; cout << "valor de sti: " << A::sti << endl; } int main() { A a1; finv(&a1, 'Y'); a1.func(&a1, 'Z'); return 0; } Salida: valor valor valor valor

de de de de

c: Y sti: 33 c: Z sti: 33

Comentario

// // // //

========================= instancia A L.30: invocar función L.31: invocar función-miembro

En este ejemplo, la clase anfitriona, A, que tiene tres miembros privados (dos propiedades y un método), declara como friend a la función finv, lo que garantiza que desde esta se pueda acceder a los miembros de A, tanto los públicos como los privados: c, sti, getC y func. Observe que aunque las funciones finv y func hacen prácticamente lo mismo y tienen definiciones idénticas; la primera es una función normal (observe su definición en L.16 y la invocación en L.30), mientras que func es una función miembro de A (como lo atestigua su definición en L.22 y su invocación en L.31). Puede comprobarse como, a pesar de ser externa a la clase, la función finv puede acceder a sus propiedades y métodos incluso privados. Es de señalar como el acceso al miembro estático sti, puede hacerse (L.19 y L.25) sin utilizar ningún objeto ( 4.11.7), mientras que el acceso a miembros normales como c en L.17 y L.23 debe hacerse refiriéndose a un objeto concreto (señalado en este caso por el puntero aptr).

§5 Ejemplo-2 Completamos el ejemplo anterior con una versión el la que en vez de ser una función, el "invitado" es una clase. Vemos como sus miembros (dos funciones) tienen acceso a los miembros, incluso privados, de la clase anfitriona:

#include using namespace std; class A { char c; char getC(void); static int sti; public: friend class B; }; int A::sti = 33; char A::getC(void) { return c; }

// // // //

clase anfitriona privado por defecto función privada miembro estático

// L.9: declara B clase invitada // inicia miembro estático // define función privada

class B { // L.14: define clase invitada public: void setAc(class A&, char); // método-1 static void getA(class A*); // método-2 }; void B::setAc(class A& aref, char ch) { // L.19 define método-1 aref.c = ch; } void B::getA(class A* aptr) { // L.22 define método-2 cout << "valor de c: " << aptr->getC() << endl; cout << "valor de sti: " << A::sti << endl; } int main() { A a1; B b1; A& aref = a1; b1.setAc(aref, 'Y'); b1.getA(&a1);

// // // // //

========================= L.28: instancia A instancia B define referencia de clase A L.31: invoca método público de objeto

B::getA(&a1); return 0; } Salida: valor valor valor valor

de de de de

c: Y sti: 33 c: Y sti: 33

Comentario En este ejemplo, la clase anfitriona A tiene una clase amiga (B) que se declara en L.9. Esta clase está definida en L.14/L.26, donde vemos que solo tiene dos métodos públicos. El primero (setAc) permite establecer el valor de la propiedad c de un objeto de la clase anfitriona A; el acceso se realiza mediante una referencia. El segundo de los métodos (getA) permite obtener los valores de las dos propiedades privadas de un objeto, aunque en este caso el acceso lo realizamos mediante un puntero. Vemos como el acceso a la propiedad c se realiza primero directamente (L.20), para acto seguido accederla a través de un método (también privado) en L.23. La función main se limita a crear los objetos y la referencia que serán utilizadas a continuación para invocar los métodos correspondientes de la clase invitada (L.31, 32 y 32). Las invocaciones de L.32 y L.32 son equivalentes. En L.32 utilizamos la notación tradicional de invocación de método de objeto; en L.33 aprovechamos la característica de que getA es estática y utilizamos la notación específica que solo está permitida con este tipo de funciones miembro (

4.11.7).

§6 La invitación puede ser restringida La invitación no tiene porqué ser a todos los miembros de una clase; puede restringirse a uno de sus métodos. Por ejemplo: class B { int i; void func(); ... }; class A { ... friend void B::func(); };

// clase anfitriona // declara B::func método invitado

§6.1 No es posible incluir una definición en la declaración de invitado. Solo es posible utilizar friend con el prototipo de la función o clase. class A { // clase anfitriona ... friend class B { // Error!! /* definición de B */

}; };

§7 Resolver problemas de declaración: Como se ha visto, la declaración de una clase o una función externa, como invitadas puede hacerse de la forma: class B; class A { friend B; friend int foo(); ... }; ... class B { /* ... */ }; int foo() { /* ... */ };

// // // // //

Declaración adelantada, para que el compilador sepa que B es una clase Clase anfitriona Declara B clase invitada L.4: Declara foo función invitada

// definición de la clase B // L.9: declaración de función foo

El trozo anterior compila correctamente. Sin embargo, si friend se refiere a una función-miembro, la forma anterior produciría un error. Por ejemplo: class B; class A { friend int B::foo(); ... }; ... class B { int x; int foo() { return x; } };

// declaración adelantada // Clase anfitriona // L.3: declara invitado el método foo de B

// definición de la clase B // L.9: método (inline)

Este trozo de código produce sendos errores en los dos compiladores probados; en MS Visual C++ 6.0: L.3: use of undefined type 'B'; en Borland C++ 5.5: L.3: 'B::foo()' is not a member of 'B'. Es fácil deducir que el error se produce porque los compiladores no son lo suficientemente "inteligentes" como para salvar la situación, ya que se está declarando invitado un método B::foo() que todavía no está definido (su definición está unas líneas más adelante, en L9). Observe que es exactamente el caso de la función foo() en la línea 4 del ejemplo anterior , donde sin embargo ambos compiladores aceptan sin rechistar que la definición de la función se efectúa unas líneas después, en L.9 [2]. Para salvar esta última situación, se recurre a definir la clase invitada antes que la anfitriona, con lo que no hay necesidad de recurrir a la declaración adelantada: class B { int x; int foo() { return x; } }; ...

// definición de la clase B // método (inline)

class A { friend int B::foo(); ... };

// definición posterior de clase anfitriona // Ok: declara foo de B método invitado

Una última observación: si el método B::foo() no fuese "inline", se podría definir después de la clase A. Para los efectos que nos ocupan, es suficiente con el prototipo existente en la definición de B: class B { int x; int foo(); }; ... class A { friend int B::foo(); ... }; ... int B::foo() { return x;

// definición de la clase B // método (prototipo)

// definición de clase anfitriona // Ok: declara f2 de B método invitado

}

// definición del método foo de B

Observe que la única forma de declarar relaciones de "amistad" mutua entre dos clases, es declarar, toda la clase definida en segundo lugar, friend de la primera: class B { friend class A; int foo() { /* ... */ }; ... }; ... class A { friend int B::foo(); ... };

// definición de clase anfitriona // Ok. declara A clase invitada // método

// definición de clase anfitriona // Ok: declara f2 de B método invitado

§9 Observaciones Respecto a este especificador es importante tener en cuenta los siguientes puntos. • • •

• • •

La invitación puede ser a una subclase de la propia clase anfitriona. Ver ejemplo ( 4.11.2b). Es indiferente la zona (pública o privada) de la clase anfitriona donde se declare el invitado; este no se ve afectado por ningún especificador de acceso (public, private o protected). Suponiendo func1 una función invitada de una clase A. Puesto que func1 no es miembro de A, no está en su ámbito. Por tanto, no puede ser referenciada como a.func1 o Aptr->func1 (donde a es un objeto de A y Aptr es un puntero-a-tipo A). Una función "invitada" a una clase no tiene porqué pertenecer a ninguna otra clase (como el caso de func1 del ejemplo ), si bien en este caso no tiene puntero this ( 4.11.6). Una función o clase pueden ser friend (invitada) en cualquier número de clases anfitrionas, pero una función solo puede ser miembro de una clase (o no pertenecer a ninguna [1]). Una función no puede ser invitada (friend) y miembro de la clase anfitriona al mismo tiempo (no tendría sentido).



Si una clase es invitada de otra, todos sus métodos son invitados de la clase anfitriona aunque



no se indiquen explícitamente; caso de las funciones setAc y getA del ejemplo ( ); ambas son funciones amigas de la clase A. La cualidad de invitado no es heredable, es decir, la clase F es invitada de la clase A, pero no lo



son las clases derivadas de F ( 4.11.2b). La accesibilidad del modificador friend es asimétrica (en realidad los "friend" no son buenos amigos ;-) es decir: los miembros de la clase invitada tienen acceso a los de la clase anfitriona, pero no a la inversa (aunque dos clases pueden declararse recíprocamente invitadas, ver el ejemplo que sigue).

class A { ... friend class B; }; class B { ... friend class A; }; este trozo de código puede ser expresado también en la forma siguiente: class B; class A { ... friend B; }; class B { ... friend A; };

// L.1: declaración anticipada de B

// L.4:

// L.8:

en esta última forma sintáctica la declaración anticipada ( 4.11.4a) de B en L.1, sirve para que el compilador sepa que B es una clase al llegar a L.4, y no produzca un error: Friends must be functions or classes, que se produciría si no se incluyera. •

La accesibilidad del modificador friend no es transitiva es decir: Si A es invitada de B y B es invitada de C, esto no implica que A sea friend de C.



Una función virtual (



Un constructor (

4.11.8a) no puede ser friend de ninguna clase [3].

4.11.2d) no puede ser friend de ninguna otra clase [4].

§10 Es posible declarar friend a una función y definirla en el mismo punto de la declaración [5]. En este caso la función es declarada implícitamente inline, y pertenece al ámbito de la clase. Considere cuidadosamente el siguiente ejemplo:

#include using namespace std; int y = 13;

class A { int x; public: static int y; friend void f1(const A o) { cout << "Miembro x = " << o.x << endl; cout << "y = " << y << endl; } friend void f2(const A o); A(int a = -1) { // constructor x = a; } }; int A::y = 31; void f2(const A o) { cout << "Miembro x = " << o.x << endl; cout << "y = " << y << endl; } void main() { // =============== A a; // M1: f1(a); // M2: f2(a); // M3: } Salida: Miembro x = -1 y = 31 Miembro x = -1 y = 13 Comentario: La clase define dos funcinoes friend, f1 y f2. La primera se define en el punto de su declaración; la segunda se define fuera de la clase. La primera observación importante es que, a pesar de lo que podría parecer, f1 no es un método de A; es una función del espacio global del fichero, igual que f2, como lo demuestra la invocación M2, que es responsable de las dos primeras salidas. Ambas funciones tienen acceso a los miembros privados de A, como lo demuestran las salidas 1 y 3. Sin embargo, las salidas 2 y 4 presentan resultados distintos. La primera, producida por f1, se refiere al miembro A::y. La segunda, producida por f2, se refiere al miembro global ::y. La razón es que el cuerpo de f1 está en el ámbito de A [6], al encontrar el compilador el identificador y en f1, el mecanismo de "Name-lookup" conduce al ámbito de A, en el que se produce una concordancia con el miembro estático A::y. Por contra, el mismo proceso realizado en f2, que está en el espacio global del fichero, conduce al ámbito global, donde la concordancia se produce con el miembro global ::y.

Tema relacionado •

El especificador friend con funciones genéricas (plantillas) (

4.12.2a)

Inicio. [1] Recuerde que C++ es un lenguaje híbrido, que incluye capacidades de POO y de programación tradicional. [2] Este tipo de "particularidades", de los actuales compiladores C++ (recuerde que tanto MS Visual C++ como Borland C++ son productos punteros), hacen que la programación C++ tenga tan mala prensa, y que en especial para el neófito pueda llegar a ser desesperante. Un caso como el presentado, sobre todo si se ha compilado antes sin problemas el ejemplo precedente, puede hacernos perder un buen rato sin entender en principio que C...~#%$!!@ está mal. En el peor de los casos pueden mandarnos a limpiar cochineras a Carolina del Norte ( [3]

Stroustrup TC++PL, & 8.5.4

[4]

Stroustrup TC++PL, & 8.5.5

1 :-)

[5] "members of the Core Language group discovered that a friend declaration of a member function can also define that member. Actually, they discovered that even though they didn't know what it meant, nothing in the ARM precluded such a definition". Dan Saks "Looking Up Names" C/C++ Users Journal August 1993. Nota: ARM es el acrónimo por el que se conoce un texto clásico sobre C++: Margaret A. Ellis and Bjarne Stroustrup. The Annotated C++ Reference Manual (Addison-Wesley, 1990)

ACRM.

[6] Observe que es un concepto difícil de asimilar si no se está acostumbrado a las sutilezas gramaticales: la función está en el ámbito de la clase, pero no es un miembro de ella

4.11.2a2 Sobrecarga de métodos §1 Sinopsis Lo mismo que ocurre con las funciones normales ( 4.4.1), en la definición de funciones-miembro puede ocurrir que varias funciones compartan el mismo nombre, pero difieran en el tipo y/o número de argumentos. Ejemplo: class hotel { char * nombre; int capacidad; public: void put(char*); void put(long); void put(int); }

La función put, definida tres veces con el mismo nombre está sobrecargada; el compilador sabe en cada caso qué definición usar por la naturaleza de los argumentos (puntero-a-char, long o int). Nota: no confundir estos casos de sobrecarga en métodos con el polimorfismo; este último se refiere a jerarquías de clases en las que pueden existir diversas definiciones de métodos en miembros de distintas generaciones (super-clases y clases derivadas) con los mismos argumentos. Son las denominadas funciones virtuales (

4.11.8a).

§2 Adecuación automática de argumentos Una característica interesante, y a nuestro entender muy desafortunada del lenguaje (por la posibilidad de errores difíciles de detectar), es que en la invocación de funciones, sean estas miembros de clase o no, el compilador automáticamente intenta adecuar el tipo del argumento actual con el formal ( Es decir, adecuar el argumento utilizado con el que espera la función según su definición.

4.4.5).

Este comportamiento puede esquematizarse como sigue: int funcion (int i) { /* ... */ .... otraFuncion (){ int x = funcion(5); int y = funcion(int('a')); int z = funcion('a'); ... }

}

// L.1:

// L.4: Ok. concordancia de argumentos // L.5: Ok. promoción explícita // L.6: Ok. promoción automática!!

Es evidente que en L.4 y L.5 los argumentos formales y los actuales concuerdan, bien porque se trata del mismo tipo, bien porque se ha realizado una promoción explícita (

4.9.9).

Lo que no resulta ya tan evidente, ni deseable a veces, es que en L.6 el compilador autorice la invocación a funcion pasando un tipo distinto del esperado. Esta promoción del argumento actual realizada de forma automática por el compilador (sin que medie ningún error o aviso -"Warning"- de compilación) puede ser origen de errores, y de que el programa use inadvertidamente una función distinta de la deseada. En este sentido podemos afirmar que el lenguaje C++ es débilmente tipado. Posiblemente esta característica sea una de las desafortunadas herencias del C clásico [1], y es origen incluso de ambigüedades que no deberían existir. Por ejemplo, no es posible definir las siguientes versiones de un constructor (

4.11.2d):

class Entero { public: int x; Entero(float fl = 1.0) { x = int(fl); } Entero(char c) { x = int(c); } } A pesar de su evidente disparidad, el compilador interpreta que existe ambigüedad en los tipos, ya que char puede ser provomido a float.

§2.1 Para complicar la cosa

Pero la conformación de argumentos no acaba con lo expuesto. Si en la invocación de una función f1 no existe correspondencia entre el tipo de un argumento actual x, y el formal y, el compilador puede buscar automáticamente en el ámbito si existe una función f2 que acepte el argumento discordante x y devuelva el tipo y deseado. En cuyo caso realizará una invocación implícita a dicha función f2(x) y aplicará el resultado y como argumento a la función primitiva. El mecanismo involucrado puede ser sintetizado en el siguiente esquema: class C { // L.1 public: int x; ... }; int getx (C c1, C c2) { return c1.x + c2.x; } C fun (float f= 1.0) { // L.6: C ct = { f }; return ct; } ... unaFuncion () { int x = 10; C c1; ... int z = getx(x, c1); // L.15 ... }

// L.5

La invocación a getx en L.15 es correcta, a pesar de no existir una definición concordante para esta función cuyos argumentos formales son dos objetos tipo C, y en este caso el primer argumento x es un int. El mecanismo utilizado por el compilador es el siguiente: Dentro del ámbito de visibilidad existe una función fun (L.6) que acepta un float y devuelve un objeto del tipo necesitado por getx. Aunque el argumento x disponible no es precisamente un float, puede ser promovido fácilmente a dicho tipo. En consecuencia el compilador utiliza en L.15 la siguiente invocación: int z = getx( fun( float(x) ), c1);

Este tipo de adecuaciones automáticas son realizadas por el compilador tanto con funciones-miembro [2] como con funciones normales. Considere cuidadosamente el ejemplo que sigue, e intente justificar el proceso seguido para obtener cada uno de sus resultados. #include using namespace std; int x = 10; long lg = 10.0; class Entero { public: int x; int getx (int i) { return x * i; } int getx () { return x; } int getx (Entero e1, Entero e2) { return e1.x + e2.x; } Entero(float f= 1.0) { x = f; } }; int getx (int i) { return x * i; } int getx () { return x; }

int getx (Entero e1, Entero e2) { return e1.getx() + e2.getx(); } Entero fun (float f= 1.0) { Entero e1 = { f }; return e1; } void main () { // ========================== Entero c1 = Entero(x); cout << "E1 = " << c1.getx(0) << endl; cout << "E2 = " << c1.getx() << endl; cout << "E3 = " << c1.getx('a') << endl; cout << "E4 = " << c1.getx(lg) << endl; cout << "E5 = " << c1.getx(x, c1) << endl; cout << "E6 = " << c1.getx('a', c1) << endl; cout << "F1 = " << getx(0) << endl; cout << "F2 = " << getx() << endl; cout << "F3 = " << getx('a') << endl; cout << "F4 = " << getx(lg) << endl; cout << "F5 = " << getx(x, c1) << endl; cout << "F6 = " << getx('a', c1) << endl; } Salida (reformateada en dos columnas): E1 E2 E3 E4 E5 E6

= = = = = =

§3

0 10 970 100 20 107

F1 F2 F3 F4 F5 F6

= = = = = =

0 10 970 100 20 107

Temas relacionados •

Sobrecarga de funciones (



Sobrecarga de funciones genéricas (plantillas) 4.12.1-2

4.4.1a)

Inicio. [1] El propio Stroustrup reconoce que la sobrecarga se basa en un conjunto relativamente complicado de reglas, y que ocasionalmente el programador puede verse sorprendido por la (no esperada) función que es invocada. Ver al respecto: Congruencia estándar de argumentos (

4.4.1a).

[2] Afortunadamente existe una opción para limitar este comportamiento "descontrolado" del compilador (

4.11.2d1 Constructores explicit).

4.11.2a3 Clases locales §1 Sinopsis Las clases C++ pueden ser declaradas dentro de funciones; en cuyo caso se denominan clases locales. Tanto el identificador como la propia clase pertenecen al ámbito de la función contenedora. Ejemplo:

void foo () { ... class C { ... }; ... C c1; // Ok. } ... C c2; // Error!! C no es visible

§2 La función contenedora no tiene especial acceso al ámbito de la clase local (salvo el dictado por las reglas generales): void foo() { ... class C { int x; public: char c; }; C c; c.x = 10; c.c = 'X'; }

// Función contenedora // clase local a foo // privado por defecto

// Error!! el miembro es privado // Ok.

El error puede ser eliminado con un pequeño cambio que permita la visibilidad de foo en el interior de la clase: void foo() { ... class C { friend void foo(); // se permite el acceso int x; public: char c; }; C c; c.x = 10; // Ok!! aunque el miembro es privado c.c = 'X'; // Ok. }

§3 La clase local solo puede utilizar typenames ( enumeradores (

4.8) del ámbito de la función contenedora. Ejemplo:

void foo() { static int counter; enum { KT1 = 33 }; int y = 2; class C { int x; public: C() {

3.2.1e); objetos estáticos (

4.11.7) y

x = KT1 + counter; C(int n) { x = n + y; }

// Ok. // Error!! y es automática

}; }

§4 La clase local no puede contener miembros estáticos ( void foo() { ... class C { int x; static int y; public: void f1(); static void f2(); ... }; }

§3

4.11.7). Ejemplo:

// Ok. // Error!! // Ok. // ERror!!

Tema relacionado •

Clases dentro de clases (

4.13.2)

4.11.2b Herencia simple (I) §1 Sinopsis A continuación exponemos otra alternativa a la definición de clases cuando no se parte desde cero ( 4.11.2a), sino de una clase previamente definida (clase-base o superclase). Esta forma es conocida como herencia simple; cuando una clase deriva de una antecesora heredando todos sus miembros (la herencia múltiple es tratada en 4.11.2c). Nota: las uniones (

Fig. 1

4.6) no pueden tener clases-base ni pueden utilizarse como tales.

Es clásico señalar como ejemplo, que la clase Triángulo deriva de la clase general Polígono, de la que también derivan las clases Cuadrado, Círculo, Pentágono, etc. (ver figura) [1]. Cualquier tipo de polígono comparte una serie de propiedades generales con el resto, aunque los triángulos tienen particularidades específicas distintas de los cuadrados, estos de los pentágonos y de los círculos, etc. Es decir, unas propiedades (comunes) son heredadas, mientras que otras (privativas) son específicas de cada descendiente.

Puesto que una clase derivada puede servir a su vez como base de una nueva herencia, se utilizan los términos base directa para designar la clase que es directamente antecesora de otra, y base indirecta para designar a la que es antecesora de una antecesora. En nuestro caso, la clase Poligono es base directa de Triángulo, y base indirecta de Isósceles. Nota: las denominaciones superclase directa y superclase indirecta tienen respectivamente el mismo significado que base directa e indirecta.

§2 Sintaxis Cuando se declara una clase D derivada de otra clase-base B, se utiliza la siguiente sintaxis: class-key nomb-clase : <mod-acceso> clase-base {<lista-miembros>}; <mod-acceso> es un especificador opcional denominado modificador de acceso, que determina como será la accesibilidad de los miembros que se heredan de la clase-base en los objetos de las subclases (

4.11.2b-II).

El significado del resto de miembros es el que se indicó al tratar de la declaración de una clase ( 4.11.2), con la particularidad que, en la herencia simple, <: lista-base> se reduce a un solo identificador: clase-base. Ejemplo class Circulo : public Poligono { <lista-miembros> }; En este caso, la nueva clase Circulo hereda todos los miembros de la clase antecesora Poligono (con las excepciones que se indican a continuación), pero debido al modificador de acceso utilizado (public), solo son utilizables los miembros que derivan de públicos y protegidos de la superclase Poligono (las cuestiones relativas al acceso se detallan en la página siguiente

Pág. 2).

En este supuesto, resulta evidente que la hipotética función mover (en el plano) de la clase círculo será distinta de la misma función en la clase triángulo, aunque ambas desciendan de la misma clase polígono. En el primer caso sería suficiente definir las nuevas coordenadas del centro, mientras que en el segundo habría que definir las nuevas coordenadas de, al menos, dos puntos.

§3 Excepciones en la herencia En principio, una clase derivada hereda todos los miembros (propiedades y métodos) de la clase base [2], con las excepciones que siguen de elementos que no pueden heredarse: •

Constructores (



Destructores (



Miembros estáticos (



Operador de asignación = sobrecargado (



Funciones friend (

§4 Razón de la herencia

4.11.2d1) 4.11.2d2) 4.11.7). 4.11.2a1).

4.9.18a)

Como se ha indicado ( 4.11.1), derivar una nueva clase de otra existente, solo tiene sentido si se modifica en algo su comportamiento y/o su interfaz, y esto se consigue de tres formas no excluyentes: • • •

Añadiendo miembros (propiedades y/o métodos), que no existían en la clase base. Estos nuevos miembros serían privativos de la clase derivada. Sobrescribiendo métodos con distintos comportamientos que en la clase primitiva (sobrecarga). Redefiniendo propiedades que ya existían en la clase base con el mismo nombre. En este caso, se crea un nuevo tipo de variable en la clase derivada con el mismo nombre que la anterior, con el resultado de que coexisten dos propiedades distintas del mismo nombre, la nueva y la heredada.

Nota: una nueva clase no puede eliminar ningún miembro de sus ancestros. Es decir, no puede realizarse una herencia selectiva de determinados miembros. Como se ha indicado se heredan todos; otra cosa es que resulten accesibles u ocultos. Solo en circunstancias muy particulares de herencia múltiple puede evitarse que se repitan miembros de clases antecesoras ( 4.11.2c1 Herencia virtual).

§4.1 Al hilo de estas consideraciones, es importante resaltar que en una clase derivada existen dos tipos de miembros (utilizaremos repetidamente esta terminología): •



Heredados: que existen en la clase derivada simplemente por herencia de la superclase. Este conjunto de miembros, que existen por herencia de la superclase, es denominado por Stroustrup subobjeto de la superclase en el objeto de la Fig. 2 clase derivada [5] Privativos: que existen en la clase derivada porque se han añadido (no existen en la superclase) o redefinido (lo que significa a la postre que existen dos versiones con el mismo nombre; una heredada y otra privativa).

Cuando varias clases son "hermanas" (derivan de una misma superclase) los miembros heredados son comunes a todas ellas, por lo que suelen utilizarse indistintamente ambos términos "heredados" y "comunes" para referirse a tales miembros. La situación se ha esquematizado en la figura 2 en la que dos subclases D1 y D2 derivan de un mismo ancestro B. Cada una tiene sus propios miembros privativos y una copia de los miembros de su ancestro (esta parte es igual en ambas clases).

§4.2 Ejemplo En este ejemplo puede comprobarse que cada instancia de la clase derivada tiene su propio juego de variables, tanto las privativas como las heredadas; también que unas y otras se direccionan del mismo modo. #include class B { public: int x; }; class D : public B { public: int y; };

// clase raíz

// D deriva de B // y es privativa de la clase D

void main() { B b1; b1.x =1; cout << "b1.x = " D d1; d1.x = 2; d1.y = 3; D d2; d2.x = 4; d2.y = 5; cout << "d1.x cout << "d1.y cout << "d2.x cout << "d2.y

// ================================ // b1 es instancia de B (clase raíz) << b1.x // // // //

= = = =

" " " "

<< << << <<

<< endl;

d1 es instancia de D (clase derivada) este elemento es herdado de la clase padre este elemento es privativo de d1 otra instancia de D

d1.x d1.y d2.x d2.y

<< << << <<

endl; endl; endl; endl;

} Salida: b1.x d1.x d1.y d2.x d2.y

= = = = =

1 2 3 4 5

§5 Ocultación de miembros Cuando se redefine un miembro heredado (que ya existe con el mismo nombre en la clase base) el original queda parcialmente oculto o eclipsado para los miembros de la clase derivada y para sus posibles descendientes. Esta ocultación se debe a que los miembros privativos dominan sobre los miembros del subobjeto heredado ( 4.11.2c). Tenga en cuenta que los miembros de las clases antecesoras que hayan sido redefinidos son heredados con sus modificaciones. §5.1 Ejemplo #include #include class X { public: int x; }; class Y : public X { public: char x; }; class Z : public Y { };

// Clase raíz // hija // nieta

void main() { // ============= X mX; // instancias de raíz, hija y nieta Y mY; Z mZ; cout << "mX.x es tipo: " << typeid(mX.x).name() << endl; cout << "mY.x es tipo: " << typeid(mY.x).name() << endl; cout << "mZ.x es tipo: " << typeid(mZ.x).name() << endl; } Salida:

mX.x es tipo: int mY.x es tipo: char mZ.x es tipo: char

5.2 Cuando los miembros redefinidos son funciones Los miembros de las clases antecesoras que pueden ser redefinidos en las clases derivadas no solo pueden ser propiedades (como en el ejemplo anterior), también pueden ser métodos. Considere el siguiente ejemplo [4]: #include class Cbase { public: int funcion(void) { return 1; } }; class Cderivada : public Cbase { public: int funcion(void) { return 2; } }; int main(void) { // ================= Cbase clasebase; Cderivada clasederivada; cout << "Base: " << clasebase.funcion() << endl; cout << "Derivada: " << clasederivada.funcion() << endl; cout << "Derivada-bis: " << clasederivada.Cbase::funcion() << endl; // M.5 } Salida: Base: 1 Derivada: 2 Derivada-bis: 1

En este caso, la clase derivada redefine el método funcion heredado de su ancestro, con lo que en Cderivada existen dos versiones de esta función; una es privativa, la otra es heredada ( ). Cuando se solicita al objeto clasederivada que ejecute el método funcion, invoca a la versión privativa; entonces se dice que la versión privativa oculta ("override") a la heredada. Para que sea invocada la versión heredada (tercera salida), es preciso indicarlo expresamente. Esto se realiza en la última línea de main (M.5). La técnica se denomina sobrecontrol de ámbito y es explicada con más detenimiento en el siguiente epígrafe. Sin embargo, la conclusión más importante a resaltar aquí, es que las dos versiones de funcion existentes en la clase derivada, no representan un caso de sobrecarga (no se dan las condiciones exigidas 4.4.1a y ) ni de polimorfismo, ya que la función no ha sido declarada como virtual ( 4.11.8a) en la superclase. Se trata sencillamente que ambas funciones pertenecen a subespacios distintos dentro de la subclase. Nota: la afirmación anterior puede extrañar al lector, ya que en capítulos anteriores ( 4.1.11c1) hemos señalado que no es posible definir subespacios dentro de las clases. Por supuesto la afirmación es cierta en lo que respecta al usuario (programador), pero no para el compilador, que

realiza automáticamente esta división para poder distinguir ambas clases de miembros (privativos y heredados).

En el capítulo dedicado al acceso a subespacios en clases, hemos visto un método (declaración using), por el que puede evitarse la ocultación de la versión heredada, lo que sitúa a ambas versiones (privativas y heredadas) en idéntica situación de visibilidad, y conduce a una situación de sobrecarga (

4.1.11c1)

§6 Sobrecontrol de ámbito Hemos señalado que cuando se redefine un miembro que ya existe la clase base, el original queda oculto para los miembros de la clase derivada y para sus posibles descendientes. Pero esto no significa que el miembro heredado no exista en su forma original en la clase derivada [3]. Está oculto por el nuevo, pero en caso necesario puede ser accedido sobrecontrolando el ámbito como se muestra en el siguiente ejemplo. #include class B { public: int x; }; class D : public B { public: char x; };

// B clase raíz // D clase derivada // redefinimos x

int main (void) { // ======================== B b0; // instancia de B b0.x = 10; // Ok: x público (int) cout << "b0.x == " << b0.x << endl; D d1; // instancia de D d1.x = 'c'; // Ok: x público (char) d1.B::x = 13; // sobrecontrolamos el ámbito !! cout << "Valores actuales: " << endl; cout << " b0.x == " << b0.x << endl; cout << " d1.x == " << d1.x << endl; cout << " d1.x == " << d1.B::x << " (oculto)" << endl; // M.10 } Salida: b0.x == 10 Valores actuales: b0.x == 10 d1.x == c d1.x == 13 (oculto) Comentario Merece especial atención observar que la notación d1.B::x (M.10) representa al elemento x en d1, tal como es heredado de la case antecesora B. Esta variación sintáctica, permite en algunos casos referenciar miembros que de otra forma permanecerían ocultos. Resulta que los miembros de la clase derivada del ejemplo se reparten en dos ámbitos de nombres, el primero D (inmediatamente accesible), contiene los miembros privativos de la clase derivada, por ejemplo d1.x. El segundo B, que contiene los miembros heredados de la superclase, es accesible solo

mediante el operador de acceso a ámbito :: ( a las clases-base virtuales (

4.9.19). Por ejemplo: d1.B::x. En el capítulo dedicado

4.11.2c1) se muestra otro ejemplo muy ilustrativo.

Una representación gráfica idealizada de la situación para la clase derivada D sería la siguiente: namespace D { char x namespace B { int x } } Cuando desde el exterior se utiliza un identificador como d1.x; los nombres exteriores ocultan los posibles identificadores interiores de igual nombre. Es decir: char x oculta a int x.

§6.1 Cuando el sobrecontrol NO es necesario Tenga muy en cuenta que las indicaciones anteriores relativas a ocultación y sobrecontrol se refieren exclusivamente al caso en que la redefinición de una propiedad o método de la superclase, motive la existencia de una versión privativa de la subclase con el mismo nombre que otra de la superclase. En caso contrario, los miembros heredados (públicos) son libremente accesibles sin necesidad de ningún mecanismo de acceso a ámbito. Considere la siguiente variación del ejemplo anterior: #include class B { public: int x; }; class D : public B { public: int y; };

// B clase raíz // D clase derivada // exclusiva (no existe en B)

int main (void) { // ======================== B b0; // instancia de B b0.x = 10; // Ok: x público (int) cout << "b0.x == " << b0.x << endl; D d1; // instancia de D d1.x = 2; // No precisa sobrecontrol!! d1.y = 20; // Ok: y público (int) cout << "Valores actuales: " << endl; cout << " b0.x == " << b0.x << endl; cout << " d1.x == " << d1.x << " (NO oculto)" << endl; cout << " d1.y == " << d1.y << endl; } Salida: b0.x == 10 Valores actuales: b0.x == 10 d1.x == 2 (NO oculto) d1.y == 20

§6.2 La misma técnica de sobrecontrol de ámbito anteriormente descrita , puede utilizarse para acceder a los miembros heredados de los antepasados más lejanos de una clase. Utilizando un símil biológico, podríamos decir que la totalidad de la carga genética de los ancestros está contenida en la instancia de cualquier clase que sea resultado de una derivación múltiple. Lo pondremos de manifiesto extendiendo el ejemplo anterior con una nueva derivación de la clase D que es ahora base de E, siendo B el ancestro más remoto de E. Ejemplo #include class B { public: int x; }; class D : public B { public: char x; }; class E : public D { public: float x; }; int main (void) { E e1; e1.x = 3.14; e1.D::x = 'd'; e1.B::x = 15; cout << "Valores en cout << " e1.x == " cout << " e1.x == " cout << " e1.x == " }

// clase raíz // clase derivada // clase derivada

// ======================== // instancia de E // Ok: x público (foat) // Ok: x público (char) // Ok: x público (int) e1: " << endl; << e1.x << endl; << e1.D::x << " (oculto)" << endl; << e1.B::x << " (oculto)" << endl;

Salida: Valores en e1: e1.x == 3.14 e1.x == d (oculto) e1.x == 15 (oculto)

Temas relacionados: •

Acceso a subespacios en clases (



El ámbito de nombres y la sobrecarga de funciones (



Acceso a través de punteros en jerarquías de clases ( 4.11.2b1). En este apartado se comprueba como el sobrecontrol de ámbito es innecesario (está implícito) en algunos casos de acceso mediante punteros.

4.1.11c1) 4.4.1a)

Inicio. [1] Es tradición que la representación gráfica de la herencia se represente por flechas que van desde la subclase a la superclase, y que adopte la forma de un árbol invertido, con las clases-base en la parte superior y las derivadas hacia abajo. [2] El hecho de que estos miembros sean o no accesibles es otra cuestión. [3] Se heredan todos los miembros de la clase base (menos las excepciones señaladas recuerde lo indicado respecto a las diferencias entre ámbito y visibilidad en C++ (

), pero

4.1.4).

[4] El ejemplo está inspirado en la consulta de un lector con el siguiente texto: "Escribo el siguiente código y el compilador no genera error, todo funciona como si definiera las funciones virtuales" (sigue el código que se muestra en el ejemplo -excepto la última línea de main M.5-). A continuación añade: "He leído que las funciones virtuales se definen para decirle al compilador que habrá una nueva versión de la misma en una, o todas las clases derivadas, pero yo la redefino sin declararla virtual y no me presenta problemas. Al instanciar los objetos todo funciona perfecto": Cbase clasebase; clasebase.funcion() Cderivada clasederivada; clasederivada.funcion()

// me devuelve 1 // me devuelve 2

[5] [TC++PL-00] §4.7

4.11.2b Herencia simple (II) La cuestión del acceso a miembros de clases §7 Presentación Ya hemos indicado que todos los objetos C++ tienen (entre otros) un atributo de visibilidad ( 4.1.4 ) y podemos añadir aquí que el interior de las clases conforma un espacio en el que, con algunas restricciones, todos sus miembros son visibles. Sin embargo, por motivos de seguridad, se ha dotado a los miembros de clases de un atributo adicional denominado accesibilidad. Se trata de una propiedad de tiempo de compilación que determina si un determinado miembro será accesible desde el exterior de la clase. Este atributo está estrechamente relacionado con cuestiones "genéticas" [3], estableciendo si un determinado miembro heredado, podrá ser accesible o no en las clases derivadas. Nota: como se indicó en el prólogo de las clases ( 4.11), la razón de la existencia de este atributo es dotarlas de un cierto sistema de seguridad, que permita al implementador garantizar que no se realizarán operaciones indebidas o no autorizadas con determinados miembros (privados) de la clase. Además permite mantener una cierta separación e independencia (encapsulamiento) entre la implementación de una clase y la interfaz que ve el cliente-programador [2], lo que a la postre significa que puede cambiarse la implementación sin que se vea afectado el código del usuario.

Además de que la accesibilidad de un miembro no es la misma desde el "exterior" que desde el resto de miembros de la clase (el interior), el hecho de que esté relacionada con la herencia, hace que sea una propiedad algo más compleja que si se aplicara a funciones o variables normales. Para un miembro determinado, depende si es privativo o heredado de la clase antecesora (

4.11.2b) y, en este último

caso, como era su accesibilidad en la clase-base y como se ha transmitido ( 4.11.2b). En este sentido es frecuente referirse a accesibilidad horizontal y vertical. La primera se refiere al acceso a los miembros desde el exterior. La segunda, al acceso desde una clase a los miembros de sus antecesoras en la jerarquía [5]. El resultado de todo esto, es que existen varias modalidades de accesibilidad para los miembros de clases. A su vez, el conjunto de reglas que define como es la acesibilidad de un miembro determinado respecto al resto de miembros y respecto al exterior de la clase, es un asunto lleno de matices (por no decir complejo).

[

Nota: no confundir la accesibilidad o propiedad de acceso con la visibilidad. Una entidad puede ser visible pero inaccesible, sin embargo, la inversa no es cierta. Una entidad no puede ser accesible si no es visible [7]. Como agravante adicional a la multitud de matices que envuelven estos conceptos, en nuestra modesta opinión, el asunto suele estar pésimamente explicado y peor expresado, resultando que al principiante le cuesta bastante terminar de entender como funciona el asunto. Por ejemplo, frases como: "En principio una clase no puede acceder a los datos privados de otra, pero podría ser muy conveniente que una clase derivada accediera a todos los datos de su clase base" [8] pueden inducir a error, ya que no se refiere literalmente a "los datos de su clase base", sino a sus propios miembros (de la clase derivada) no privativos, que existen en la clase derivada por herencia y no por declaración explícita.

§8 Tipos de miembros De forma simplificada [4], para fijar ideas, puede decirse que atendiendo a esta accesibilidad, los miembros pueden ser de tres tipos: privados, públicos y protegidos. Como se verá inmediatamente, el tipo de accesibilidad se manifiesta en la propia sintaxis de creación de la clase, y viene determinada por dos atributos que denominamos especificadores de acceso

y modificadores de acceso

.

§8.1 Miembros privados Solo son accesibles por miembros de la propia clase; no desde el exterior. Suele decirse de ellos que solo son accesibles por el programador de la clase. "Cuando se deriva una clase, los miembros privados no son accesibles en la clase derivada". Nota: aunque la práctica totalidad de la bibliografía existente utiliza esta frase, o parecida, para referirse a la transmisión por herencia de este tipo de miembros, en nuestra opinión es desafortunada y oscurece la cabal comprensión del asunto. Los no accesibles no son los miembros privados de la clase antecesora, sino los miembros de la propia clase derivada no privativos (heredados). Es interesante señalar que los miembros privados de una clase no son accesibles ni aún desde las posibles clases anidadas. Ejemplo: class X { // clase contenedora typedef int ENTERO; int i; public: typedef float FRACCIONARIO; class XX { // clase anidada int i; ENTERO ii; // Error: X::ENTERO es privado FRACCIONARIO f; // Ok: X::FRACCIONARIO es público }; };

§8.2 Miembros públicos Son accesibles desde el exterior de la clase; pueden ser referenciados desde cualquier sitio donde la clase sea visible y constituyen en realidad su interfaz, por lo que suele decirse de ellos que son accesibles por los usuarios de la clase.

§8.3 Miembros protegidos Tienen el mismo comportamiento que los privados (no son accesibles desde el exterior de la clase), pero difieren en cuanto a la accesibilidad de sus descendientes. Los miembros de clases derivadas que son descendientes de miembros protegidos, son accesibles por el resto de los miembros (cosa que no ocurre con los descendientes de miembros privados). Más sobre las formas de transmitirse la visibilidad de los ancestros en las clases derivadas en: Declaración por herencia ( 4.11.2c).

4.11.2b) y Agregaciones (

§8.4 Las posibilidades de acceso a los diversos tipos están esquematizadas en la figura

§9 Especificador opcional de acceso La declaración de cada miembro de una clase puede incluir un especificador opcional que, en principio, define la accesibilidad del miembro. C++ tiene tres palabras clave específicas para este fin: public, private y protected. Nota: no confundir este especificador opcional de acceso, que se incluye en la declaración individualizada de los miembros de la clase, con el modificador opcional de acceso que se utiliza en la declaración de clases derivadas ( heredados.

4.11.2b) y afecta a la totalidad de los miembros

Observe que los elementos declarados después de las etiquetas public, private y protected, son de dicho tipo hasta tanto no se defina una nueva etiqueta (los dos puntos : son imprescindibles al final de esta).

Por defecto los elementos de clases son privados y los de estructuras son públicos.

Ejemplo: class Vuelo { char nombre[30]; // private (por defecto) int capacidad; // private (por defecto) private: float peso; // private protected: void carga(&operacion}; // protected

public: void despegue(&operacion}; // public void crucero(&operacion); // public }; Las propiedades nombre, capacidad y peso solo pueden ser accedidos por las funciones carga, despegue y crucero; estas dos últimas, que a su vez son declaradas públicas, pueden ser accedidas desde el exterior (por los usuarios de la clase). Resulta evidente que los usuarios externos pueden utilizar nombre, capacidad y peso solo de forma indirecta. Como puede verse, si prescindimos de cuestiones "Genéticas" (accesibilidad de miembros de los descendientes), en realidad solo existen dos tipos de miembros: públicos y privados. Lo usual es que todas las propiedades (variables) de la clase se declaren privadas, y las funciones (la mayoría) se declaren públicas. En caso que se necesite acceder a las propiedades desde el exterior, el acceso se realizará a través de una función pública. Por ejemplo, si en el caso anterior se necesita acceder a la propiedad nombre del Vuelo, puede definirse una función ex profeso: class Vuelo { char nombre[30]; // private (por defecto) int capacidad; // private (por defecto) private: float peso; // private protected: void carga(&operacion}; // protected public: void despegue(&operacion}; // public void crucero(&operacion); // public char* getName(); // obtener el nombre del vuelo };

A estas funciones que controlan el acceso a las propiedades de la clase se las denomina accesores ("accessor functions"). Su nombre suele empezar con get (si se trata de obtener el valor) o set (si se trata de modificarlo) y terminan con el nombre de la propiedad accedida. En el primer caso devuelven un valor del mismo tipo que la variable que se accede. En el ejemplo anterior podrían ser los métodos: int getCapacidad () { return capacidad; } void setCapacidad (int valor) { if (valor >= 0 && valor < VMAX ) capacidad = valor; else cout << "El valor proporcionado no es correcto!!" << endl; } Es bastante frecuente que los accesores sean definidos como funciones in-line. Algunos programadores utilizan directamente propiedades públicas para aquellas variables que deben ser visibles desde el exterior, lo que es contrario a la buena práctica y solo tiene la justificación de ahorrarse el trabajo de escribir la función adecuada. Además, en ocasiones puede resultar peligroso, ya que el usuario puede utilizar cualquier valor, quizás uno inadecuado. Por ejemplo un cero, que puede aparecer más tarde como cociente de una división, o un valor excesivamente largo en una cadena, que puede ocasionar un problema de desbordamiento de buffer [1]. Por su parte las funciones se suelen declarar públicas, a excepción de las utilizadas para realizar tareas repetitivas dentro de la clase (en realidad funciones auxiliares), que se declaran privadas.

Nota: no hace falta decir que el hecho de no disponer el acceso a la propiedades directamente, sino a través de funciones, no es una cuestión de masoquismo (ganas de trabajar más). Se supone que en la función se implementan los mecanismos necesarios para garantizar que el valor suministrado por el usuario está dentro de los parámetros de seguridad pertinentes.

§9.1 Salvo indicación en contrario, la accesibilidad de un miembro es la misma que la del miembro que le precede. Es decir, la del último miembro cuya accesibilidad se declaró explícitamente. Por ejemplo: class Vuelo { ... protected: void carga(&operacion}; void descarga(&operacion}; void flete{&operacion}; ... }

// protected // protected como el anterior // ídem

Ya se ha indicado que si se alcanza el principio de la declaración de una clase y no aparece ninguna etiqueta explícita, los miembros son privados (públicos si se trata de una estructura).

§9.2 Para facilitar la legibilidad, es costumbre agrupar los miembros del mismo tipo precedidos por los declaradores correspondientes. El orden puede ser cualquiera, creciente o decreciente [6], de forma que la declaración adopta el aspecto siguiente: class Unaclase { private: ... protected: ... public: ... }

// en realidad este especificador no sería necesario // declaracion de los miembros privados // aquí todos los miembros protegidos // todos los miembros públicos

§10 Modificador de acceso Con lo señalado hasta aquí, las propiedades de acceso a los miembros de las clases quedarían suficientemente definidas si estas existieran aisladamente. Pero se ha dicho que en los casos de herencia, menos las excepciones señaladas ( pág.1), todos los miembros de la base directa son heredados por la clase derivada. Surge entonces una cuestión: ¿Como se hereda el atributo de accesibilidad que tiene cada miembro de la base?. La respuesta es "depende". Para controlar este aspecto de la transmisión de la accesibilidad se utiliza el <mod-acceso> indicado en la sintaxis de la página anterior ( 4.11.2b). Este especificador puede ser cualquiera de las palabras clave ya conocidas: public, protected y private. Su influencia en los miembros de la clase resultante depende del atributo de acceso que tuviese inicialmente el miembro en la superclase (suponemos B la clase-base y D la clase-derivada): Modificador public. Ejemplo: class D : public B { ... };

• • •

miembros públicos en B resultan públicos en D. miembros protegidos en B resultan protegidos en D. miembros privados en B resultan privados en D.

Modificador protected. Ejemplo: class D : protected B { ... }; • • •

miembros públicos en B resultan protegidos en D. miembros protegidos en B resultan protegidos en D. miembros privados en B resultan privados en D.

Modificador private. Ejemplo: class D : private B { ... }; • • •

miembros públicos en B resultan privados en D. miembros protegidos en B resultan privados en D. miembros privados en B resultan privados en D.

Lo anterior puede esquematizarse en el siguiente cuadro: Visibilidad del miembro en la clase-base public protected private

Modificador de acceso utilizado en la declaración de la clase derivada public protected private public protected private (accesible) protected protected private (accesible) private (no accesible private (no accesible private (no accesible directamente) directamente) directamente)

Nota: puesto que las propiedades de acceso de la clase derivada se transmiten a sus descendientes, y la clase-base B puede ser a su vez una clase derivada, el asunto de las propiedades de acceso es el resultado de una cadena en la que es posible remontarse hacia atrás, hasta alcanzar la raíz de las clases antecesoras (las que no derivan de ninguna otra). Es interesante resaltar que la acción del modificador de acceso permite reducir, o en el mejor de los casos igualar, la visibilidad de los miembros originales de la superclase, pero nunca aumentarla. Al tratar de la herencia múltiple ( 4.11.2c) veremos que las restricciones generales impuestas por este modificador pueden revertirse individualmente.

§10.1 Modificador por defecto Salvo indicación explícita en contrario, el modificador de acceso se supone private si se trata de la declaración de una clase y public si de una estructura que siguen son equivalentes. class C : private B { <lista-miembros> }; class C : B { <lista-miembros> }; struct E : public B { <lista-miembros> };

4.5). Por ejemplo, las definiciones de C y E

struct E : B1 { <lista-miembros> }; Ejemplo #include class B { public: int pub; }; // clase raíz class D1 : B { public: int pub1; }; // derivada (private por defecto) class D2 : public B { public: int pub2; }; // derivada int main (void) { B bc; bc.pub = 3; D1 d1; d1.pub1 = 11; d1.pub = 12; D2 d2; d2.pub2 = 21; d2.pub = 22; }

// ======================== // instancia de B // Ok: pub es pública // instancia de D1 // Ok: d1.pub1 es pública // Error: d1.pub es privada !! // instancia de D2 // Ok: d2.pub2 es pública // Ok: d2.pub es pública

§10.2 Recuerde que los miembros heredados que son privados en la superclase B, son inaccesibles a las funciones miembro de la clase derivada D (y en general a todo el mundo). Por ejemplo: class B { private: int pri; }; class D1 : public B { public: int getpri() { return pri; } };

// Clase raíz // clase derivada // Error: miembro inaccesible

La única forma de saltarse esta restricción y acceder a estos miembros, es que en la superclase se haya garantizado explícitamente el acceso mediante la declaración friend ( el siguiente ejemplo:

4.11.2a1) como se muestra en

#include class D1; class B { private: int pri; friend class D1; };

// declaración adelantada // clase raíz

class D1 : private B { // clase derivada public: int getpri() { return pri; } // Ok: acceso permitido void putpri(int i) { pri = i; } // ídem. }; class D2 : private B { // clase derivada public: int getpri() { return pri; } // Error: miembro pri inaccesible void putpri(int i) { pri = i; } // Error: miembro pri inaccesible };

int main (void) { // ======================== D1 d1; // instancia de D d1.putpri(7); cout << "d1.pri == " << d1.getpri() << endl; } Salida (después de eliminar sentencias erróneas): d1.pri == 7

§10.3 En otras ocasiones caben más estrategias: #include class B { // clase raíz public: int x; B(int n = 0) { x = n+1;} // constructor por defecto }; class D1 : private B { // clase derivada public: int y; D1(int n = 0) { y = n+2;} }; class D2 : private B { // clase derivada public: B::x; int y; D2(int n = 0) { y = n+3;} }; class D3 : private B { // clase derivada public: using B::x; int y; D3(int n = 0) { y = n+3;} }; int main (void) D1 d1(10); D2 d2(20); D3 d3(30); //cout << "d1.x cout << "d1.y cout << "d2.x cout << "d2.y cout << "d3.x cout << "d3.y } Salida:

{ // ========================

== == == == == ==

" " " " " "

<< << << << << <<

d1.x d1.y d2.x d2.y d3.x d3.y

<< << << << << <<

endl; endl; endl; endl; endl; endl;

Error B::x is not accesible

d1.y d2.x d2.y d3.x d3.y

== == == == ==

12 1 23 1 33

Comentario El programa declara una superclase B de la que derivan privadamente otras tres. En cada subclase existen dos propiedades: una x es heredada, la otra y, es privativa. Como consecuencia de la forma de herencia (private) establecida, los miembros públicos en B son privados en las subclases, y por lo tanto, las propiedades x no son accesibles desde el exterior de las subclases. Precisamente esta es la causa del error señalado en la función main. Para evitar este inconveniente se utilizan dos procedimientos distintos en las subclases D2 y D3: En D2 se utiliza el sistema tradicional: declarar público el miembro heredado de la superclase (observe la notación utilizada para referirnos a este miembro de la subclase). En D3 se utiliza el método preconizado por la última revisión del Estándar: utilizar la declaración using para hacer visible un objeto (

4.1.11c).

§11 Reajuste de propiedades de acceso Como se ha señalado, las propiedades de acceso de los miembros de las clases base pueden heredarse con ciertas modificaciones mediante los modificadores de acceso de la lista de miembros, pero además, pueden todavía realizarse ajustes particulares utilizando referencias adecuadas en la clase derivada. Ejemplo: class B { int a; public: int b, c; int Bfun(void); };

// declaración de B por 'definición' // a es privado (por defecto)

class X : private B { int d;

// b, c, Bfun son 'ahora' privados en X // d es privado (por defecto) // Observe que a no es accesible en X

public: B::c; int e; int Xfun(void); }; int Efun(X& x);

// c, que era privado, vuelve a ser público

// Efun es externa a B y X

Como resultado, puede comprobarse que: • •

La función Efun() puede usar solo nombres públicos: c, e y Xfun(). La función Xfun()definida en X (que a su vez deriva de B como private), tiene acceso a:



o El entero c definido en B y posteriormente reajustado a public. o Miembros de X privados: b y Bfun() o Los propios miembros de X públicos y privados: d, e, y Xfun() Desde luego Xfun() no puede acceder a los miembros privados de B, como a.

§11.1 Como se ha indicado anteriormente , el modificador de acceso puede mantener o reducir la visibilidad de los miembros heredados respecto de la que tuviesen en la superclase, pero nunca aumentarla. Además la redefinición solo puede aplicarse para volver a convertir a public o protected, tal como se comprueba en el ejemplo. class B { private: int pri; protected: int pro; public: int pub; };

// Superclase

class D : private B { public: B::pri; protected: B::pri; public: B::pro; protected: B::pub protected: B::pro; public: B::pub; };

// // // // // // //

pro y pub son 'ahora' privados en D Error: intento de aumentar la visibilidad Error: ídem. Error: ídem. Error: intento de disminuir la visibilidad Ok: pro vuelve a ser privado en D. Ok: pub vuelve a ser público en D.

§12 Excepciones a las reglas de acceso §12.1 Acceso a miembros estáticos El especificador private para un miembro de clase, o el especificador de herencia private o protected no tienen efecto para los miembros estáticos ( en el siguiente ejemplo:

4.11.7) de las superclases, como se pone en evidencia

#include using namespace std; class B { static int sb; // privado por defecto int nb; // ídem. public: int b; B() : nb(11), b(12) {} }; int B::sb =10; // inicialización de miembro estático class C : private B { static int sc; int nc; public: int c; }; int C::sc = 20;

class D: public C { public: D() { // constructor cout << "miembro B::sb: " << B::sb << endl; // Ok. cout << "miembro B::nb: " << B::nb << endl; // Error!! // cannot access private member declared in class 'B' cout << "miembro B::sb: " << B::b << endl; // Error!! // not accessible because 'C' uses 'private' to inherit from 'B' cout << "miembro C::sc: " << C::sc << endl; // Ok. cout << "miembro C::nc: " << C::nc << endl; // Error!! // cannot access private member declared in class 'C'. } }; int main() { D d; return 0; }

// ================

Salida después de eliminadas las sentencias erróneas: miembro B::sb: 10 miembro C::sc: 20

§12.2 Acceso a funciones virtuales Las funciones virtuales ( 4.11.8a) se utilizan en jerarquías de clases y se caracterizan por tener definiciones distintas en cada clase de la jerarquía que solapan u ocultan las definiciones en las superclases. Esta diferencia de comportamiento (especialmente en subclases hermanas) es lo que les confiere su carácter polimórfico y constituye la justificación de su existencia. Como se muestra en el siguiente ejemplo, en ocasiones la redefinición puede incluir una modificación de los atributos de acceso respecto de los que tiene en la superclase. En estos casos la accesibilidad de la función viene determinada por su declaración, y no se ve afectada por la existencia de modificadores que afectaran a la accesibilidad de las versiones de las subclases. class B { public: virtual void foo(); }; class D: public B { private: void foo(); }; int main() { D d; B* bptr = &d; D* dptr = &d; bptr->foo() dptr->foo() ...

// ============

// M4: Ok. // M5: Error!

} La diferencia de comportamiento de ambas invocaciones se justifica en que el compilador realiza el control de acceso en el punto de invocación, utilizando la accesibilidad de la expresión utilizada para designar la función. En M4 se utiliza un puntero a la superclase, y en este punto foo es pública, con lo que el acceso es permitido. En cambio, en la sentencia M5 se utiliza un puntero a la subclase y en esta el método es privado, con lo que el acceso es denegado.

§13 Resumen La definición de las clases puede incluir unos especificadores de acceso public, private y protected, que determinan la accesibilidad de los miembros . En casos de herencia, la accesibilidad de los miembros heredados puede ser modificada globalmente mediante modificadores de acceso que se aplican a cada una de las posibles bases

. Finalmente la accesibilidad resultante de los miembros

heredados puede ser retocada individualmente dentro de ciertas limitaciones

.

Como puede verse, todo el sistema está diseñado de forma que consigue un gran control sobre las propiedades de acceso de cada uno de los miembros de una clase, sea o no derivada de otra/s, aunque el sistema sea en sí mismo bastante prolijo en detalles. Sobre todo si tenemos en cuenta que se completan con un proceso de semiocultación de miembros que tiene lugar cuando alguno de los miembros de una superclase es redefinido en la clase derivada. Este proceso ha sido estudiado con detalle en la página anterior (

4.11.2b).

Nota: recordemos que, además de los especificadores de acceso ya señalados, existe una forma adicional de garantizar el acceso a los miembros de una clase mediante palabra clave: friend ( 4.11.2a1). Son las denominadas clases o funciones amigas o invitadas.

En el siguiente programa se presenta un amplio muestrario de la casuística que puede presentarse. Debe examinar detenidamente el código cotejándolo con los comentarios del final. #include class B { // clase raíz int pri; // privado por defecto protected: int pro; public: int pub; void putpri(int i) { pri = i; } void putpro(int i) { pro = i; } int getpri() { return pri; } int getpro() { return pro; } }; class D1 : public B { // deriva de B int pri1; protected: int pro1; public: int pub1; //int getprib() { pri; } Error: miembro no accesible int getprob() { return pro; } }; class D2 : private B { // deriva de B int pri2; protected: int pro2; public: int pub2;

}; int main (void) { // ======================== B b0; // instancia de B b0.putpri(1); // Ok: acceso a pri mediante método público b0.putpro(2); // Ok: ídem b0.pub = 3; // Ok: pub es público cout << "Salida-1 Valores en b0:" << endl; cout << " b0.pri == " << b0.getpri() << endl; cout << " b0.pro == " << b0.getpro() << endl; cout << " b0.pub == " << b0.pub << endl; D1 d1; // instancia de D1 d1.putpri(11); // Ok: acceso a pri mediante método público d1.putpro(12); // Ok: ídem d1.pub = 13; // Ok: pub es público d1.pub1 = 14; // Ok: ídem cout << "Salida-2 Valores en d1:" << endl; cout << " d1.pri == " << d1.getpri() << endl; cout << " d1.pro == " << d1.getpro() << endl; cout << " d1.pro == " << d1.getprob() << endl; cout << " d1.pub == " << d1.pub << endl; cout << " d1.pub1 == " << d1.pub1 << endl; D2 d2; // d2.putpri(21); // d2.putpro(22); // d2.pub = 23; d2.pub2 = 24; cout << "Salida-3 // cout << " d2.pri // cout << " d2.pro // cout << " d2.pub cout << " d2.pub2 }

// instancia de D2 Error: función no accesible Error: función no accesible Error: pub no accesible // Ok: pub2 es público Valores en d2:" << endl; == " << d2.getpri() << endl; Error función no accesible == " << d2.getpro() << endl; Error función no accesible == " << d2.pub << endl; Error propiedad no accesible == " << d2.pub2 << endl;

Salida: Salida-1 Valores en b0: b0.pri == 1 b0.pro == 2 b0.pub == 3 Salida-2 Valores en d1: d1.pri == 11 d1.pro == 12 d1.pro == 12 d1.pub == 13 d1.pub1 == 14 Salida-3 Valores en d2: d2.pub2 == 24 Comentario

a: Cada clase tiene miembros públicos, privados y protegidos; a su vez, la clase base B da lugar a dos descendientes: D1 y D2 (muy parecidos excepto en el especificador de acceso). Estas dos clases derivadas tienen los mismos miembros que la clase base además de otros privativos. b: En la superclase B se definen cuatro funciones (públicas): putpri, putpro, getpri y getpro, que sirven para acceder desde el exterior a los miembros protegidos y privados de la clase. Es típico disponer funciones de este tipo cuando se desea permitir acceso desde el exterior a miembros privados o protegidos de las. Normalmente estas funciones están diseñadas de forma que el acceso solo se realice en los términos que estime convenientes el diseñador de la clase. No es necesario declararlas de nuevo en las clases derivadas D1, D2 y D3 que ya disponen de ellas por herencia (ver Salida-2 en main). Sin embargo, observe (que por ejemplo), pri1 y pro1 no son accesibles desde el exterior (para estas propiedades no se han definido funciones análogas a las anteriores). c: En la clase D1 se han previsto dos métodos públicos: getprib y getprob, para acceder a las propiedades privadas y protegida de la clase-base. La primera de ellas conduce a un error, ya que los miembros que son privados en la superclase (B) son inaccesibles en la clase derivada, así que D1.pri es inaccesible. La segunda devuelve el miembro protegido D1.pro, que si es accesible, aunque esta función es redundante, devuelve el mismo valor que la función D1.getpro que existe en D1 como herencia pública de B, lo que se demuestra en la Salida-2. d: Cuando se pretende repetir en D2 las mismas sentencias que en D1, se producen errores, porque los valores que son accesibles en aquella (por derivar públicamente de la superclase B), aquí son inaccesibles (D2 deriva privadamente de B). Inicio. [1] Este es un procedimiento muy corriente de "ataque" a aplicaciones, especialmente las que corren en Red. Hoy día prácticamente todas!. [2] Precisamente este encapsulamiento es uno de los paradigmas de la POO. [3] Para ilustrar más claramente algunas características de los mecanismos de la herencia de clases, en ocasiones utilizaremos una cierta analogía biológica. [4] El asunto es algo más complicado que lo aquí expuesto porque, como se ha señalado ( 3.2.1c), el especificador const con miembros de clase también impone un matiz de accesibilidad entre miembros, y el especificador friend afecta igualmente la accesibilidad de los miembros desde el exterior. [5] Como señalamos a continuación, en realidad esta expresión es incorrecta (aunque muy gráfica), ya que no se refiere literalmente a "miembos de sus antecesoras", sino a sus propios miembros (de la clase derivada) no privativos (que existen en la clase derivada por herencia y no por declaración explícita). [6] Stroustrup aconseja colocar en primer lugar los miembros públicos ( Stroustrup & Ellis: ACRM §11.1) argumentando que haciéndolo así, si el usuario cambia los miembros privados pero deja intacta su interfaz, el código ya compilado que utiliza solamente los miembros públicos no necesita ser recompilado de nuevo. [7] La primera acción del compilador en una expresión para determinar las características de una entidad (identificador), es el proceso de búsqueda de nombres ("Name Lookup"

1.2.1), que está

relacionado con la visibilidad. A continuación de efectúa una comprobación estática de tipos. Finalmente se realizan las verificaciones de acceso. [8] "Aprenda C++ como si estuviera en primero" Javier García de Jalón, José Ignacio Rodriguez y otros. Escuela Superior de Ingenieros Industriales. Navarra (España

4.11.2b1 Punteros en jerarquías de clases §1 Sinopsis Consideremos el caso de la clase general Polígono del epígrafe anterior ( 4.11.2b). Expandiendo la subclase de los triángulos tendríamos una jerarquía de clases como en la figura: Es evidente que todos los isósceles son triángulos y que todos los triángulos son polígonos. Esto significa que un puntero a tipo polígono (Poligono*) puede señalar a un objeto triángulo (1) y a un objeto isósceles (2). Del mismo modo, un puntero a triángulo (Triangulo*) puede señalar a un objeto isósceles (3). Es decir: Poligono Po; Poligono* ptrP = &Po; Triangulo PoTr; Triangulo* ptrT = &PoTr; Isosceles PoTrIs; Isosceles* ptrI = &PoTrIs; pero también: ptrT = &PoTrIs; ptrP = &PoTrIs; ptrP = &PoTr;

// 3 correcto todo Isósceles es un triángulo // 2 correcto todo Isósceles es un polígono // 1 correcto todo triángulo es un polígono

Observe que en los tres casos, se están utilizando punteros a una superclase para designar objetos de la clase derivada. O dicho de otro modo: la dirección de un objeto de la subclase se expresa mediante un puntero de tipo puntero-a-superclase. Esta posibilidad, conocida como "upcast", es tremendamente útil en determinadas circunstancias [3].

Tenga en cuenta que el razonamiento inverso no es necesariamente cierto. Es decir, un polígono no es necesariamente triángulo y un triángulo no es necesariamente isósceles; en otras palabras: un puntero a isósceles no puede utilizarse indiscriminadamente para señalar a un triángulo ni a un polígono: ptrI = &PoTr; ptrI = &Po; ptrT = &Po;

§2 Teorema

// Error: un triángulo puede no ser isósceles // Error: un polígono puede no ser isósceles // Error: un polígono puede no ser un triángulo

Estas consideraciones pueden generalizarse en el siguiente enunciado: si una subclase S tiene una clase-base pública B, entonces S* puede ser asignada a una variable de tipo B* sin ninguna conversión explícita de tipo (esto es lo que expresan las sentencias 3 y 1 anteriores). Lo inverso no es cierto; en estos casos se hace necesaria una conversión explícita de tipo (

4.9.9).

Lo anterior puede resumirse en los siguientes axiomas (que son equivalentes): • •

Los objetos de las clases derivadas pueden tratarse como si fuesen objetos de sus clases-base cuando se manipulan mediante punteros y referencias. Un puntero de una clase-base puede contener direcciones de objetos de cualquiera de sus clases derivadas. Ejemplo [1]:

class B { .... }; class D : public B { ... }; ... func () { B* bptr = new D; // puntero a superclase asignado a objeto de subclase ... delete bptr; } Nota: el hecho de que el puntero a una superclase pueda ser utilizado como puntero a objeto de cualquier subclase de su jerarquía (una especie de "puntero genérico" para los objetos de la familia), se cumple también en otros lenguajes como Eiffel o Java. Como estos lenguajes proporcionan una superclase de la que derivan todas las demás (en Java es la clase Object), resulta que un puntero a esta superclase Java equivale funcionalmente al papel del puntero void* en C++ (

4.2.1d).

Observará el lector que los razonamientos anteriores §1 : "Todo isósceles es un triángulo" y "un triángulo puede no ser isósceles", son desde luego válidos en geometría, pero no necesariamente en las jerarquías de clases. Sobre todo el primero de tales razonamientos podría no ser cierto en algún caso concreto, ya que el programador es totalmente libre para definirla. Sería posible diseñar una jerarquía "enloquecida" [4], en la que no se cumpliesen estas premisas lógicas. Como en cualquier caso el compilador garantizará la validez del teorema anterior, la congruencia y la lógica aconsejan que en el diseño de jerarquías de clases se cumpla el denominado principio de sustitución de Liskov o LSP ("Liskov Substitution Principle"), según el cual las clases se deben diseñar de forma que cualquier clase derivada sea aceptable donde lo sea su superclase.

§3 Acceso a través de punteros Cuando se tienen objetos de clases aisladas (que no derivan de ninguna otra), el acceso mediante punteros a dichas clases, no presenta dificultad alguna ( (

4.2.1f), basta utilizar el selector indirecto ->

4.9.16). Ejemplo:

class C { public: int x; } ... C c; // Objeto (instancia) de la clase C* ptr = &c; // puntero a clase ptr->x = 10; // Ok. acceso a miembro del objeto: c.x == 10

Sin embargo, cuando las clases pertenecen a una jerarquía, su acceso mediante punteros puede

convertirse en una pesadilla si no se conoce íntimamente como se comportan frente a los espacios de nombres implícitos en tales clases (

4.11.2b). Considere detenidamente el siguiente ejemplo:

#include class B { // Superclase (raíz) public: int f(int i) { cout << "Funcion-Superclase "; return i; } }; class D : public B { // Subclase (derivada) public: int f(int i) { cout << "Funcion-Subclase "; return i+1; } }; int main() { D d; D* dptr = &d; B* bptr = dptr; cout cout cout cout cout cout

<< << << << << <<

// // // //

========== instancia de subclase puntero-a-subclase señalando objeto puntero-a-superclase señalando objeto de subclase

"d.f(1) "; d.f(1) << endl; "dptr->f(1) "; dptr->f(1) << endl; "bptr->f(1) "; bptr->f(1) << endl;

// acceso directo al método (que es público) // acceso a través del puntero // idem.

} Salida: d.f(1) Funcion-Subclase 2 dptr->f(1) Funcion-Subclase 2 bptr->f(1) Funcion-Superclase 1 Comentario Vemos que en la primera y segunda salida las cosas ocurren como de costumbre, ya sea utilizando el operador de acceso directo o el indirecto (mediante puntero). En ambos casos, la nueva definición de f en la clase derivada, oculta la definición en la superclase. La sorpresa ocurre en la tercera salida, donde a pesar de que el puntero señala al mismo (y único objeto) d, se accede directamente al subespacio B del objeto, con lo que la versión f utilizada es la existente en el mismo; la heredada de la superclase [2]. Este último resultado podría obtenerse también mediante: cout << d.B::f(1) << endl; Así pues, como corolario de lo anterior, tenga en cuenta que (en condiciones normales *), el acceso a objetos d de subclases D mediante punteros a superclases B*, lleva implícita la referencia el subespacio B existente en el objeto d. * Al tratar de las funciones virtuales ( 4.11.8a), veremos que la "sorpresa" anterior puede evitarse con una pequeñísima modificación en la definición del método f de la superclase (declarándolo virtual). Inicio.

[1] En estos casos es muy importante tener en cuenta las indicaciones al respecto en Destructores virtuales (

4.11.2d2).

[2] La mayoría de los textos se refieren a los elementos del subespacio B como "miembros de la superclase". Esta terminología es poco precisa y puede inducir a confusión, ya que todos los miembros pertenecen al objeto d, y este es una instancia de la subclase. La confusión semántica es todavía mayor si se instancia un objeto b de la superclase ( B b; ) coexistiendo con el anterior. [3] En realidad el control de tipos realizado por el compilador podría haber sido mucho más estricto, no permitiendo que los objetos de subclases pudiesen ser señalados mediante punteros a sus superclases. Sin embargo la seguridad se relajó aquí intencionadamente para permitir ciertos artificios usados con las clases polimórficas. [4] Un caso similar se presenta en los casos de sobrecarga de operadores ( 4.9.18), donde son teóricamente posibles comportamientos sobrecargados que no mantengan la más mínima homogeneidad conceptual con las versiones globales

4.11.2b2 El ámbito de nombres y la sobrecarga de métodos 1 Sinopsis Se ha indicado ( 4.4.1a) que en C++ no existe el concepto de sobrecarga a través de ámbitos de nombres, y los ámbitos de las clases derivadas no son una excepción a esta regla general. Esto significa que el mecanismo de sobrecarga no funciona para las clases derivadas. Lo pondremos de manifiesto con un sencillo ejemplo modificando ligeramente el anterior ( 4.11.2b1), de forma que las versiones del método f en la clase-base B y en la derivada D, pudieran ser objeto de sobrecarga: #include class B { // Superclase public: int f(int i) { cout << "Funcion-Superclase "; return i; } }; class D : public B { // Subclase public: float f(float f) { cout << "Funcion-Subclase "; return f+0.1; } }; int main() { D d; D* dptr = &d; B* bptr = dptr; cout << "d.f(1) cout << "d.f(1.1)

// // // //

================= instancia de subclase puntero-a-subclase señalando objeto puntero-a-superclase señalando objeto de subclase

" << d.f(1) << endl; " << d.f(1.1) << endl;

cout << "dptr->f(1) " << dptr->f(1) << endl; cout << "dptr->f(1.1) " << dptr->f(1.1) << endl; cout << "bptr->f(1) " << bptr->f(1) << endl; cout << "bptr->f(1.1) " << bptr->f(1.1) << endl; }

Salida: d.f(1) d.f(1.1) dptr->f(1) dptr->f(1.1) bptr->f(1) bptr->f(1.1)

Funcion-Subclase 1.1 Funcion-Subclase 1.2 Funcion-Subclase 1.1 Funcion-Subclase 1.2 Funcion-Superclase 1 Funcion-Superclase 1

Comentario Comprobamos que cualquiera que sea la forma de invocación utilizada, en ningún caso se produce sobrecarga de la función; siempre se accede a la misma versión, dependiendo del subespacio de nombres B o D referenciado. Nota: si el mecanismo de sobrecarga de funciones hubiese funcionado entre los subespacios de nombres del objeto d, las salidas habrían sido: d.f(1) d.f(1.1) dptr->f(1) dptr->f(1.1) bptr->f(1) bptr->f(1.1)

Funcion-Superclase 1 Funcion-Subclase 1.2 Funcion-Superclase 1 Funcion-Subclase 1.2 Funcion-Superclase 1 Funcion-Subclase 1.2

En estas condiciones cabe preguntarse ¿Que podríamos hacer si realmente necesitamos el funcionamiento del mecanismo de sobrecarga?. Es decir, si deseamos que, en concordancia con los argumentos de llamada, sea invocada la versión de f definida en la subclase o de la superclase. La solución está en utilizar la declaración using ( 4.1.11c). Veámoslo mediante un ejemplo modificando ligeramente el caso anterior (para simplificar el código se han disminuido las salidas). #include class B { // Superclase public: int f(int i) { cout << "Funcion-Superclase "; return i; } }; class D : public B { // Subclase public: using B::f; // Ok. acceder a las versiones de f en B float f(float f) { cout << "Funcion-Subclase "; return f+0.1; } }; int main() { D d; D* dptr = &d; B* bptr = dptr; cout cout cout cout }

<< << << <<

// =================

dptr->f(1) << endl; dptr->f(1.0) << endl; bptr->f(1) << endl; bptr->f(1.0) << endl;

Salida [1]: Funcion-Superclase 1 Funcion-Subclase 1.1 Funcion-Superclase 1 Funcion-Superclase 1 Comentario En este caso comprobamos como la sobrecarga ha funcionado en el sentido D B. Cuando accedemos al espacio de nombres D, se invoca correctamente la versión adecuada de f, pero cuando accedemos a B, el espacio D sigue siendo invisible para el mecanismo de sobrecarga. La representación gráfica idealizada de la situación para la clase derivada D sería la siguiente: namespace D { using B::f; float f(float f); namespace B { int f(inf i); } } Cuando se utiliza un identificador como d.f(), o su equivalente dptr->f(), se accede directamente al subespacio D, aunque las versiones interiores de f son accesibles por la declaración using; pero cuando se accede directamente al subespacio B mediante expresiones como bptr->f(), la versión de f en este subespacio sigue ocultando cualquier otro identificador exterior. Ver otro ejemplo en: "Acceso a subespacios en clases" (

4.1.11c1)

Inicio. [1] Aunque los comportamientos descritos en el ejemplo son los Estándar, el compilador Borland C++ 5.5 para Win-32 presenta un error, por lo que los resultados se han obtenido con el compilador MS Visual C++ 6.0 que en este punto se acerca más a la norma.

4.11.2c Declaración por herencia múltiple §1 Sinopsis La tercera forma de crear una nueva clase es por herencia múltiple, también llamada agregación o composición [1]. Consiste en el ensamblando una nueva clase con los elementos de varias clasesbase. C++ permite crear clases derivadas que heredan los miembros de una o más clases antecesoras. Es clásico señalar el ejemplo de un coche, que tiene un motor; cuatro ruedas; cuatro amortiguadores, etc. Elementos estos pertenecientes a la clase de los motores, de las ruedas, los amortiguadores, etc. Como en el caso de la herencia simple, aparte de los miembros heredados de cada clase antecesora, la nueva clase también puede tener miembros privativos (

§2 Sintaxis

4.11.2b)

Cuando se declara una clase D derivada de varias clases base: B1, B2, ... se utiliza una lista de las bases directas (

4.11.2b) separadas por comas. La sintaxis general es:

class-key nomb-clase <: lista-base> { <lista-miembros> }; El significado de cada miembro se indicó al tratar de la declaración de una clase ( de D seria:

4.11.2). En este caso, la declaración

class-key D : { <listamiembros> };

D hereda todos los miembros de las clases antecesoras B1, B2, etc, y solo puede utilizar los miembros que derivan de públicos y protegidos en dichas clases. Resulta así que un objeto de la clase derivada contiene sub-objetos de cada una de las clases antecesoras.

§2.1 Restricciones Tenga en cuenta que las clases antecesoras no pueden repetirse, Fig. 1 es decir: class B { ... }; class D : B, B, ... { ... };

// Ilegal!

Aunque la clase antecesora no puede ser base directa más que una vez, si puede repetirse como base indirecta. Es la situación recogida en el siguiente ejemplo cuyo esquema se muestra en la figura 1: class class class class

Fig. 2

B { ... }; C1 : public B { ... }; C2 : public B { ... }; D : public C1, C2 { ... };

Aquí la clase D tiene miembros heredados de sus antecesoras D1 y D2, y por consiguiente, dos subobjetos de la base indirecta B.

El mecanismo sucintamente descrito, constituye lo que se denomina herencia múltiple ordinaria (o simplemente herencia). Como se ha visto, tiene el inconveniente de que si las clases antecesoras contienen elementos comunes, estos se ven duplicados en los objetos de la subclase. Para evitar estos problemas, existe una variante de la misma, la herencia virtual ( 4.11.2c1), en la que cada objeto de la clase derivada no contiene todos los objetos de las clases-base si estos están duplicados. Las dependencias derivadas de la herencia múltiple suele ser expresada también mediante un grafo denominado DAG ("Direct acyclic graph"), que tiene la ventaja de mostrar claramente las dependencias en casos de composiciones complicadas. La figura 2 muestra el DAG correspondiente al ejemplo anterior. En estos grafos las flechas indican el sentido de la herencia, de forma que A --> B indica que A

deriva directamente de B. En nuestro caso se muestra como la clase D contiene dos sub-objetos de la superclase B. Nota: la herencia múltiple es uno de los puntos peliagudos del lenguaje C++ (y de otros que también implementan este tipo de herencia). Hasta el extremo que algunos teóricos consideran que esta característica debe evitarse, ya que además de las teóricas, presenta también una gran dificultad técnica para su implementación en los compiladores. Por ejemplo, surge la cuestión: si dos clases A y B conforman la composición de una subclase D, y ambas tienen propiedades con el mismo nombre, ¿Que debe resultar en la subclase D? Miembros duplicados, o un miembro que sean la agregación de las propiedades de A y B?. Como veremos a continuación, el creador del C++ optó por un diseño que despeja cualquier posible ambigüedad, aunque ciertamente deriva en una serie de reglas y condiciones bastante intrincadas.

§3 Ambigüedades La herencia múltiple puede originar situaciones de ambigüedad cuando una subclase contiene versiones duplicadas de sub-objetos de clases antecesoras o cuando clases antecesoras contienen miembros del mismo nombre: class B { public: int b; int b0; }; class C1 : public B { public: int b; int c; }; class C2 : public B { public: int b; int c; }; class D: public C1, C2 { public: D() { c = 10; // L1: Error C1::c = 110; // L2: Ok. C2::c = 120; // L3: Ok. b = 12; Error!! // L4: Error C1::b = 11; // L5: Ok. C2::b = 12; // L6: Ok. C1::B::b = 10; // L7: Error B::b = 10; // L8: Error b0 = 0; // L9: Error C1::b0 = 1; // L10: Ok. C2::b0 = 2; // L11: Ok.

ambigüedad C1::c o C2::c ?

ambigüedad C1::b domina sobre C1::B::b C2::b domina sobre C2::B::b de sintaxis! ambigüedad. No existe una única base B ambigüedad

} };

Los errores originados en el constructor de la clase D son muy ilustrativos sobre los tipos de ambigüedad que puede originar la herencia múltiple (podrían haberse presentado en cualquier otro método D::f() de dicha clase). En principio, a la vista de la figura 1 , podría parecer que las ambigüedades relativas a los miembros de D deberían resolverse mediante los correspondientes especificadores de ámbito: C1::B::m C2::B::m C1::n C2::n

// // // //

miembros miembros miembros miembros

m m n n

en en en en

C1 C2 C1 C2

heredados de B heredados de B privativos privativos

Como puede verse en la sentencia L7 , por desgracia el asunto no es exactamente así (otra de las inconsistencias del Fig. 3 lenguaje). El motivo es que el esquema mostrado en la figura es méramente conceptual, y no tiene que corresponder necesariamente con la estructura de los objetos creados por el compilador. En realidad un objeto suele ser una región continua de memoria. Los objetos de las clases derivadas se organizan concatenando los sub-objetos de las bases directas, y los miembros privativos si los hubiere; pero el orden de los elementos de su interior no está garantizado (depende de la implementación). La figura 3 muestra una posible organización de los miembros en el interior de los objetos del ejemplo. El crador del lenguaje indica al respecto [2] que las relaciones contenidas en un grafo como el de la figura 2 representan información para el programador y para el compilador, pero que esta información no existe en el código final. El punto importante aquí es entender que la organización interna de los objetos obtenidos por herencia múltiple es idéntico al de los obtenidos por herencia simple. El compilador conoce la situación de cada miembro del objeto en base a su posición, y genera el código correspondiente sin indirecciones u otros mecanismos innecesarios (disposición del objeto D en la figura 3).

§3.1 Desde el punto de vista de la sintaxis de acceso, cualquier miembro m privativo de D (zona-5) de un objeto d puede ser referenciado como d.m. Cualquier otro miembro del mismo nombre (m) en alguno de los subobjetos queda eclipsado por este. Se dice que este identificador domina a los demás [3]. Nota: este principio de dominancia funciona también en los subobjetos C1 y C2. Por ejemplo: si un identificador n en el subobjeto C1 está duplicado en la parte privativa de C1 y en la parte heredada de B, C1::n tienen preferencia sobre C1::B::n.

Cualquier objeto c privativo de los subobjetos C1 o C2 (zonas 2 y 4) podría ser accedido como d.c. Pero en este caso existe ambigüedad sobre cual de las zonas se utilizará. Para resolverla se utiliza el especificador de ámbito: C1::c o C2::c. Este es justamente el caso de las sentencias L1/L3 del ejemplo:

c = 10; C1::c = 110; C2::c = 120;

// L1: Error ambigüedad C1::c o C2::c ? // L2: Ok. // L3: Ok.

Es también el caso de las sentencias L4/L6. Observe que en este caso no existe ambigüedad respecto a los identificadores b heredados (zonas 1, y 2) porque los de las zonas 2 y 4 tienen preferencia sobre los de las zonas 1 y 2. b = 12; Error!! C1::b = 11; C2::b = 12;

// L4: Error ambigüedad // L5: Ok. C1::b domina sobre C1::B::b // L6: Ok. C2::b domina sobre C2::B::b

Es interesante señalar que estos últimos, los identificadores b de las zonas 1 y 2 (heredados de B) no son accesibles porque siempre quedan ocultos por los miembros dominantes, y la gramática C++ no ofrece ninguna forma que permita hacerlo en la disposición actual del ejemplo. Son los intentos fallidos señalados en L7 y L8: C1::B::b = 10; B::b = 10;

// L7: Error de sintaxis! // L8: Error ambigüedad. No existe una unica base B

El error de L8 se refiere a que existen dos posibles candidatos (zonas 1 y 2). Al tratar de la herencia virtual (

4.11.2c1) veremos un método de resolver (parcialmente) este problema.

Cuando no existe dominancia, los identificadores b0 de las zonas 1 y 2 si son visibles, aunque la designación directa no es posible porque existe ambigüedad sobre la zona 1-2 a emplear. Es el caso de las sentencias L9/L11: b0 = 0; C1::b0 = 1; C2::b0 = 2;

// L9: Error ambigüedad // L10: Ok. // L11: Ok.

§4 Modificadores de acceso Los modificadores de acceso en la lista-base pueden ser cualquiera de los señalados al referirnos a la herencia simple (public, protected y private ancestros. Ejemplo:

4.11.2b-1), y pueden ser distintos para cada uno de los

class D : public B1, private B2, ... { <lista-miembros> }; struct T : private D, E { <lista-miembros> }; // Por defecto E equivale a 'public E' Inicio. [1] Preferimos el apelativo herencia múltiple, frente al de agregación o composición, porque estos últimos también se utilizan cuando en el cuerpo de una clase se incluyen instancias de otra clase. Por ejemplo: classA {...}; class B { ...

A a1; }; [2]

Stroustrup & Ellis: ACRM §10.1

[3] Un nombre D::n domina a otro B::n si B es una superclase de D (D deriva de B). En este caso, frente a cualquier ambigüedad que pueda existir entre ambos identificadores se utiliza el nombre dominante (si existe tal posibilidad).

4.11.2c1 Clases-base virtuales

§1 Sinopsis Hemos señalado que en herencia múltiple, las clases antecesoras no pueden repetirse: class B { .... }; class D : B, B, ... { ... };

Fig. 1

// Ilegal!

aunque si pueden repetirse indirectamente: class class class class

B { .... }; C1 : public B { ... }; C2 : public B { ... }; D : public C1, public C2 { ... };

Fig. 2 // Ok.

En este caso, cada objeto de la clase D tiene dos subobjetos de la clase B. Es la situación mostrada en el ejemplo de la página anterior (

4.11.2c).

Si esta duplicidad puede causar problemas, o sencillamente no se desea, puede añadirse la palabra virtual a la declaración de las clases-base, con lo que B es ahora una clase-base virtual y D solo contiene un subobjeto de dicha clase: class class class class

B { .... }; C1 : virtual public B { ... }; C2 : virtual public B { ... }; D : public C1, public C2 { ... };

// Ok.

La nueva situación se muestra en la figura 1 y en el DAG de la figura 2.

§2 virtual (palabra-clave) virtual es una palabra-clave C++ que tiene dos acepciones completamente diferentes dependiendo del contexto de su utilización. Utilizada con nombres de clase sirve para controlar aspectos del mecanismo de herencia; utilizada con nombres de funciones-miembro, controla aspectos del polimorfismo y del tipo de enlazado que se utiliza para tales funciones [1].

§2.1 Sintaxis La sintaxis de la palabra-clave virtual admite dos variantes que reflejan la dualidad de su utilización: virtual <nombre-de-clase> virtual <nombre-de-funcion-miembro>

La primera forma sintáctica, cuando se antepone al nombre de una clase-base, como en el caso anterior, declara una clase-base virtual, que da lugar a un mecanismo denominado herencia virtual (en contraposición con la herencia ordinaria), cuya descripción abordamos en este epígrafe. Ejemplo: class D : virtual B { /* ... */ }; es equivalente a: class D : private virtual B { /* ... */ }; Tenga en cuenta que en estos casos, el calificador virtual solo afecta al identificador que le sigue inmediatamente. Ejemplo: class E : public virtual B, C, D { /* ... */ }; En este caso las clases B, C y D son bases públicas directas, pero solo B es virtual.

La segunda forma, cuando se aplica a la función miembro de una clase-base, define dicho método como función virtual, lo que permite que las clases derivadas puedan definir diferentes versiones de la misma función-base (virtual) aún en el caso de que coincidan los argumentos (no se trate por tanto de un caso de sobrecarga), y les proporciona un método especial de enlazado (Enlazado Retrasado

1.4.4).

Las funciones virtuales redefinidas en las clases derivadas, solapan u ocultan a la versión definida en la superclase, dándole a las clases a que pertenecen el carácter de polimórficas (

4.11.8). Todos los

aspectos de este segundo uso, se detalla en el apartado correspondiente a las Funciones virtuales ( 4.11.8a). Las clases-base virtuales son clases perfectamente normales, y nada impide que puedan definirse en ellas funciones virtuales.

§3 Herencia virtual Hemos señalado, que en una herencia múltiple ordinaria, en la que indirectamente se repite una superclase (como en el ejemplo inicial ), los objetos de la clase derivada contienen múltiples subobjetos de la superclase. Esta duplicidad puede evitarse declarando virtuales las superclases. Como ejemplo de aplicación se incluye un caso análogo al estudiado en el capítulo anterior ( 4.11.2c) en el que se utiliza herencia virtual (en esta ocasión utilizamos una versión ejecutable para comprobar las salidas). #include using namespace std; class B { public: int b;

int b0; }; class C1 : public virtual B { public: int b; int c; }; class C2 : public virtual B { public: int b; int c; }; class D: public C1, C2 { public: D() { c = 10; // L1: Error C1::c = 110; // L2: Ok. C2::c = 120; // L3: Ok. b = 12; Error!! // L4: Error C1::b = 11; // L5: Ok. C2::b = 12; // L6: Ok. C1::B::b = 10; // L7: Error B::b = 10; // L8: Ok.!! b0 = 0; // L9: Ok.!! C1::b0 = 1; // L10: Ok. C2::b0 = 1; // L11: Ok. } };

ambigüedad C1::c o C2::c ?

ambigüedad C1::b domina sobre C1::B::b C2::b domina sobre C2::B::b de sintaxis!

int main() { // ================ D d; cout << "miembro b0: " << d.C1::b0 << endl; // M2: ++d.C2::b0; cout << "miembro b0: " << d.C1::b0 << endl; // M4: return 0; } Salida: miembro b0: 1 miembro b0: 2 Comentario: La figura 3 muestra la posible disposición de los miembros en los objetos de las clases utilizadas. Compárese con la correspondiente figura del capítulo anterior cuando la herencia Fig. 3 no es virtual ( Figura). El comportamiento y las medidas adoptadas para evitar los errores son los ya descritos. Con la diferencia de que aquí han desaparecido

los errores de las sentencias L8 y L9. La razón es la misma en ambos casos: como se indica en la figura, los objetos de la clase D solo tienen un subobjeto heredado de la superclase B, por lo que no hay ambigüedad al respecto. Es significativo que a pesar de ser posible la asignación directa de L9. Aún son posibles las sentencias L10 y L11. En realidad, las tres últimas sentencias inicializan al mismo miembro. Esto puede comprobarse en las sentencias de salida M2 y M4, en las que se incrementa el miembro b0 como subobjeto de C2 y se interroga como subobjeto de C1. §3.1 Para mayor abundamiento, se añade otro ejemplo que muestra como, en una herencia múltiple ordinaria, en la que indirectamente se repite una superclase, los objetos de la clase derivada contienen múltiples subobjetos de la superclase. Su existencia se pone de manifiesto sobrecontrolando adecuadamente los identificadores (

4.11.2b):

#include class B { // public: int pub; B() { pub = 0; } // }; class X : public B {}; // class Y : public B {}; // class Z : public X, public Y int main () Z z1; cout << cout << cout << z1.X::pub z1.Y::pub cout << cout << cout << }

{ "Valores " z1.pub " z1.pub = 1; = 2; "Valores " z1.pub " z1.pub

clase raíz constructor por defecto L.7 L.8 {};

// ===================== // instancia de Z iniciales:" << endl; (de X) == " << z1.X::pub << endl; (de Y) == " << z1.Y::pub << endl; // sobrecontrolamos el identificador // L.17 finales:" << endl; (de X) == " << z1.X::pub << endl; (de Y) == " << z1.Y::pub << endl;

Salida: Valores z1.pub z1.pub Valores z1.pub z1.pub

iniciales: (de X) == 0 (de Y) == 0 finales: (de X) == 1 (de Y) == 2

Cuando no sea deseable esta repetición de subobjetos, la herencia virtual evita la multiplicidad. Observe el resultado del ejemplo anterior declarando las clases X e Y como clases-base virtuales. El programa sería exactamente igual, excepto el cambio de las líneas 7 y 8 por las siguientes: class X : virtual public B {}; class Y : virtual public B {}; Salida:

// L.7 bis // L.8 bis

Valores iniciales: z1.pub (de X) == z1.pub (de Y) == Valores finales: z1.pub (de X) == z1.pub (de Y) ==

0 0 2 2

En este caso, los identificadores z1.X::pub y z1.Y::pub señalan al mismo elemento, rezón por la que las dos últimas salidas corresponden a la última asignación realizada (línea 17).

§3.1 Constructores de clases-base virtuales Los constructores de las superclases virtuales son invocados antes que los de cualquier otra superclase no-virtual. Si la jerarquía de clases contiene múltiples clases-base virtuales, sus respectivos constructores son invocados en el mismo orden que fueron declaradas. Después se invocan los constructores de las clases-base no virtuales, y por último el constructor de la clase derivada. No obstante lo anterior, si una clase virtual deriva de una superclase no-virtual, el constructor de esta es invocado primero, a fin de que la clase virtual derivada pueda ser construida adecuadamente. Por ejemplo las sentencias: class X : public Y, virtual public Z {}; X clasex; originan el siguiente orden de invocación de constructores: Z(); Y(); X();

// inicialización de la clase-base virtual // inicialización de la clase-base no-virtual // inicialización de la clase derivada

Inicio. [1] Esta suerte de sobrecarga que realiza C++ con algunas palabras-clave, con dos o incluso tres significados totalmente distintos, e incluso (a veces) contradictorios, es uno de los muchos reproches que se le hacen al lenguaje, y una de las causas de su complejidad sintáctica y semántica

4.11.2d Construcción y destrucción de objetos §1 Sinopsis: Recordemos que los objetos pueden ser creados y destruidos bajo diversas circunstancias: •

Los objetos automáticos son creados cada vez que el proceso encuentra una declaración y destruidos cuando la ejecución sale del bloque en que son declarados (

• • •

4.1.5).

Los objetos persistentes son creados con el operador new y destruidos con delete ( 4.1.5) Un miembro no estático de un objeto es creado y destruido cuando el objeto del que es miembro es a su vez creado y destruido. Un objeto estático local es creado la primera vez que se encuentra la declaración durante la ejecución del programa, y es destruido cuando el programa finaliza (

4.1.5).



Un objeto estático global es creado al principio del programa, durante la ejecución del módulo



inicial ( 1.5) y destruido cuando el programa finaliza. Un objeto temporal puede ser creado como parte de la evaluación de una expresión y destruido al final de la evaluación misma.

Las condiciones enunciadas de destrucción de objetos son las que podríamos considerar "normales"; en el sentido que ocurren durante la ejecución normal del programa. Pero existen otras causas de destrucción de objetos que acontecen durante el proceso de limpieza de pila ("Stack unwinding" que forma parte del mecanismo C++ de excepciones.

1.6)

§2 Unas funciones muy especiales La adecuada creación y destrucción de los objetos es cuestión de la mayor importancia para la correcta ejecución de un programa, y una de las causas más frecuentes de errores en los programas C++. Como botón de muestra, recordemos que al tratar de la declaración de punteros ya advertimos lo fácil que resulta causar un desastre con un simple error en su inicialización ( 4.2.1a). Para evitar este tipo de inconvenientes, C++ implementa mecanismos que garantizan en lo posible la correcta creación, inicialización y destrucción de objetos. En la definición de clases se utilizan ciertas funciones-miembro un tanto particulares que son responsables de la creación, inicialización, copia y destrucción de los objetos de la clase. Son los constructores ( 4.11.2d1) y destructores ( 4.11.2d2). De los primeros hay dos variedades: los que crean un objeto desde cero (especificando sus propiedades), y los que lo crean e inicializan como imagen de otro ya existente que sirve de modelo, son los denominados constructor-copia ( 4.11.2d4). Estas funciones gozan de las características del resto de las funciones-miembro; son declaradas y definidas dentro de la clase (o declaradas dentro y definidas fuera); como la mayoría de las funciones C++, los constructores pueden tener argumentos por defecto, o utilizar listas de argumentos de inicialización (los destructores no reciben argumentos), pero unos y otros tienen características que los hace especiales. En atención a esta singularidad, el Estándar las denomina funciones-miembro especiales ("special member funcitons").

§2.1 Denominación: La primera singularidad está en el nombre de estas funciones: los constructores adoptan el mismo nombre que la clase a que pertenecen; por su parte los destructores adoptan el nombre precedido por la tilde ~. Ejemplo: class C { // definición de una clase ... C() { /* definición de constructor */ } ~C() { /* definición del destructor */ } };

Evidentemente, caso de existir varios constructores, lo que es muy frecuente, se trata de versiones sobrecargadas de la misma función, porque al compartir todos el mismo identificador, se diferencian solo en el número y tipo de los argumentos que aceptan. En este caso, son las reglas de resolución de sobrecarga de funciones (

4.4.1a) las que deciden cual de ellos será invocado para construir el objeto.

Puesto que el destructor no acepta argumentos y tiene un nombre específico la consecuencia inmediata es que existe un solo destructor para cada clase (no existe posibilidad de sobrecarga).

§2.2 Su declaración no especifica ningún valor devuelto (ni siquiera void). En este sentido, constructores y destructores son un caso especialísimo de funciones!! Nota: el hecho que no puedan devolver ningún valor, hace que, por lo general, no existan procedimientos sencillos o elegantes para tratar los errores que pudieran producirse durante la fase de creación del objeto.

§2.3 Estos miembros no pueden ser heredados, aunque una clase derivada puede invocar a los constructores y destructores de su clase-base si son accesibles. Es decir: si no han sido declarados privados o protegidos (

4.11.2d1).

§2.4 Un constructor no puede ser friend ( declarados virtual (

4.11.2a1) de ninguna otra clase. Tampoco pueden ser

4.11.2d2), static, const o volatile.

Nota: aunque el lenguaje no contempla la posibilidad de declarar constructores virtuales, sí ofrece soporte para algunas técnicas que permiten simular este comportamiento (

4.13.5).

§2.5 No pueden obtenerse sus direcciones, por lo que no pueden definirse punteros a este tipo de funciones miembro. La sentencia del ejemplo es ilegal int main (void) { ... void* ptr = base::base; ... }

// ilegal

§2.6 Recuerde que, como en el resto de las funciones miembro no estáticas, el primer argumento (oculto) de constructores y destructores es el puntero this ( sobre que objeto debe actuar para inicializarlo o destruirlo.

4.11.6), a través del cual la función sabe

§2.7 Tenga en cuenta que un objeto que tenga constructor o destructor no puede ser utilizado como miembro de una unión (

4.6).

§3 Invocación explícita de constructores y destructores Al margen de la particularidad que representan sus invocaciones implícitas, en general su invocación sigue las pautas del resto de los métodos. Ejemplos: X x1; X::X(); X x1 = X::X() X x1 = X(); X x1(); x1.X();

// // // // // //

Ok. Invocación implícita del constructor Error: invocación ilegal del constructor Error: invocación ilegal del constructor Ok. Invocación legal del constructor Ok. Variación sintáctica del anterior [2] Error: no se puede invocar el constructor de un objeto después de creado

.... X* p = &x1;

// p es puntero-a-X señalando al objeto x1

... p->~X(); p–>X::~X(); x1.~X()

// Ok: Invocación legal del destructor del objeto x1 // Ok: variación sintáctica del anterior // Ok: otra posible invocación

Recordar que cuando se trata de iniciar o destruir objetos de tipos definidos por el usuario (clases), los operadores new y delete pueden realizar invocaciones implícitas a los constructores y destructores de tales clases. A su vez, constructores y destructores pueden realizar invocaciones explícitas a los operadores new (

4.9.20) y delete (

del objeto (ver ejemplo

4.9.21) si se requiere espacio persistente para algún miembro

4.11.2d1).

§3.1 El compilador invoca los constructores y destructores correspondientes cuando se definen y destruyen objetos (estas invocaciones pueden ser explícitas o implícitas). El constructor correspondiente crea un objeto y lo inicia. Después, cuando estos objetos deben ser destruidos, sus destructores invierten el proceso destruyendo los objetos creados. Lo mismo que ocurre con los tipos simples, los objetos abstractos pueden ser creados en memoria dinámica o ser automáticos. En el primer caso su destrucción debe realizarse explícitamente. En caso de ser automáticos, los destructores son invocados por el compilador en el momento que los objetos salen de ámbito. Son las situaciones esquematizadas en el siguiente ejemplo: { C c1; C* cpt = new C; delete cpt; }

// // // //

objeto automático objeto persistente (anónimo) + objeto cpt automático destrucción explícita del objeto anónimo destrucción implícita de c1 y cpt

§3.2 Si en una clase X no se ha definido ningún constructor para aceptar un tipo de argumento particular, el compilador no se realizará ningún intento para encontrar otro constructor, o alguna conversión para convertir un valor asignado a un tipo aceptable para algún constructor de dicha clase. Esta regla se aplica solo a constructores con un parámetro y sin iniciadores, que utilice la sintaxis de asignación “=”. Por ejemplo: class X { ... X(int); }; class Y { ... Y(X); }; Y a = 1;

// constructor

// constructor

// ilegal: No es transformado a Y(X(1))

§3.3 Cualquiera que sea el método de invocación del constructor (implícito o explícito) para crear un objeto, si la clase contiene miembros abstractos ADTs ( 2.2), el constructor invoca a su vez los constructores de estos miembros. Como el constructor de los tipos escalares reserva espacio en memoria, pero no realiza ningún tipo de inicialización concreta, los miembros de tipo escalar quedan sin una correcta inicialización a menos que esta sea proporcionada explícitamente por el programador.

Como consecuencia de la regla anterior, en el diseño de constructores no es generalmente necesario preocuparse de la iniciación de los miembros abstractos, ya que sus constructores serán invocados automáticamente y (suponemos) han sido correctamente establecidos al definir sus clases. En cambio, dado que los tipos escalares (tipos simples preconstruidos en el lenguaje) no reciben automáticamente una correcta iniciación, es probable que sus contenidos iniciales (basura) puedan ocasionar problemas, por lo que es generalmente necesario proporcionarles una correcta inicialización en el constructor. Nota: algunos autores sostienen que en estos casos, la iniciación de los miembros escalares debe realizarse en la lista de iniciadores ( 4.11.2d3a), dejando el cuerpo del constructor para cualquier lógica adicional que sea necesaria durante la construcción [1]. La razón argumentada es que al agruparlas así, se facilita la legibilidad de código y el manejo de excepciones en los procesos de creación de objetos. El consejo llega al extremo de recomendar que, si es imprescindible realizar al alguna manipulación de estos miembros en el cuerpo del constructor, al menos se inicien con un valor adecuado en la lista de iniciadores. Valor que será actualizarlo después en el cuerpo del constructor. La destrucción sigue el proceso inverso, de forma que la destrucción de estos objetos implica a su vez la invocación de los destructores de los objetos contenidos. Ejemplo: class Coordenada { public: int x; int y; }; class Triangulo { public: int color; Coordenada verticeA; Coordenada verticeB; Coordenada verticeC; }; ... Triangulo T1;

Al instanciar el objeto T1, el constructor de la clase Triangulo invocará tres veces al constructor de la clase Coordenada. Así mismo, el destructor de T1 también invocará al destructor de Coordenada. Sin embargo, tanto la propiedad Triangulo::color como los miembros x e y de los vértices, permanecerán sin una inicialización específica, aunque desde luego, serán destruidos cuando el objeto T1 sea destruido.

§4 Tipos de constructores Una vez que se ha definido un tipo abstracto C (una clase), su utilización supone una operatoria mínima que puede ser esquematizada en las siguientes sentencias: { C c1, c2; C c3 = c1; c2 = c1; }

// // // //

Creación Creación a partir de un modelo Asignación destrucción

La primera exige la existencia de un constructor; en este caso un constructor que se encargue de crear los objetos con las inicializaciones por defecto. La segunda exige la presencia de un constructor capaz de crear un objeto a imagen de otro tomado como referencia. La tercera exige la presencia de un operador de asignación capaz de realizar la asignación de los miembros del Rvalue en el Lvalue.

Finalmente se precisa de destructores que garanticen la correcta destrucción de los objetos y que puedan ser invocados automáticamente por el compilador al llegar al final del ámbito. Nota: se podría argumentar que la segunda puede ser sustituida por la creación de un objeto c3 desde "scratch" (caso primero) seguido de una asignación c3 = c1 (caso tercero). Sin embargo, esta no es la solución adoptada. Todas las consideraciones de diseño del lenguaje han gravitado alrededor de la eficiencia del código, de forma que se ha preferido mantener una sola operación. Es importante entender que una sentencia como esta no implica ninguna asignación. Es una forma de expresar la utilización de un constructor especial que acepta un objeto como argumento, objeto que será utilizado como modelo para la creación del nuevo; algo como: C::C(c1);.

Los diseñadores del lenguaje decidieron que estas utilidades eran imprescindibles, y además pretendieron simplificar el trabajo del usuario, de forma que, aunque dejaron al programador facultad para definir sus propios constructores, destructores y operadores, decidieron que, en caso de no hacerlo explícitamente, el compilador debería proporcionarlos por defecto. Estos algoritmos se denominan oficiales o de oficio para distinguirlos de los creados por el programador, a los que llamaremos explícitos. Cualquier constructor, destructor u operador de asignación "oficial" (generado por el compilador) es público. Nota: es importante distinguir estos conceptos; "de oficio" (generados por el compilador) y "explícitos" (definidos por el usuario), y no confundirlos con el concepto "por defecto" (que puede ser invocado sin argumentos 4.11.2d1). Desgraciadamente son confundidos y/o no suficientemente enfatizadas sus diferencias en la mayoría de los textos. Otra cosa es que, por ejemplo, el constructor "oficial" sea además "por defecto" (puede ser invocado sin argumentos).

Si el programador no define explícitamente ningún constructor, el compilador genera un constructor oficial o de oficio (

4.11.2d1), que hace posible expresiones como:

C c1; Sin embargo, si el programador proporciona un constructor explícito (con o sin argumentos) el constructor oficial no es generado. En consecuencia, si se proporciona un constructor explícito con argumentos y además se desea un constructor por defecto (sin argumentos), este último debe ser proporcionado por el programador de la clase. Si el programador no define ningún constructor adecuado, el compilador genera un constructor-copia oficial (

4.11.2d4) que hace posible que puedan utilizarse expresiones como:

C c2 = c1; Si no se define explícitamente una versión de la función operator=() para miembros de la clase, el compilador genera un operador de asignación oficial que hace posible expresiones como: c3 = c1; Finalmente, si el programador no define explícitamente un destructor, el compilador proporciona un destructor oficial.

Todos estos algoritmos "oficiales" suministrados por el compilador cuando no hay ninguno explícito, tienen un comportamiento predefinido que será comentando más adelante. Por ahora indicaremos que

cuando existen versiones explícitas, el compilador aporta automáticamente algunos detalles si la versión explícita los omite. Estos detalles tienden a garantizar un comportamiento correcto del algoritmo desde el punto de vista lógico y varían en función del algoritmo (constructor, constructor-copia o destructor). Es significativo que, aparte de las invocaciones explícitas o implícitas a los constructores, que ocurren cuando se instancia deliberadamente un objeto, el compilador también crea infinidad de objetos temporales (cuya existencia pasa más o menos inadvertida) utilizando el mencionado constructor-copia. Considere el siguiente ejemplo: class UnaClase { ... }; UnaClase func (UnaClase) { ... return UnaClase; } ... { UnaClase obj1; UnaClase obj2 = obj1; UnaClase obj3 = func(obj1); onj2 = onj3; }

// // // // // //

Bloque B. L.1 L.2 L.3 L.4 L.5

Estas sentencias provocan las siguientes invocaciones (ver en la página adjunta un ejemplo ejecutable de verificación

ejemplo):

L.1: Invocación al constructor por defecto (sin argumentos). Este constructor puede ser oficial o explícito, según el diseño de la clase. L.2: Invocación al constructor-copia (crea un objeto obj2 con el mismo contenido que obj1). L.3: a.- Invocación de la función: Es invocado el constructor-copia para crear un objeto temporal tmp local a la función, e igual que el objeto obj1 pasado como argumento. Al terminar la función, el objeto tmp es finalmente destruido, junto con el resto de objetos locales, mediante la invocación a su destructor. b.- Invocación del constructor-copia para crear un objeto obj3, con el mismo contenido que el valor devuelto por la función. L.4: No se invoca ningún constructor. La asignación es totalmente distinta de la construcción y de la construcción-copia (se realiza entre objetos ya creados). L.5: Invocación de los destructores de los objetos obj1, obj2 y obj3.

Tamas relacionados: •

Constructores de conversión (



Operadores de conversión (



Control de recursos (

4.11.2d1) 4.9.18k)

4.1.5a)

Inicio. [1] "C++ Cookbook" por D. Ryan Stephens, Christopher Diggins, Jonathan Turkamis y Jeff Cogswell. O'Relly Media Inc 2a Ed. 2006.

[2] Evidentemente, el hecho que X x1 = X::X(); no sea correcta, es una excepción en las reglas generales de la gramática C++. Por su parte, la expresión X x1 = X(); es correcta precisamente porque el compilador puede deducir por el contexto, que el Rvalue es X::X().

4.11.2d1 Constructores §1 Sinopsis Podemos imaginar que la construcción de objetos tiene tres fases:

1. instanciación, que aquí representa el proceso de asignación de espacio al objeto, de forma que este tenga existencia real en memoria.

2. Asignación de recursos. Por ejemplo, un miembro puede ser un puntero señalando a una zona de memoria que debe ser reservada; un "handle" a un fichero; el bloqueo de un recurso compartido o el establecimiento de una línea de comunicación.

3. Iniciación, que garantiza que los valores iniciales de todas sus propiedades sean correctos (no contengan basura). La correcta realización de estas fases es importante, por lo que el creador del lenguaje decidió asignar esta tarea a un tipo especial de funciones (métodos) denominadas constructores. En realidad la consideraron tan importante, que como veremos a continuación, si el programador no declara ninguno explícitamente, el compilador se encarga de definir un constructores de oficio , encargándose de utilizarlo cada vez que es necesario. Aparte de las invocaciones explícitas que pueda realizar el programador, los constructores son frecuentemente invocados de forma implícita por el compilador. Es significativo señalar que las fases anteriores se realizan en un orden, aunque todas deben ser felizmente completadas cuando finaliza la labor del constructor.

§2 Descripción Para empezar a entender como funciona el asunto, observe este sencillo ejemplo en el que se definen sendas clases para representar complejos; en una de ellas definimos explícitamente un constructor; en otra dejamos que el compilador defina un constructor de oficio: #include using namespace std; class CompleX { // Una clase para representar complejos public: float r; float i; // Partes real e imaginaria CompleX(float r = 0, float i = 0) { // L.7: construtor explícito this->r = r; this->i = i; cout << "c1: (" << this->r << "," << this->i << ")" << endl; } }; class CompX { // Otra clase análoga public: float r; float i; // Partes real e imaginaria };

void main() { // ====================== CompleX c1; // L.18: CompleX c2(1,2); // L.19: CompX c3; // L.20: cout << "c3: (" << c3.r << "," << c3.i << ")" << endl; } Salida: c1: (0,0) c2: (1,2) c3: (6.06626e-39,1.4013e-45) Comentario En la clase CompleX definimos explícitamente un constructor que tiene argumentos por defecto ( así en la clase CompX en la que es el propio compilador el que define un constructor de oficio.

), no

Es de destacar la utilización explícita del puntero this ( 4.11.6) en la definición del constructor (L.8/L.9). Ha sido necesario hacerlo así para distinguir las propiedades i, j de las variables locales en la función-constructor (hemos utilizado deliberadamente los mismos nombres en los argumentos, pero desde luego, podríamos haber utilizado otros ;-) En la función main se instancian tres objetos; en todos los casos el compilador realiza una invocación implícita al constructor correspondiente. En la declaración de c1, se utilizan los argumentos por defecto para inicializar adecuadamente sus miembros; los valores se comprueban en la primera salida. La declaración de c2 en L.19 implica una invocación del constructor por defecto pasándole los valores 1 y 2 como argumentos. Es decir, esta sentencia equivaldría a: c2 = CompleX::CompleX(1, 2); // Hipotética invocación explícita al constructor Nota: en realidad esta última sentencia es sintácticamente incorrecta; se trata solo de un recurso pedagógico, ya que no es posible invocar de esta forma al constructor de una clase ( alternativa correcta a la declaración de L.19 sería:

4.11.2d). Una

CompleX c2 = CompleX(1,2); El resultado de L.19 puede verse en la segunda salida. Finalmente, en L.20 la declaración de c3 provoca la invocación del constructor de oficio construido por el propio compilador. Aunque la iniciación del objeto con todos sus miembros es correcta, no lo es su inicialización ( 4.1.2). En la tercera salida vemos como sus miembros adoptan valores arbitrarios. En realidad se trata de basura existente en las zonas de memoria que les han sido adjudicadas. El corolario inmediato es deducir lo que ya señalamos en la página anterior: aunque el constructor de oficio inicia adecuadamente los miembros abstractos ( 4.11.2d), no hace lo mismo con los escalares. Además, por una u otra causa, en la mayoría de los casos de aplicaciones reales es imprescindible la definición explícita de uno o varios de estos constructores

.

§3 Técnicas de buena construcción Recordar que un objeto no se considera totalmente construido hasta que su constructor ha concluido satisfactoriamente. En los casos que la clase contenga sub-objetos o derive de otras, el proceso de creación incluye la invocación de los constructores de las subclases o de las super-clases en una secuencia ordenada que se detalla más adelante

.

Los constructores deben ser diseñados de forma que no puedan (ni aún en caso de error) dejar un objeto a medio construir. En caso que no sea posible alistar todos los recursos exigidos por el objeto, antes de terminar su ejecución debe preverse un mecanismo de destrucción y liberación de los recursos que hubiesen sido asignados. Para esto es posible utilizar el mecanismo de excepciones.

§4 Invocación de constructores Al margen de la particularidad que representan sus invocaciones implícitas, en general su invocación sigue las pautas del resto de los métodos. Ejemplos: X x1; X::X(); X x2 = X::X() X x3 = X(); X x4();

// // // // //

L.1: Ok. Invocación implícita del constructor Error: invocación ilegal del constructor [4] Error: invocación ilegal del constructor L.4: Ok. Invocación legal del constructor [5] L.5: Ok. Variación sintáctica del anterior [6]

Nota: observe como la única sentencia válida con invocación explícita al constructor (L.4) es un caso de invocación de función miembro muy especial desde el punto de vista sintáctico (esta sintaxis no está permitida con ningún otro tipo de función-miembro, ni siquiera con funciones estáticas o destructores). La razón es que los constructores se diferencian de todos los demás métodos no estáticos de la clase en que no se invocan sobre un objeto (aunque tienen puntero this 4.11.6). En realidad se asemejan a los dispositivos de asignación de memoria, en el sentido que son invocados desde un trozo de memoria amorfa y la convierten en una instancia de la clase [7].

Como ocurre con los tipos básicos (preconstruidos en el lenguaje), si deseamos crear objetos persistentes de tipo abstracto (definidos por el usuario), debe utilizarse el operador new ( 4.9.20). Este operador está íntimamente relacionado con los constructores. De hecho, para invocar la creación de un objeto a traves de él, debe existir un constructor por defecto Si nos referimos a la clase CompleX definida en el ejemplo

.

, las sentencias:

{ CompleX* pt1 = new(CompleX); CompleX* pt2 = new(CompleX)(1,2); } provocan la creación de dos objetos automáticos, los punteros pt1 y pt2, así como la creación de sendos objetos (anónimos) en el montón. Observe que ambas sentencias suponen un invocación implícita al constructor. La primera al constructor por defecto sin argumentos, la segunda con los argumentos indicados. En consecuencia producirán las siguientes salidas: c1: (0,0) c1: (1,2)

Observe también, y esto es importante, que los objetos pt1 y pt2 son destruidos automáticamente al salir de ámbito el bloque. No así los objetos señalados por estos punteros (ver comentario al respecto 4.11.2d2).

§5 Propiedades de los constructores Aunque los constructores comparten muchas propiedades de los métodos normales, tienen algunas características que las hace ser un tanto especiales. En concreto, se trata de funciones que utilizan rutinas de manejo de memoria en formas que las funciones normales no suelen utilizar. §5.1 Los constructores se distinguen del resto de las funciones de una clase porque tienen el mismo nombre que esta. Ejemplo: class X { public: X(); };

// definición de la clase X // constructor de la clase X

§5.2 No se puede obtener su dirección, por lo que no pueden declararse punteros a este tipo de métodos. §5.3 No pueden declararse virtuales ( class C { ... virtual C(); };

4.11.8a). Ejemplo:

// Error !!

La razón está en la propia idiosincrasia de este tipo de funciones. En efecto, veremos que declarar que un método es virtual ( 4.11.8a) supone indicar al compilador que el modo concreto de operar la función será definido más tarde, en una clase derivada. Sin embargo, un constructor debe conocer el tipo exacto de objeto que debe crear, por lo que no puede ser virtual.

§5.4 Otras peculiaridades de los constructores es que se declaran sin devolver nada, ni siquiera void, lo que no es óbice para que el resultado de su actuación (un objeto) sí pueda ser utilizado como valor devuelto por una función: class C { ... }; ... C foo() { return C(); }

§5.5 No pueden ser heredados, aunque una clase derivada puede llamar a los constructores y destructores de la superclase siempre que hayan sido declarados public o protected ( 4.11.2a). Como el resto de las funciones (excepto main), los constructores también pueden ser sobrecargados; es decir, una clase puede tener varios constructores.

En estos casos, la invocación (incluso implícita) del constructor adecuado se efectuará según los argumentos involucrados. Es de destacar que en ocasiones, la multiplicidad de constructores puede conducir a situaciones realmente curiosas; incluso se ha definido una palabra clave, explicit, para evitar los posibles efectos colaterales

.

§5.5 Un constructor no puede ser friend (

4.11.2a1) de ninguna otra clase.

§5.6 Una peculiaridad sintáctica de este tipo de funciones es la posibilidad de incluir iniciadores ( 4.11.2d3), una forma de expresar la inicialización de variables fuera del cuerpo del constructor. Ejemplo: class X { const int i; char c; public: X(int entero, char caracter): i(entero), c(caracter) { }; };

§5.7 Como en el resto de las funciones, los constructores pueden tener argumentos por defecto. Por ejemplo, el constructor: X::X(int, int = 0) puede aceptar uno o dos argumentos. Cuando se utiliza con uno, el segundo se supone que es un cero int. De forma análoga, el constructor X::X(int = 5, int = 6) puede aceptar dos, uno, o ningún argumento. Los valores por defecto proporcionan la información necesaria cuando faltan datos explícitos. Observe que un constructor sin argumentos, como X::X(), no debe ser confundido con X::X(int=0), que puede ser llamado sin argumentos o con uno, aunque en realidad siempre tendrá un argumento. En otras palabras: que una función pueda ser invocada sin argumentos no implica necesariamente que no los acepte.

§5.8 Cuando se definen constructores deben evitarse ambigüedades. Es el caso de los constructores por defecto del ejemplo siguiente: class X { public: X(); X(int i = 0); }; int main() { X uno(10); X dos;

// Ok; usa el constructor X::X(int) // Error: ambigüedad cual usar? X::X() o X::X(int = 0)

return 0; }

§5.9 Los constructores de las variables globales son invocados por el módulo inicial antes de que sea llamada la función main y las posibles funciones que se hubiesen instalado mediante la directiva #pragma startup (

1.5).

§5.10 Los objetos locales se crean tan pronto como se inicia su ámbito. También se invoca implícitamente un constructor cuando se crea, o copia, un objeto de la clase (incluso temporal). El hecho de que al crear un objeto se invoque implícitamente un constructor por defecto si no se invoca ninguno de forma explícita, garantiza que siempre que se instancie un objeto será inicializado adecuadamente. En el ejemplo que sigue se muestra claramente como se invoca el constructor tan pronto como se crea un objeto. #include using namespace std; class A { // definición de una clase public: int x; A(int i = 1) { // constructor por defecto x = i; cout << "Se ha creado un objeto" << endl; } }; int main() { // ========================= A a; // se instancia un objeto cout << "Valor de a.x: " << a.x << endl; return 0; } Salida: Se ha creado un objeto Valor de a.x: 1 §5.11 El constructor de una clase no puede admitir la propia clase como argumento (se daría lugar a una definición circular). Ejemplo: class X { public: X(X); };

// Error: ilegal

§5.12 Los parámetros del constructor pueden ser de cualquier tipo, y aunque no puede aceptar su propia clase como argumento, en cambio sí pueden aceptar una referencia a objetos de su propia clase, en cuyo caso se denomina constructor-copia (su sentido y justificación lo exponemos con más detalle en el apartado correspondiente

4.11.2d4).

Ejemplo: class X { public: X(X&); };

// Ok. correcto

Aparte del referido constructor-copia, existe otro tipo de constructores de nombre específico: el constructor oficial y el constructor por defecto

.

§6 Constructor oficial Si el programador no define explícitamente ningún constructor, el compilador proporciona uno por defecto al que llamaremos oficial o de oficio. Es público, "inline" ( 4.11.2a), y definido de forma que no acepta argumentos. Es el responsable de que funcionen sin peligro secuencias como esta: class A { int x; }; ... A a;

// C++ ha creado un constructor "de oficio" // invocación implícita al constructor de oficio

Recordemos que el constructor de oficio invoca implícitamente los constructores de oficio de todos los miembros. Si algunos miembros son a su vez objetos abstractos, se invocan sus constructores. Así sucesivamente con cualquier nivel de complejidad hasta llegar a los tipos básicos (preconstruidos en el lenguaje 2.2) cuyos constructores son también invocados. Recordar que los constructores de los tipos básicos inician (reservan memoria) para estos objetos, pero no los inicializan con ningún valor concreto. Por lo que en principio su contenido es impredecible (basura) [1]. Dicho en otras palabras: el constructor de oficio se encarga de preparar el ambiente para que el objeto de la clase pueda operar, pero no garantiza que los datos contenidos sean correctos. Esto último es responsabilidad del programador y de las condiciones de "runtime". Por ejemplo: struct Nombre { char* nomb; }; struct Equipo { Nombre nm; size_t sz; }; struct Liga { int year; char categoria; Nombre nLiga; Equipo equipos[10]; }; ... Liga primDiv;

En este caso la última sentencia inicia primDiv mediante una invocación al constructor por defecto de Liga, que a su vez invoca a los constructores por defecto de Nombre y Equipo para crear los miembros nLiga y equipos (el constructor de Equipo es invocado diez veces, una por cada miembro de la

matriz). A su vez, cada invocación a Equipo() produce a su vez una invocación al constructor por defecto de Nombre (size_t es un tipo básico y no es invocado su constructor 4.9.13). Los miembros nLiga y equipos son iniciados de esta forma, pero los miembros year y categoria no son inicializados ya que son tipos simples, por lo que pueden contener basura. Si el programador define explícitamente cualquier constructor, el constructor oficial deja de existir. Pero si omite en él la inicialización de algún tipo abstracto, el compilador añadirá por su cuenta las invocaciones correspondientes a los constructores por defecto de los miembros omitidos (

Ejemplo).

§6.1 Constructor trivial Un constructor de oficio se denomina trivial si cumple las siguientes condiciones: •

La clase correspondiente no tiene funciones virtuales ( superclase virtual.



Todos los constructores de las superclases de su jerarquía son triviales



Los constructores de sus miembros no estáticos que sean clases son también triviales

4.11.8a) y no deriva de ninguna

§7 Constructor por defecto Constructor por defecto de la clase X es aquel que "puede" ser invocado sin argumentos, bien porque no los acepte, bien porque disponga de argumentos por defecto (

4.4.5).

Como hemos visto en el epígrafe anterior, el constructor oficial creado por el compilador si no hemos definido ningún constructor, es también un constructor por defecto, ya que no acepta argumentos. Tenga en cuenta que diversas posibilidades funcionales y sintácticas de C++ precisan de la existencia de un constructor por defecto (explícito u oficial). Por ejemplo, es el responsable de la creación del objeto x en una declaración del tipo X x;.

§8 Un constructor explícito puede ser imprescindible En el primer ejemplo , el programa ha funcionado aceptablemente bien utilizando el constructor de oficio en una de sus clases, pero existen ocasiones en que es imprescindible que el programador defina uno explícitamente, ya que el suministrado por el compilador no es adecuado. Consideremos una variación del citado ejemplo en la que definimos una clase para contener las coordenadas de puntos de un plano en forma de matrices de dos dimensiones: #include using namespace std; class Punto { public: int coord[2]; }; int main() { // ================== Punto p1(10, 20); // L.8: cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl; }

Este programa produce un error de compilación en L.8. La razón es que si necesitamos este tipo de inicialización del objeto p1 (utilizando una lista de argumentos), es imprescindible la existencia de un constructor explícito (

4.11.2d3). La versión correcta del programa seria:

#include using namespace std; class Punto { public: int coord[2]; Punto(int x = 0, int y = 0) { coord[0] = x; coord[1] = y; } };

// construtor explícito // inicializa

int main() { // ================== Punto p1(10, 20); // L.8: Ok. cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl; }

§8.1 La anterior no es por supuesto la única causa que hace necesaria la existencia de constructores explícitos. Más frecuente es el caso de que algunas de las variables de la clase deban ser persistentes. Por ejemplo, supongamos que en el caso anterior necesitamos que la matriz que almacena las coordenadas necesite este tipo de almacenamiento. En este caso, puesto que la utilización del especificador static aplicado a miembros de clase puede tener efectos colaterales indeseados ( 4.11.7), el único recurso es situar el almacenamiento en el montón (

1.3.2), para lo que utilizamos el

operador new ( 4.9.20) en un constructor definido al efecto. La definición de la clase tendría el siguiente aspecto [8]: class Punto { public: int* coord; Punto(int x = 0, int y = coord = new int[2]; coord[0] = x; coord[1] cout << "Creado punto; << coord[0] << "; } };

0) {

// construtor por defecto // asigna espacio // inicializa

= y; X == " Y == " << coord[1] << endl;

Posteriormente se podrían instanciar objetos de la clase Punto mediante expresiones como: Punto p1; Punto p2(3, 4); Punto p3 = Punto(5, 6); Punto* ptr1 = new(Punto) Punto* ptr2 = new(Punto)(7, 8)

// // // // //

invocación invocación invocación invocación invocación

implícita implícita explícita implícita implícita

con con sin con

argumentos argumentos argumentos argumentos

§9 Orden de construcción Dentro de una clase los constructores de sus miembros son invocados antes que el constructor existente dentro del cuerpo de la propia clase. Esta invocación se realiza en el mismo orden en que se hayan declarado los elementos. A su vez, cuando una clase tiene más de una clase base (herencia múltiple

4.11.2c), los constructores de las clases base son invocados antes que el de la clase derivada y en el mismo orden que fueron declaradas. Por ejemplo en la inicialización: class Y {...} class X : public Y {...} X one; los constructores son llamados en el siguiente orden: Y(); X();

// constructor de la clase base // constructor de la clase derivada

En caso de herencia múltiple: class X : public Y, public Z X one; los constructores de las clase-base son llamados primero y en el orden de declaración: Y(); Z(); X();

// constructor de la primera clase base // constructor de la segunda clase base // constructor de la clase derivada

Nota: al tratar de la destrucción de objetos ( 4.11.2d2), veremos que los destructores son invocados exactamente en orden inverso al de los constructores.

§9.1 Los constructores de clases base virtuales ( 4.11.8a) son invocados antes que los de cualquier clase base no virtual. Si la jerarquía contiene múltiples clases base virtuales, sus constructores son invocados en el orden de sus declaraciones. A continuación de invocan los constructores del resto de las clase base, y por último el constructor de la clase derivada.

§9.2 Si una clase virtual deriva de otra no virtual, primero se invoca el constructor de la clase base (no virtual), de forma que la virtual (derivada) pueda ser construida correctamente. Por ejemplo, el código: class X : public Y, virtual public Z X one; origina el siguiente orden de llamada en los constructores: Z(); Y(); X();

// constructor de la clase base virtual // constructor de la clase base no virtual // constructor de la clase derivada

Un ejemplo más complicado: class base; class base2; class level1 : public base2, virtual public base; class level2 : public base2, virtual public base; class toplevel : public level1, virtual public level2; toplevel view;

El orden de invocación de los constructores es el siguiente: base(); base2(); level2(); base2(); level1();

// // // // // // //

clase virtual de jerarquía más alta base es construida solo una vez base no virtual de la base virtual level2 debe invocarse para construir level2 clase base virtual base no virtual de level1 otra base no virtual

toplevel();

§9.3 Si una jerarquía de clases contiene múltiples instancias de una clase base virtual, dicha base virtual es construida solo una vez. Aunque si existen dos instancias de la clase base: virtual y no virtual, el constructor de la clase es invocado solo una vez para todas las instancias virtuales y después una vez para cada una de las instancias no virtuales. §9.4 En el caso de matrices de clases, los constructores son invocados en orden creciente de subíndices.

§10 Los constructores y las funciones virtuales Debido a que los constructores de las clases-base son invocados antes que los de las clases derivadas, y a la propia naturaleza del mecanismo de invocación de funciones virtuales ( 4.11.8a), el mecanismo virtual está deshabilitado en los constructores, por lo que es peligroso incluir invocaciones a tales funciones en ellos, ya que podrían obtenerse resultados no esperados a primera vista. Considere los resultados del ejemplo siguiente, donde se observa que la versión de la función fun invocada no es la que cabría esperar en un funcionamiento normal del mecanismo virtual. #include <string> #include using namespace std; class B { // superclase public: virtual void fun(const string& ss) { cout << "Funcion-base: " << ss << endl; } B(const string& ss) { // constructor de superclase cout << "Constructor-base\n"; fun(ss); } }; class D : public B { // clase derivada string s; // private por defecto public: void fun(const string& ss) { cout << "Funcion-derivada\n"; s = ss; } D(const string& ss) :B(ss) { // constructor de subclase cout << "Constructor-derivado\n"; } };

int main() { D d("Hola mundo"); }

// ============= // invocación implícita a constructor D

Salida: Constructor-base Funcion-base: Hola mundo Constructor-derivado Nota: la invocación de destructores ( 4.11.2d2) se realiza en orden inverso a los constructores. Las clases derivadas se destruyen antes que las clases-base [2]. Por esta razón el mecanismo virtual también está deshabilitado en los destructores (lo que no tiene nada que ver con que los destructores puedan ser en sí mismos funciones virtuales 4.11.2d2). Así pues, en la ejecución de un destructor solo se invocan las definiciones locales de las funciones implicadas. De lo contrario se correría el riesgo de referenciar la parte derivada del objeto que ya estaría destruida.

§11 Constructores de conversión Normalmente a una clase con constructor de un solo parámetro puede asignársele un valor que concuerde con el tipo del parámetro. Este valor es automáticamente convertido de forma implícita en un objeto del tipo de la clase a la que se ha asignado. Por ejemplo, la definición: class X { public: X(); X(int); X(const char*, int = 0); };

// constructor C-1 // constructor C-2 // constructor C-3

en la que se han definido dos constructores que pueden ser utilizados con un solo argumento, permite que las siguientes asignaciones sean legales: void f() { X a; X b = X(); X c = X(1); X d(1); X e = X("Mexico"); X f("Mexico"); X g = 1; X h = "Madrid"; a = 2; }

// // // // // // // // //

Ok invocado C-1 Ok ídem. Ok invocado C-2 Ok igual que el anterior Ok invocado C-3 Ok igual que el anterior L.1 Ok. L.2 Ok. L.3 Ok.

La explicación de las tres últimas sentencias es la siguiente: En L.1, el compilador intenta convertir el Rvalue (que aquí es una constante numérica entera de valor 1) en el tipo del Lvalue, que aquí es la declaración de un nuevo objeto (una instancia de la clase). Como necesita crear un nuevo objeto, utilizará un constructor, de forma que busca si hay uno adecuado en X que acepte como argumento el tipo situado a la derecha. El resultado es que el compilador supone un constructor implícito a la derecha de L.1:

X a = X(1);

// interpretación del compilador para L.1

El proceso se repite en la sentencia L.2 que es equivalentes a: X B = X("Madrid");

// L.2bis

La situación en L.3 es completamente distinta, ya que en este caso ambos operandos son objetos ya construidos. Para poder realizar la asignación, el compilador intenta convertir el tipo del Rvalue al tipo del Lvalue, para lo cual, el mecanismo de conversión de tipos busca si existe un constructor adecuado en X que acepte el operando derecho. Caso de existir se creará un objeto temporal tipoX que será utilizado como Rvalue de la asignación. La asignación propiamente dicha es realizada por el operador correspondiente (explícito o implícito) de X. La página adjunta incluye un ejemplo que muestra gráficamente el proceso seguido (

Ejemplo)

Este tipo de conversión automática se realiza solo con constructores que aceptan un argumento o que son asimilables (como C-2), y suponen una conversión del tipo utilizado como argumento al tipo de la clase. Por esta razón son denominadas conversiones mediante constructor, y a este tipo de constructores constructores de conversión ("Converting constructor"). Su sola presencia habilita no solo la conversión implícita, también la explícita. Ejemplo: class X { public: X(int); };

// constructor C-2

la mera existencia del constructor C-2 en la clase X, permite las siguientes asignaciones: void f() { X a = X(1) X a = 1; a = 2; a = (X) 2; a = static_cast<X>(2); }

// // // // //

L1: Ok. Ok. Ok. Ok.

Ok. invocación explícita al constructor invocación implícita X(1) invocación implícita X(2) casting explícito (estlo tradicional) casting explícito (estilo C++)

Si eliminamos el constructor C-2 de la declaración de la clase, todas estas sentencias serían erróneas. Observe que en L1 cabría hacerse una pregunta: ¿Se trata de la invocación del constructor, o un modelado explícito al estilo tradicional?. La respuesta es que se trata de una invocación al constructor, y que precisamente el modelado (explícito o implícito) se apoya en la existencia de este tipo de constructores para realizar su trabajo.

Temas relacionados •

Operadores de conversión (



Conversión automática a tipos simples (

4.9.18k) 4.13.6)

§12 Constructor explicit El problema es que en ocasiones el comportamiento descrito en el epígrafe anterior puede resultar indeseable y enmascarar errores. Es posible evitarlo declarando el constructor de la clase con la palabra

clave explicit, dando lugar a los denominados constructores explicit [3]. En estos casos, los objetos de la clase solo podrán recibir asignaciones de objetos del tipo exacto. Cualquier otra asignación provocará un error de compilación.

§12.1 La sintaxis de utilización es: explicit <declaración de constructor de un solo parámetro> Aplicándolo al ejemplo anterior: class X { public: explicit X(int); // constructor C-2b explicit X(const char*, int = 0); // constructor C-3b }; ... void f() { X a = 1; // L.1 Error!! X B = "Madrid"; // L.2 Error!! a = 2; // L.3 Error!! } Ahora los objetos de la clase X, dotada con constructores explicit, solo pueden recibir asignaciones de objetos del mismo tipo: void f() { X a = X(1); X b = X("Madrid", 0); a = (X) 2; }

// L.1 Ok. // L.2 Ok. // L.3 Ok.

En L.3 se ha utilizado una conversión de tipos ("Casting") explícita ( 4.9.9). Para realizarla, el mecanismo de conversión busca si en la clase X existe un constructor que acepte como argumento el tipo de la derecha, con lo que estaríamos en el caso de L.1.

§13 Constructores privados y protegidos Cuando los constructores no son públicos (son privados o protegidos 4.11.2b-1), no pueden ser accedidos desde el exterior, por lo que no pueden ser invocados explícita ni implícitamente al modo tradicional (§4

). Ejemplo:

class C { int x; C(int n=0): x(n) {} // privado por defecto }; ... void foo() { C c(1); // Error!! cannot access private member }

Además, como las clases derivadas necesitan invocar los constructores de las superclases para instanciar sus objetos, caso de no ser protegidos o públicos también pueden existir limitaciones para su creación. Ejemplo: class B { int x; B (): x(10) {} }; class D : public B { ... }; void foo() { D d; // Error!! no appropriate default constructor available ... }

Puesto que los miembros private o protected no pueden ser accedidos desde el exterior de la clase, este tipo de constructores se suelen utilizar siempre a través de funciones-miembro públicas con objeto de garantizar cierto control sobre los objetos creados. El esquema de utilización sería el siguiente: class C { C(int n) { /* constructor privado */ } public: static C makeC(int m) { ... return C(m); } ... }; void foo() { C c = C::makeC(1); }

// Ok.

Observe que makeC() es estática para que pueda ser invocada con independencia de la existencia de cualquier objeto (

4.11.7). Observe también que mientras una expresión como:

C c = C(1); es una invocación al constructor, en cambio, la sentencia C c = C::makeC(1); es una invocación al operador de asignación, ya que la función devuelve un objeto que será tomado como Rvalue de la asignación. Esta técnica, que utiliza constructores privados o protegidos junto con métodos públicos para accederlos, puede prevenir algunas conversiones de tipo no deseadas, pudiendo constituir una alternativa al uso de constructores explicit (§12 Inicio.

).

[1] Las razones de este trato desigual entre ambos tipos de miembros se debe a la herencia de C y a consideraciones de eficiencia. [2] Debe tomarse también en el sentido de que las partes privativas del objeto se destruyen antes que sus partes heredadas (los miembros del espacio de nombres local se destruyen antes que los miembros del espacio de nombres de sus ancestros). [3] En vez de llamarlos "constructores explícitos", preferimos mantener aquí la denominación inglesa, manteniendo la primera para aquellos constructores que han sido explícitamente definidos en el programa, distinguiéndolos así de los constructores "de oficio". [4] Aún en el caso de que esta invocación al constructor instanciase un objeto, este sería de imposible acceso, ya que carecería de cualquier identificador o puntero. Además el constructor tiene que saber sobre que objeto debe actuar, cosa que no ocurre en esta expresión. [5] Observe que aunque válida, esta sentencia es un tanto extraña, habida cuenta que se repite hasta la saciedad que los constructores no devuelven nada "ni siguiera void"; en este caso, la sentencia parece asignar al Lvalue el valor "devuelto" por el constructor. [6] Esta forma es utilizada cuando hay que pasar argumentos al constructor. [7]

Stroustrup & Ellis: ACRM §12.1

[8] En aras de la simplicidad del ejemplo, hemos omitido intencionadamente cualquier referencia al destructor de la clase, que en este caso, exigiría evidentemente una definición explícita para liberar la zona de memoria reservada en el montón

4.11.2d2 Destructores §1 Sinopsis Los destructores son un tipo especial de función miembro, estrechamente relacionados con los constructores. Son también funciones que no devuelven nada (ni siquiera void). Tampoco aceptan ningún parámetro, ya que la destrucción de un objeto no acepta ningún tipo de opción o especificación particular y es idéntica para todos los objetos de la clase. Los destructores no pueden ser heredados, aunque una clase derivada puede llamar a los destructores de su superclase si no han sido declarados privados (son públicos o protegidos). Lo mismo que ocurre con los constructores, tampoco puede obtenerse su dirección, por lo que no es posible establecer punteros a este tipo de funciones. La misión más común de los destructores es liberar la memoria asignada por los constructores, aunque también puede consistir en desasignar y/o liberar determinados recursos asignados por estos. Por ejemplo, cerrar un fichero o desbloquear un recurso compartido previamente bloqueado por el constructor. Se ha señalado que, si el programador no define uno explícitamente, el compilador C++ proporciona un destructor de oficio, que es declarado público y puede ser invocado sin argumentos. Por lo general en la mayoría de los casos este destructor de oficio es suficiente, por lo que el programador no necesita definir uno por sí mismo, a no ser que la clase incluya la inicialización de objetos persistentes ( 1.3.2). Por ejemplo, matrices que necesiten del operador new en el constructor para su inicialización, en cuyo caso es responsabilidad del programador definir un destructor adecuado (ver ejemplo

).

Los destructores son invocados automáticamente (de forma implícita) por el programa en multitud de ocasiones; de hecho es muy raro que sea necesario invocarlos explícitamente. Su misión es limpiar los miembros del objeto antes que el propio objeto se auto-destruya.

§2 Declaración Los destructores se distinguen porque tienen el mismo nombre que la clase a que pertenecen precedido por la tilde ~ para simbolizar su estrecha relación con los constructores que utilizan el mismo nombre (son el "complemento" de aquellos). Ejemplo: class X { public: ~X(); }; ... X::~X() { ... }

// destructor de la clase X

// definición (off-line) del destructor

Ejemplo: La clase Punto definida en el epígrafe anterior ( 4.11.2d1) sería un buen exponente del caso en que es necesario definir un destructor explícito que se encargue de las correcta destrucción de los miembros. En efecto, manteniendo aquella definición, una sentencia del tipo: { ... Punto p1(2,3); ... } provoca la creación de un objeto en memoria dinámica. El miembro coord es un puntero-a-int que señala un área en el montón capaz para albergar dos enteros. Cuando la ejecución sale del ámbito del bloque en que se ha creado el objeto, es invocado el destructor de oficio y el objeto es destruido, incluyendo su único componente, el puntero coord; sin embargo el área señalada por este permanece reservada en el montón, y por tanto irremediablemente perdida. La forma sensata de utilizar tales objetos sería modificando la definición de la clase para añadirle un destructor adecuado. La versión correcta tendría el siguiente aspecto: class Punto { public: int* coord; Punto(int x = 0, int y = 0) { // construtor coord = new int[2]; coord[0] = x; coord[1] = y; } ~Punto() { // destructor delete [] coord; // L.8: }; En este caso, la sentencia de la línea 8 provoca que al ser invocado el destructor del objeto, se desasigne el área del montón señalada por el puntero (recuerde que, al igual que el resto de las

funciones-miembro, los destructores también tienen un argumento oculto this, por lo que la función sabe sobre que objeto tiene que operar en cada caso).

§3 Invocación Como hemos señalado, los destructores son invocados automáticamente por el compilador, y es muy raro que sea necesario invocarlos explícitamente. §3.1 Invocación explícita de destructores En caso necesario los destructores pueden ser invocados explícitamente de dos formas: indirectamente, mediante una llamada a delete (

4.9.21) o directamente utilizando el nombre cualificado completo.

Ejemplo class X {...}; // X es una clase ... { X obj1; // L.4: objeto automático X* ptr = new(X) // L.5: objeto persistente X* pt2 = &obj1; // Ok: pt2 es puntero a obj1 de la clase X ... pt2–>X::~X(); // L.8: Ok: llamada legal del destructor // pt2->~X(); L.9: Ok: variación sintáctica de la anterior // obj1.~X(); L.10: Ok otra posibilidad análoga X::~X(); // L.11: Error: llamada ilegal al destructor [1] delete ptr; // L.12: Ok. invocación implícita al destructor } Comentario L.4 crea el objeto obj1 en la pila ( 1.3.2), se trata de un objeto automático, y en cuanto el bloque salga de ámbito, se producirá una llamada a su destructor que provocará su eliminación. Sin embargo, el objeto anónimo señalado por ptr es creado en el montón. Observe que mientras ptr es también un objeto automático, que será eliminado al salir del bloque, el objeto al que señala es persistente y su destructor no será invocado al salir de ámbito el bloque. Como se ve en el punto siguiente, en estos casos es imprescindible una invocación explícita al destructor mediante el operador delete (cosa que hacemos en L.12), en caso contrario, el espacio ocupado por el objeto se habrá perdido. Es muy importante advertir que la invocación explícita al destructor de obj1 en L.8 (o su versiones equivalentes L.9 y L.10) son correctas, aunque muy peligrosas [2]. En efecto, en L.8 se produce la destrucción del objeto, pero en el estado actual de los compiladores C++, que no son suficientemente "inteligentes" en este sentido [3], al salir el bloque de ámbito vuelven a invocar los destructores de los objetos automáticos creados en su interior, por lo que se producirá un error de ejecución irrecuperable (volcado de memoria si corremos bajo Linux).

§3.1.1 Los objetos que han sido creados con el operador new ( 4.9.20) deben destruirse obligatoriamente con una llamada explícita al destructor. Ejemplo:

#include <stdlib.h> class X { // clase public: ... ~X(){}; // destructor de la clase }; void* operator new(size_t size, void *ptr) { return ptr; } char buffer[sizeof(X)]; // matriz de caracteres, del tamaño de X void main() { X* ptr1 = new X; X* ptr2; ptr2 = new(&buffer) X; ... delete ptr1; ptr2–>X::~X(); buffer }

// // // //

======================== puntero a objeto X creado con new puntero a objeto X se inicia con la dirección de buffer

// delete destruye el puntero // llamada directa, desasignar el espacio de

§3.2 Invocación implícita de destructores Además de las posibles invocaciones explícitas, cuando una variable sale del ámbito para el que ha sido declarada, su destructor es invocado de forma implícita. Los destructores de las variables locales son invocados cuando el bloque en el que han sido declarados deja de estar activo. Por su parte, los destructores de las variables globales son invocados como parte del procedimiento de salida ( después de la función main (

1.5)

4.4.4).

§3.2.1 En el siguiente ejemplo se muestra claramente como se invoca el destructor cuando un objeto sale de ámbito al terminar el bloque en que ha sido declarado. #include using namespace std; class A { public: int x; A(int i = 1) { x = i; } // constructor por defecto ~A() { // destructor cout << "El destructor ha sido invocado" << endl; } }; int main() { // ========================= { A a; // se instancia un objeto cout << "Valor de a.x: " << a.x << endl; } // punto de invocación del destructor de a return 0; }

Salida: Valor de a.x: 1 El destructor ha sido invocado

§3.2.2 En el ejemplo que sigue se ha modificado ligeramente el código anterior para demostrar como el destructor es invocado incluso cuando la salida de ámbito se realiza mediante una sentencia de salto (omitimos la salida, que es idéntica a la anterior): #include using namespace std; class A { public: int x; A(int i = 1) { x = i; } // constructor por defecto ~A() { // destructor cout << "El destructor ha sido invocado" << endl; } }; int main() { // ========================= { A a; // se instancia un objeto cout << "Valor de a.x: " << a.x << endl; goto FIN; } FIN: return 0; }

§3.2.3 Una tercera versión, algo más sofisticada, nos muestra como la invocación del destructor se realiza incluso cuando la salida de ámbito se realiza mediante el mecanismo de salto del manejador de excepciones, y cómo la invocación se realiza para cualquier objeto, incluso temporal, que deba ser destruido (

Ejemplo).

Recordar que que cuando los punteros a objetos salen de ámbito, no se invoca implícitamente ningún destructor para el objeto, por lo que se hace necesaria una invocación explícita al operador delete (

4.9.21) para destruir el objeto (§3.1.1

).

§4 Propiedades de los destructores Cuando se tiene un destructor explícito, las sentencias del cuerpo se ejecutan antes que la destrucción de los miembros. A su vez, la invocación de los destructores de los miembros se realiza exactamente en orden inverso en que se realizó la invocación de los constructores correspondientes ( 4.11.2d1). La destrucción de los miembros estáticos se ejecuta después que la destrucción de los miembros no estáticos.

Los destructores no pueden ser declarados const (

3.2.1c) o volatile (

invocados desde estos objetos. Tampoco pueden ser declarados static ( poder invocarlos sin la existencia de un objeto que destruir.

3.2.1d), aunque pueden ser 4.11.7), lo que supondría

§5 Destructores virtuales Como cualquier otra función miembro, los destructores pueden ser declarados virtual ( destructor de una clase derivada de otra cuyo destructor es virtual, también es virtual (

4.11.8a).

El

4.11.8a).

La existencia de un destructor virtual permite que un objeto de una subclase pueda ser correctamente destruido por un puntero a su clase-base [4]. Ejemplo: class B { // Superclase (polimórfica) ... virtual ~B(); // Destructor virtual }; class D : public B { // Subclase (deriva de B) ... ~D(); // destructor también virtual }; void func() { B* ptr = new D; delete ptr; }

// puntero a superclase asignado a objeto de subclase // Ok: delete es necesario siempre que se usa new

Comentario Aquí el mecanismo de llamada de las funciones virtuales permite que el operador delete invoque al destructor correcto, es decir, al destructor ~D de la subclase, aunque se invoque mediante el puntero ptr a la superclase B*. Si el destructor no hubiese sido virtual no se hubiese invocado el destructor derivado ~D, sino el de la superclase ~B, dando lugar a que los miembros privativos de la subclase no hubiesen sido desasignados. Tendríamos aquí un caso típico de "misteriosas" pérdidas de memoria, tan frecuentes en los programas C++ como difíciles de depurar. A pesar de todo, el mecanismo de funciones virtuales puede ser anulado utilizando un operador de resolución adecuado (

4.11.8a). En el ejemplo anterior podría haberse puesto:

void func() { D d1, d2; foo(d1, d2); } void foo(B& b1, B& b2) { // referencias a la superclase!! b1.~B(); // invocación virtual a ~D() b2.B::~B(); // invocacion estática a B::~B() }

§5.1 En el siguiente ejemplo se refiere a un caso concreto de la hipótesis anterior. Muestra como virtual

afecta el orden de llamada a los destructores. Sin un destructor virtual en la clase base, no se produciría una invocación al destructor de la clase derivada. #include class color { public: virtual ~color() { std::cout << "Destructor } }; class rojo : public color { public: ~rojo() { std::cout << "Destructor } }; class rojobrillante : public public: ~rojobrillante() { std::cout << "Destructor } };

// clase base // destructor virtual de color\n";

// clase derivada (hija) // también destructor virtual de rojo\n";

rojo {

// clase derivada (nieta)

// también destructor virtual de rojobrillante\n";

int main() { // =========== color *palette[3]; // matriz de tres punteros a tipo color palette[0] = new rojo; // punteros a tres objetos en algún sitio palette[1] = new rojobrillante; palette[2] = new color; // llamada a los destructores de rojo y color (padre). delete palette[0]; std::cout << std::endl; // llamada a destructores de rojobrillante, rojo (padre) y color (abuelo) delete palette[1]; std::cout << std::endl; // llamada al destructor de la clase raíz delete palette[2]; return 0; } Salida: Destructor Destructor Destructor Destructor Destructor Destructor Comentario

de de de de de de

rojo color rojobrillante rojo color color

Si los destructores no se hubiesen declarado virtuales las sentencias delete palette[0], delete palette[1], y delete palette [2] solamente hubiesen invocado el destructor de la clase color. Lo que no hubiese destruido correctamente los dos primeros elementos, que son del tipo rojo y rojobrillante.

§6 Los destructores y las funciones virtuales El mecanismo de llamada de funciones virtuales está deshabilitado en los destructores por las razones ya expuestas al tratar de los constructores (

4.11.2d1)

§7 Los destructores y exit Cuando se invoca exit ( &1.5.1) desde un programa, no son invocados los destructores de ninguna variable local del ámbito actual. Las globales son destruidas en su orden normal.

§8 Los destructores y abort Cuando se invoca la función abort ( &1.5.1) en cualquier punto de un programa no se invoca ningún destructor, ni aún para las variables de ámbito global. Inicio. [1] Falta indicación del objeto sobre el que actuará el destructor. [2] Recuerde que desde el punto de vista del lenguaje C++, dejar de destruir un objeto persistente no es un error, a lo sumo una pérdida de espacio. Sin embargo, intentar destruir un objeto dos veces sí lo es. La mayoría de las veces se producirá un error fatal (core dump) en tiempo de ejecución. [3] El propio Stroustrup ( TC++PL &10.4.5) nos informa que "desgraciadamente, las implementaciones (de los compiladores C++) no pueden detectar eficazmente este tipo de errores". [4] Ellis y Stroustrup ( ACRM §12.4) aconsejan que como regla general, se declare un destructor virtual en cualquier clase que tenga una función virtual

4.11.2d3 Iniciar miembros (I) §1 Sinopsis Hemos señalado ( 4.11.2d1), que la misión de los constructores es la correcta iniciación de los miembros cuando se crean objetos de la clase. Esta iniciación puede hacerse de varias formas, que en algunos casos pueden coexistir. Tres de ellas se utilizan para instanciar objetos. La última es un recurso a utilizar en la definición de la clase. •

Asignación directa al instanciar el objeto



Lista de iniciadores al instanciar el objeto



Invocación implícita o explícita al constructor con una lista de argumentos al instanciar el objeto



Lista de Iniciadores en el constructor de la clase (Pág. siguiente

)

§2 Asignación directa Es el caso siguiente: class X { static int is; public: int i; char c; }; // El compilador proporciona un constructor por defecto int X::is = 33; ... int main() { X x; x.i = 1; x.c = 'a'; ... }

// L.8: Solo con propiedades estáticas!! // // // //

======== L.9: invocación implícita al constructor por defecto asignación directa de miembro Ídem

Este sistema es permitido, aunque muestra una técnica de programación deficiente, pues todas las variables deben ser públicas y viola el principio de encapsulación. La expresión de L.9 exige que haya un constructor por defecto (puede ser el proporcionado automáticamente por el compilador si no se ha definido algún otro de forma explícita) [2]. Recordemos que la asignación de L.8 es un caso especialísimo. Solo es posible con variables miembro estáticas (sean públicas, protegidas o privadas) y que este tipo de asignación es posible incluso antes de haber instanciado ningún objeto concreto de la clase (

4.11.7).

§3 Lista de iniciadores También es permitida una variación sintáctica de la anterior aunque más compacta: X x = {1, 'a'};

// invocación implícita al constructor + asignación

Este tipo de iniciación, que utiliza una lista de iniciadores entre corchetes (como en el caso de las matrices), exige que todas las variables sean públicas, ninguna e ellas sea una clase, y no se haya definido un constructor explícito (es el caso típico de las estructuras 4.5.2) [1]. Los iniciadores de la lista deben estar en el mismo orden en que aparecen en la definición de la clase. §3.1 Ejemplo: #include class Punto { public: int x; int y; };

int main() { // =================== Punto p = {1,2}; cout << "Valor p.x = " << p.x << endl; } Salida: Valor p.x = 1

§3.2 Esta técnica es es válida incluso con clases anidadas siempre que cumplan con las condiciones señaladas: #include using namespace std; class Vector { public: float x, y; class Punto { public: int x, y; }; Punto pa; }; void main() { // ============ Vector v1 = { 2.0, 3.0, {4, 5}}; Vector v2 = { 2.0, 3.0, 4, 5 }; // Ok. equivalente al anterior cout << "V1.x = " << v1.x << endl; cout << "V1.y = " << v1.y << endl; cout << "V1.pa.x = " << v1.pa.x << endl; cout << "V1.pa.y = " << v1.pa.y << endl; } Salida: V1.x = 2 V1.y = 3 V1.pa.x = 4 V1.pa.y = 5

§3.3 Sin embargo esta técnica no es válida si las variables de la lista son instancias de una clase. Ejemplo: class Punto { public: int x, y; }; class Triangulo { public: Punto p1, p2, p3; }; ... Punto p1 = {1, 2}, p2 = {3, 4}, p3 = {5, 6}; // Ok. Triangulo t1 = { p1, p2, p3}; // Error!! Triangulo t2 = {{1, 2},{3, 4},{5, 6}}; // Ok.

§4 Lista de argumentos

Si la clase tiene un constructor explícito, puede utilizarse una invocación implícita o explícita al constructor con una lista de argumentos entre paréntesis, como se muestra en el ejemplo. class X { int i; // privadas por defecto char c; public: // a partir de aquí todos los miembros son públicos X(int entero, char caracter){ // Constructor explícito i = entero; c = caracter; }; }; ... En este caso, caben dos sintaxis para invocar al constructor (ambas con argumentos): X x(1, 'a'); X x = X(1, 'a');

// invocación implícita // invocación explícita

Observe que en este caso no sería ya posible utilizar la iniciación del ejemplo anterior un constructor explícito: X x = {1, 'a'};

, ya que existe

// Error:

Observe también la definición del constructor. Como función-miembro de clase, tiene acceso directo a todas sus variables, incluso las privadas, por lo que en su cuerpo podemos referirnos directamente a ellas. Es decir, podemos utilizar las expresiones: i = entero; c = caracter; en vez de: X::i = entero; X::c = caracter; que serían equivalentes aunque innecesarias. Esta capacidad permanece incluso si la definición del constructor (o cualquier otro método) se ha sacado fuera del cuerpo de la definición de la clase. Es decir, aunque hubiese sido: class X { int i; char c; public: X(int, char); };

// privadas por defecto // a partir de aquí todos los miembros son públicos // Prototipo de Constructor

X::X(int entero, char caracter){ // Definición del constructor i = entero; c = caracter; }; ...

§4.1 Ejemplo #include class C { public: int x; int y; C (int i, int j) { x = i; y = j; } };

// Constructor

int main() { // ============= // C c = {1,2}; Error!! forma no permitida aquí C c = C(1,2); // Ok. invocación explícita al constructor cout << "Valor c.x = " << c.x << endl; } Salida: Valor c.x = 1

§5 Paso de argumentos y sentencias de asignación en el cuerpo del constructor La iniciación de miembros puede realizarse mediante asignaciones en el cuerpo del constructor. Puede disponerse que este acepte determinados argumentos que determinarán los valores de inicio, pero entonces es preciso disponer de valores por defecto para que pueda ser utilizado como constructor por defecto. Es el caso del siguiente ejemplo: class X { int i; char c; public: // a partir de aquí todos los miembros son públicos X(int entero = 0, char caracter = 0){ // C1: constructor-1 i = entero; c = caracter; }; X(const X& obj) { // C2: constructor-2 X* ptr = new X; ptr.i = obj.i; ptr.c = obj.c; }; }; Comentario Para exponer la casuística de forma más completa, hemos definido dos constructores para la clase del ejemplo. Al primero le asignamos argumentos por defecto, la razón es que pueda servir como constructor por defecto, pues el operador new del cuerpo del segundo necesita que exista un constructor por defecto definido en la clase (

4.9.20).

El funcionamiento del segundo es un tanto especial; acepta una referencia a un objeto de la propia clase. Crea un objeto nuevo del tipo de la clase con new, e inicia sus elementos copiando de sus homónimos correspondientes. Veremos que, debido a su forma especial de trabajar, este tipo de constructor se denomina constructor copiador o sencillamente constructor-copia (

4.11.2d4).

Una vez definida la clase y sus constructores, más tarde pueden ser llamados con los parámetros adecuados para construir un objeto inicializado a los valores deseados: X* ptr; ptr–>X::X(3, 'a'); X x = X(4, 'b'); X y(5, 'c'); X z;

// // // // //

declara ptr puntero-a-tipo-X invocación explícita a C1 mediante puntero invocación explícita a C1 invocación implícita a C1 invocación implícita a C1

Las tres primeras formas de invocar al constructor le pasan argumentos, la última utiliza los argumentos por defecto y todas son igualmente correctas. Observe que en la primera tenemos un objeto anónimo, solo puede ser accedido a través de su puntero. La tercera puede enunciarse diciendo: las clases con constructor pueden ser inicializadas con una lista de expresiones entre paréntesis; expresiones que actúan como argumentos de una invocación implícita del constructor. El segundo constructor C2 , puede servir igualmente a nuestros fines, solo que ahora debemos pasarle como argumento una referencia a un objeto de la clase. Por ejemplo: X uno = X(x); X dos = y; X bis(z);

// invocación implícita a C2 // invocación implícita a C2 // invocación implícita a C2

§5.1 Ejemplo Todo lo anterior en un código funcional: #include class X { public: int i; char c; X(int = 0, char = NULL); // C1: prototipo (constructor por defecto) X(const X&); // C2: prototipo (constructor copia) }; X::X(int entero, char caracter) { // definición de C1 i = entero; c = caracter; } X::X(const X& obj) { // definición de C2 X* ptr = new X; ptr->i = obj.i; ptr->c = obj.c; } void main() { X * ptr; ptr = &X::X(1, 'a'); X x = X(2, 'b'); X y(3, 'c');

// ========================= // se crean tres objetos

cout << "Valor ?: " << ptr->i << ", " << ptr->c << endl; cout << "Valor x: " << x.i << ", " << x.c << endl; cout << "Valor y: " << y.i << ", " << y.c << endl;

X uno = X(x); X dos = y; X bis(z);

// invocación implícita a C2 // invocación implícita a C2 // invocación implícita a C2

cout << "Valor uno: " << uno.i << ", " << uno.c << endl; cout << "Valor dos: " << dos.i << ", " << dos.c << endl; cout << "Valor bis: " << bis.i << ", " << bis.c << endl; } Salida: Valor: 1, a Valor: 2, b Valor: 3, c Comentario Observe que se ha sacado la definición de los constructores C1 y C2 fuera del cuerpo de la clase (dejamos en esta solo el prototipo) utilizando una propiedad de todos los métodos: la definición puede estar fuera del cuerpo de la propia clase si se añade el especificador adecuado. Inicio. [1] Este tipo de iniciación no es posible si la clase es virtual. Es decir, si el objeto es polimórfico. [2] Ver al respecto la nota en

4.11.2d4

4.11.2d3 Iniciar miembros (II) §6 Iniciadores El sistema de argumentos y sentencias de asignación descritos en la página anterior, tiene serios problemas. Por ejemplo, ¿Qué hacer si un miembro es constante?. Supongamos el caso siguiente, que sigue las pautas anteriores: class X { char c; const int k; public: X(char caracter, int kte) { c = caracter; k = kte; // Error: asignar a constante!! }; }; Este problema no solo se presenta con miembros constantes. También cuando un miembro es instancia de una clase que no tiene constructor por defecto, o es una referencia a un objeto. Son los casos resumidos en el siguiente código: class A { int i; char c; };

class B { int ib; char cb; const int k; A a; A& ar; }; Desde luego es posible definir un constructor de la clase B que inicie adecuadamente los miembros ib y cb. Pero con los medios tradicionales no es posible inicializar el resto de miembros: la constante k; la instancia a de A (que no tiene constructor por defecto que inicialice adecuadamente este objeto), ni a la referencia ar (

4.2.3).

Para resolver este problema se utiliza un recurso denominado lista de iniciadores, que adopta el aspecto del ejemplo que sigue. class A { int i; char c; }; class B { int ib; char cb; const int k; A a; public: // <----- argumentos -------> : <-- lista-de-iniciadores ---> B (int ent, char car, int kte): k(kte), cb(car), ib(ent), a() { ... // cuerpo del constructor (bloque de su definición) }; };

Como puede verse, la lista de iniciadores es un extraño artificio sintáctico utilizado por los diseñadores de C++ como un añadido de los constructores para solventar problemas que de otra forma, violarían reglas de sintaxis bien establecidas (por ejemplo asignación a constantes). Nota: no confundir esta lista de iniciadores incluida en el constructor durante el diseño de la clase, con la lista de iniciadores señalada antes que se utiliza como lista de argumentos que pasan al constructor para instanciar objetos de la clase.

§6.1 Los iniciadores siguen ciertas reglas de sintaxis: •

Aparecen entre la lista de parámetros formales del constructor y el bloque de su definición.



Están precedidos de dos puntos ( : ) y separados entre sí por comas ( , ).



Adoptan la misma forma que las funciones, donde el nombre de la función sería el del miembro a iniciar y el argumento, el correspondiente de la lista de parámetros formales del constructor (aunque puede ser otro valor).



Un miembro de la lista puede adoptar la forma de función con más de un argumento. Por ejemplo: C(int n, char c, int k): kte(k), ch(c, n) { ... };



Si un iniciador no necesita argumento puede omitirse de la lista. El constructor de la clase anterior sería equivalente a: B(int ent, char car, int kte): k(kte), cb(car), ib(ent) { };

§6.2 Conceptualmente las asignaciones implícitas en la lista de iniciadores se efectúan antes que se ejecute el constructor, y por tanto antes que el resto de sentencias contenidas en él (esta circunstancia es aprovechada en ocasiones para algunas técnicas de programación avanzada). Considere detenidamente el resultado de los dos ejecutables: #include using namespace std; struct C { int x; void show() { cout << "Valor de x: " << x << endl; } C (int n = 33) { // L7: Constructor por defecto show(); x = n; show(); } }; int main() { C c1; return 0; }

// ========

Salida: Valor de x: 6758844 Valor de x: 33

// basura!!

Después de cambiar la sentencia L7 por C (int n = 33) : x(1) {

// L7bis

se obtiene el siguiente resultado: Valor de x: 1 Valor de x: 33

§6.3 La lista de iniciadores puede utilizarse con parámetros del constructor que tengan valores por defecto. #include using namespace std; class C { char ch; public: C (char c = 'X') : ch(c) {

// Constructor

if (ch == 'X') cout << "Valor por defecto else cout << "Recibido argumento "; cout << " .Valor actual: " << ch << endl;

";

} }; int main(void) { C c1; C c2('A'); C c3 = { 'B' }; C c4 = C('C'); return 0; }

// // // // //

========= invocación implícita invocación implícita variación sintáctica invocación explícita

al al de al

constructor por defecto constructor con argumentos la anterior constructor con argumentos

Salida: Valor por defecto Recibido argumento Recibido argumento Recibido argumento

.Valor .Valor .Valor .Valor

actual: actual: actual: actual:

X A B C

§6.4 Recordar que, con independencia del orden utilizado por el programador para la lista de iniciadores, el orden de iniciación de los miembros coincide con el orden en que estos aparecen en el cuerpo del constructor. La razón es que con independencia del orden utilizado en el fuente, el compilador transforma la lista para que coincida con el orden de declaración de los miembros de la clase [1]. Generalmente este cambio no tiene importancia práctica, pero pueden darse circunstancias en que no sea así. El programa del ejemplo funciona correctamente y proporciona la salida indicada, porque en este caso, el orden de inicio está determinado en el cuerpo del constructor C-1: #include using namespace std; struct C { char* cptr; int size; C(unsigned int n = 2) { // constructor C-1 size = (n < 27 ? n : 1); cptr = new char [size]; for (int i=0 ; i<size ; i++) { cptr[i] = i+65; } } ~C() { delete[] cptr; } }; void main() { // =============== C c1(12); cout << "Miembros: "; for (int i=0 ; i
Miembros: [A], [B], [C], [D], [E], [F], [G], [H], [I], [J], [K], [L], Un nuevo diseño del constructor C-1, pasando la asignación del miembro size a una lista de iniciadores, no afecta el resultado: C(unsigned int n=2): size(n < 27 ? n : 1) { // constructor C-2 cptr = new char [size]; for (int i=0 ; i<size ; i++) { cptr[i] = i+65; } } Sin embargo, un intento de pasar también la asignación de cptr a la lista de iniciadores, origina resultados indefinidos que pueden dar lugar a errores de runtime (observe que se ha definido antes cptr que size): C(unsigned int n=2): size (n < 27 ? n : 1), cptr = new char [size] { // C-3 for (int i=0 ; i<size ; i++) { cptr[i] = i+65; } }

§6.5 En el siguiente ejemplo mostramos la utilización de la lista de iniciadores para iniciar una constante, y un miembro que es a su vez instancia de otra clase: #include using namespace std; class A { public: int x, y; A(int i, int j) { x = i+1, y = j+2; } }; class B { public: class A a; const int k1; B(int i=0) : k1(i), a(i+20, i) { cout << "Constructor-B" << endl; } }; int main() { B b(10); cout << b.k1 << endl; cout << b.a.x << endl; cout << b.a.y << endl; } Salida: Constructor-B 10 31 12 Comentario

// // // // //

// Constructor A

// Constructor B

========== invocación implícita al constructor B Salidas de comprobación Observe la notación para acceso a miembro-de-miembro

En este ejemplo puede comprobarse como el término a(i+20,i) de la lista de iniciadores del constructor B() actúa como una invocación, con argumentos, al constructor A(). Tenga en cuenta que esta sintaxis exige la existencia de un constructor explícito en la clase

.

Observe que un término de la lista de iniciadores puede funcionar como una función con varios argumentos. En este caso el término a tiene dos argumentos: i+10 e i.

§6.6 La iniciación del constructor de la subclase de una jerarquía puede incluir al constructor de la superclase. Sin embargo, no puede ser utilizada para miembros heredados; solo pueden ser iniciados de esta forma los miembros privativos (

4.11.2b). Considere la sintaxis del ejemplo:

#include using namespace std; class B { // Superclase public: int x: const int k1; B(int i=0) :k1(i+1) { cout << "Constructor-B" << endl; } }; class D : public B { // Subclase public: int k2; D(int i = 0) :B(i+10), // L12: Ok. x(2), // Error!! x es miembro heredado k2(i) // Ok. { cout << "Constructor-D" << endl;} }; int main() { // =============== D d(5); // invocación implícita a constructor D cout << d.k1 << endl; // Comprobación cout << d.k2 << endl; } Salida (después de eliminado el iniciador erróneo): Constructor-B Constructor-D 16 5 En este caso, el término B(i+10) de la lista de iniciadores actúa como una invocación al constructor del espacio de nombres B del objeto d. También aquí se exige la existencia de un constructor explícito en la superclase B ( derivada (

). Observe como el constructor de clase-base es invocado antes que el de la clase

4.11.2d1).

Nota: la sentencia L12: D(int i = 0) :B(i+10), k2(i) { /* ... */}

puede ser expresada también D(int i = 0) : (i+10), k2(i) { /* ... */} En esta caso se sobreentiende que el argumento (i + 10) se refiere al constructor de la superclase [3].

§6.7 Algunos problemas El código del ejemplo anterior ( &6.5), compila sin dificultad. Sin embargo, haciendo una pequeña modificación, en el sentido de que la clase B derive de una superclase C. Se presentan algunos problemas: #include using namespace std; class A { public: int x, y; A(int i, int j) { x = i+1, y = j+2; } }; class C { public: int m, n; C(int i, int j) { m = i+2, n = j+3; } }; class B : public C { public: class A a; const int k1; B(int i=0) : k1(i), a(i+20, i) { cout << "Constructor-B" << endl; } };

// Constructor A

// Constructor C

// Constructor B (Lin.18)

int main() { // ========== B b(10); // invocación implícita al constructor B cout << b.k1 << endl; // Salidas de comprobación cout << b.a.x << endl; cout << b.a.y << endl; } Con este diseño, el compilador GNU G++ 3.4.2-20040916-1 para Windows produce los siguientes errores de compilación: In constructor `B::B(int)': 18: no matching function for call to `C::C()' candidates are: C::C(const C&) C::C(int, int) Por su parte, Borland C++ 5.5 para Win32 muestra el siguiente resultado:

Error E2251 18: Cannot find default constructor to initialize base class 'C' in function B::B(int) ...

En realidad, el problema no está motivado por la presencia de los iniciadores del constructor (hemos visto en el ejemplo original que funcionan correctamente), sino en la iniciación de los miembros C::m y C::n de B heredados de C [4]. Estos miembros no pueden ser iniciados porque la superclase C no dispone de un constructor por defecto (el hecho de que hayamos definido un constructor explícito ha evitado que el compilador incluya uno de oficio). La solución puede ser añadir un constructor por defecto (que pueda ser invocado sin argumentos). El más simple podría ser un constructor nulo (que en realidad no hace nada). El diseño de C quedaría como sigue: class C { public: int m, n; C() { } // constructor por defecto C(int i, int j) { m = i+2, n = j+3; } // Constructor explícito }; La nueva versión compila sin errores, pero el problema es que los miembros C::m y C::n de los objetos B no serán inicializados correctamente (contendrán basura), lo que en ocasiones no es deseable ni conveniente. Aprovechado que la superclase C tiene un constructor (aunque no sea por defecto), podemos conseguir una mejor solución incluyendo un iniciador que se refiera a los miembros heredados. Para ello, dejamos la definición de la clase C con su diseño original, y modificamos el constructor de B, queda como sigue: B(int i=0) : k1(i), C(0, 0), a(i+20, i) { cout << "Constructor-B" << endl; }

// Constructor B-bis

Hemos indicado que el iniciador C(0, 0) equivale a una invocación al constructor de la superclase con los argumentos correspondientes. Observe que hemos utilizado 0 como valor para los argumentos, pero podrían haber sido otros cualquiera. Incluso dependientes del argumento i del constructor de la subclase. Los valores de inicio de estos miembros pueden ser inspeccionados añadiendo dos sentencias al cuerpo de main: cout << b.m << endl; cout << b.n << endl;

// -> 2 // -> 3

§6.8 En la práctica La justificación y génesis de la lista de iniciadores es la que se ha expuesto. Sin embargo, una vez establecido el mecanismo, es muy frecuente su utilización para iniciar miembros que no son constantes, ni instancias de otras clases, ni referencias. Es decir, su utilización con miembros que técnicamente no requieren del artificio. Como señalábamos al tratar de construcción y destrucción de objetos ( 4.11.2d), algunos autores recomiendan que la iniciación de los miembros escalares, cualquiera que sea su tipo, se realice siempre en la lista de iniciadores, dejando el cuerpo del constructor para cualquier lógica adicional que sea necesaria durante la construcción. Como también señalamos al tratar de las reglas de buena práctica ( 4.13.1), esta opinión es compartida por Scott Meyers, que entre otros consejos, incluye el de preferir la iniciación de miembros (en la lista de iniciadores) antes que su asignación directa en el cuerpo del constructor.

Nota: el compilador GNU gcc incluye la opción -Weffc++ que avisa si se transgrede alguno de los consejos de Meyers, por lo que entre otras, avisa si en el diseño de los constructores no se respeta la mencionada recomendación. Las razones son varias, incluyendo la argumentación de que se mejora la legibilidad del código; la concisión, o tal vez cuestión de gusto personal. La consecuencia es que es frecuente encontrar definiciones de clases en las que el cuerpo del constructor está vacío { }, agrupándose las iniciación de miembros en la lista de iniciadores. Sin embargo prescindiendo de consideraciones de tipo estético, en algunas circunstancias esta sintaxis podría estar perfectamente justificada y suponer una mejora de la seguridad y del rendimiento. El hecho de que las asignaciones implícitas en la lista de iniciadores se efectúen antes que se ejecute el cuerpo del constructor (antes que el resto de sentencias contenidas en él) puede ser aprovechada si hay que incluir mecanismos de manejo de excepciones en el constructor, ya que elimina la innecesaria creación y subsiguiente destrucción de objetos que puede ocurrir si la iniciación de miembros se realiza en el cuerpo del constructor.

§6.9 Ejemplos adicionales class C { int x; char* str; A a; public: C () { // constructor por defecto x = 0; str = NULL; a = A(); } C (int i, char* ch) { // constructor explícito x = i; str = ch; } }; es muy frecuente que encontremos la siguiente alternativa al diseño anterior: class C { int x; char* str; public: C () : x(0), str(NULL), a(A()) {} C (int i, char* ch): x(i), str(ch) {} };

// constructor por defecto // constructor

Incluso es posible utilizarlo con argumentos que tengan valores por defecto: class C { char ch; public: C(char c = '\0') : ch(c) {} }; También puede utilizarse: class C { char ch;

// constructor

public: C() : ch('\0') {}

// Constructor

}; Otro ejemplo: class C { public: size_t size; char* string; C (size_t n=0): size(n), string( new char[size] ) {} ... };

// constructor

§7 Los tipos básicos Una cuestión que inicialmente puede resultar extraña (aunque meditada un poco más no lo es tanto), es que los denominados tipos básicos, escalares o primitivos ( 2.2), pueden ser considerados también como clases. Serían unas clases preconstruidas en el lenguaje a las que no podemos modificar su diseño. Generalmente su utilización sigue la sintaxis ya conocida, pero se les puede aplicar igualmente la sintaxis utilizada con las clases. Por ejemplo, en C++ son perfectamente válidas las siguientes sentencias: int x = int(); int y = int(5); char c = char(); char d('Z'); char e = char('X'); float f = float(1.23);

// L1: // L3: // L4: // L5:

La expresión L1 equivale a: int x = int(0); mientras que la expresión L3 equivale a: char c = char(´\0´); Igualmente les puede ser aplicada la sintaxis utilizada con los operadores new y delete para los tipos abstractos: int* iptr = new int; const char* cptr = new char('Z'); delete cptr; delete iptr; No obstante, salvo estas últimas expresiones, que podrían ser utilizadas para construir estos objetos en memoria persistente, la sintaxis anterior no es habitual para iniciar tipos básicos. Es desde luego más frecuente encontrar char d = 'Z'; que expresiones como L4 o L5.

Inicio. [1] El compilador GNU cpp dispone de una opción de compilación específica: -Wreorder, que advierte cuando el orden de los miembros de la lista de iniciadores no coincide con el orden en que deben ser construidos (orden de aparición en la definición). Por ejemplo: class A { public: int i; int j; A(): j(0), i(1) { } }; En cualquier caso, con independencia de la existencia de esta opción, el compilador siempre reordena la lista para que corresponda con la prevista en la declaración. [3]

Stroustrup & Ellis: ACRM §18.3.2

[4] Como puede verse, en esta ocasión del mensaje de error de Borland es mucho más explícito que el de GNU

4.11.2d4 Copiar objetos §1 Preámbulo Al reseñar los mecanismos generales de creación y destrucción de objetos ( 4.11.2d), indicamos que cuando se manejan objetos abstractos (instancias de clases), el operador de asignación = tiene un sentido por defecto que supone la asignación miembro a miembro del operando derecho en el izquierdo. Dicho en otras palabras: cuando se define una clase, el compilador define automáticamente una funciónoperador operator=() que realiza una asignación miembro-a-miembro de los elementos del operando derecho en el izquierdo. Por ejemplo, si c1 e c2 son objetos de la clase C, la asignación c1 = c2;

// S1:

supone la asignación de todos los miembros de c2 en sus homónimos de c1. Sin embargo, cuando el Lvalue implica la declaración de un nuevo objeto, la expresión C c1 = c2;

// S2:

no implica ninguna asignación. En realidad la sintaxis anterior es una convención para representar la invocación de un constructor especial, denominado constructor-copia que acepta una referencia al objeto a copiar c2 (Rvalue de la expresión) y produce un nuevo objeto c1 que es un clon del anterior. Podríamos suponer que la sentencia S2 es equivalente a una invocación del tipo: C::C(c2); y que el objeto producido se identifica con el nombre c1 (recordemos que los constructores no "devuelven" nada).

§2 El constructor-copia

El constructor-copia de la clase X es aquel que puede ser invocado con un argumento, que es precisamente una referencia a la propia clase X como en cualquiera de las definiciones siguientes: X::X(X&) { /*...*/ }; X::X(const X&){ /*...*/ }; X::X(const X&, int i) { /*...*/ };

El programador es libre para asignar al constructor-copia el significado que desee. De hecho, siempre que se respeten las reglas de sobrecarga de funciones, pueden existir múltiples versiones de estos constructores, cada uno con su propia finalidad. Como puede verse en la tercera sentencia, el constructor-copia también puede tener otros argumentos, pero en cualquier caso, la referencia a la propia clase debe ser el primero de ellos. Como sugiere su nombre, el constructor-copia se utiliza para copiar objetos. Sin embargo, el concepto "copia" puede tomarse con diversos significados en C++: existen copias ligeras y profundas; también copias lógicas ("Shadow copy") y físicas. En general "copiar" un objeto supone la generación física de otro objeto, pero no siempre el nuevo es necesariamente una réplica exacta del original (aunque pueda parecerlo). En ocasiones el nuevo objeto es un manejador "handle" del original, de forma que el usuario percibe "como si" estuviera ante una copia real. Este tipo de copia, denominada lógica o ligera, presenta la ventaja de su rapidez y economía de espacio. La duplicación completa de objetos grandes puede ser costosa en términos de espacio y tiempo, además de no ser siempre estrictamente necesaria o conveniente. Las copias profundas, también denominadas físicas, suponen la replicación completa del objeto original, reproduciendo todos sus miembros ("Memberwise") a todos los niveles de anidamiento (los objetos copiados pueden ser tipos abstractos cuyos miembros pueden ser a su vez tipos abstractos, cuyos miembros pueden ser a su vez tipos abstractos, y así sucesivamente con cualquier nivel de anidación).

§2.1 Este tipo de constructores son invocados automáticamente por el compilador en muchas circunstancias (siempre que se presenta la necesidad de crear un nuevo objeto igual a otro existente): •

Cuando se pasa un objeto (por valor) como argumento a una función; en cuyo caso se crea una copia del objeto en la función invocada (

4.4.5). Ejemplo:

class C { .... } c1; void func(C c) { ...; } // función que acepta un objeto ... func(c1); •

Cuando una función devuelve un objeto, en cuyo caso se crea una copia del objeto en la función invocante (

4.4.7). Ejemplo:

class C { ... } c1; C func(C& ref) { // función que devuelve un objeto ... return ref; } ... { ...

c2 = func(c1);

// se crea una copia local del objeto ref

} • •

Cuando se lanza una excepción ( 1.6). Ver Ejemplo. Cuando se inicia un objeto; típicamente cuando se declara e inicia un objeto mediante asignación de otro de la misma clase. Es la situación de las sentencias L.2/3 (ver class C { .... ... C c1; C c2 = c1; C c3(c1); c3 = c2;

Ejemplo):

}; // // // //

L.1 L.2 L.3 L.4

invocación implícita al constructor por defecto invocación implícita al constructor-copia ídem (variación sintáctica del anterior) invocación del operador de asignación

§2.2 El compilador crea un constructor-copia oficial para cualquier clase en la que no se hubiese definido ningún otro explícitamente (un constructor que pueda ser invocado con un solo argumento que sea una referencia al propio tipo). Este copiador de oficio realiza un clon exacto del original replicando todos los miembros a cualquier profundidad, y permite programar de forma segura con muchos tipos abstractos. Sin embargo, como veremos a continuación , cuando el programa crea tipos abstractos por agregación de otros tipos complejos como clases, estructuras, matrices y objetos persistentes que se manejan mediante punteros, puede ser necesario definir un constructor-copia explícito. También si se sobrecarga el operador de asignación (§3

).

Como todos los constructores, el constructor-copia se define como una función que no devuelve nada (ni siquiera void) y su primer parámetro es una referencia constante a un objeto de la clase [1]. Puede aceptar más parámetros; tantos como sean necesarios, siempre que se respeten las reglas de sobrecarga de funciones, de forma que no exista ambigüedad entre dos definiciones. Por ejemplo: class X { int i; char c; public: X(const X&): i(0), c('X') {} X(const X& ref, int n = 0); { i = n; c = ref.c; }; class Y { int i; char c; public: Y(const Y&): i(0), c('X') {} Y(const Y& ref, int n); { i = n; c = ref.c; };

// Error! ambigüedad con el anterior

// Ok.

Cuando se define explícitamente un constructor-copia, el compilador no necesita crear ninguna versión oficial. En tal caso, las sentencias en que se requiere la utilización de un constructor-copia utilizarán la versión explícita. Pero entonces es responsabilidad del programador cubrir todos los detalles

pertinentes. En todos los casos en que, en la definición de una versión explícita, se omita especificar la asignación de una propiedad de la clase, el compilador incluirá automáticamente una invocación al constructor por defecto del miembro. Observe que esto implica que no se realizará copia de los miembros omitidos, cuyos valores corresponderán a objetos de nueva construcción. Supone también que si los miembros omitidos son tipos simples (cuyo constructor por defecto no los inicializa con ningún valor concreto), los miembros del objeto resultante contendrán basura (

4.11.2d1).

La página adjunta muestra un ejemplo que pone de manifiesto las implicaciones de lo indicado en párrafo anterior (

Ejemplo)

§2.3 Ejemplo: A continuación exponemos el ejemplo de clase para la que se define un constructor-copia (un perfeccionamiento de la clase Punto ya comentada 4.11.2d2). Aunque se trata de un caso sencillo, ilustra con claridad la técnica seguida, así como el proceso de invocación automática de dichos métodos realizada por el compilador. #include using namespace std; #define Abcisa coord[0] #define Ordenada coord[1] class Punto { public: int* coord; Punto(int x = 1, int y = 2) { // construtor por defecto coord = new int[2]; Abcisa = x; Ordenada = y; cout << "Creado objeto: X == " << Abcisa << "; Y == " << Ordenada << endl; } ~Punto() { // destructor cout << "Objeto (" << Abcisa << ", " << Ordenada << ") destruido!" << endl; delete [] coord; } Punto(const Punto& ref) { // construtor-copia coord = new int[2]; Abcisa = ref.Abcisa; Ordenada = ref.Ordenada; cout << "Copiado objeto: X == " << Abcisa << "; Y == " << Ordenada << endl; } }; int main() { Punto p1; Punto p2(3, 4); Punto p3 = p2; return 0; } Salida:

// // // // //

====================== L.25: p1 instancia de Punto L.26: p2 instancia de Punto L.27: p3 instancia de Punto L.28: fin del programa

Creado objeto: X == 1; Y == 2 Creado objeto: X == 3; Y == 4 Copiado objeto: X == 3; Y == 4 Objeto (3, 4) destruido! Objeto (3, 4) destruido! Objeto (1, 2) destruido! Comentario Tenga en cuenta los "defines" Abcisa y Ordenada, sustituyéndolos mentalmente por los valores respectivos. En L.25 se instancia el objeto p1; se invoca el constructor por defecto y se utilizan los argumentos por defecto. En L.26 se instancia el objeto p2; en este caso se realiza una invocación implícita del constructor pasándole dos argumentos. En L.27 se instancia el objeto p3; en esta ocasión se produce una invocación implícita del constructor copia Al alcanzarse en L.28 el final del programa, son invocados automáticamente los destructores de los objetos creados en el ámbito de la función main. En este momento se invocan los destructores de los objetos p3, p2 y p1 (en orden inverso a su creación). Ver otro ejemplo de la necesidad y utilización del constructor-copia (

4.9.18d1).

§3 El operador de asignación oficial Si en la definición de la clase no se define ninguna versión explícita de la función operator=(), el compilador proporciona una versión oficial, que puede ser utilizada con miembros de la clase. Esta versión oficial realiza una copia profunda (miembro a miembro) del operando derecho en el izquierdo. Es el que permite utilizar expresiones como c1 = c2; con objetos de tipo C aunque no se haya definido explícitamente este operador para miembros de la clase. Su funcionamiento es análogo al del constructor-copia oficial, aunque en este caso ambos operandos deben ser objetos creados previamente. Recordaremos que esta función también recibe una referencia a un objeto de la clase, aunque se diferencia del constructor-copia en que devuelve una referencia (los constructores no devuelven nada, simplemente crean un objeto). C& operator= (const C&

c);

// operador de asignación

El constructor-copia y el operador de asignación no tienen porqué coincidir en su diseño. De hecho suelen existir diferencias substanciales, debido a que el constructor-opia está relacionado con la creación de objetos, de forma que implicará la asignación de recursos (por ejemplo memoria). Por contra, la asignación maneja objetos ya construidos, lo que implica la reasignación de recursos y eventualmente, desasignación de los anteriores. En cualquier caso, el mecanismo de copiado "clónico" de la asignación oficial presenta los mismos inconvenientes que el constructor-copia oficial (ver a continuación §4 pero el programador tiene libertad para definir su propia versión.

),

La descripción del operador de asignación operator= y la forma de sobrecargarlo, ha sido discutida con detalle en el apartado correspondiente ( 4.9.18a), pero debemos advertir que si se proporciona una versión explícita, es responsabilidad del usuario cubrir los detalles pertinentes, ya que solo se efectuarán aquellas asignaciones que sean especificadas de forma explícita. Los miembros para los que no exista una asignación explícita mantendrán sus antiguos valores. La página adjunta incluye un ejemplo que muestra claramente este comportamiento (

Ejemplo).

§4 Inconvenientes de la copia miembro-a-miembro En ocasiones, la réplica miembro a miembro efectuada por el constructor-copia oficial, o por el operador de asignación =, presenta riesgo de resultados indeseados cuando algunos miembros son punteros. También en los procesos de destrucción de tales objetos, cuando son invocados los destructores al finalizar su ámbito. Para ilustrarlo con un ejemplo, supongamos una clase Cliente para manejar los clientes de un hotel: class Cliente { public: char* nombre; ... Cliente(char*) { // constructor nombre = new char[30]; ... } ~Cliente() { // destructor delete[] nombre; } }; Si construimos dos instancias de la clase: void foo() { Cliente c1( "Pau Casals" ); Cliente c2 = c1; } En el primer caso se ha realizado una llamada implícita al constructor con los argumentos situados en la lista de inicialización. En el segundo, se ha realizado una llamada implícita al constructor-copia oficial

.

Cualquiera que sea el método de construcción seguido, resulta evidente que los miembros del objeto c2 serán exactamente iguales que los de c1. Existe duplicidad de miembros entre ambos objetos pero la matriz alfanumérica es compartida. Concretamente el puntero nombre de ambas instancias señala la misma dirección del montón, donde se aloja la cadena "Pau Casals" que fue creada por c1 (al que consideramos "Propietario" del contenido). Cualquier modificación del contenido de esta cadena desde un objeto tiene repercusión en el otro, que "ve" el nuevo contenido. Pero si c1 cambia totalmente contenido y situación. Por ejemplo, cambiando el nombre de cliente actual por otro más largo, lo que supone rehusar la matriz actual y crear otra de mayor dimensión en otra zona del montón, el puntero de c2 queda descolgado. Cualquier intento de acceder al nombre del cliente desde c2 proporcionará basura. la situación es aún más conflictiva al finalizar la función foo. Ya sabemos ( 4.1.5) que las variables locales son automáticamente destruidas al finalizar su ámbito.; En este caso serán invocados

automáticamente los destructores de c2 y c1 que destruirán los objetos. Cuando le llegue el turno a c2, su destructor intentará realizar un delete[] utilizando un puntero inválido, lo que probablemente producirá un error fatal de runtime. Como puede verse, las copias miembro-a-miembro, ya sean originadas por un constructor-copia o por el operador de asignación, no son siempre deseables. Incluso pueden resultar peligrosas, en especial cuando en los constructores se utilizan punteros y/o objetos persistentes creados con new.

Tema relacionado: •

El operador de asignación con miembros de clases (

4.9.2a).

Inicio. [1] Por lo general el primer parámetro del constructor-copia es una referencia constante a un objeto de la clase porque el objeto copiado (Rvalue) no debe ser modificado. Sin embargo esto no es siempre así. Al tratar de los objetos-puntero ilustramos un ejemplo en el que el constructor-copia modifica también el objeto copiado (

4.13.3).

4.11.2d5 Observaciones y errores más frecuentes §1 Presentación Muchos de los problemas que se presentan en casos prácticos programados en C++ se derivan del diseño inadecuado de constructores y destructores, en especial algunos de esos errores inexplicables que pueden acabar con los nervios del más templado. A continuación se muestra una selección con algunos de los que considero más probables, o que he tenido que sufrir en mis propias carnes.

§2 Errores en el Constructor §2.1 Constructor inadecuado class C { ... char* string; C(char*); };

// constructor explícito

C::C(char* st) { // definición ... strcpy(string, st.string); // L1: } Se pretende que la clase acepte sentencias en las que el miembro string pueda ser iniciado con una cadena de caracteres constantes estilo C mediante sentencias como: C c1("Contiene el path-name de un fichero"); El programa compila sin dificultad pero genera un error de runtime; concretamente el consabido: Este programa ha ejecutado una operación no válida y será interrumpido....

El problema aquí es que nos hemos pasado de listos. Hemos pretendido copiar el "contenido" de la cadena en lugar del puntero (nuestro miembro es un puntero). El diseño correcto en este caso sería: class C { ... char* string; C(char*); // constructor explícito }; C::C(char* st) { // definición ... string = st.string; // L1: } Sin embargo este diseño tiende a presentar problemas, ya que la información (en este caso una cadena alfanumérica) está fuera del cuerpo del objeto y por tanto fuera de su control. Es mejor un diseño alternativo en el que los datos pertenezcan al cuerpo de la clase: #define LSTRING 250 class C { ... char string[LSTRING]; C(char*); // constructor explícito }; C::C(char* st) { // definición ... strcpy(string, st.string); // L1: } Esta forma tiene el pequeño inconveniente de que debemos prever de antemano el tamaño máximo LSTRING, que debemos reservar para la matriz, y que la cadena de inicio suministrada no desbordará el tamaño de nuestro buffer si pretendemos itroducir una cadena demasiado larga (en principio el usuario de nuestro programa quizás no esté advertido de este límite interno). Esto es fácil insertando una sentencia de control en el constructor: C::C(char* st) { // definición ... if (strlen(st) < LSTRING) strcpy( string, st.string); else // medidas de control }

// L1:

Esta última forma tiene la ventaja de que nuestra aplicación no será susceptible de un ataque de desbordamiento de buffer.

§3 Errores en el Constructor-copia §3.1 Constructor-copia inadecuado class C { ... X* xptr; C(C&);

// constructor-copia

}; C::C(C& c1) { xptr = c1.xptr; }

// L1:

Cuando la clase contiene miembros que son punteros, hay que meditar muy detenidamente si al copiar un objeto, los punteros del nuevo deben apuntar al mismo objeto que en el original o no. La omisión o la inclusión de una sentencia como L1 supone que los punteros de ambos objetos señalan a la misma entidad, lo que puede derivar en innumerables problemas. §3.2 class C { ... char* string; C(C&); // constructor-copia }; C::C(C& c1) { // definición ... strcpy(string, c1.string); // L1: } Es una versión del error cometido antes en el constructor explícito (§2.1 ), pero aplicado al constructor-copia. El origen; el resultado y la solución son los mismos que en aquel caso. La versión correcta seria: class C { ... char* string; C(C&); // constructor-copia }; C::C(C& c1) { // definición ... string = c1.string; // L1: } Observe que en este caso podría haberse omitido la sentencia L1, ya que el compilador proporciona por su cuenta una versión por defecto

4.11.2e Acceso a miembros §1 Sinopsis: El acceso a miembros de clases (propiedades y métodos), incluyendo el acceso a través de punteros, tiene una notación un tanto especial. En principio C++ dispone de tres operadores específicos de acceso que suelen ser causa de confusión en el principiante y precisamente uno de los "reproches" a C++ por parte de sus competidores (C# por ejemplo). Son los siguientes: :: Operador de resolución de ámbito o acceso a ámbito ( .

Selector directo de miembro (

4.9.16)

-> Selector indirecto de miembro (

4.9.16)

4.9.19)

Aunque la explicación detallada de cada uno está en el apartado que se cita, intentaremos dar aquí una sinopsis conjunta de su utilización, reconociendo de antemano que en este punto tienen razón sus detractores, la utilización es confusa y para terminar de arreglarlo, con algunas excepciones.

§2 Acceso a miembros de clase En principio, el operador de resolución de ámbito ::, se utiliza para acceso a miembros de subespacios y para miembros de clases, lo cual tiene su lógica, habida cuenta que las clases constituyen en sí mismas una especie de subespacios particulares (

4.1.11;

4.1.11c1).

§2.1 Ejemplo: #include namespace ALPHA { long double LD; class C { public: int x; C(int = 0); }; } ALPHA::C::C(int p1) { x = p1; }

// declaración de una variable en el subespacio // definición de clase (subespacio en subespacio)

// prototipo de constructor por defecto

// L.10 definición del constructor por defecto

int main() { // =================== ALPHA::LD = 1.0; // M.1 Acceso a variable LD del subespacio cout << "Valor LD = " << ALPHA::LD << endl; ALPHA::C c1 = ALPHA::C(3); nbsp; // M.3 cout << "Valor c1.x: " << c1.x << endl; // M.4 } Salida: Valor LD = 1 Valor c1.x: 3 Comentario: Observe la notación de acceso al constructor de la clase (función-miembro) en L.10, así como en la sentencia de declaración del objeto c1 en M.3, incluyendo su invocación explícita al constructor de la clase. Es interesante resaltar como, a pesar de lo que pudiera parecer, el objeto c1 pertenece al ámbito de la función main, no al subespacio ALPHA. Esto es lo que permite que pueda utilizarse la sintaxis de M4. En caso contrario habría sido cout << "Valor c1.x: " << ALPHA::c1.x << endl;

// M.4bis

§2.2 En realidad, la mayoría de las veces, este tipo de acceso a miembros de clase solo se utiliza para

la definición de funciones miembro (como en L.10), ya que las propiedades no tienen existencia antes de la instanciación de objetos concretos, por lo que en este contexto, no tendría sentido una expresión como: ALPHA::C::x = 10;

§2.3 Para acceder a la propiedad x es necesario referirse a una instancia concreta de la clase, aunque como se ve a continuación , en este caso ya no se utiliza el operador de resolución de ámbito :: sino otro. Sin embargo, en los casos en que estos miembros tienen existencia real fuera de las instancias concretas (como es el caso de los miembros estáticos válida. Por ejemplo:

4.11.7), la notación anterior es perfectamente

#include using namespace std; namespace ALPHA { class C { // definición de clase (subespacio en subespacio) public: static int x; // variable estática static void fun() {cout << "Soy una función estática" << endl; } }; int C::x = 10; // definición de la variable estática } int main() { // =================== cout << "Variable ALPHA::C::x == " << ALPHA::C::x << endl; // L.13: Ok. ALPHA::C::fun(); // L.14: Ok. } Salida: Variable ALPHA::C::x == 10 Soy una función estática

§3 Acceso a miembros de objetos Hemos adelantado que el operador de acceso a ámbito :: no puede ser utilizado para acceder a miembros de objetos (instancias concretas de las clases), ya que los objetos no se consideran ámbitos. Por ejemplo: #include namespace ALPHA { class C { public: int x; }; } int main () { ALPHA::C c; c::x = 33;

// ============== // instancia un objeto de la clase // Error: c no es una clase o nombre de subespacio!!

cout << "Valor c::x = " << c::x << endl;

// Error!!

} En estos casos la sintaxis correcta implica utilizar el selector directo de miembro . como se muestra en el siguiente ejemplo: #include using namespace std; namespace ALPHA { class C { char c; public: C(char ch = 'x') { c = ch; x = 0; } // Constructor int x; char getC() { return c; } }; } int main () { // =================== ALPHA::C c1; // instanciar objeto de la clase cout << "Valor x de c1: " << c1.x << endl; // L.16: cout << "Valor c de c1: " << c1.getC() << endl; // L.17: } Salida: Valor x de c1: 0 Valor c de c1: x Comentario Observe como el selector directo permite acceder a cualquier tipo de miembro (público), tanto propiedades (L.16) como invocación de métodos (L.17).

§3.1 Si el acceso al objeto debe realizarse mediante un puntero, puede utilizarse el procedimiento general de utilizar el operador de indirección ( anteriormente mencionado.

4.9.11a) o bien el más específico selector indirecto

Ejemplo: class C { char c; public: C(char ch = 'x') { c int x; char getC() { return }; ... C c1, d1; // C* ptrc = &c1; // (*ptrc).x = 10; // indirección d.x = (*ptrc).x; // ptrc->x = 10; //

= ch; x = 0; }

// Constructor

c; }

objetos c1 y d1, instancias de la clase C define ptrc puntero-a-C señalando a objeto c1 Acceso a miembro x de c1 mediante operador de idem (notación desaconsejada para miembros) Ok: Acceso a propiedad mediante selector indirecto

d1.x = ptrc->x; ptrc->getC(); ptrc->C::getC();

// ídem (notación aconsejada para estos casos) // Ok: Acceso a método mediante selector indirecto // Ok: variación sintáctica de la anterior

§3.2 Ejemplo de acceso a miembros utilizando ambos selectores. #include #include class X { // clase raíz public: void func1() {std::cout << "func1" << std::endl; } }; class Y : public X { // Y deriva de X public: void func2() {std::cout << "func2" << std::endl; } }; void main() { // ========================== X mX; // mX es instancia de X (clase raíz) mX.func1(); // llamada a func1 de mX //primera forma de invocación (selector directo) Y mY; // mY es instancia de Y (clase derivada) mY.func2(); // llamada a función miembro (privativa) de mY mY.func1(); // llamada a función miembro de mY (heredada) //segunda forma de invocación, mediante puntero (selector indirecto) Y* ptY = & mY; // puntero a mY ptY->func2(); // llamada a función miembro (privativa) de mY ptY->func1(); // llamada a función miembro de mY (heredada) } Salida: func1 func2 func1 func2 func1

§5 Acceso a miembros de clases desde funciones miembro Es interesante señalar que los miembros de clase, incluso privados, son accesibles desde el interior de las funciones miembro de dichas clases (

4.1.3). La razón hay que buscarla en la forma en que está

diseñado el mecanismo C++ de búsqueda de nombres ("Name-lookup Considere el siguiente ejemplo: #include int x = 10; class CL { int x, y; public:

// L.3: // L.5: privados por defecto

1.2.1).

void setx(int i) { x = i; } // L.7: void sety(int y) { CL::y = y; } int getxy(); }; int CL::getxy() { return x + y; } int main() { // ================ CL c; c.setx(1); c.sety(2); std::cout << "X + Y == " << c.getxy() << std::endl; return 0; } Salida: X + Y == 3 Comentario Puede verse que las propiedades x e y (privadas), son directamente accesibles desde el interior de las funciones miembro setx, sety y getxy, sin que sea necesario ningún tipo de especificador de acceso; incluso a pesar de que esta última función se define fuera del cuerpo de la clase. La razón es que cualquier referencia a una variable en una función miembro, provoca que el compilador compruebe si existe tal variable en ese ámbito (el de la clase) antes que en el espacio exterior. De esta forma, no existe ambigüedad en la invocación a x dentro de la función setx de L.7. El compilador sabe que se refiere al miembro x definido en L.5 y no a la variable del ámbito global definida en L.3. Observe que la presencia del especificador CL::y en el cuerpo de sety (L.8) se justifica porque la variable local y oculta al miembro del mismo nombre. Más adelante, al tratar del puntero this, se expone otra versión de la sintaxis utilizada en L.8 para distinguir entre la variable local y del miembro de igual nombre (

4.11.6).

Nota: precisamente el hecho de que los miembros de clase, incluso privados, sean accesibles desde el interior de sus funciones miembro, es origen de otra de las ventajas de utilizar funciones miembro (métodos) y clases de la POO frente a funciones normales. En efecto, las primeras suelen necesitar la utilización de menor número de parámetros que las funciones estándar, ya que la totalidad de propiedades del objeto a que se refieren, son accesibles directamente desde el interior del método (sin necesidad de que le sean pasados sus valores en forma de argumento).

§6 Recuerde que la invocación explícita de constructores o destructores de clase, exige una sintaxis especial ligeramente diferente (

4.11.2d).

4.11.3 Nombres de clases §1 Objetos anónimos Los nombres de clase deben ser únicos dentro de su ámbito. Sin embargo, en determinados casos pueden omitirse los nombres de uniones, estructuras y clases, dando lugar a elementos sin nombre (anónimos).

Las clases sin nombre pueden usarse para declarar que determinados elementos son instancias de una determinada clase, pero con el inconveniente de que no pueden declararse instancias adicionales en ninguna otra parte, y que los miembros de la clase no pueden contener constructores ni destructores explícitos; tampoco referencias a ella misma (auto-referencias), por ejemplo, punteros a objetos de la propia clase. Es la situación siguiente: class { ... } C1, C2, *pc, arrc[10];

§2 Ventaja de nombres explícitos La sentencia anterior declara C1, C2, *pc y arrc como elementos derivados de la que se define en el bloque. Sin embargo, no es posible definir nuevos objetos del mismo tipo más adelante; tampoco clases derivadas de ella, cosa que no ocurriría si la clase no fuese anónima. Por ejemplo: class X { ... } C1, C2, *pc, arrc[10]; ... X C4; // nueva instancia de X class Xb : X { ... }; // nueva clase derivada de X

Nota: existe una cierta tradición en C++ de utilizar mayúsculas en la inicial de nombres de clases.

§3 Usar typedefs En ocasiones, en vez de asignar un nombre a la clase, se utiliza un typedef ( 3.2.1a), lo que es exactamente análogo. Esta sustitución de nombres de clases por "typedefs" es una práctica muy frecuente en las clases y estructuras definidas en los ficheros de cabecera de los propios compiladores. Aquí existen dos formas posibles de uso: utilizando clases anónimas o nominadas (con nombre). Serían las situaciones siguientes: typedef class { ... COMPLEX* cptr; COMPLEX() { /* ... */ } ~CPMPLEX() { /* ... */ }

// Error!! // Error!! // Error!!

} COMPLEX; // typedef de clase anónima ... COMPLEX c1, *cPtr, arrc[10]; El problema es el ya señalado de que la clase anónima no puede contener constructores o destructores explícitos ni auto-referencias. Por esta razón es muy raro utilizar typdefs sobre clases anónimas. Sí en cambio sobre estructuras: typedef struct { ... COMPLEX* cptr;

// Error!!

} COMPLEX; // typedef de estructura anónima ... COMPLEX c1, *cPtr, arrc[10];

El uso de un typedef con una estructuras y clases nominadas es la solución que suele utilizarse en los ficheros de cabecera de los compiladores. Respondería al siguiente esquema: typedef class C { ... C* cptr; // Ok. C() { /* ... */ } ~C() { /* ... */ }

// Ok. // Ok.

} COMPLEX; // typedef de clase con nombre ... COMPLEC c1, *cPtr, arrc[10]; Esta definición permite que los objetos de la clase C puedan contener auto-referencias, así como constructores y destructores explícitos. Como se ha visto, en ambos casos es posible obtener instancias de la clase o estructura utilizando el typedef en sustitución del nombre. Nota: el ANSI C++ no permite estructuras anónimas que no declaren un objeto. Sin embargo, aunque los compiladores C++ de Borland y Microsoft lo permiten en algunas circunstancias ( 4.11.3a Estructuras anónimas), recomendamos vivamente seguir el Estándar

4.11.3a Estructuras anónimas Nota: características como las descritas a continuación, que no son estándar del lenguaje, sino particularidades de algunos compiladores, son desaconsejables.

§1 Presentación El ANSI C++ permite solamente estructuras anónimas que declaren un objeto (como en &3 ), pero el compilador Borland C++ permite varios tipos de estructuras anónimas que extienden el ANSI (que no declaren un objeto, como en &2

).

Estas estructuras anónimas tienen la siguiente forma: struct { lista-de-miembros };

&2 Estas estructuras anónimas deben ser anidadas, es decir, declaradas dentro de otra clase (clase, estructura o unión) por ejemplo: struct my_struct { int x; struct { // estructura anónima anidada en my_struct int i; }; inline int func1(int y); } ;

&3 La estructura externa debe estar identificada, es decir, tener un nombre (como en el ejemplo anterior) o declarar un objeto como en el ejemplo que sigue (también pueden tener ambas cosas):

struct { int x; struct { // estructura anónima anidada en my_struct int i; }; inline int func1(int y); } S;

&4 Puesto que no existe variable de instancia, la sintaxis C++ para las estructuras anónimas no pueden referenciar el puntero this ( 4.11.6). Por consiguiente, mientras que las estructuras C++ tienen generalmente funciones miembro, las anónimas no pueden tenerlas (solo pueden tener datos). Estos datos (propiedades) pueden ser accedidos directamente, dentro del ámbito en que la estructura ha sido declarada, sin utilizar la sintaxis x.y o p->y. Ejemplo #include using namespace std; struct my_struct { int x; struct{ // estructura anónima anidada int i; }; int func1(int y) {return y + i + x;} } S; int main() { // ================ S.x = 6; S.i = 4; // Observe este acceso al miembro i int y = S.func1(3); cout << "y == " << y << endl; return 0; } Salida: y == 13

4.11.4 Ámbito de nombres de clase §1 Sinopsis El ámbito de los nombres de clase es local. Comienza en el punto de declaración de la clase y termina el final del bloque en que se ha declarado. Un nombre de clase oculta a cualquier otra clase, objeto, enumerador, o función del mismo nombre en el mismo ámbito, y se exigen ciertas condiciones especiales si el mismo nombre de clase aparece más de una vez en el mismo ámbito.

§2 Si se declara una clase en un ámbito que contenga la declaración de un objeto, función, o enumerador del mismo nombre, la clase solo puede ser referenciada usando un especificador de tipo,

lo que significa que debe utilizarse uno de los especificadores: class, struct o union junto con el nombre de la clase. Por ejemplo: struct S { ... }; int S(struct S *Sptr); void func(void) { S t; utiliza

// declara una clase S de tipo struct // declara función S, homónima con la clase S // ILEGAL!! la clase S no está visible y no se // especificador de tipo // Ok: se utiliza un especificador de tipo (struct) // Ok: llamada a función S que es visible

struct S s; S(&s); }

§3 Declaración adelantada Una declaración de clase se dice adelantada o incompleta cuando no se hace la correspondiente definición como en el ejemplo: class X;

// Aún no tiene definición!

La declaración adelantada puede ser útil porque una declaración de este tipo ya permite definir ciertas referencias a la clase declarada (generalmente referencias a punteros-a-objetos de la clase), antes que la misma haya sido completamente definida. Ejemplo: struct X;

// declaración incompleta

struct B { struct X *pa }; struct X { struct B *pb }; La primera ocurrencia de X se llama incompleta porque aún no existe definición en dicho punto. Este tipo de declaración se permite en este caso, porque la declaración de B no precisa del tamaño de X. §3.1 La declaración adelantada también se suele utilizar en la declaración friend ( clases, cuando esta declaración es recíproca. Ejemplo: class B; class A { ... friend B; }; ... class B { ... friend A; };

4.11.2a1) de

// L.1: declaración anticipada de B

En todos los casos la entidad de declaración adelantada debe ser completamente definida antes de poder utilizarla para otros usos que requieran conocer el tamaño del objeto. Ejemplo: class B; class C; class A {

// declaración adelantada de B // declaración adelantada de C

... friend B; // Ok. B b1; // Error! clase B no definida aún friend C::operator* (A&, C&); // Ok!. }; ... class B {...}; class C {...};

§3.2 typedef (

3.2.1a) no puede ser utilizado con clases de declaración adelantada.

typedef struct COMPLEX;

// Ilegal!!

Para más información ver "Declaración de miembros". Ver también las declaraciones adelantadas para las clases VCL

4.11.5 Instanciado de clases: Objetos §1 Sinopsis Existen varios conceptos y fases en la existencia de un programa que no conviene confundir: la declaración de una clase; su definición; su instanciación o concreción en un objeto-clase determinado, y la inicialización del objeto (aunque los dos últimos procesos pueden ejecutarse en la misma sentencia).

§2 Declaración de clase El primero, declaración de clase, es simplemente asignarle un nombre; una sentencia que establece la conexión entre el identificador y el objeto que representa (en este caso una clase). La declaración asocia el nombre con un tipo de dato, lo que supone definir como se usa, que operaciones son permitidas y que sentido tienen estas operaciones [6]. La declaración sería algo así: class Hotel; Una declaración de este tipo, sin definición, se denomina adelantada (

4.11.4a).

§3 Definición de clase La definición de clase es el proceso de definir cuales serán sus propiedades y métodos; proceso que crea un nuevo tipo [2]. Lo mismo que con las variables normales, con frecuencia la declaración y definición de una clase ocurren simultáneamente en la misma sentencia (a menos que se trate de una declaración adelantada

).

La definición de la clase puede ser un proceso muy simple (caso de la herencia simple o múltiple). Ejemplo: class Hotel: public Pension, Residencia {};

También puede ser un proceso más elaborado: class Hotel { char nombre[30]; int room; public: int getnom(char *nom); void putnom(char *nom); char * getroom(int num); };

§4 El objeto-clase Cuando la clase está declarada y definida, termina el trabajo del programador. A partir de aquí, el compilador traslada dicha declaración a una función-clase, que es la forma en que existe la clase en el ejecutable. Más tarde, en tiempo de ejecución, la función-clase crea un objeto-clase que representa desde este instante a la clase en cuestión. Existe un solo objeto-clase de cada clase y solo él puede crear instancias de dicha clase. Como se ha señalado, este objeto-clase tiene sus propias variables y métodos (propiedades de clase y métodos de clase). Una variable (propiedad) declarada como "de clase" existe una sola vez en cada clase y es similar a una variable definida como estática de fichero en la programación clásica. Por su parte, como veremos inmediatamente , los métodos son por definición, y por lógica [4], "de clase". Un método de clase solo puede ser ejecutado por un objeto (instancia) de dicha clase. Por definición un objeto-clase tiene como mínimo cuatro métodos de clase: un constructor por defecto; un destructor; un constructor-copia y una función-operador de asignación operator=(). En caso que el programador no los haya definido de forma exsplícita, son proporcionados por el compilador. Un objeto-clase no puede ser usado directamente, podemos figurarnos que no es un objeto concreto. Del mismo modo que para usar un entero hay que declarar uno, con un nombre, especificando que pertenece a la clase de los enteros y en su caso, iniciarlo a un valor. Para utilizar un objeto-clase hay que declararlo; en estos caso más que "declarar" un objeto de la clase se dice instanciar la clase, que equivale a disponer de un objeto concreto (instancia) de la clase. Nota: por supuesto es necesario hacer una definición completa de la clase, con todos sus miembros, antes de que pueda instanciarse uno de sus objetos. Además, algunos tipos de clases, las denominadas abstractas, no sirven para instanciar objetos directamente, solo para derivar de ellas otras clases en las que se perfilarán detalles concretos, lo que les permitirá ser instanciables ( 4.11.8c).

El término instancia se refiere siempre a un objeto creado por el objeto-clase en tiempo de ejecución y por supuesto, pueden tener propiedades y métodos. En este caso, se denominan formalmente propiedades de instancia y métodos de instancia. Cada instancia (objeto) creado desde una clase tiene su propio juego de variables independientes y distintas de los demás objetos “hermanos”, pero todos pueden acceder (leer/modificar) las propiedades de clase de su ancestro, el objeto-clase. Puesto que los valores de las variables de clase del ancestro son únicos, estos aparecerán iguales para todos los descendientes, con independencia cual de sus instancias sea la que acceda a ellas (

4.11.7).

§5 Las funciones-miembro Al llegar a este punto es preciso hacer una observación de la mayor trascendencia: cuando se instancia una clase, se crea un objeto que contiene un subconjunto particular de todas las variables (no estáticas 4.11.7) de la clase a que pertenece. Pero aunque coloquialmente se dice que el objeto también "tiene" los métodos de la clase, en realidad esto no es cierto. El objeto no contiene una copia de todos sus métodos, lo que supondría una repetición innecesaria del mismo código en todos los objetos de la clase. Los métodos solo existen en el objeto-clase descrito anteriormente. En las instancias concretas solo hay una tabla de direcciones (denominada vtable) a los métodos de la clase, y el acceso a dichas funciones se realiza a través de esta tabla de punteros [1]. En el apartado dedicado al puntero this ( 4.11.6) se amplía información sobre esta importante cuestión teórica y sus implicaciones prácticas.

§6 El resumen del proceso hasta aquí descrito puede ser sintetizado como sigue: Programador

Declaración & definición de clase (en programa fuente)

Compilador

Función-clase (en fichero ejecutable)

Ejecución

Objeto-clase (en memoria)

Ejecución

Objeto-instancia (en memoria)

§7 Siguiendo con el ejemplo anterior

, podríamos señalar que la sentencia:

// §7a

Hotel playa;

declara playa como perteneciente a la clase Hotel; esta sentencia relaciona el identificador con un tipo específico de objeto (de la clase Hotel [3]), del mismo modo que la sentencia int x; relaciona el identificador x con el tipo de los enteros. Una vez tenemos un objeto, habría que inicializarlo (aquí se dice construirlo), asignándole espacio en memoria e iniciando en su caso sus variables [5]. En el caso del entero, aunque la expresión int x; no es formalmente una definición, en realidad contiene todo lo que el compilador necesita saber sobre ella para poder asignarle un espacio en memoria, aunque inicialmente este espacio pueda contener basura si no ha sido inicializado a ningún valor concreto (

4.1.2).

En el caso del objeto playa, en realidad el compilador toma la sentencia §7a como una declaración mas una definición. Siempre que se crea un objeto se realiza una llamada implícita o explícita a un constructor que se encarga de inicializar los miembros del objeto. En el caso de §7a, además de asociar el identificador playa con el tipo Hotel, el compilador incluye una invocación al constructor por defecto de la clase (

4.11.2d1), que se encarga a su vez de inicializar el nuevo objeto correctamente

(ver 4.11.2d3 para una más detallada exposición del proceso de inicialización de los miembros de los objetos). El resultado es que a partir de dicha declaración-definición del objeto, ya podemos utilizarlo directamente. Por ejemplo, en este caso, utilizando uno de sus métodos públicos getroom(): playa.getroom(37);

Por supuesto pueden definirse más objetos del mismo tipo (instancias de la clase) y objetos derivados del nuevo tipo, como punteros-a, referencias-a, matrices-de, etc.) class X { ... }; // define la clase X X x, &xr, *xptr, xarray[10]; /* instancia 4 objetos derivados de X: tipo X, referencia-a-X, puntero-a-X y matriz-de-X */ Incluso pueden crearse otros objetos de la misma clase por copia del existente por ejemplo: X y = x; Aquí el objeto y se crea por copia del objeto x ya existente. En este caso, se invoca un tipo especial de constructor, el constructor copia (

4.11.2d4).

§8 Destrucción Finalmente debemos añadir que cuando el objeto sale definitivamente de ámbito, es destruido. De esta tarea se encarga un operador especial, que tiene la forma de un método-de-clase, que se encarga limpiar los miembros del objeto y de liberar los recursos asignados inicialmente (como mínimo, espacio de memoria) antes que el propio objeto se auto-destruya (destructores

4.11.2d2).

Inicio.

[1] Esta es precisamente la razón por la que se pueda afirmar ( B. Eckel) que el tamaño que ocupan los objetos C++ en memoria es aproximadamente el mismo que ocuparían las estructuras C equivalentes. Aunque evidentemente, estas últimas no incluyen la "funcionalidad" que acompaña a aquellos. En este sentido una clase es simplemente una estructura a la que se ha añadido una v-table. [2] Como veremos más adelante, al hablar de clases abstractas y funciones virtuales, estas definiciones pueden ser dejadas muy en el aire, de forma que puedan ser concretadas más tarde. Este "más tarde" significa que la clase que se define no será utilizada (instanciada) como tal, sino a través de clases derivadas; las definiciones se concretan en estas subclases. [3] No se olvide que en este sentido, clase es sinónimo de tipo de objeto; cada clase define un tipo nuevo y específico. Este es precisamente uno de los paradigmas de la POO, los tipos de datos son infinitos, solo dependen de la imaginación y necesidad del usuario. [4] Lo contrario exigiría de existencia de una copia de cada método en cada objeto, algo a todas luces ilógico y contrario al espíritu de lo que es una función (en cambio sí existe una copia de cada propiedad en cada objeto). [5] Se prefiere el término "construcción" a "inicio" porque el primero tiene un sentido más amplio. Se refiere a conformar el ambiente adecuado para que los métodos de la clase puedan operar sobre el objeto. Esto puede referirse no solo a inicializar las propiedades, también proporcionar determinados recursos, por ejemplo espacio de memoria, un fichero, el bloqueo de un recurso compartido, o el establecimiento de una línea de comunicación. Este tópico puede verse con más detalle en el apartado correspondiente (Constructores

4.11.2d1).

[6] Recordar que en determinadas circunstancias, la declaración de tipo class, puede ser sustituida por un typename (

3.2.1e).

4.11.6 El puntero this Nota: los aspectos generales relativos a punteros a clases y a miembros de clase, son tratados en los epígrafes

4.2.1f y

4.2.1g respectivamente.

§1 Sinopsis Hemos dicho anteriormente ( 4.11.2b) que cada instancia de una clase tiene su propio juego de variables; propiedades privativas o heredadas (según el caso), y que unas y otras se direccionan del mismo modo. Sin embargo, aunque esto es cierto para las propiedades no estáticas (

4.11.7); no lo

es para los métodos. Existe una sola versión de los métodos de clase en el objeto-clase ( 4.11.5), que solo pueden ser invocados por los objetos de la clase (podríamos decir que son funciones de uso restringido). Pero entonces surge una cuestión: cuando un objeto invoca uno de estos métodos ¿Cómo sabe la función sobre que instancia de la clase debe operar?. O dicho con otras palabras: ¿Que juego de variables debe utilizar ?. Para fijar ideas consideremos el caso del ejemplo siguiente: #include using namespace std; class X { public: int x; void pow2() {cout << "El cuadrado es: " << x*x << endl; } }; void main() { X x1; x1.x = 2; X x2; x2.x = 5; x1.pow2(); x2.pow2(); }

// ========= // x1 una instancia de X // x2 otra instancia de X // M.5: invocación de func desde x1 // M.6: invocación de func desde x2

Salida: El cuadrado es: 4 El cuadrado es: 25 Observando la salida se hace evidente que en uno y otro caso, la invocación a pow2 se ha realizado correctamente (aunque no se le hayan pasado parámetros !!). El compilador sabe sobre que instancia debe operar y ha utilizado el juego de variables correspondiente. Recuerde que aunque coloquialmente se pueda decir que x2.pow2() es la "Invocación del método pow2() del objeto x2", en el fondo es incorrecto. No existe tal pow2() del objeto x2. Es más cercano a la realidad decir: "es la invocación del método pow2() de la clase X utilizando el juego de variables del objeto x2".

§2 El argumento oculto

La respuesta a como las funciones miembro operan con el conjunto de variables de los objetos para los que son invocadas, está en que C++ incluye en tales funciones (como pow2) un parámetro especial oculto denominado this (es una palabra clave C++). this es un puntero al objeto invocante [2]. Este puntero es pasado automáticamente por el compilador como argumento en todas las llamadas a funciones miembro (no estáticas). Como su inclusión es automática y transparente para el programador, es frecuente referirse a él como argumento implícito u oculto. El resultado es que cuando el compilador encuentra una invocación (con o sin argumentos) del tipo x.pow2(), calcula la dirección del objeto (&x), y realiza una invocación del tipo pow2(&x), utilizando esta dirección como valor del argumento oculto this. Por supuesto, el compilador añade por su cuenta el argumento correspondiente en la definición de la función X::pow2(). Nota: en realidad, el objeto x de una clase C tiene existencia en memoria en forma de una estructura que contiene los miembros no estáticos de la clase. La dirección del objeto es la del primer miembro de esta estructura. Las direcciones del resto de miembros se consideran desplazamientos respecto a esta dirección inicial. Esta arquitectura es la clave del funcionamiento de los punteros-a-clases (

4.2.1f) y punteros-a-miembros (

En el caso del ejemplo anterior las cosas ocurren "como si"

4.2.1g).

la definición de la clase X hubiese sido:

class X { public: int x; void pow2(X* this) { cout << "El cuadrado es: " << this->x * this->x << endl; } }; y a su vez, las invocaciones de M.5 y M.6 se sustituyen por: X* xptr1 = &x1; X* xptr2 = &x2; X::pow2(xptr1); X::pow2(xptr2);

// // // //

puntero-a-tipoX señalando a x1 puntero-a-tipoX señalando a x2 invocación de X::func referida a x1 invocación de X::func referida a x2

Tenga en cuenta que estas líneas solo tratan de ofrecer una imagen didáctica, ya que this es un puntero muy especial que no puede ser declarado explícitamente, por lo que la definición void pow2(X* this) { /* ... */ } no sería válida. Tampoco puede tomarse su dirección o ser utilizado como Rvalue para una asignación del tipo this = x (si puede en cambio ser utilizado como Lvalue). Por otra parte, las invocaciones en la forma X::pow2(xptr); tampoco son correctas. La forma sintácticamente correcta más parecida a la imagen que queremos transmitir sería la siguiente: #include using namespace std; class X { public: int x; void pow2(X* xpt) { cout << "El cuadrado es: " << xpt->x * xpt->x << endl; }

}; void main() { X x1; x1.x = 2; X x2; x2.x = 5; X* xpt1 = &x1; X* xpt2 = &x2; x1.pow2(xpt1); x2.pow2(xpt2); }

// ====================== // x1 una instancia de X // x2 otra instancia de X // puntero-a-tipoX señalando a x1 // puntero-a-tipoX señalando a x2

El problema de identificación antes señalado no existe para las funciones miembro estáticas; veremos ( 4.11.7) que no tienen puntero this, por lo que este tipo de funciones pueden ser invocadas sin referirse a ningún objeto particular. La contrapartida es que un método estático no puede acceder a miembros no estáticos sin utilizar los selectores de miembro . o -> ( indicarle explícitamente sobre que juego de variables debe operar. Las funciones friend (

4.11.2e). Es decir, hay que

4.11.2a1) que no son miembros de ninguna clase tampoco disponen de este

argumento oculto (no disponen de puntero this). Tampoco las estructuras anónimas (

4.11.3a)

§3 this es una variable local Como ocurre con todos los parámetros de funciones, resulta que this es una variable local (puntero) presente en el cuerpo de cualquier función miembro no estática. this no necesita ser declarado, y es raro que sea referenciado explícitamente en la definición de alguna función, lo que no es obstáculo para que sea utilizado dentro de la propia función para referenciar a los miembros [3]. Según lo anterior resulta evidente que, como tal variable local, esta palabra clave no puede ser usada fuera del cuerpo de un método de una clase. Nota: el conocimiento de los dos puntos mencionados (que es una variable local y que es un puntero al objeto) son la clave para manejar this con cierta soltura si las circunstancias lo requieren. Por ejemplo, si se invoca x.func(y), donde y es un miembro de X, la variable this adopta el valor &x e y adopta el valor this->y, lo que equivale a x.y.

En las funciones-miembro no-constantes ( 3.2.1c) de una clase C, el tipo de this es C* ( contra, en los métodos constantes su tipo es const C* (puntero-a-constante) [1].

4.2.1f). Por

§4 En el siguiente ejemplo se definen dos clases, idénticas salvo en la forma de referenciar a sus miembros; en una se utiliza el puntero this de forma explícita, en otra de forma implícita, ambas son equivalentes, aunque es más normal utilizar la forma implícita. #include class X { int x; public:

int getx() { return x; } void putx (int i) { x = i; } }; class Y { int x; public: int getx() { return this->x; } void putx (int i) { this->x = i; } }; void main() { // ========== X x1; x1.putx(10); cout << "Valor de x1.x: " << x1.getx() << endl; Y y1; y1.putx(20); cout << "Valor de y1.x: " << y1.getx() << endl; } Salida: Valor de x1.x: 10 Valor de y1.x: 20

§5 A continuación se expone una variación del ejemplo presentado al tratar del acceso a miembros de clases desde funciones miembro ( operador de acceso a ámbito.

4.11.2e). En esta ocasión se utiliza el puntero this en vez del

#include int x = 10; // L.3: class CL { int x, y; // L.5: privados por defecto public: void setx(int i) { x = i; } // L.7: void sety(int y) { this->y = y; } int getxy(); }; int CL::getxy() { return x + y; } int main() { // ============= CL c; c.setx(1); c.sety(2); cout << "X + Y == " << c.getxy() << endl; return 0; } Salida: X + Y == 3

Comentario En esta ocasión la presencia explícita del puntero this en el cuerpo de sety (L.8) se justifica porque la variable local y oculta al miembro del mismo nombre. Puesto que, según hemos declarado , el puntero this referencia al objeto invocante, la expresión *this representa al objeto, por lo que suele utilizarse cuando se quiere devolver el objeto invocado por la función-miembro.

§6 Considere una variación sobre el primer ejemplo del método pow2 haciendo que devuelva un objeto.

, en el que modificamos ligeramente la definición

#include using namespace std; class X { public: int x; X pow2() { x = x * x; return *this; } }; void main() { // ========= X x1, x2; // instancias de X x1.x = 2; x2.x = 5; x1.pow2(); // invocación de pow2 desde x1 x2.pow2(); // invocación de pow2 desde x2 cout << "x1 = " << x1.x << endl; cout << "x2 = " << x2.x << endl; x2 = x1.pow2(); // M.7: cout << "x2 = " << x2.x << endl; } Salida: x1 = 4 x2 = 25 x2 = 16 Comentario Comprobamos que las salidas son las mismas que en el primer ejemplo. Las operaciones se han realizado sobre las variables del objeto correspondiente, pero además, en este caso el método devuelve un objeto, por lo que puede ser utilizado como Rvalue de la asignación M.7. En consecuencia, el valor 4 del objeto x1 es transformado a 4 * 4 = 16 en la invocación del lado derecho de la asignación. Posteriormente este valor es asignado al objeto x2. Este es el resultado que se obtiene en la tercera salida.

§7 La expresión *this también puede ser utilizada para devolver una referencia (

4.2.3) al objeto

invocado por la función-miembro. El ejemplo puede modificarse para hacer que pow2 devuelva una referencia al objeto: #include using namespace std; class X { public: int x; X& pow2() { x = x * x; return *this; } }; void main() { // ============== X x1, x2; // instancias de X x1.x = 2; x2.x = 5; x1.pow2(); // invocación de func desde x1 x2.pow2(); // invocación de func desde x2 cout << "x1 = " << x1.x << endl; cout << "x2 = " << x2.x << endl; x2.pow2() = x1.pow2(); // M.7: cout << "x2 = " << x2.x << endl; } Salida: x1 = 4 x2 = 25 x2 = 16 Comentario Las salidas son las mismas que en el ejemplo anterior, aunque con una diferencia significativa: la referencia devuelta por la invocación pow2 sobre el objeto x1 (en el lado derecho de la asignación M.7), es utilizado como Rvalue de la asignación, y aplicada a la referencia devuelta por la asignación del lado izquierdo (que es ahora un Lvalue). Esta capacidad de las referencias: ser un Rvalue cuando se utilizan a la derecha de una asignación y un Lvalue, cuando se sitúan a la izquierda, es precisamente la razón por la que se introdujeron en el lenguaje, siendo una propiedad muy utilizada en la sobrecarga de las funciones-operador. En el capítulo dedicado a la sobrecarga del operador preincremento se expone en detalle esta característica ( 4.9.18c).

Otros ejemplos en:

4.9.18.e y

4.9.18.e;

Inicio. [1] Los usuarios de GNU Cpp deben tener en cuenta que este compilador el tipo de this es C* const, es decir: puntero-constante-a-tipoC ( 4.2.1e). Lo que a la postre significa que no puede recibir asignaciones en su ámbito de visibilidad (el cuerpo de los métodos no estáticos). Sin embargo, existen

muchos otros compiladores en los que esto no es así (this no es un puntero constante). Para mantener la compatibilidad, g++ dispone de la opción de compilación -fthis-is-variable, que permite asignar a this las características tradicionales. Nota: puede conseguirse el mismo resultado con la opción -traditional, aunque esta última tiene otros efectos adicionales. [2] Se puede expresar de varias formas según el punto de vista o la imagen mental que se prefiera. Por ejemplo, desde el punto de vista de la POO clásica: "puntero al objeto sobre el que se aplica el método". También: "puntero al objeto para el que se invoca el método", o: "puntero al objeto cuyas propiedades serán utilizadas en la invocación". [3] Lo de que sea "rara" su utilización es desde luego relativo. No es demasiado frecuente en programas simples de usuario, pero las aplicaciones sofisticadas, y las macros de los propios compiladores están a veces plagadas de referencias a este puntero

4.11.7 Miembros estáticos §1 Sinopsis El especificador de tipo de almacenamiento static ( 4.1.8c) puede utilizarse en la declaración de propiedades y métodos de clases. Tales miembros se denominan estáticos y tienen distintas propiedades que el resto. En cada instancia de la clase existe una copia de los miembros no-estáticos; por contra, hay una sola de los estáticos para todas las instancias. La singularidad de estas copias conduce a algunas particularidades no permitidas al resto de los miembros. Por ejemplo, pueden ser accedidas sin referencia a ninguna instancia concreta de la clase y deben ser definidas como si fuesen variables estáticas normales; además se permiten algunas singularidades en la notación de acceso a estos miembros.

§2 Propiedades estáticas El punto importante para comprender el porqué y el cómo de los miembros estáticos, es conocer que con ellos ocurre algo parecido que con las funciones-miembro ( 4.11.5). Aunque convencionalmente se acepta que los objetos contienen un sub-conjunto de "todas" las propiedades de la clase, esto solo es cierto para las variables no-estáticas. Las propiedades estáticas se comportan aquí de forma parecida a las variables estáticas de funciones normales. Solo existe una copia de ellas, que en realidad está en el objeto-clase ( 4.11.5). Se trata por tanto de propiedades de clase. A cada instancia le basta un puntero a los valores del objeto-clase y cuando desde una instancia cualquiera accedemos a una de estas propiedades, en realidad se accede a esta única copia. Lo ponemos de manifiesto con un sencillo experimento: #include class C { public: static int x; }; int C::x = 13; // L.3: definición int main () { // ================ C c1, c2; cout << "Valor c1.x == " << c1.x << endl; cout << "Valor c2.x == " << c2.x << endl;

c1.x = 22; cout << "Valor c1.x == " << c1.x << endl; cout << "Valor c2.x == " << c2.x << endl; } Salida: Valor Valor Valor Valor

c1.x c2.x c1.x c2.x

== == == ==

13 13 22 22

En el ejemplo se han instanciado dos objetos, c1 y c2. El valor inicial 13 asignado a la variable estática x en L.3 (volveremos de inmediato a esta "extraña" sentencia ), es puesto de manifiesto en las dos primeras salidas. A continuación vemos como la modificación del valor x en el objeto c1 tiene la virtualidad de cambiar dicha propiedad x en el segundo objeto. Observe que definiciones como la de L.3 del ejemplo anterior, solo son permitidas para miembros estáticos y son posibles con independencia de que sean públicos o privados es decir: class C { static char* public: static char* char* ptr3; }; char* C::ptr1 = char* C::ptr2 = char* C::ptr3 =

ptr1;

// privado por defecto

ptr2;

// declaración

"Adios"; "mundo"; "cruel";

// Ok. Definición // Ok // Error !! ptr3 no es estática

Lo anterior no significa que las propiedades estáticas, privadas o protegidas, puedan ser accedidas directamente desde el exterior es decir: ... func () { ... cout << "Valor de ptr1: " << C::ptr1 << endl; cout << "Valor de ptr2: " << C::ptr2 << endl; }

// Error: no accesible! // Ok: -> "mundo"

De todo esto se derivan algunas consecuencias teóricas y prácticas. La primera importante es que al existir en el objeto-clase, las propiedades estáticas no dependen de ninguna instancia para su existencia. Es decir, existen incluso antes que ninguna instancia de la clase. Esto ha sido ya puesto de manifiesto en la línea 3 del ejemplo anterior , donde hemos iniciado la variable estática x antes que se haya instanciado ningún objeto de la clase.

§3 Definición de miembros estáticos Puesto que las declaraciones de miembros estáticos existentes en la declaración de una clase no son definiciones (

4.1.2), y estos miembros existen antes que ninguna instancia de la clase, la

consecuencia es que debe proporcionarse una definición en algún sitio para proveer de espacio de almacenamiento e inicialización (en condiciones normales, esta tarea es encomendada a los constructores

4.11.2d1). Considere el siguiente ejemplo:

class C { static y; // int por defecto en algunos compiladores public: int x; static int* p; static char* c; static int gety () { return y; } }; Al compilar se producirán tres errores de enlazado Unresolved external..., correspondientes a los miembros C::y, C::p y C::c; señalando que las variables estáticas están declaradas pero no definidas (no tienen espacio de almacenamiento). Para evitarlo, podemos hacer: class C { static y; public: int x; static int* p; static char* c; static int gety () { return y; } }; ... int C::y = 1; // no es necesario poner static (pero si int!!) int* C::p = &C::y; // ídem int* char* C::c = "ABC"; // ídem char* ...

Las asignaciones de las tres últimas líneas proporcionan espacio de almacenamiento, a la vez que pueden servir de inicializadores de los miembros correspondientes. Observe especialmente la notación empleada. Los especificadores de tipo: int, int* y char* son necesarios. En cambio, no es preciso repetir aquí la palabra static. Nota: esta inicialización de las constantes estáticas fuera del cuerpo de la clase, es una excepción de la regla general C++ de que las propiedades de clases solo pueden inicializarse en el cuerpo del constructor o en la lista de inicializadores ( 4.11.2d3). La razón es que los miembros estáticos no son parte de los objetos de la clase sino objetos independientes [3].

§4 Iniciar miembros estáticos §4.1 Iniciar constantes estáticas Recordemos que excepcionalmente, las constantes estáticas pueden ser iniciadas en el cuerpo de la clase (

4.11.2a). Es decir, se permiten expresiones del tipo [1]:

class C { static const int k1 = 2; // Ok: static const float f1 = 2.0; // Ok: static Etiquetas objetos[MAXNUM]; ...

}; const int C::k1; cons float C::f1; Etiquetas C::cargos[MAXNUM]; Observe que en este caso aún es necesario declarar el miembro fuera de la clase, aunque no es necesaria aquí su inicialización. Nota: recordar que es posible sustituir una constante estática entera por un enumerador ( 4.11.2a).

A este respecto tenga en cuenta que los miembros estáticos de una clase global pueden ser inicializados como objetos globales ordinarios, pero solo dentro del ámbito del fichero. Es oportuno señalar que la inclusión de un constructor explícito en la declaración de la clase no hubiese evitado tener que incluir las tres últimas sentencias para definición de los miembros respectivos. Por ejemplo, el código que sigue daría los mismos errores de compilación que el anterior. class C { static y; public: int x; static int* p; static char* c; static int gety () { return y; } C () { // constructor por defecto y = 1; p = &y; c = "ABC"; }; La razón es evidente: el constructor es invocado cuando se instancia un miembro de la clase, mientras que los miembros estáticos (que en realidad "pertenecen" a la clase y no a las instancias), tienen existencia incluso antes de existir ninguna instancia concreta

.

Como consecuencia directa, si se incluye una asignación a un miembro estático dentro del constructor, al ser esta asignación posterior a la que se realiza en la definición, los valores indicados en el constructor machacarán a los que existieran en la definición. Ejemplo #include using namespace std; class A {a public: static int x; // miembro estático A(int i = 12) { x = i; } // constructor por defecto }; int A::x = 13; // definición de miembro int main() { // ============== cout << "Valor de A.x: " << A::x << endl; A a1; // Invoca al constructor.

cout << "Valor de A.x: " << A::x << endl; return 0; } Salida: Valor de A.x: 13 Valor de A.x: 12

Para verificar la sucesión de los hechos, construimos otro sencillo experimento, añadiendo algunas instrucciones al ejemplo anterior: #include class C { static y; // int por defecto public: int x; static int* p; static char* c; static int gety () { return y; } C () { // constructor por defecto y = 1; // iniciadores de "instancia" x = 3; p = &y; c = "ABC"; } }; int C::y = 20; // iniciadores de "clase" int* C::p = &C::y; char* C::c = "abc"; int main () { cout << "Valor cout << "Valor cout << "Valor /*cout << "Valor Member C::x C c1; cout << "Valor cout << "Valor cout << "Valor cout << "Valor }

// =============== .y == " << C::gety() << endl; .p == " << *(C::p) << endl; .c == " << C::c << endl; .x == " << C::x << endl; ERROR: cannot be used without an object in function main() */

Salida:

c1.y c1.p c1.c c1.x

Valor Valor Valor Valor Valor Valor Valor

.y == 20 .p == 20 .c == abc c1.y == 1 c1.p == 1 c1.c == ABC c1.x == 3

== == == ==

" " " "

<< << << <<

c1.gety() << endl; *(c1.p) << endl; c1.c << endl; c1.x << endl;

§5 Características de los miembros estáticos En este sencillo programa comprobamos un buen montón de las características especiales de los miembros estáticos: Las tres primeras salidas se producen antes de instanciar ningún objeto: Los miembros estáticos tienen existencia antes que cualquier instancia de la clase. Los valores iniciales son debidos a los iniciadores de clase. El intento de construir una cuarta salida siguiendo la pautas de las anteriores con el miembro x noestático conduce al previsible error de compilación (muy explícito por cierto).

§5.1 Particularidades de la notación. Observe las tres primeras sentencias de salida: Los miembros estáticos pueden ser accedidos mediante una declaración explícita (

4.1.11c) utilizando

el operador de resolución de ámbito :: ( 4.9.19) con la notación C::miembro, que sabemos es propia de los miembros de clases y otros subespacios de nombres. Comprobamos que las invocaciones a miembros se han realizado sin utilizar ninguna instancia concreta de C; es decir, la invocación a miembros estáticos puede realizarse sobre la clase, sin tener ningún objeto particular "en mente". Las cuatro últimas salidas se refieren a una instancia concreta: Los miembros son accedidos utilizando la notación convencional mediante el selector de miembro . ( 4.9.16). Esta notación también está permitida en miembros estáticos si se utiliza una instancia concreta. Si bien el manual nos advierte que en estos casos, si c es un objeto de la clase C, y cptr es un puntero a objeto de dicha clase, puede utilizarse la notación: c.x y cptr->x, aunque las expresiones c y cptr no sean evaluadas

.

Como era de esperar, los valores obtenidos corresponden a los proporcionados por el constructor de la clase, que es invocado automáticamente cuando se instancia un miembro. A partir de este momento, los valores quedan modificados para cualquier otra instancia ya existente de la clase. La última salida se refiere a un miembro no estático (privativo de esta instancia), que ha sido inicializado por el constructor. Mientras que un miembro estático puede ser accedido con o sin la sintaxis especial de los miembros de clase, el resto de los miembros (no estáticos) tiene que ser forzosamente referenciados mediante los operadores de acceso a miembros, directo . (4.9.16) o indirecto -> (4.9.16).

§5.2 Otras propiedades de las propiedades estáticas Las peculiaridades de las constantes estáticas enteras no se limitan a que puedan ser inicializadas en el cuerpo de la clase , también pueden ser accedidas desde el exterior con el operador de acceso a ámbito, aún cuando sean declaradas miembros privados [2]. Ejemplo: class C { const int static x = 13;

// Privado por defecto

... }; ... func () { cout << "Miembro privado C.x = " << C::x; ... }

// Ok:

Las clases declaradas locales a una función no pueden tener miembros estáticos [4]. Los miembros estáticos de clases globales tienen enlazado externo (

1.4.4) y almacenamiento estático (

4.1.8c).

Los miembros estáticos, anidados a cualquier nivel, siguen las reglas normales de acceso a miembros, excepto que pueden ser inicializados. class C { static int x; static const int size = 5; class Interior { static float f; void func(void); }; public : char array[size]; }; int C::x = 1; float C::Interior::f = 3.14; void C::Interior::func(void) {

// clase dentro de una clase // declaración anidada // declaración anidada

// inicializa x // initialización de estática anidada /* defina la función anidada */ }

Por razón de esta particularidad de ser comunes a todas las instancias de la clase, los punteros a miembros estáticos tienen características especiales no disponibles en los punteros a miembros no estáticos (

4.2.1g).

§6 Métodos estáticos A tenor de lo comentado hasta ahora y de lo señalado para las funciones-miembro ( 4.11.5), debemos admitir que no tiene mucho sentido hablar de funciones-miembro estáticas, ya que por definición los métodos son "estáticos", en el sentido que se alojan en el objeto-clase (en las instancias de clase solo existen punteros a dichas funciones). La razón última de que el Estándar admita la existencia de funciones miembro estáticas, es tanto garantizar una cierta uniformidad en la categoría, como en el hecho de que en realidad, las características especiales de estos miembros les hace constituir una especie de "club" aparte del resto de miembros no estáticos. En efecto, considere la función gety del ejemplo anterior . Teóricamente este método podría funcionar antes de la declaración de ningún objeto de la clase sin necesidad de ser estático, ya que por sí mismo cumple con los dos requisitos: existe en el objeto-clase y solo utiliza variables estáticas (que tienen existencia igualmente en el objeto-clase). La razón de que la gramática C++ exija declararla estática explícitamente, es doble: de un lado avisar al compilador que estas funciones no necesitan puntero this ( 4.11.6), ya que pueden ser invocadas sin hacer referencia a ningún objeto particular. De otro lado, implementar un cierto mecanismo de control, sin el cual surgiría una cuestión: si gety puede ser

invocada antes que exista una instancia concreta de la clase C ¿Que pasa si en el cuerpo de esta función se hace referencia a una propiedad no estática?. Por ejemplo, si en el programa del ejemplo sustituimos el método gety haciendo que devuelva una variable no estática en la siguiente forma: static int getx () { return x; } En este caso el compilador lanza un mensaje de error: "Member C::x cannot be used without an object in function C::getx()" que también es bastante explícito respecto a su causa. La solución dada para salvar el problema es que, puesto que los métodos estáticos no tienen puntero this, para acceder a las propiedades no estáticas, es necesario especificar el objeto al que correspondan dichos miembros y para esto no es posible utilizar los selectores . o -> ( continuación

4.11.2e). Ver ejemplo a

.

Nota: observe que esto no es necesario en las funciones miembro no estáticas que sí tienen dicho puntero. Cuando en el cuerpo de una de estas funciones se hace referencia a una propiedad, se sabe inmediatamente que se refiere al objeto señalado por this (el objeto que invocó la función).

Ejemplo Una redefinición simplificada del ejemplo anterior, que permitiese cumplir con los requisitos exigidos, sería la siguiente: #include class C { int x; public: static int y; // static int getx () { return x; } Error!! // illegal reference to data member 'C::x' in a static member function static int getx (C* cptr) { return cptr->x; } // Ok. C () { // constructor por defecto x = 3; y = 1; } }; int C::y = 20; // definición int main () { //cout << "Valor //cout << "Valor cout << "Valor C c1; C* cptr = &c1; cout << "Valor cout << "Valor cout << "Valor return 0; } Salida:

// =============== .x == " << C::getx(...) << endl; c1.x == " << c.getx(...) << endl; .y == " << C::y << endl;

No es posible ahora!! No es posible ahora!! // Ok.

// puntero a c1 c1 .x == " << C::getx(cptr) << endl; // Ok. c1.x == " << c1.getx(cptr) << endl; // Ok. c1.y == " << c1.y << endl; // Ok.

Valor Valor Valor Valor

.y == 20 c1.y == 1 c1.x == 3 c1.x == 3

§6.1 Los métodos de clases gobales tienen enlazado externo (

1.4.4).

§6.2 Las funciones estáticas se asocian exclusivamente con la clase en que son declaradas, por tanto no pueden ser virtuales (

4.11.8a).

§6.3 Es ilegal tener funciones estáticas y no-estáticas del mismo nombre y con los mismos argumentos.

§7 Utilidad y empleo La utilidad principal de los miembros estáticos es guardar información común a todos los objetos de una clase. Por ejemplo, el número de instancias creadas ( Ejemplo), o el último recurso utilizado de entre un conjunto compartido por todas las instancias. También se utilizan para: •

Reducir el número de nombres globales visibles. Los miembros estáticos ejercen el papel de miembros globales de la clase sin polucionar el espacio global.



Agrupar y evidenciar qué objetos estáticos pertenecen a qué clase



Permitir control de acceso a sus nombres

Inicio. [1] Esta particularidad se introdujo en la revisión del Estándar de Julio 1998, de forma que el comportamiento de algunos compiladores respecto a este punto, depende de su grado de adhesión al Estándar. Por ejemplo, el compilador GNU cpp 2.95 se adapta al Estándar; Visual C++ 6.0 de MS no permite esta inicialización de constantes estáticas en el interior de la clase y Borland C++ 5.5 solo lo permite con constantes estáticas enteras. [2] Este comportamiento anómalo es específico del compilador Borland C++ 5.5 y probablemente se trate de un error, por lo que esta característica no debería ser utilizada en ningún programa C++. [3]

Stroustrup & Ellis: ACRM §9.4

[4] La razón es que tales miembros no tienen enlazado y en consecuencia no podrían ser inicializados fuera de la declaración de la clase

4.11.8 Clases polimórficas §1 Sinopsis En la introducción a la POO ( 1.1) se señaló que uno de sus pilares básicos es el polimorfismo; la posibilidad de que un método tenga el mismo nombre y resulte en el mismo efecto básico pero esté implementado de forma distinta en clases distintas de una jerarquía. En este capítulo y siguiente describiremos los mecanismos que utiliza C++ para hacer esto posible. La explicación involucra varios

conceptos estrechamente relacionados entre si: el de función virtual ( dinámico (

4.11.8a), el enlazado

1.4.4) y el de clase polimórfica que analizamos en este capítulo.

§2 Clases polimórifcas Como se verá muy claramente en el ejemplo que sigue, las técnicas de POO necesitan a veces de clases con una interfaz muy genérica que deben servir a requerimientos variados (de ahí el nombre "polimórficas"), por lo que no es posible establecer "a priori" el comportamiento de algunos de sus métodos, que debe ser definido más tarde en las clases derivadas. Dicho en otras palabras: en ciertas súper-clases, denominadas polimórficas, solo es posible establecer el prototipo para algunas de sus funciones-miembro, las definiciones deben ser concretadas en las subclases, y generalmente son distintas para cada descendiente [1]. Estas funciones-miembro son denominadas virtuales y C++ establece para ellas un mecanismo de enlazado especial (dinámico) que posibilita que las diversas subclases de la familia ofrezcan diferentes comportamientos (diferencias que dan lugar precisamente al comportamiento polimórfico de la súper clase). Para que una clase sea polimórfica debe declarar o heredar al menos una función virtual o virtual pura (aclararemos este concepto más adelante

4.11.8a). En este último caso la clase polimórfica es

además abstracta ( 4.11.8c). Así pues, clases polimórficas son aquellas que tienen una interfaz idéntica, pero son implementadas para servir propósitos distintos en circunstancias distintas, lo que se consigue incluyendo en ellas unos métodos especiales (virtuales) que se definen de forma diferente en cada subclase. Así pues, el polimorfismo presupone la existencia de una súper-clase polimórfica de la que deriva más de una subclase.

§2.1 Generalmente las clases polimórficas no se utilizan para instanciar objetos directamente [2], sino para derivar otras clases en las que se detallan los distintos comportamientos de las funciones virtuales. De esta segunda generación si se instancian ya objetos concretos. De esta forma, las clases polimórficas operan como interfaz para los objetos de las clases derivadas. Es clásico el caso de la clase Poligono, para manejar Cuadrados, Círculos, Triángulos, etc. class Poligono { Punto centro; Color col; ... public: point getCentro() { return centro; } void putCentro(Punto p) { centro = p; } void putColor(Color c) { col = c; } void mover(Punto a) { borrar(); centro = a; dibujar(); } virtual void borrar(); virtual void dibujar(); virtual void rotar(int); ... };

Esta clase es polimórfica; Punto y Color son a su vez sendas clases auxiliares. Los métodos borrar, dibujar y rotar (que se declaran virtuales) tienen que ser definidos más tarde en las clases derivadas, cuando se sepa el tipo exacto de figura, ya que no se requiere el mismo procedimiento para rotar un triángulo que un círculo. De esta clase no se derivan objetos todavía.

Para definir una figura determinada, por ejemplo un círculo, lo primero es derivar una clase concreta de la clase genérica de las figuras (en el ejemplo la clase Circulo). En esta subclase, que en adelante representará a todos los círculos, debemos definir el comportamiento específico de los métodos que fueron declarados "virtuales" en la clase base. class Circulo : public Poligono { // la clase circulo deriva de Poligono int radio; // propiedad nueva (no existe en la superclase) public: void borrar() { /* procesos para borrar un círculo */ } void dibujar() { /* procesos para dibujar un círculo */ } void rotar(int) {} // en este caso una función nula! void putRadio(int r) { radio = r; } };

El último paso será instanciar un objeto concreto (un círculo determinado). Por ejemplo: Circulo circ1; Punto centr1 = {1, 5}; circ1.putCentro(centr1); circ1.putColor( rojo ); circ1.putRadio( 25 ); circ1.dibujar();

§2.2 Veremos que, además de la herencia y las funciones virtuales, hay otro concepto íntimamente ligado con la cuestión: las funciones dinámicas ( 4.11.8b), aunque esta es una particularidad de C++Builder y como tal no soportado por el Estándar. Inicio. [1] Los únicos tipos que soportan polimorfismo son class y struct. [2] Decimos "generalmente" porque cuando se tiene la certeza de que no se instanciarán objetos de la superclase, se recurre a declararla abstracta

4.11.8a Funciones virtuales §1 Sinopsis: Las funciones virtuales permiten que clases derivadas de una misma base (clases hermanas) puedan tener diferentes versiones de un método. Se utiliza la palabra-clave virtual para avisar al compilador que un método será polimórfico y que en las clases derivadas existen distintas definiciones del mismo. En respuesta, el "Linker" utiliza para ella una técnica especial, enlazado retrasado (

1.4.4). La

declaración de virtual en un método de una clase, implica que esta es polimórfica ( 4.11.8), y que probablemente no se utilizará directamente para instanciar objetos, sino como super-clase de una jerarquía. Observe que esta posibilidad, que un mismo método puede exhibir distintos comportamientos en los descendientes de una base común, es precisamente lo que posibilita y define el polimorfismo. En estos casos se dice que las descendientes de la función virtual solapan o sobrecontrolan ("Override") la versión de la superclase, pero esta versión de la superclase puede no existir en absoluto

. Es probable

que en ella solo exista una declaración del tipo: vitual void foo(); sin que exista una definición de la misma. Para que pueda existir la declaración de un método sin que exista la definición correspondiente, el método debe ser virtual puro

(un tipo particular dentro de los virtuales).

Utilizaremos la siguiente terminología: "función sobrecontrolada" o "solapada", para referirnos a la versión en la superclase y "función sobrecontroladora" o "que solapa" para referirnos a la nueva versión en la clase derivada. Cualquier referencia al sobrecontrol o solapamiento ("Overriding") indica que se está utilizando el mecanismo C++ de las funciones virtuales. Parecido pero no idéntico al de sobrecarga; conviene no confundir ambos conceptos. Más adelante intentamos aclarar sus diferencias .

§2 Sintaxis Para declarar que un método de una clase base es virtual, su prototipo se declara como siempre, pero anteponiendo la palabra-clave virtual [3], que indica al compilador algo así como: "Será definido más tarde en una clase derivada". Ejemplo: virtual void dibujar(); Cuando se aplica a métodos de clase [6], el especificador virtual debe ser utilizado en la declaración, pero no en la definición si esta se realiza offline (fuera del cuerpo de la clase). Ejemplo: class CL { ... virtual void func1(); virtual void func2(); virtual void func3() { ... } }; virtual void CL::func1() { ... } void CL::func2() { ... }

// Ok. definición inline // Error!! // Ok. definición offline

§3 Descripción Al tratar del enlazado se indicaron las causas que justifican la existencia de este tipo de funciones en los lenguajes orientados a objeto (que aconsejamos repasar ahora E1.4.4). Para ayudarnos a comprender el problema que se pretende resolver con este tipo de funciones, exponemos una continuación del ejemplo de la clase Poligono al que nos referimos al tratar de las clases polimórficas:

class Color { public: int R, G, B; }; class Punto { public: float x, y; };

// clase auxiliar // clase auxiliar

class Poligono { // clase-base (polimórfica) protected: // interfaz para las clases derivadas Punto centro; Color col; ... public: // interfaz para los usuarios de poligonos virtual void dibujar() const; virtual void rotar(int grados); ... };

class Circulo : public Poligono { protected: int radio; ... public: void dibujar() const; void rotar(int) { } ... };

// Un tipo de polígono

class Triangulo : public Poligono { protected: Punto a, b, c; ... public: void dibujar() const; void rotar(int); ... };

// Otro tipo de pológono

Lo que se pretende con este tipo de jerarquía es que los usuarios de subclases manejen los distintos polígonos a través de su interfaz pública (sus miembros públicos), mientras que los implementadores (creadores de los diversos tipos de polígonos que puedan existir en la aplicación), compartan los aspectos representados por los miembros protegidos. Los miembros protegidos son utilizados también para aquellos aspectos que son dependientes de la implementación. Por ejemplo, aunque la propiedad color será compartida por todas las clases de polígonos, posiblemente su definición dependa de aspectos concretos de la implementación, es decir, del concepto de "color" del sistema operativo, que probablemente estará en definiciones en ficheros de cabecera. Los miembros en la superclase (polimórfica) representan las partes que son comunes a todos los miembros (a todos los polígonos) pero en la mayoría de los casos no es sencillo decidir cuales serán estos miembros (propiedades o métodos) compartidos por todas las clases derivadas. Por ejemplo, aunque se puede definir un centro para cualquier polígono, mantener su valor puede ser una molestia innecesaria en la mayoría de los polígonos y sobre todo en los triángulos, mientras es imprescindible en los círculos y muy útil en el resto de polígonos equiláteros.

§3.1 Ejemplo-1 #include using namespace std; class B { // L.4: Clase-base public: virtual int fun(int x) {return x * x;} }; class D1 : public B { // Clase derivada-1 public: int fun (int x) {return x + 10;} }; class D2 : public B { // Clase derivada-2 public: int fun (int x) {return x + 15;} };

// L.5 virtual

// L.8 virtual

// L.11 virtual

int main(void) { B b; D1 d1; D2 d2; cout << "El valor es: cout << "El valor es: cout << "El valor es: cout << "El valor es: cout << "El valor es: return 0; }

" " " " "

// // << << << << <<

========= M.1 b.fun(10) d1.fun(10) d2.fun(10) d1.B::fun(10) d2.B::fun(10)

<< << << << <<

endl; endl; endl; endl; endl;

// // // // //

M.2: M.3: M.4: M.5: M.6:

Salida: El El El El El

valor valor valor valor valor

es: es: es: es: es:

100 20 25 100 100

Comentario Definimos una clase-base B, en la que definimos una función virtual fun; a continuación derivamos de ella dos subclases D1 y D2, en las que definimos sendas versiones de fun que solapan la versión existente en la clase-base. Observe que según las reglas §4.1 y §4.2 indicadas más adelante , las versiones de fun en L.8 y L.11 son virtuales, a pesar de no tener el declarador virtual indicado explícitamente. Estas versiones son un caso de polimorfismo, y solapan a la versión definida en la clase-base. La línea M.1 instancia tres objetos de las clases anteriormente definidas. En las líneas M.2 a M.4 comprobamos sus valores, que son los esperados (en cada caso se ha utilizado la versión de fun adecuada al objeto correspondiente). Observe que la elección de la función adecuada no se realiza por el análisis de los argumentos pasados a la función como en el caso de la sobrecarga (aquí los argumentos son iguales en todos los casos), sino por la "naturaleza" del objeto que invoca la función; esta es precisamente la característica distintiva del del polimorfismo. Las líneas M.5 y M.6 sirven para recordarnos que a pesar del solapamiento, la versión de fun de la superclase sigue existiendo, y es accesible en los objetos de las clases derivadas utilizando el sobrecontrol adecuado (

4.11.2b).

Nota: la utilización del operador :: de acceso a ámbito ( 4.9.19) anula el mecanismo de funciones virtuales, pero es aconsejable no usarlo en demasía, pues conduce a programas más difíciles de mantener. Como regla general, este tipo de calificación solo debe utilizarse para acceder a miembros del subobjeto heredado (

4.11.2b) como es el caso del ejemplo.

§3.2 Ejemplo-2 Hagamos ahora de abogados del diablo compilando el ejemplo anterior con la única modificación de que el método fun de B no sea virtual, sino un método normal. Es decir, la sentencia L.5, quedaría como: public: int fun(int x) {return x * x;}

// L.5b no virtual!!

En este caso, comprobamos que el resultado coincide exactamente con el anterior, lo que nos induciría a preguntar ¿Para qué diablos sirven entonces las funciones virtuales?.

§3.3 Ejemplo-3 La explicación podemos encontrala fácilmente mediante una pequeña modificación en el programa: en vez de acceder directamente a los miembros de los objetos utilizando el selector directo de miembro . ( 4.9.16) en las sentencias de salida, realizaremos el acceso mediante punteros a las clases correspondientes. De estos punteros declararemos dos tipos: a la superclase y a las clases derivadas (por brevedad hemos suprimido las sentencias de comprobación M.5 y M.6). El nuevo diseño sería el siguiente: #include using namespace std; class B { public: virtual }; class D1 : public public: int fun }; class D2 : public public: int fun };

// L.4: Clase-base int fun(int x) {return x * x;}

// L.5 virtual

B { // Clase derivada-1 (int x) {return x + 10;}

// L.8 virtual

B { // Clase derivada-2 (int x) {return x + 15;}

// L.11 virtual

int main(void) { // ========= B b; D1 d1; D2 d2; // M.1 B* bp = &b; B* bp1 = &d1; B* bp2 = &d2; D1* d1p = &d1; D2* d2p = &d2; cout << "El valor es: " << bp->fun(10) << endl; cout << "El valor es: " << bp1->fun(10) << endl; cout << "El valor es: " << bp2->fun(10) << endl; cout << "El valor es: " << d1p->fun(10) << endl; cout << "El valor es: " << d2p->fun(10) << endl; return 0; }

// // // // // // //

M.1a M.1b M.2a: M.3a: M.3b: M.4a: M.4b:

Salida: El El El El El

valor valor valor valor valor

es: es: es: es: es:

100 20 25 20 25

Comentario La sentencia M.1a define tres punteros a la clase-base. No obstante, dos de ellos se utilizan para señalar objetos de las sub-clases. Esto es típico de los punteros en jerarquías de clases ( 4.11.2b1). Precisamente se introdujo esta "relajación" en el control de tipos, para facilitar ciertas funcionalidades de las clases polimórficas.

La sentencia M.1b define sendos punteros a subclase, que en este caso si son aplicados a entidades de su mismo tipo. Teniendo en cuenta las modificaciones efectuadas, y como no podía ser menos, las nuevas salidas son exactamente análogas a las del ejemplo inicial.

§3.3 Ejemplo-4 Si suprimimos la declaración de virtual para la sentencia L.5 y volvemos a compilar el programa, se obtienen los resultados siguientes: El El El El El

valor valor valor valor valor

es: es: es: es: es:

100 100 100 20 25

Observamos como, en ausencia de enlazado retrasado, las sentencias M.3a y M.3b, que acceden a métodos de objetos a través de punteros a la superclase, se refieren a los métodos heredados (que se definieron en la superclase). En este contexto pueden considerarse equivalentes los siguientes pares de expresiones: bp1->fun(10) bp2->fun(10)

== ==

d1.B::fun(10) d2.B::fun(10)

Hay ocasiones es que este comportamiento no interesa. Precisamente en las clases abstractas, en las que la definición de B::fun() no existe en absoluto, y expresiones como M.3a y M.3b conducirían a error si fun() no fuese declarada virtual pura en B.

§3.4 Ejemplo-5 Presentamos una variación muy interesante del primer ejemplo , en el que simplemente hemos eliminado la línea 11, de forma que no existe definición específica de fun en la subclase D2. #include using namespace std; class B { public: }; class D1 : public: }; class D2 :

// L.4: Clase-base virtual int fun(int x) {return x * x;}

// L.5 virtual

public B { // Clase derivada int fun (int x) {return x + 10;}

// L.8 virtual

public B { };

int main(void) { B b; D1 d1; D2 d2; cout << "El valor es: cout << "El valor es: cout << "El valor es: cout << "El valor es:

// Clase derivada

" " " "

// // << << << <<

========= M.1 b.fun(10) d1.fun(10) d2.fun(10) d1.B::fun(10)

<< << << <<

endl; endl; endl; endl;

// // // //

M.2: M.3: M.4: M.5:

cout << "El valor es: " << d2.B::fun(10) << endl; return 0;

// M.6:

} Salida: El El El El El

valor valor valor valor valor

es: es: es: es: es:

100 20 100 100 100

Comentario El objeto d2 no dispone ahora de una definición específica de la función virtual fun, por lo que cualquier invocación a la misma supone utilizar la versión heredada de la superclase. En este caso las invocaciones en M.4 y M.6 utilizan la misma (y única) versión de dicha función.

§3.5 No confundir el mecanismo de las funciones virtuales con el de sobrecarga y ocultación. Sea el caso siguiente: class Base { public: void fun (int); // ... }; class Derivada : public public: void fun (int); // void fun (char); // ... };

No virtual!!

Base { Oculta a Base::fun versión sobrecargada de la anterior

Aquí pueden declararse las funciones void Base::fun(int) y void Derivada::fun(int); incluso sin ser virtuales. En este caso, se dice que void Derivada::fun(int) oculta cualquier otra versión de fun(int) que exista en cualquiera de sus ancestros ( 4.11.2b). Además, si la clase Derivada define otras versiones de fun(), es decir, existen versiones de Derivada::fun() con diferentes definiciones, entonces se dice de estas últimas son versiones sobrecargadas de Derivada::fun(). Por supuesto, estas versiones sobrecargadas deberán seguir las reglas correspondientes (

4.4.1a).

§3.6 Ejemplo-6 Para ilustrar el mecanismo de ocultación en un ejemplo ejecutable, utilizaremos una pequeña variación del ejemplo anterior (Ejemplo-3

):

#include using namespace std; class B { // L.4: Clase-base public: virtual int fun(int x) {return x * x;}

// L.5 virtual

}; class D1 : public public: int fun }; class D2 : public public: int fun };

B { // Clase derivada-1 (int x, int y) {return x + 10;}

// L.8 NO virtual

B { // Clase derivada-2 (int x) {return x + 15;}

// L.11 virtual

int main(void) { // ========= B b; D1 d1; D2 d2; // M.1 B* bp = &b; B* bp1 = &d1; B* bp2 = &d2; D1* d1p = &d1; D2* d2p = &d2; // M.1b cout << "El valor es: " << bp->fun(10) << endl; cout << "El valor es: " << bp1->fun(10) << endl; cout << "El valor es: " << bp2->fun(10) << endl; cout << "El valor es: " << d1p->fun(10) << endl; cout << "El valor es: " << d2p->fun(10) << endl; return 0; }

// M.1a // // // // //

M.2a: M.3a: M.3b: M.4a: M.4b:

En este caso nos hemos limitado a añadir un segundo argumento a la definición de D1::fun de la clase derivada-1 (L8). Como consecuencia, la nueva función no es virtual, ya que no cumple con las condiciones exigidas

. El resultado es que en las instancias de D1, la definición B::fun de la

superclase queda ocultada por la nueva definición (más detalles del macanismo utilizado en Namelookup), con la consecuencia de que la sentencia M.4a, que antes de la modificación funcionaba correctamente, produce ahora un error de compilación porque los argumentos actuales (un entero) no concuerdan con los argumentos formales esperados por la función (dos enteros). Además el compilador nos advierte de la ocultación mediante una advertecia; en Borland C++: 'D1::fun(int,int)' hides virtual function 'B::fun(int)' [5]. Nota: al lector puede parecerle (no sin razón) que las diferencias enumeradas son meramente sintácticas, y en realidad, salvo que se utilicen punteros en jerarquías de clases polimórficas y normales, las diferencias son indetectables. En la página adjunta intentamos aclarar un poco más estas diferencias (

Polimorfismo versus Sobrecarga).

&4 Cuando se declare una función como virtual tenga en mente que: •

Solo pueden ser métodos (funciones-miembro).



No pueden ser declaradas friend de otras clases (



No pueden ser miembros estáticos (



Los constructores no pueden ser declarados virtuales (



Los destructores sí pueden ser virtuales (

4.11.2a1).

4.11.7) 4.11.2d1)

4.11.2d2).

§4.1 Como hemos comprobado en el Ejemplo-5 , las funciones virtuales no necesitan ser redefinidas en todas las clases derivadas. Puede existir una definición en la clase base y todas, o algunas de las subclases, pueden llamar a la función-base.

§4.2 Para redefinir una función virtual en una clase derivada, las declaraciones en la clase base y en la derivada deben coincidir en cuanto a número y tipo de los parámetros. Excepcionalmente pueden diferir en el tipo devuelto; este caso es discutido más adelante

.

§4.3 Una función virtual redefinida, que solapa la función de la superclase, sigue siendo virtual y no necesita el especificador virtual en su declaración en la subclase (caso de las declaraciones de L.8 y L.11 en el ejemplo anterior ). La propiedad virtual es heredada automáticamente por las funciones de la subclase. Aunque si la subclase va a ser derivada de nuevo, entonces sí es necesario el especificador. Ejemplo: class B { // Superclase public: virtual int func(); }; ... class D1 : public B { // Derivada public: int fun (); // virtual por defecto }; class D2 : public B { // Derivada public: virtual int fun (); // virtual explícita }; class D1a : public D1 { // Derivada public: int fun (); // No virtual!! }; class D2a : public D2 { // Derivada public: int fun (); // Ok Virtual!! int fun (char); // No virtual!! }; Como puede verse, de la simple inspección de las dos últimas líneas, no es posible deducir que el método fun de la clase D2a es un método virtual. Esto representa en ocasiones un problema y puede ser motivo de confusión, ya que es muy frecuente que las definiciones de las superclases se encuentren en ficheros de cabecera, y el programador que utiliza tales superclases para derivar versiones específicas debe consultar dichos ficheros. Sobre todo, porque como se indicó en el epígrafe anterior, la declaración en la subclase debe coincidir en cuanto a número y tipo de los parámetros. En caso contrario se trataría de una nueva definición, con lo que estamos ante un caso de sobrecarga y se ignora el mecanismo de enlazado retrasado [8].

§5 Invocación de funciones virtuales Lo que realmente caracteriza a las funciones virtuales es la forma muy especial que utiliza el compilador para invocarlas; forma que es posible gracias a su tipo de enlazado (dinámico 1.4.4). Por lo demás, el mecanismo externo (la sintaxis utilizada) es exactamente igual que la del resto de funciones miembro. He aquí un resumen de esta sintaxis:

class CB { // Clase-base public: void fun1(){...} // definición-10 void virtual fun2(){...} // definición-20 ... }; class D1 : public CB { // Derivada-1 public: void fun1() {...} // definición-11 void fun2() {...} // definición-12 }; class D2 : public CB { // Derivada-2 public: void fun1() {...} // definición-21 void fun2() {...} // definición-22 }; ... CB obj; D1 obj1; D2 obj2; // se instancian objetos de las clases ... obj.fun1(); // invoca definición-10 obj.fun2(); // invoca definición-20 obj1.fun1(); // invoca definición-11 obj1.fun2(); // invoca definición-12 obj1.CB::fun1(); // invoca definición-10 obj1.CB::fun2(); // invoca definición-20 obj2.fun1(); // invoca definición-21 obj2.fun2(); // invoca definición-22 obj2.CB::fun1(); // invoca definición-10 obj2.CB::fun2(); // invoca definición-20 Observe que la dierencia entre las invocaciones obj.fun1() y obj1.CB::fun1(), estriba en que la misma función se ejecuta sobre distinto juego de variables. Por contra, en obj.fun1() y obj.fun2(), funciones distintas se ejecutan sobre el mismo juego de variables. CB* ptr = &obj; D1* ptr1= &obj1; D2* ptr2= &obj2; ... ptr->fun1(); ptr1->fun1(); ptr2->fun1();

// se definen punteros a los objetos

// invoca definición-10 // invoca definición-11 // invoca definición-21

Cuando desde un objeto se invoca un método (virtual o no) utilizando el nombre del objeto mediante los operadores de acceso a miembros, directo . ( 4.9.16) o indirecto -> ( 4.9.16), se invoca el código de la función correspondiente a la clase de la que se instancia el objeto. Es decir, el código invocado solo depende del tipo de objeto (y de la sintaxis de la invocación). Esta información puede conocerse en tiempo de compilación, en cuyo caso se utilizaría enlazado estático. En otros casos este dato solo es conocido en tiempo de ejecución, por lo que debería emplearse enlazado dinámico. Recuerde que cualquiera que sea el mecanismo para referenciar al objeto que invoca a la función (operador de acceso directo -1- o indirecto -2-): obj.fun1() ptr->fun1()

// -1// -2-

la función conoce cual es el objeto que la invoca -que juego de variables debe utilizar- a través del argumento implícito this (

4.11.6).

§5.1 Invocación en jerarquías de clases Sea B es una clase base, y otra D derivada públicamente de B. Cada una contiene una función virtual vf, entonces si vf es invocada por un objeto de D, la llamada que se realiza es D::vf(), incluso cuando el acceso se realiza vía un puntero o referencia a la superclase B. Ejemplo: #include using namespace std; class C { // Clase-base public: virtual int get() {return 10;} }; class D : public C { // Derivada public: virtual int get() {return 100;} }; int main(void) { // ========= D d; C* cptr = &d; C& cref = d; cout << d.get() << endl; cout << cptr->get() << endl; cout << cref.get() << endl; return 0; } Salida: 100 100 100

§5.1a Resulta muy oportuno aquí volver al caso mostrado al tratar el uso de punteros en jerarquías de clases ( 4.11.2b1), donde nos encontrábamos con una "sorpresa" al acceder al método de un objeto a través de un puntero a su superclase (repase el ejemplo original para situarse en la problemática que allí se describe). Como ya anunciábamos en el referido ejemplo, basta una pequeña modificación en la definición de la función f en la superclase B para evitar el problema. En este caso basta la definición de dicha función como virtual. Observe la diferencia de salidas en ambos casos y como el nuevo diseño es en lo demás exactamente igual al original. #include class B { // Superclase (raíz) public: virtual int f(int i) { cout << "Funcion-Superclase "; return i; } }; class D : public B { // Subclase (derivada) public: int f(int i) { cout << "Funcion-Subclase "; return i+1; }

}; int main() { D d; D* dptr = &d; B* bptr = dptr; cout cout cout cout cout cout

<< << << << << <<

// // // //

========== instancia de subclase puntero-a-subclase señalando objeto puntero-a-superclase señalando objeto de subclase

"d.f(1) "; d.f(1) << endl; "dptr->f(1) "; dptr->f(1) << endl; "bptr->f(1) "; bptr->f(1) << endl;

// acceso mediante puntero a subclase // acceso mediante puntero a superclase

} Salida: d.f(1) Funcion-Subclase 2 dptr->f(1) Funcion-Subclase 2 bptr->f(1) Funcion-Subclase 2

§6 Tabla de funciones virtuales Las funciones virtuales pagan un tributo por su versatilidad. Cada objeto de la clase derivada tiene que incluir un puntero (vfptr) a una tabla de direcciones de funciones virtuales, conocida como vtable [2]. La utilización de dicho puntero permite seleccionar desde el objeto, la función correspondiente en tiempo de ejecución. Como resultado, el mecanismo de invocación de estas funciones (enlazado dinámico es mucho menos eficiente que el de los métodos normales (enlazado estático), por lo que debe reservarse su utilización a los casos estrictamente necesarios.

1.4.4)

Es fácil poner en evidencia la existencia del puntero vfptr mediante un sencillo experimento [7] que utiliza el operador sizeof (

4.9.13):

struct S1 { int n; double get() { return n; } // método auxiliar normal }; struct S2 { int n; virtual double get() { return n; } // método virtual }; ... size_t tamS1 = sizeof(S1); size_t tamS2 = sizeof(S2);

// -> 4 // -> 8

Comentario El resultado del tamaño de ambos tipos es respectivamente 4 y 8 en cualquiera de los compiladores comprobados: Borland C++ 5.5 y gcc-g++ 3.4.2-20040916-1 para Windows. La diferencia de 4 Bytes obtenida corresponde precisamente a la presencia del mencionado puntero oculto.

Nota: Tenga en cuenta que, en ambos casos, el tamaño resultante puede venir enmascarado por fenómenos de alineamiento interno. De forma que los tamaños respectivos y sus diferencias, generalmente no coincidirán con los valores teóricos que cabría esperar.

§7 Función virtual pura En ocasiones se lleva al extremo el concepto "virtual" en la declaración de una súper clase ("esta función será redefinida más tarde en las clases derivadas"), por lo que en ella solo existe una declaración de la función, relegándose las distintas definiciones a las clases derivadas. Entonces se dice que esta función es virtual pura. Esta circunstancia hay que advertirla al compilador; es una forma de decirle que la falta de definición no es un olvido por nuestra parte (de lo contrario el compilador nos señala que se nos ha olvidado la definición); esto se hace igualando a cero la declaración de la función [1]: virtual int funct1(void); virtual int funct2(void) = 0;

// Declara función virtual // Declara función virtual pura

Como hemos señalado, la existencia de una función virtual basta para que la clase que estamos definiendo sea polimórfica (

4.11.8). Si además igualamos la función a cero, la estaremos declarando

como función virtual pura, lo que automáticamente declara la clase como abstracta (

4.11.8c

Es muy frecuente que las funciones virtuales puras se declaren además con el calificador const ( 3.2.1c), de forma que es usual encontrar expresiones del tipo: virtual int funct2(void) const = 0; constante

// Declara función virtual pura y

§7.1 Ejemplo El ejemplo que sigue es una modificación del anterior , en el que declaramos la función fun de la clase-base B como virtual pura, con lo que podemos omitir su definición. #include using namespace std; class B { // L.4: Clase-base public: virtual int fun(int x) = 0; }; class D1 : public B { // Clase derivada public: int fun (int x) {return x + 10;} }; class D2 : public B { // Clase derivada public: int fun (int x) {return x + 15;} }; int main(void) { // ========= D1 d1; D2 d2; // M.1 cout << "El valor es: " << d1.fun(10) cout << "El valor es: " << d2.fun(10) return 0; }

// L.5 virtual pura

// L.8 virtual

// L.11 virtual

<< endl; << endl;

// M.3: // M.4:

Salida: El valor es: 20 El valor es: 25 Comentario El resto del programa se mantiene prácticamente igual al modelo anterior, con la salvedad de que ahora en M1 no podemos instanciar un objeto directamente de la superclase B; la razón es que la superclase está incompleta (falta la definición de fun). Si lo intentáramos, el compilador nos mostraría un error señalando que no se puede instanciar un objeto de la clase B y que dicha clase es abstracta; además las clases abstractas no son instanciables por definición. En lo que respecta a las salidas, comprobamos que son los valores esperados.

§7.2 Una declaración de función no puede ser al mismo tiempo una definición y una declaración de virtual pura. Ejemplo: struct Est { virtual void f() { /* ... */ } = 0; };

// Error!!

La forma legal de proporcionar una definición es: struct Est { virtual void f(void) = 0; }; virtual void Est::f(void) { /* código de la función f };

// declara f virtual pura // definición posterior de f */

§7.3 Un mundo de excepciones Seguramente el lector que haya llegado hasta aquí experimente una cierta perplejidad (que comparto). En el párrafo §7 decimos que se utiliza el recurso de declarar una función virtual pura para poder omitir la definición, y a continuación, en el párrafo §7.2 exponemos la forma legal de proporcionarla... El lector puede comprobar que esta aparente contradicción es asumida por el compilador sin protestas. En efecto, considere el ejemplo siguiente en el que modificamos el anterior función virtual pura fun.

añadiendo una definición a la

#include using namespace std; class B { // L.4: Clase-base public: virtual int fun(int x) = 0; }; int B::fun(int x) {return x * x} class D1 : public B { // Clase derivada public: int fun (int x) {return x + 10;} };

// L.5 virtual pura // L.7 definición de fun

// L.9 virtual

class D2 : public B { // Clase derivada public: int fun (int x) {return x + 15;} }; int main(void) { D1 d1; D2 d2; cout << "El cout << "El cout << "El cout << "El return 0;

valor valor valor valor

// L.12 virtual

// ========= // M.1 es: es: es: es:

" " " "

<< << << <<

d1.fun(10) d2.fun(10) d1.B::fun(10) d2.B::fun(10)

<< << << <<

endl; endl; endl; endl;

// // // //

M.3: M.4: M.5: M.6:

} Salida: El El El El

valor valor valor valor

es: es: es: es:

20 25 100 100

Comentario En M1 seguimos sin poder instanciar un objeto de la superclase B (porque es abstracta). En cambio, ahora podemos insertar las líneas M5 y M6 (que ya utilizamos en la primera versión del ejemplo ). La razón es que ahora invocamos las versiones de fun (que sí está definida) heredadas de la superclase en los objetos d1 y d2. Ver clases abstractas ( puras.

4.11.8c) para una discusión más amplia de las funciones virtuales

§8 Tipos devueltos por las funciones virtuales Generalmente, cuando se redefine una función virtual no se puede cambiarse el tipo de valor devuelto. Para redefinir una función virtual en alguna clase derivada, la nueva función debe coincidir exactamente en número y tipo con los parámetros de la declaración inicial (la "firma" -Signature- de ambas funciones deben ser iguales). Si no coinciden en esto, el compilador C++ considera que se trata de funciones diferentes (un caso de sobrecarga) y se ignora el mecanismo de funciones virtuales. Nota: para prevenir que puedan producirse errores inadvertidos, el compilador C++ GNU dispone de la opción -Woverloaded-virtual, que produce un mensaje de aviso, si se redefine un método declarado previamente virtual en una clase antecesora, y no se cumple la condición de igualdad de firmas. No obstante lo anterior, hay casos en que las funciones virtuales redefinidas en clases derivadas devuelven un tipo diferente del de la función virtual de la clase base. Esto es posible solo cuando se dan simultáneamente las dos condiciones siguientes [4]: •

La función virtual sobrecontrolada devuelve un puntero o referencia a clase base [1].



La nueva versión devuelve un puntero o referencia a la clase derivada [2].

§8.1 Ejemplo struct X {}; struct Y : X {}; struct B { virtual void vf1(); virtual void vf2(); virtual void vf3(); void f(); virtual X* pf(); }; class D : public B { public: virtual void vf1(); void vf2(int); // char vf3(); void f(); Y* pf();

// // // //

clase base. clase derivada (:public X por defecto). clase base. L.4:

/* L.8: [1] devuelve puntero a clase base, esta función virtual puede ser sobrecontrolada */ // clase derivada // L.12: Especificador virtual, legal pero redundante /* L.13: No virtual, oculta B::vf2() dado que usa otros argumentos */ // L.14: Ilegal! cambia solo el tipo devuelto! // L.15: privativa de D (no virtual) /* L.16: [2] función sobrecontrolante; difiere solo en el tipo devuelto. Devuelve puntero a subclase */

}; void extf() { D d; B* bp = &d;

// d objeto de la clase D (instancia) /* L.20: Conversión estándar D* a B* Inicializa bp con la tabla de funciones del objeto d. Si no existe entrada para una función en dicha tabla, utiliza la función de la tabla de la clase B */ bp–>vf1(); // invoca D::vf1 bp–>vf2(); // invoca B::vf2 (D::vf2 tiene diferentes argumentos) bp–>f(); // invoca B::f (not virtual) X* xptr = bp–>pf(); /* invoca D::pf() y convierte el resultado en un puntero a X */ D* dptr = &d; Y* yptr = dptr–>pf(); /* inicializa yptr; este puntero invocará a D::pf() No se realiza ninguna conversión */

}

L.12: La versión D::vf1 es virtual automáticamente, dado que devuelve lo mismo y tiene los mismos parámetros que su homónima en la superclase B. El especificador virtual puede ser utilizado en estos casos pero no es estrictamente necesario a no ser que se vayan a derivar nuevas subclases de la clase derivada.

Tema relacionado •

Destructores virtuales (

4.11.2d2)

Inicio. [1] Desde luego, el recurso notacional adoptado (igualar una función a cero) es cualquier cosa menos lógico y elegante; otra de las particularidades sintácticas de C++. Para evitarlo, Joyner ( utilizar un #define (

4.9.10b) del tipo

7) aconseja

#define abstracta =0; con lo que la declaración de virtual pura (que da lugar a que la clase correspondiente sea abstracta) virtual void func() =0; podría ser escrito como virtual void func() abstracta; [2] También denominadas vtables en la literatura inglesa (pronunciado vee-tables). [3] El compilador GNU Cpp dispone de la opción -fall-virtual que hace que todas las funciones miembro de la clase sean declaradas implícitamente virtuales (excepto los constructores y las funcionesoperador new y delete). [4] Esta posibilidad fue añadida en la última versión del Estándar (Julio 1998). [5] El compilador GNU cpp dispone de una opción especial de compilación para advertir de este tipo de errores: -Woverloaded-virtual. [6] Recordemos que la palabra clave virtual puede ser utilizada también en la definición de clases ( 4.11.2c1). [7] Debo agradecer a Adán Román Ruiz, de la Universidad de Oviedo (España) la observación y la idea del ejemplo. [8] Puesto que los mecanismos de enlazado involucrados son totalmente distintos, y sus rendimientos también son distintos, para evitar confusiones, Microsoft ha introducido dos nuevas palabras-clave (que por el momento no son estándar) para la definición de métodos en su compilador Visual C++ 2005 y Visual Studio 2005

4.11.8b Funciones dinámicas §1 Antecedentes Las funciones dinámicas son una particularidad del compilador C++Builder, y están relacionadas con las clases VCL, otra peculiaridad de este compilador. Nota: como tales peculiaridades no se corresponden con el Estándar ANSI C++. Estas y en general, todas las características que no estén bien definidas o sean particularidades de algún fabricante es preferible obviarlas. En especial si la portabilidad del código es un factor a tener en cuenta. Presentan además el peligro añadido que el fabricante decida discontinuarlas en futuras versiones del compilador.

§2 Las clases VCL (Visual Component Library), son colecciones de objetos en Pascal. Se trata de una serie de recursos pre-construidos de los que puede echar mano el programador para integrarlos en sus aplicaciones. Constituyen una parte muy importante del IDE (Integrated Development Environment) de C++Builder, que como indica su nombre, es un Entorno Integrado de Desarrollo (eminentemente visual) con el que se pueden diseñar, compilar y depurar aplicaciones C++ con una mínima escritura manual de

código. Es similar a la STL (Standard Template Library) de C++ [1]; a la MFC (Microsoft Foundation Classes) de MicroSoft [2]; a la AWT de Java, etc. Nota: este principio de utilización de elementos preconstruidos ha estado presente desde siempre en la programación (es la esencia de una función o subrutina), aunque actualmente alcanza proporciones e importancia nunca vistas. Es el principio de un nuevo paradigma de programación, en especial de la informática distribuida, donde el asunto deriva hacia una serie de objetos que se interrogan mutuamente y se intercambian información, no importa en que soporte físico, donde estén ubicados, ni en que lenguaje están escritos. Los modernos IDE representan sin duda un gran avance, aunque a los viejos programadores nos de un poco de vértigo la pérdida de contacto con el código que subyace en estas herramientas visuales.

Borland C++ utiliza esta misma terminología VCL para una parte de su librería de objetos. De hecho, la VCL de BC++ se desarrolló inicialmente en Pascal, posteriormente se utilizó para Delphi (este compilador ha sido durante mucho tiempo el buque insignia de esta empresa de software), y está construida (1999) en Object Pascal (otro compilador importante de dicha empresa). En consecuencia es también el resultado de un "principio de reutilización de recursos" por parte de este fabricante de software. Según indica al respecto la propia ayuda de C++Builder, la Librería de Componentes Visuales (VCL) utiliza un modelo PME (basado en propiedades, métodos y eventos). Este modelo define las propiedades; los métodos para operar con ellas, y un medio para interactuar con los usuarios de la clase (los eventos). La librería VCL está construida como una jerarquía de objetos escritos en Object Pascal y ligados al IDE de C++Builder, que permite el desarrollo rápido de aplicaciones. El IDE de C++Builder espera que el nombre de cada clase VCL comience por la letra “T”. Por tanto, dicha T es ignorada siempre cuando se le asigna identificación por el compilador. Por ejemplo, una declaración class Test origina un identificador en el programa sin la T inicial.

§3 Sinopsis Las funciones dinámicas son similares a las funciones virtuales ( 4.11.8a) excepto en la forma en que alojan sus direcciones en la tabla virtual. Mientras que las funciones virtuales ocupan un espacio en la tabla virtual de la clase en que son definidas y en la de cada subclase descendiente, las funciones dinámicas en cambio, ocupan un espacio en la tabla de cada clase que las define, pero no en las subclases descendientes. Es decir, las funciones virtuales son como las dinámicas pero utilizan tablas más reducidas. Como consecuencia de lo anterior, cuando se invoca una función dinámica y esta función no está definida en la clase de la que se instancia el objeto, se recorre hacia arriba la jerarquía de clases hasta que se encuentra la definición en la tabla de alguna de ellas. La segunda consecuencia inmediata es que el tamaño de las tablas virtuales se reduce en perjuicio de la eficacia de ejecución, ya que este recorrido retrospectivo de la jerarquía de clases en búsqueda de las direcciones de las funciones consume su tiempo.

§4 Sintaxis: La declaración de una función como dinámica exige una sintaxis especial con la utilización del declarador __declspec(dynamyc):

__declspec(dynamic) nombre-funcion ( <parametro>, ... )

§5 Comentario Las funciones dinámicas se permiten solo en clases que deriven de una clase especial denominada TObject. Por esta razón se genera un error cuando se utilizan en una clase normal. Por ejemplo, las sentencias: class dynfunc { int __declspec(dynamic) bar() { return 5; } }; produce un error de compilación: “Error: Storage class 'dynamic' is not allowed here”. Sin embargo, el código que sigue es correcto: #include #include <stdio.h> class __declspec(delphiclass) func1 : public TObject { public: func1() { } int virtual virtbar() { return 5; } int __declspec(dynamic) dynbar() { return 5; } }; class __declspec(delphiclass) func2 : public func1 { public: func2() { } }; class __declspec(delphiclass) func3 : public func2 { public: func3() { } int virtbar() { return 10; } int dynbar() { return 10; } }; int main() { func3 * Func3 = new func3; func1 * Func1 = Func3; printf("func3->dynbar: %d\n", Func3->dynbar()); printf("func3->virtbar: %d\n", Func3->virtbar()); printf("func1->dynbar: %d\n", Func1->dynbar()); printf("func1->virtbar: %d\n", Func1->virtbar()); delete Func3; func2 * Func2 = new func2; printf("func2->dynbar: %d\n", Func2->dynbar()); printf("func2->virtbar: %d\n", Func2->virtbar()); delete Func2; return 0; } Salida: func3->dynbar: 10 func3->virtbar: 10

func1->dynbar: 10 func1->virtbar: 10 func2->dynbar: 5 func2->virtbar: 5

§5.1 El atributo dinámico es heredable Lo mismo que en las funciones virtuales, el atributo dinamic también es heredado automáticamente por las subclases, lo que puede ser comprobado ejecutando el ejemplo anterior. Cuando se genera una salida en ensamblador mediante el comando -S del compilador C++Borland ("bcc32 -S") pueden examinarse las tablas virtuales de las clases func1, func2, y func3, donde puede comprobarse como func2 no tiene entrada para la función dinámica dynbar, pero si para virtbar. A pesar de lo cual, puede invocarse dynbar en el objeto instanciado de la clase func2.

§5.2 Las funciones dinámicas no pueden ser virtuales y viceversa No está permitido redefinir una función virtual para que sea dinámica; así mismo, tampoco puede redefinirse una función dinámica para que sea virtual. El ejemplo que sigue produce errores: #include #include <stdio.h> class __declspec(vclclass) func1 : public TObject { public: func1() { } int virtual virtbar() { return 5; } int __declspec(dynamic) dynbar() { return 5; } }; class __declspec(vclclass) func2 : public foo1 { public: func2() { } int __declspec(dynamic) virtbar() { return 10; } int virtual dynbar() { return 10; } }; Resultado de la compilación Error: Error:

Cannot override a virtual with a dynamic function Cannot override a dynamic with a virtual function

Inicio. [1] STL (Standard Template Library). Este es el nombre que recibe un gran conjunto de estructuras de datos y algoritmos que conforman una parte importante de la Librería Estándar de C++ (

5).

[2] La MFC (Microsoft Foundation Classes) es la librería de clases C++ para programación en el entorno Windows. Está incluida en el compilador Microsoft Visual C++.

4.11.8c Clases abstractas §1 Sinopsis

La abstracción es un recurso de la mente (quizás el más característico de nuestra pretendida superioridad respecto del mundo animal). Por su parte, los lenguajes de programación permiten expresar la solución de un problema de forma comprensible simultáneamente por la máquina y el humano. Constituyen un puente entre la abstracción de la mente y una serie de instrucciones ejecutables por un dispositivo electrónico. En consecuencia, la capacidad de abstracción es una característica deseable de los lenguajes artificiales, pues cuanto mayor sea, mayor será su aproximación al lado humano. Es decir, con la imagen existente en la mente del programador. En este sentido, la introducción de las clases en los lenguajes orientados a objetos ha representado un importante avance respecto de la programación tradicional y dentro de ellas, las denominadas clases abstractas grado de abstracción.

son las que representan el mayor

De hecho, las clases abstractas presentan un nivel de "abstracción" tan elevado que no sirven para instanciar objetos de ellas. Representan los escalones más elevados de algunas jerarquías de clases y solo sirven para derivar otras clases, en las que se van implementando detalles y concreciones, hasta que finalmente presentan un nivel de definición suficiente que permita instanciar objetos concretos. Se suelen utilizar en aquellos casos en que se quiere que una serie de clases mantengan una cierta característica o interfaz común. Por esta razón a veces se dice de ellas que son pura interfaz. Resulta evidente en el ejemplo de la figura que los diversos tipos de motores tienen características diferentes. Realmente tienen poco en común un motor eléctrico de corriente alterna y una turbina de vapor. Sin embargo, la construcción de una jerarquía en la que todos motores desciendan de un ancestro común, la clase abstracta "Motores", presenta la ventaja de unificar la interfaz. Aunque evidentemente su definición será tan "abstracta", que no pueda ser utilizada para instanciar directamente ningún tipo de motor. El creador del lenguaje dice de ellas que soportan la noción de un concepto general del que solo pueden utilizarse variantes más concretas [2].

§2 Clases abstractas Una clase abstracta es la que tiene al menos una función virtual pura (como hemos visto, una función virtual es especificada como "pura" haciéndola igual a cero

4.11.8a).

Nota: recordemos que las clases que tienen al menos una función virtual (o virtual pura) se denominan clases polimórficas ( 4.11.8). Resulta por tanto, que todas las clases abstractas son también polimórficas, pero no necesariamente a la inversa.

§3 Reglas de uso:



Una clase abstracta solo puede ser usada como clase base para otras clases, pero no puede ser instanciada para crear un objeto 1.



Una clase abstracta no puede ser utilizada como argumento o como retorno de una función 2.



Si puede declararse punteros-a-clase abstracta 3 [1].



Se permiten referencias-a-clase abstracta, suponiendo que el objeto temporal no es necesario en la inicialización 4.

§4 Ejemplo class Figura { // clase abstracta (CA) point centro; ... public: getcentro() { return center; } mover(point p) { centro = p; dibujar(); } virtual void rotar(int) = 0; // función virtual pura virtual void dibujar() = 0; // función virtual pura virtual void brillo() = 0; // función virtual pura ... }; ... Figura x; // ERROR: intento de instanciar una CA.1 Figura* sptr; // Ok: puntero a CA. 3 Figura f(); // ERROR: función NO puede devolver tipo CA. 2 int g(Figura s); // ERROR: CA NO puede ser argumento de función 2 Figura& h(Figura&); // Ok: devuelve tipo "referencia-a-CA" 4 int h(Figura&); // Ok: "referencia-a-CA" si puede ser argumento 4

§5 Suponiendo que A sea una clase abstracta y que D sea una clase derivada inmediata de ella, cada función virtual pura fvp de A, para la que D no aporte una definición, se convierte en función virtual pura para D. En cuyo caso, D resulta ser también una clase abstracta. Por ejemplo, suponiendo la clase Figura definida previamente: class Circulo : public Figura { // Circulo deriva de una C.A. int radio; // privado por defecto public: void rotar(int); // convierte rotar en función no virtual }; En esta clase, el método heredado Circulo::dibujar() es una función virtual pura. Sin embargo, Circulo::rotar() no lo es (suponemos que definición se efectúa off-line). En consecuencia, Circulo es también una clase abstracta. En cambio, si hacemos: class Circulo : public Figura { // Circulo deriva de una C.A. int radio; public: void rotar(int); // convierte rotar en función no virtual pura void dibujar(); // convierte dibujar en función no virtual pura };

la clase Circulo deja de ser abstracta.

§6 Las funciones-miembro pueden ser llamadas desde el constructor de una clase abstracta, pero la llamada directa o indirecta de una función virtual pura desde tal constructor puede provocar un error en tiempo de ejecución. Sin embargo, son permitidas disposiciones como la siguiente: class CA { // clase abstracta public: virtual void foo() = 0; // foo virtual pura CA() { // constructor CA::foo(); // Ok. }; ... void CA::foo() { // definición en algún sitio ... } La razón es la ya señalada ( 4.11.8a), de que la utilización del operador :: de acceso a ámbito anula el mecanismo de funciones virtuales. Inicio. [1] Precisamente, la invocación de métodos de clases derivadas mediante punteros a la superclase, es una de las características esenciales de la tecnología COM de Microsoft. [2]

Stroustrup & Ellis: ACRM §10.3.

Related Documents

Clases
May 2020 19
Clases
June 2020 8
Clases
October 2019 37
Clases
July 2020 13
Clases
May 2020 16
Clases
October 2019 31