| Versión 3.0 |
| 800x600 mínimo |
|
En esta lección: Encapsulado ----- Clases ----- Miembros public y private ----- Constructores y destructores ----- Métodos const ----- Implementación en línea ----- Empaquetado ----- Descargas ----- Otras secciones: Conceptos básicos ----- Programando en C ----- Programando en C++ ----- Programando Windows 9x. ----- Teoría electrónica ----- Circuitos electrónicos ----- Actividades adicionales ----- Hipervínculos ----- Contácteme: Dudas y comentarios ----- |
Clases y encapsuladoLa Programación Orientada a Objetos trae consigo una serie de términos nuevos, familiarizarnos y aprender a manejar éstos conceptos es de suma importancia para el futuro del programador moderno. Empezamos éste artículo dando una definición: nos referimos al encapsulado como el proceso de formar objetos. Un objeto encapsulado también es a menudo llamado un tipo de dato abstracto que es uno de los temas principales de la Programación Orientada a Objetos, sin el encapsulado, lo cual involucra el uso de una ó más clases no existiría la Programación Orientada a Objetos (POO para abreviar). En la programación por procedimientos tradicional, digamos utilizando C, es normal declarar una serie de variables con nombres representativos de un tipo específico para el dato en cuestión, así, si declaramos algunas variables de tipo int sabemos que podemos asignarles valores numéricos dentro de un rango específico, también podemos realizar ciertas operaciones con éstas variables, como sumar, restar ú otros cálculos y además podemos asignar los resultados a otra variable del mismo tipo. El tipo de las variables indica:
En términos generales podemos decir que un tipo es una categoría. Tipos que nos resultan familiares pueden ser, carro, casa, persona y forma entre muchas más. En C++, el programador puede crear el tipo que sea necesario con toda la funcionalidad y potencia que aporta el lenguaje. La necesidad de crear tipos nuevos está en el hecho de que programamos para resolver problemas de la vida real, como por ejemplo simular el funcionamiento de un sistema de calefacción. Aunque es posible describirlo utilizando valores numéricos y cadenas de caracteres, si utilizamos representaciones de los objetos con los que estamos trabajando, sobre todo tratándose de problemas complejos, resulta mucho más fácil lograr resultados satisfactorios al crear variables que representen por ejemplo, sensores de calor, termostatos y calefactores. Cuanto más cercanas se correspondan las variables a la realidad, tanto más fácil será escribir el programa. Pero, ¿Para qué necesitamos encapsular datos? EncapsuladoComo sabemos, una estructura es una colección de tipos de datos previamente definidos y ordenados en una forma conveniente para el programa. En C++ la estructura presenta una mejora con respecto a su prima del lenguaje C, permite especificar funciones miembro lo que resulta muy práctico en términos de ordenar diversas partes del código de acuerdo a su funcionalidad. Veamos el primer código de este artículo:
Como puede ver, declaro una estructura llamada ohm que contiene tres variables miembro de tipo float llamadas voltaje, resistencia y corriente, también está contenida dentro de la estructura el prototipo de la función miembro llamada amperios que toma dos parámetros de tipo float y devuelve un valor de tipo float. En el cuerpo de la función principal, en primer lugar declaramos dos variables de tipo ohm llamadas calculo1 y calculo2 además de una variable convencional de tipo float llamada resultado. En ANSI-C es obligatorio especificar la palabra clave struct al declarar variables, no así en C++ como está demostrado en las líneas 24 y 25. Al declarar variables de tipo ohm decimos que son objetos de la estructura llamada ohm, por lo tanto calculo1 y calculo2 son también conocidos como objetos de tipo ohm. En las líneas 28 a la 31 del programa inicializamos las variables con algunos valores utilizando el operador punto de la misma manera como se hace en C, la principal diferencia la tenemos en la línea 33 donde asignamos a la variable calculo1.corriente el valor devuelto por la función miembro llamada calculo1.amperios( ), de manera similar, en la línea 35 asignamos a la variable de tipo float llamada resultado el valor devuelto por la función calculo2.amperios( ), finalmente se despliegan los resultados en pantalla. Deducimos fácilmente que éste procedimiento para hacer un cálculo resulta riesgoso, sencillamente podemos cometer un error que resultaría difícil de corregir. Se supone que al declarar una función miembro de la estructura ohm es para facilitar el manejo del programa al tener reunidos en una sola sección los elementos relacionados del código, pero vemos una desventaja al acceder libremente tanto a las variables como a la función de tipo ohm. ClasesC++ proporciona una alternativa muy interesante llamada clase, con ésta instrucción propia de C++ y de la POO podemos agrupar efectivamente una serie de variables y funciones miembro, y al igual que con la estructura, la clase nos sirve para construir nuevos tipos de datos. Una clase es una colección de variables, a menudo de diferentes tipos, combinadas con funciones relacionadas. Una clase nos permite encapsular o agrupar una serie de variables y funciones en una colección a la cuál llamamos objeto. Similarmente a la estructura, llamamos a las variables como variables miembro en tanto que a las funciones les llamamos funciones miembro, también conocidas como métodos. Para declarar una clase utilizamos la palabra clave class. El siguiente programa demuestra el encapsulado y la manera para declarar y utilizar una clase:
Veamos las partes nuevas que nos presenta el programa clases02.cpp y hagamos una comparación funcional con respecto al programa clases01.cpp. Al principio tenemos la declaración de una clase llamada ohm y entre las llaves que delimitan a ésta se encuentran las variables miembro llamadas voltaje, resistencia y corriente todas de tipo float. También tenemos a las funciones miembro llamadas asigna( ) que toma dos parámetros de tipo float, y muestra( ) que no lleva parámetros. Ambas funciones devuelven un valor de void, es decir, no devuelven valor alguno. La declaración de la clase termina con un símbolo de punto y coma después de la llave de cierre, nos indica que se trata en efecto de una instrucción C++ válida. Al declarar ésta clase no se está asignando memoria para ohm, sencillamente se le está indicando al compilador que cosa es ohm, que tipo de datos contiene (voltaje, resistencia y corriente) y que puede hacer(asigna( ) y muestra( )). También le indica al compilador el tamaño que se requiere para cada ohm creado en el programa, como tenemos tres variables de tipo float, cada una de 4 bytes, en total tenemos un tamaño para ohm de 12 bytes. Los métodos no toman espacio porque no es necesario reservar espacio en memoria para las funciones, éstas son parte integral del programa al ser compilado. En comparación con la estructura definida en el programa clases01.cpp existen algunas diferencias mínimas que más adelante discutiremos. Después de la declaración de la clase sigue la definición de sus métodos. Como se trata de funciones se aplica lo visto en el capítulo anterior, funciones en C++ con una excepción, en el encabezado utilizamos en primer lugar el tipo de dato que el método devuelve seguido del nombre de la clase a la que pertenece el método, el operador de resolución global (::) y por último el nombre del método. El primer método, llamado asigna( ) toma dos parámetros de tipo float, uno para asignar el valor de voltaje y otro para asignar el valor de la corriente, el método calcula internamente el correspondiente valor para la resistencia y asigna el resultado a la variable miembro llamada resistencia, de ésta manera, el método asigna( ) como su nombre lo indica, asigna a las variables miembro de la clase ohm sus respectivos valores de acuerdo a la ley de Ohm. El segundo método, llamado muestra( ) simplemente despliega en pantalla el resultado de los cálculos efectuados basados en la ley de Ohm. En la función principal main( ) en primer lugar declaramos dos objetos de la clase ohm llamados calculo1 y calculo2 en la línea 35. En la línea 36 se declaran de forma tradicional dos variables de tipo float llamadas volt y ampere. El programa empieza solicitándole que introduzca en primer lugar el valor correspondiente al voltaje y lo almacena en la variable local llamada volt, lo mismo ocurre para el valor de la corriente que se alamacena en la variable local llamada ampere, después de ésto el programa efectúa dos cálculos, el primero en la línea 44. Para el objeto de la clase ohm llamado calculo1 llamamos a la función miembro llamada asigna( ) utilizando el operador punto (.) de la misma manera que se hace al utilizar estructuras, utilizando como parámetros los valores previamente almacenados en las variables volt y ampere. Para el objeto de la clase ohm llamado calculo2 a su vez llamamos a su método llamado asigna( ) pero el valor correspondiente al primer parámetro lo multiplicamos por 4 en tanto que el valor de la corriente lo dividimos entre 2, ésto simplemente para tener dos valores diferentes y así poder distinguir los resultados obtenidos como se aprecia en la línea 45. De ésta manera hemos asignado de manera efectiva los respectivos valores para el calculo1 y el calculo2. En comparación con el programa clases01.cpp Usted podrá observar que es posible efectuar un cálculo de corriente utilizando la función miembro llamada amperios( ) pasando como parámetros los valores almacenados en calculo2.corriente y calculo1.voltaje dondo como resultado un obvio error. Sin embargo éste error no se detecta al momento de compilar el programa, ¿Cómo entonces el programa clases02.cpp resuelve éste conflicto? Miembros public y privateC++ define tanto para las estructuras como para las clases tres características aplicables a los miembros de éstas de tal manera que pueden ser public, private ó protected, éstas son palabras clave de C++. La palabra clave public implica que es posible tener libre acceso tanto a las variables como a las funciones miembro de un objeto utilizando el operador punto, como se hace en el programa clases01.cpp. Por el contrario, la palabra clave private condiciona el acceso a los miembros de una clase solamente utilizando los métodos de la clase en sí. La palabra clave protected la estudiaremos más adelante. De lo dicho señalaré la primera diferencia entre una estructura y una clase en C++, por defecto, los miembros de una estructura son de caracter public en tanto que los miembros de una clase tienen la propiedad private, con respecto a la clase ohm del programa clases02.cpp las variables miembro tienen todas el caracter de private, Usted puede verificar ésto tratando de asignar un valor de la siguiente manera:
En el programa clases02.cpp solo es posible asignar valores a las variables utilizando el método llamado asigna( ) y justamente podemos utilizar éste método porque lo hemos declarado como public, está claramente señalado al utilizar la palabra clave public en la línea 14 del programa, ésto significa que en la clase llamada ohm todos los miembros declarados, sean variables ó métodos en seguida de la palabra clave public son de libre acceso utilizando el operador punto. En la POO se dice que los miembros de caracter private constituyen datos ocultos, por lo ya expuesto, las variables miembro de la clase ohm están ocultas para el resto del programa. Es práctica general en C++ declarar a las variables miembro de una clase como private en tanto que a los métodos les corresponde el caracter de public, sin embargo ésto depende de las necesidades propias del programa. El programa clases02.cpp demuestra claramente la importancia de mantener ocultas a las variables miembro, de ésta manera se evitan errores de cálculo atribuibles al diseño del programa. Bjarne Stroustrup, el inventor de C++ dijo "Los mecanismos de control de acceso de C++ proveen protección contra accidentes, no contra fraudes" (ARM, 1990). En el archivo comprimido clases03.zip que puede descargar al final de éste artículo encontrará un programa adicional llamado clases03.cpp que le permitirá comparar la funcionalidad del programa clases02.cpp pero utilizando una estructura, ahora bién, si es posible efectuar el mismo trabajo con dos palabras clave diferentes, ¿Cuál es la razón de la existencia de éstas dos entidades aparentemente iguales? La respuesta a ésta pregunta es de carácter histórico, C++ evolucionó a partir de C, de hecho, el nombre inicial de C++ era "C con clases". C++ aporta en todo caso una estructura más poderosa y además agrega el uso de clases que como veremos a continuación tienen características adicionales no compartidas por las estructuras. Constructores y destructoresSabemos ya que al declarar variables de clase estamos construyendo objetos. Con frecuencia es conveniente que las variables miembro de una clase sean inicializadas a un valor específico de acuerdo a las necesidades del programa, éste proceso de inicialización asegura que los datos del programa no contengan valores inservibles. Para inicializar a las variables miembro de clase se utiliza una función miembro especial llamada constructor. Un constructor puede tomar los parámetros que sean necesarios pero no devuelve valor alguno, ni aún de tipo void, además, un constructor lleva el mismo nombre que la clase a la que pertenece. Un constructor ejecuta todas las instrucciones que sean requeridas para el correcto funcionamiento del objeto creado. Naturalmente, si hablamos de una función especial para inicializar los objetos de nuestro programa, natural es que también exista una función especial que nos ayude a destruir los objetos cuando no sean ya necesarios, dicha función se llama apropiadamente, destructor. Una función destructor no toma parámetros y al igual que el constructor, no devuelve valor alguno, además, el destructor lleva el mismo nombre de la clase a la que pertenece anteponiéndole una tilde (~). En el programa clases02.cpp se utilizó un método llamado asigna( ) para inicializar las variables miembro de caracter private. Una función constructora hace éste mismo trabajo pero de una manera más eficente ya que un constructor se ejecuta automáticamente cada vez que un objeto sea creado. Similarmente, un destructor se ejecuta automáticamente cada vez que un objeto sea destruido. Estas características propias de los constructores y destructores son muy útiles cuando los objetos hacen uso de los recursos del sistema (por ejemplo, la asignación dinámica de memoria) y se requiere que éstos recursos sean liberados para un posterior uso. El programa clases04.cpp demuestra los puntos principales a considerar para el uso de los constructores y destructores:
Un cuidadoso estudio del código y los conceptos serán fácilmente asimilados. En este caso tenemos una clase llamada cubo declarada en las líneas 9 a la 27, contiene tres variables miembro de tipo float y de caracter private llamadas ancho, fondo, altura y una más de tipo int llamada id, la función de ésta última es dar a los objetos un elemento de identificación tal que al ejecutar el programa nos ayude a comprender su funcionamiento. Un método de caracter private llamado contenido( ) determina la cantidad de piezas que cada caja contiene de acuerdo a los valores pasados como parámetros, el valor obtenido es ficticio pero demuestra una característica interesante como veremos más adelante. En la sección public de la clase encontramos la declaración de un par de constructores, uno en la línea 19 que toma cuatro parámetros y otro más en la línea 22. Por lo estudiado en el capítulo anterior, Funciones en C++, nos damos cuenta que la función constructora llamada cubo está sobrecargada, el programa llamará a la función constructora adecuada en base al número y tipo de parámetros pasados a la función. En la línea 24 está la declaración de la función destructora, observe que lleva una tilde (~) en el nombre. Por último, en la línea 26 tenemos el prototipo del método llamado volumen( ) que sirve para determinar el volumen de los objetos de tipo cubo que el programa crea. Como los constructores, el destructor y los métodos son funciones de C++ tenemos en las líneas 30 a la 72 las respectivas definiciones que el estudiante no tendrá dificultad alguna en comprender. Observe que en ambos constructores se llama a la función miembro llamada volumen( ), en las líneas 36 y 47, y que no se hace uso del operador punto para tener acceso a la función. El destructor de hecho no hace otra cosa que desplegar un mensaje en pantalla para indicarnos el momento en que se ejecuta, es normal que una función destructora no lleve instrucciones ejecutables, sin embargo debemos utilizar una función destructora cada vez que utilizemos un constructor. La función constructora se ejecuta cada vez que creamos un objeto de tipo cubo, en la línea 74 se declara un objeto de éste tipo llamado caja4, mas que la declaración de una variable parece la llamada a una función, y en efecto, la función que estamos llamando es la función constructora de la clase cubo, como se puede ver, tiene cuatro parámetros, los primeros tres corresponden de acuerdo al prototipo dado para el constructor, el ancho, el fondo y la altura del cubo llamado caja4, el cuarto parámetro tiene la función de identificador. por lo tanto, al declarar al objeto llamado caja4 se ejecuta la función constructora que toma cuatro parámetros, es decir, la ejecución del programa salta de la línea 74 a la línea 40. Dentro del constructor se asignan los respectivos valores para el ancho, el fondo y la altura de la caja y se llama al método volumen( ), línea 47, o sea que la ejecución del programa brinca de la línea 47 a la 63. En el método volumen( ) se calcula el volumen de la caja y después se llama a la función contenido( ) de caracter private para determinar la cantidad de piezas que "contiene" la caja. Como ya se dijo, éste valor es ficticio, pero demuestra el concepto de ocultar información. Observe que no existe mecanismo alguno que nos permita conocer el contenido de las cajas cuando son éstas creadas, tampoco es posible llamar al método contenido( ) desde ninguna otra parte del programa que no sea desde el interior del método volumen( ). El concepto de ocultar información tiene gran valor en la POO. Observe además que al crear el objeto llamado caja4 el constructor se ejecuta incluso antes de la función principal main( ). Ya en la función principal creamos otras tres cajas llamadas caja1, caja2 y caja3, al crear ésta última se ejecuta el constructor correspondiente de acuerdo a los parámetros incluidos, es decir, ninguno para la caja3. El programa le solicita que introduzca nuevos valores para la caja1 y llama al método volumen( ) para calcular las nuevas dimensiones de la caja, dicho en otras palabras, la caja1 cambiará de tamaño y por tanto de contenido. Los nuevos valores para la caja2 se obtienen de unas sencillas operaciones aritméticas efectuadas con los valores dados para la caja1, línea 94 del programa. Insisto, el contenido de la caja constituye una verdadera sorpresa, en otras palabras, el "contenido" de las cajas se trata como una "caja negra". Compile, ejecute y experimente con éste programa, probablemente descubra características adicionales con respecto al uso de los constructores y destructores. Métodos constAl declarar un método de clase como de tipo const se dá la promesa de que el método no cambiará el valor de ninguno de los miembros de la clase. Para declarar un método de clase como constante se coloca la palabra clave const después del paréntesis pero antes del símbolo de punto y coma, similarmente se incluye en el encabezado de la definición del método. En el programa clases04.cpp el método llamado volumen( ) no puede ser declarado como const porque éste método asigna valores a las variables miembro, sin embargo el método contenido si puede ser declarado como const ya que su función no implica cambiar los datos en la clase, por lo tanto el método contenido puede ser declarado así:
La definición de éste método será ahora así:
Utilizar la palabra clave const es de ayuda para evitar errores en tiempo de compilación, se considera una buena práctica de programación declarar como const a todos aquellos métodos que no afectan a los miembros de una clase, al hacer ésto se establece una especie de contrato con el compilador, aclaro éste concepto. LLamamos clientes a aquellas partes de un programa que crean y utilizan objetos de su clase, Usted puede pensar en la interfaz de la clase, es decir, la declaración de la clase, como un contrato con éstos clientes, éste contrato indica qué datos de su clase están disponibles y cómo los manejará su clase. Al declarar al método contenido( ) como const se está estableciendo en el contrato que éste método no afectará el volumen de la caja, o sea, el contenido de la caja no tiene nada que ver con el tamaño de la misma. Implementación en líneaComo los métodos de una clase son en realidad funciones, podemos entonces indicarle al compilador que haga éstas funciones en línea. La palabra clave inline se coloca antes del tipo de dato que la función devuelve, por ejemplo, el método contenido( ) podemos declararlo en línea de la siguiente manera:
En forma similar podemos hacer el método en línea incluyendo la definición dentro de la declaración de la clase:
Observe que para el método llamado contenido( ) en realidad ya no existe prototipo, se eliminó el símbolo de punto y coma, además se insertó el cuerpo de la función inmediatamente después de la declaración del método. EmpaquetadoCada función que se declara en una clase debe tener una definición. A la definición también se le llama implementación de la función, como toda función, la definición de un método de clase tiene un encabezado y un cuerpo de la función. La definición debe estar en un archivo que el compilador pueda encontrar, éste archivo lleva por lo general la extensión *.cpp. Usted tiene la libertad de poner la declaración del método (el prototipo) en éste archivo pero ésto no se considera como una buena práctica de programación, lo que hacen la mayoría de los programadores es poner la declaración del método en un archivo de cabecera que lleva la extensión *.h ó *.hpp. Por ejemplo, podemos colocar la declaración del la clase cubo en un archivo de cabecera llamado cubo.h y a las implementaciones de las funciones miembro en un archivo llamado cubo.cpp, después indicamos al preprocesador que incluya el archivo de cabecera cubo.h agregando ésta línea en el archivo cubo.cpp:
La declaración de una clase le indica al compilador qué es la clase, qué datos almacena y qué funciones desempeña. La declaración de una clase es llamada su interfaz porque le indica al usuario cómo interactuar con la clase. La interfaz generalmente se almacena en un archivo de cabecera con la extensión *.h ó *.hpp. La definición de una función le indica al compilador cómo trabaja la función. La definición de una función es llamada la implementación del método de clase y se guarda en un archivo con la extensión *.cpp. Los detalles de la implementación de una clase son de importancia para el desarrollador de la misma. Los clientes de la clase, ésto es, el programa que utiliza la clase, no necesitan saber cómo se han implementado las funciones de la clase. Con referencia al programa clases04.cpp podemos reordenarlo de la siguiente manera, en un archivo de cabecera incluimos la definición de la clase llamada cubo, el archivo se llamará entonces cubo.h:
La implementación de la clase cubo ahora está en un archivo llamado cubo.cpp, observe que incluye una llamada al preprocesador para indicar que anexe el contenido del archivo llamado cubo.h, en la línea 8.
El programa que hace uso de la clase cubo está especificado en el código llamado clases05.cpp, observe que es posible crear varios programas diferentes que utilicen más de una clase, cada una con un propósito diferente. Interesante es hacer notar que el archivo cubo.cpp es posible distribuirlo en forma precompilada en forma de librería estática, posiblemente se llamaría cubo.lib, de tal manera que los usuarios de la clase cubo jamás se enterarían de cómo se implementaron los métodos de la clase. El programa clases05.cpp es el siguiente:
El hecho de separar la declaración de una clase en un archivo de cabecera y la implementación de sus métodos en otro tiene también un motivo práctico, habrá observado que C++ no está hecho para programas sencillos pues para efectuar tareas relativamente simples se utilizan una mayor cantidad de líneas de código, la gran ventaja de empaquetar objetos la encontraremos cuando se trate el tema de la herencia, además el empaquetado facilita la reutilización de código y la implementación de bibliotecas de funciones que son dos puntos muy fuertes en la programación en C++. DescargasEl código fuente de los programas utilizados en éste capítulo los encuentra en el archivo comprimido clases03.zip (4.16 Kb.), para descargarlo haga clic aquí. |