El no haber definido desde un principio mecanismos con los cuales se pudiera llevar a cabo programación orientada a objetos en C no fue una omisión deliberada. En esos tiempos, aunque en las mentes de varios programadores y maestros de instituciones académicas trabajando con tecnología de punta había ya un entendimiento de lo que se podía lograr recurriendo a esas cosas abstractas llamadas “objetos”, la comunidad de programadores profesionales en su gran mayoría no podían ver claramente la utilidad de tal tecnología informática, al no haber ejemplos claros con los cuales se pudieran ver tales conceptos en acción. Puesto de otra manera, para el mercado de consumo masivo de las computadoras personales caseras que se estaban comercializando ya a gran escala no había realmente ni una necesidad para la programación aplicada a objetos, ni había algún ejemplo de aplicaciones prácticas en donde se pudieran implementar tales conceptos considerados hasta cierto punto esotéricos. Bastaba con la disponibilidad de programas tales como el interpretador BASIC que venía incluído en las computadoras caseras como cortesía adicional junto con el sistema operativo DOS, o con programas tales como el procesador de palabras WordStar y las hojas de trabajo Lotus 1-2-3, lo demás era considerado superfluo. Los creadores de C, en su afán por mantener el lenguaje lo más sencillo posible, no consideraron prudente incluír algo que apenas estaba en ciernes y que no era suficientemente bien entendido. Sirva como exculpante el que, todavía hasta el día de hoy, no resulta fácil para muchos mentores hacer comprender a sus alumnos la verdadera naturaleza y ventajas que hay detrás de una programación orientada a objetos.
La vieja interfaz visual tipo DOS basada en una ventana de comandos de línea que ocupaba todo el espacio disponible en las pantallas de los monitores monocromáticos sin capacidades gráficas no daba mucha oportunidad para poder “ver” en acción el concepto de la programación orientada a objetos. Los usuarios de tales máquinas se daban por satisfechos con el conocimiento y manipulación de comandos básicos para el manejo de archivos, comandos tales como RENAME para cambiarle nombres a los archivos, COPY para copiar un archivo de un lugar a otro, o EXIT para dar por terminada la sesión antes de apagar la máquina.
Pero la situación cambió con la introducción de la interfaz visual Windows en las máquinas de 16 bits, lo cual alcanzó su cenit con Windows 3.1, acelerándose el paso con la promoción de Windows de mera interfaz visual a sistema operativo de 32 bits con la introducción de Windows 95 y la introducción simultánea del entorno de programación Visual Basic. Con la programación en Visual Basic, los programadores por vez primera se pudieron percatar de lo que realmente trataba la programación orientada a objetos, y podían apreciar las ventajas de una programación visual manejando objetos de todo tipo en la pantalla. En pocas palabras, Visual Basic introdujo la programación orientada a objetos en forma sencilla e intuitiva a los programadores del mundo entero, y esto fue un cambio revolucionario que sigue causando ecos y reverberaciones en nuestros días. La programación usando el entorno Visual Basic, permitiendo construír la interfaz visual de un programa de aplicación insertando dentro de una ventana objetos de todo tipo tales como botones de comando, cajas de texto, etiquetas, cajas de imagen y botones de opción, todos ellos manipulables a través de sus propiedades y sus métodos, permite “ver” la programación orientada a objetos en acción, haciendo algo práctico y útil por nosotros, sacada fuera de los confines de cursos de postgrado en informática para ponerla al alcance de cualquiera. Esta es la razón por la cual, aunque Visual Basic no existía cuando el lenguaje C se empezó a utilizar, en esta obra en vez de seguir un estricto orden cronológico en la presentación de temas que hubiera requerido estudiar el lenguaje C antes de estudiar Visual Basic, el orden de presentación de temas fue invertido para permitir que desde un principio el lector pudiera comprender de manera fácil e intuitiva la manera en la cual actúa y se puede utilizar la programación orientada a objetos. El tema no está limitado a los objetos que vemos en las interfaces visuales de los programas de aplicación, se puede aplicar también a la construcción de bases de datos orientadas a objetos, a la construcción de páginas Web en las que el pilar son objetos predefinidos, e inclusive a la programación de redes de computadoras.
Una vez entendida la programación orientada a objetos con el estudio de algo como el entorno de programación Visual Basic, damos un paso hacia atrás con el estudio del lenguaje C, pero en realidad no se trata de un retroceso, porque hay muchas instancias en las cuales el concepto de la programación orientada a objetos no resulta de mucha utilidad, como en el caso de los módems que son conectados a una computadora para permitirle una comunicación con otras computadoras por la vía de Internet. Por regla general, una computadora no requiere más de un módem para poder establecer su conexión, y resulta superfluo tratar al módemo como un “objeto” ya que en este caso sólo hay un “objeto”. Igual ocurre con las impresoras, en muchas instalaciones sólo hay disponible una impresora ya sea para una computadora o para varias que acceden a la impresora en tiempo compartido, es sumamente raro tener conectadas dos o más impresoras a una misma computadora, y por lo tanto no resulta de gran utilidad el tratar a una impresora como un “objeto”. Cuando estamos trabajando al nivel del hardware, casi al nivel del lenguaje de máquina por la vía de un lenguaje ensamblador o por la vía del lenguaje C, estamos prácticamente atados al viejo estilo de programar, estamos anclados a la programación procedimental con instrucciones elaboradas en la versión moderna de algún lenguaje clásico. El estudio del lenguaje C nos permite trabajar en dos mundos distintos, en el mundo del creador de programas de aplicación, y el mundo del programador de sistemas que se las tiene que ver directamente con el hardware.
Es importante recalcar que, en la programación Windows tal y como se comenzó a llevar a cabo al principio, en ausencia de las cómodas interfaces visuales que Visual Basic permitía construír en tiempo récord la herramienta principal para la programación Windows era el lenguaje C. Por ello es posible aprender la programación Windows, con algunas modificaciones (como el usar <b>WinMain()</b> como la función principal proporcionando el punto de entrada a los programas ejecutables diseñados para correr bajo un sistema operativo Windows, en lugar de <b>main()</b> como la función principal que proporcionaba el punto de entrada a los programas ejecutables diseñados para correr bajo la vieja interfaz de líneas de comandos), usando única y exclusivamente el lenguaje C, sin meter en el asunto la programación orientada a objetos.
De cualquier manera, después de unos cuantos años y aún antes de que Windows se enraizara como sistema operativo, se vió la necesidad de crear un lenguaje como C en el que se pudiera llevar a cabo la programación orientada a objetos. Empezar de cero inventando algo completamente nuevo y distinto a C no parecía una buena idea, no solo porque a nadie le gusta aprender más y más lenguas y dialectos que no tienen mucho en común con lo que ya sabe, sino porque había ya mucho código robusto y estable escrito en C completamente depurado de errores representando cientos de millones de líneas de código y el esfuerzo de decenas de miles de programadores profesionales del mundo entero. Lo ideal era construír algo que pudiera incorporar lo que ya se tenía, ¿pero qué?
A estas alturas el lector tal vez pudiera pensar: en las máquinas de aquél entonces en las cuales las memorias RAM apenas rondaban a duras penas en los 64 kilobytes de memoria, y ello haciendo una buena inversión económica en la adquisición de tales memorias RAM, ¿acaso la incorporación de la programación orientada a objetos en las máquinas de ese entonces no habría presionado al máximo los requerimientos por más memoria RAM, habido el hecho de que muchos programas C sin programación orientada a objetos ya estaban estresando a las máquinas a su límite? ¿Acaso no se contrapone la programación orientada a objetos a la necesidad de actuar como un avaro en la entrega de los entonces escasos y limitados recursos de RAM a este tipo de programación? Curiosamente, la respuesta es no. Independientemente de otras razones que se puedan presentar para convencerse de la utilidad de la programación orientada a objetos, el uso de tal paradigma en máquinas sumamente limitadas en recursos de memoria RAM puede y debe reducir los requerimientos de memoria en lugar de aumentarlos. ¿Pero cómo puede ser esto posible?, se preguntará el lector. Tal cosa es posible porque objetos que provienen de una misma clase (botones de comando, por ejemplo) comparten la misma estructura fundamental Considérese una ventana propia de una interfaz visual windows, cuyo código consume 8 kilobytes de memoria RAM. En un programa en el cual se requiera abrir cuatro ventanas, cada una de las cuales tendrá un tamaño diferente, si el bloque de código que corresponde a una ventana consume 8 kilobytes entonces las cuatro ventanas consumirán 32 kilobytes. Otras cuatro ventanas adicionales consumirían 32 kilobytes adicionales, requiriendo 64 kilobytes de memoria RAM y agotando por completo la disponibilidad de memoria RAM en una máquina de 64 kilobytes sin dejar espacio alguno ni siquiera para el sistema operativo que debe estar presente y residente todo el tiempo en la máquina. Pero teniendo en cuenta el hecho de que todas las ventanas comparten el mismo “esqueleto”, la misma estructura básica, y en lo único en lo que difiere cada objeto es en las propiedades tales como la anchura y la altura de la ventana, ¿por qué no usar el mismo código para todas las ventanas, cambiando únicamente lo que haya que cambiar para cada ventana en particular a través de las propiedades de cada ventana? De este modo, si el código común a todas las ventanas, el código que corresponde a la clase a partir de la cual se crean los objetos, consume 6 kilobytes de RAM, y los datos que fijan las propiedades de cada ventana en lo particular consumen los restantes 2 kilobytes, entonces con 6 kilobytes de RAM de código compartido y 2 kilobytes para cuatro ventanas se consume un total de 14 kilobytes, un requerimiento de consumo de memoria mucho menor que el requerimiento de 32 kilobytes. Y en caso de que se abran ocho ventanas en total, se podrían manejar con un total de 22 kilobytes, lo cual está dentro del espacio de memoria de 64 kilobytes de nuestra máquina hipotética. De este modo, los programas Windows que no se podían correr en la máquina se pueden correr perfectamente sin problema alguno. De pronto lo difícil o imposible se vuelve posible. ¡Y todo ello gracias a la programación orientada a objetos!
Si la programación orientada a objetos no hubiera existido en lo absoluto antes de las interfaces visuales tipo Windows, los creadores de estas interfaces visuales habrían tenido que inventar ellos mismos la programación orientada a objetos más por cuestiones de necesidad que por satisfacer disquisiciones teóricas del mundo académico.
De cualquier modo, la programación Windows, que originalmente se podía llevar a cabo usando el lenguaje C ordinario y se podía beneficiar ampliamente mediante la programación orientada a objetos, no es el único beneficiario de esta tecnología informática. Ya desde antes se entendía bien la manera en la cual se podía aplicar el concepto en máquinas tipo UNIX para la construcción de cosas tales como bases de datos orientadas a objetos. La necesidad de que el lenguaje C evolucionara hacia algo posterior ya estaba presente antes del advenimiento de Windows, y de hecho tal cosa ocurrió desde antes de que las interfaces visuales enraizadas sobre objetos fuesen cosa común en el uso cotidiano de las computadoras caseras.
La respuesta a la necesidad se llamó C++.
El doble signo “++” usado como operador de incremento en el lenguaje C y que se puede leer como “incrementar en una unidad” puede ser interpretado en nuestro nuevo lenguaje como “un paso hacia adelante” (posteriormente vendría Microsoft con “C#” que, en virtud de que en música el símbolo “#” es usado para simbolizar los tonos de “sostenido” que ordenan que el tono de la nota sea subido “hacia arriba” en medio tono de la escala musical, se puede leer como “C sostenido” o C sharp).
Empezemos por afirmar que C++ no es un reemplazo del lenguaje C en el sentido de que no se trata de un dialecto nuevo de C o de una versión distinta e incompatible de C. El primero incorpora y expande al segundo permitiéndole llevar a cabo lo que se conoce como la programación orientada a objetos. Se trata, en efecto, de un superconjunto del lenguaje C.
El lenguaje C++ fue desarrollado en 1983 por Bjarne Stroustrup en el mismo centro de investigación Bell Laboratories en donde Dennis Ritchie empezó la creación del lenguaje C en 1972, habiendo transcurrido más de una década desde la creación del lenguaje C (las interfaces visuales tipo Windows no existían en 1983 y menos aún en 1972). Ya para cuando el nuevo lenguaje estaba disponible para fines de desarrollo comercial de software, la compañía Bell Telephone (fundada por Alexander Graham Bell) que mantenía el monopolio telefónico en los Estados Unidos estaba en proceso de desmembramiento y su parte principal terminaría por adoptar el nombre comercial AT&T, y fué bajo estas siglas cuando el nuevo lenguaje alcanzó su versión robusta y refinada, la versión 2.0.
La necesidad de algo como C++ parte de finales de los años 60 y principios de los años 70, cuando se hablaba de la “crisis del software” en la que, en contraste con los avances impresionantes que se estaban dando en la física de los transistores y la microelectrónica, los avances en el arte de la programación iban a paso de tortuga, y ya para los años 80 se hablaba de la “crisis del software” con el advenimiento de proyectos de complejidad creciente en los cuales la cantidad de líneas de código estaba rebasando ya las 10 mil líneas de código. Esto fue lo que condujo a la creación de la programación estructurada diseñada para simplificar los procedimientos de elaboración de software. Usando lenguajes estructurados, por vez primera fue posible escribir de manera relativamente fácil programas moderadamente complejos. Sin embargo, aún con los métodos de la programación estructurada, una vez que un proyecto alcanza cierto tamaño el programa empieza a salir fuera de control; y la razón por la cual se sale fuera de control es porque su complejidad excede aquello que el programador puede manejar mediante las técnicas de la programación estructurada. A cada hito en el desarrollo de la programación, se fueron creando nuevos métodos para permitirle al programador enfrentar la complejidad creciente en los programas. A finales de los 80 muchos proyectos estaban en el punto en el cual la sola programación estructurada no era suficiente, pero ya para entonces había terminado de madurar la tecnología de la programación orientada a objetos, incorporando los tres pilares de que consta cualquier lenguaje de programación estructurada: objetos, polimorfismo y herencia. Gracias a estos conceptos, en el libro The C++ Programming Language considerado hoy como un “clásico” del arte de la programación Bjarne Stroustrup afirmó que los programas grandes podían ser estructurados “de modo tal que nos irrazonable que una sola persona pueda estar a la par con 25,000 líneas de código”. A continuación daremos un repaso a estos tres conceptos, agregándose que aquellos que ya leyeron las entradas previas puestas en la temática “El entorno Visual Basic” encontrarán que mucho de esto ya les es familiar y podrán avanzar y entender con mucha mayor rapidez.
La característica más importante de un lenguaje orientado a objetos es, desde luego, el objeto. Puesto en términos simples, un objeto es una entidad lógica que contiene datos y código que manipula esos datos. Dentro de un objeto, algo del código y/o de los datos será privado al objeto e inaccesible en forma directa por cualquier cosa que esté fuera del objeto. De este modo, un objeto proporciona un nivel significante de protección en contra de otra parte no relacionada del programa que pueda modificar o manejar incorrectamente las partes privadas del objeto. El encadenamiento de código y datos llevado a cabo de esta manera dentro de un objeto es lo que se conoce como encapsulación. Para todos los intentos y propósitos, un objeto es una variable de un tipo definido por el usuario. Tal vez al principio pueda parecer extraño el ver a un objeto, que encadena código y datos, como una variable. Sin embargo, en la programación orientada a objetos, este es precisamente el caso. Cuando definimos un objeto estamos creando implícitamente un nuevo tipo de dato.
Los lenguajes de la programación orientada a objetos dan soporte al polimorfismo, que esencialmente significa que un mismo nombre puede ser usado para varios propósitos relacionados pero ligeramente diferentes. El propósito del polimorfismo es permitir que un mismo nombre pueda ser usado para especificar una clase general de acciones. Sin embargo, dependiendo del tipo de datos con los que se está tratando, se ejecuta una instancia especial del caso general. Por ejemplo, podemos tener un programa que define tres diferentes tipos de pilas (stack). Una pila es usada para valores enteros, otra pila es usada para valores flotantes, y otra es usada para valores del tipo long. En virtud del polimorfismo, podemos crear tres conjuntos de funciones para estas tres pilas, llamadas push() y pop(), y el compilador escogerá la rutina apropiada dependiendo del tipo de dato con el cual se invoca a la función. En este ejemplo, el concepto general es el de empujar (push) y botar (pop) datos hacia y de la pila. Las funciones definen la manera específica en la cual esto se lleva a cabo para cada tipo de dato.
La herencia es el proceso mediante el cual un objeto puede adquirir las propiedades de otro objeto. Esto es importante porque da soporte al concepto de la clasificación. Si se medita bien sobre el asunto, gran parte de nuestros conocimientos acumulados es manejable gracias a las clasificaciones jerárquicas. Por ejemplo, una deliciosa manzana roja es parte de la clasificación manzana, la cual a su vez es parte de la clasificación fruta, que a su vez es parte de la clasificación mayor comida. Sin el uso de las clasificaciones, cada objeto tendría que definir explícitamente todas y cada una de sus características. Sin embargo, al usar clasificaciones, un objeto solo tiene que definir aquellas características que lo hacen único dentro de su clase. Puede heredar únicamente aquellas cualidades que comparte con las clases más generales. El mecanismo de la herencia es lo que hace posible que un objeto sea una instancia específica de un caso más general.
Hemos hablado de C++ como un lenguaje C orientado a objetos. Pero seguramente el lector prefiere ver una muestra de dicho lenguaje. Empezaremos con el que quizá sea el programa más sencillo de todos, el típico programa de bienvenida “Hola, mundo!”:
// El archivo de cabecera #include ya no es el archivo STDIO.H,
// en el entorno IDE Borland C++ 4.0 es IOSTREAM.H, en otros
// entornos puede ser STREAM.HXX, o bien puede ser otra cosa
#include <iostream.h>
main() {
cout << "Hola, mundo! Hoy es " << 19 << " de enero\n";
}
Si se compila y se ejecuta, el programa anterior imprime en una ventana en modo de texto:
Hola, mundo! Hoy es 19 de enero
Y en C++ seguimos teniendo la funciónn principal main() que hace el mismo papel que la función principal main() en C. Obsérvese primero que nada la manera de hacer comentarios. Se pone una doble diagonal (//) al principio de cada línea, y con ello todo lo que sigue hasta el final de la línea será ignorado, pero solo hasta el final de la línea; de este modo cada línea requerirá su propia doble diagonal de comentario. Sin embargo, podemos seguir haciendo comentarios como en el C tradicional, empezando el comentario con el par /* y terminando el comentario con el par */ aunque el comentario ocupe varias líneas de texto en la pantalla. Tenemos, pues, dos maneras distintas de hacer comentarios.
Para enviar datos a la terminal (o consola, o monitor), usamos la palabra reservada cout cuyo significado podemos tomar como “salida a la consola” (console output). Para enviar datos a la salida estándard, usamos el operador “<<”. Pero... ¡un momento! ¿Acaso “<<” no se usa en el lenguaje C para efectuar un corrimiento bitwise (al modo de bits, bit por bit) hacia la izquierda? En efecto, pero aquí lo podemos usar también para enviar datos a la salida. Esta capacidad de poder darle a un mismo operador varios significados dependiendo del contexto en el cual se esté utilizando es lo que se conoce como sobrecargado (overloading). Cuando sobrecargamos un operador, le damos un nuevo significado dependiendo de aquello sobre lo cual esté actuando el operador. En el contexto presente, el operador “<<” significa “enviar a”.
En el breve programa, a cout se le presentan varios argumentos, los cuales imprime siguiendo un orden de izquierda a derecha. Lo que estamos haciendo aquí es usar la clase de corrientes predefinidas. La clase de corrientes maneja entrada de y salida hacia archivos. Con las corrientes, podemos hilvanar una serie de argumentos como los que se muestran arriba, lo cual hace que la clase de corrientes sea fácil de utilizar. Al igual que en C, el texto entre dobles comillas es una hilera. Los argumentos de hileras y los números constantes son mezclados en el enunciado cout, lo cual es posible porque el operador “<<” está sobrecargado, y podemos enviar a cout una variedad de argumentos distintos (hileras, enteros, flotantes, etcétera), y el compilador sabrá qué hacer con el mensaje. Esto es más cómodo y más fácil de usar que el enunciado printf() al cual hay que proporcionarle formatos de código para los tipos de datos que se van a imprimir en la pantalla.
Por último, vemos que en C++ al igual que en el lenguaje C ordinario, se requiere de un semicolon al final de una instrucción para actuar como un marcador de terminación de la instrucción.
Veamos otro ejemplo de programa en C++:
// El archivo de cabecera STDIO.H tiene que ser incluido
// porque tiene el prototipo de la funcion printf() que aparece
// en el programa. De no hacerse asi, el programa no seria
// compilado porque la funcion printf() apareceria como
// una funcion que carece de prototipo.
#include <stdio.h> /* Para entrada/salida en C */
#include <iostream.h> // Para entrada/salida en C++
main(void)
{
int i;
char hilera[80];
cout << "C++ es divertido\n";
/* podemos meter comentarios C ordinarios en donde se quiera */
printf("Puedes usar printf() si lo deseas\n");
// dar entrada a un numero usando >>
cout << "dame un numero: ";
cin >> i;
// darle salida al numero usando <<
cout << "tu numero es " << i << "\n";
// leer una hilera
cout << "dame una hilera: ";
cin >> hilera;
// imprimir la hilera
cout << hilera;
return 0;
}
Este es un programa interactivo en el cual se espera entrada del usuario. En la emulación de la ventana DOS, se imprime algo como lo siguiente:
Como puede verse, el programa tiene un aspecto algo diferente de un programa C ordinario. El archivo de cabecera IOSTREAM.H tiene que ser incluído porque es lo que se requiere para dar soporte a las operaciones de entrada/salida.
Además de usarse una corriente de salida para imprimir, en el programa también se usa la función printf(). El programa nos muestra además cómo usamos cin (console input) para obtener entrada del usuario.
Veamos otro ejemplo de un programa interactivo:
#include <iostream.h>
// Las "corrientes" de entrada/salida a traves de cin y cout tienen su
// prototipo definido en el archivo de cabecera IOSTREAM.H
#include <iomanip.h>
// El prototipo de setprecision() se encuentra en el archivo de
// cabecera IOMANIP.H
#include <string.h>
// El prototipo de la funcion strlen() se encuentra en el archivo de
// cabecera STRING.H, y hay que declararselo a C++ para que el
// pueda ser compilado
#define DENSIDAD 62.4
// Densidad del cuerpo humano en libras por pie cubico
void main(void) {
float peso, volumen;
int cuerpo, letras;
char nombre[40];
cout << "Dame tu nombre: ";
cin >> nombre;
cout << nombre <<", dame tu peso en libras: ";
cin >> peso;
cuerpo = sizeof nombre;
letras = strlen(nombre);
volumen = peso/DENSIDAD;
cout << "Bien, " << nombre << ", tu volumen es ";
cout << setprecision(4) << volumen;
cout << " pies cubicos.\n";
cout << "Tambien, tu nombre tiene ";
cout << letras <<" letras,\n";
cout << "y necesitamos " << cuerpo;
cout << " bytes para almacenarlo.\n";
}
En la emulación de la ventana DOS, se imprime algo como lo siguiente:
Habiendo visto algunos programas C++ elementales, es tiempo de familiarizarnos con uno de los conceptos más importantes que podamos encontrar en un lenguaje orientado a objetos, la clase. Es lo que se requiere para poder crear un objeto. La clase es lo que nos proporciona el “esqueleto”, la armazón fundamental, la estructura básica. Para crear un objeto definimos primero su forma general con la palabra reservada class. Empezaremos ilustrando su uso mediante el siguiente ejemplo con el cual crearemos la clase Cola:
class Cola {
private:
int q[100];
int sloc, rloc;
public:
void Init(void);
void Cponer(int i);
int Ctomar(void);
};
Obsérvese la armazón que se ha utilizado en la declaración de una clase con la palabra reservada class. La sintaxis general usada para declarar una clase es la siguiente:
class nombre de la clase {
datos y funciones privados
public:
datos y funciones públicas
};
Obsérvese en la definición de la declaración de una clase un detalle muy importante: el semicolon puesto al final inmediatamente después del corchete de cierre.
En el ejemplo que se acaba de dar creando la clase Cola, podemos ver que se usó la palabra reservada private:
class Cola {
private:
int q[100];
int sloc, rloc;
public:
void Init(void);
void Cponer(int i);
int Ctomar(void);
};
El uso de la palabra reservada private no es un requerimiento indispensable, ya que en forma predeterminada (por default), los miembros de una clase declarados al comienzo de la clase son PRIVADOS (accesibles unicamente por las funciones de la clase), y por lo tanto aqui el uso de la palabra clave private es opcional. Para quienes prefieren adherirse a la formalidad (lo cual a veces resulta útil para darle legibilidad a los programas), la sintaxis más general usada para declarar una clase es la siguiente:
class nombre de la clase {
private:
datos y funciones privados
public:
datos y funciones públicas
};
Quienes se familiarizaron con C++ después de haberse familiarizado primero con el entorno Visual Basic pueden establecer algunas comparaciones. Lo que aquí declaramos como los datos de una clase corresponden a lo que en Visual Basic son las propiedades de un objeto. En Visual Basic podemos meter dentro de una forma (ventana) varios objetos, por ejemplo unas cuatro cajas de texto, las cuales tienen propiedades tales como la anchura de cada caja y la altura de cada caja. Todas las cajas de texto en Visual Basic tienen estas propiedades, lo único que varía de una caja de texto a otra son los valores que poseen estas propiedades comunes. No hay una sola caja de texto tomada de la Caja de Herramientas (Toolbox) que posea menos propiedades o más propiedades que otra caja de texto, todas las cajas de texto poseen exactamente el mismo conjunto de propiedades. Y antes de meter la primera caja de texto dentro de una forma (ventana), la estructura básica, el “esqueleto”, la clase para cada objeto ya existe. ¿En dónde? Pues en la Caja de Herramientas. Al hacer clic en uno de los controles de la Caja de Herramientas (por ejemplo, una caja de imagen) en realidad estamos seleccionando una clase de la Caja de Herramientas; aún no existe el objeto. Una vez que hemos seleccionado un control, al hacer clic con el Mouse dentro de la forma (ventana) empezamos a crear un objeto a partir de la clase antes seleccionada. Creado el objeto, podemos crear un segundo objeto de la misma clase haciendo clic con el Mouse en la Caja de Herramientas en el mismo control y repitiendo el procedimiento. De este modo, cada objeto puesto dentro de una forma es una instancia de una clase. Podemos tener una o varias instancias (objetos) de una clase dentro de una misma forma, cada una de ellas tendrá el mismo conjunto de propiedades pero los valores en cada caso (como el título de cada ventan seleccionable con la propiedad Caption) podrá ser diferente.
En la declaración de la clase Cola, podemos ver que tiene lo que en el entorno Visual Basic vendría siendo el equivalente de tres propiedades, un array de nombre q con capacidad para cien elementos del tipo int, y dos variables de tipo int llamadas sloc y rloc:
int q[100];
int sloc, rloc;
Como ya se señaló, aunque no se use la palabra reservada private estos elementos son privados a la clase, nadie más puede tener acceso a ellos más que un objeto creado a partir de la misma clase.
Además de los elementos privados, podemos ver que hay varias funciones que son públicas, lo cual está rigurosamente estipulado mediante la palabra reservada public la cual sí es requerida y no es optativa:
void Init(void);
void Cponer(int i);
int Ctomar(void);
Hay dos funciones de tipo void llamadas Init() y Cponer(), la primera usada para la inicialización de un objeto de la clase Cola y la segunda usada para poner un número entero dentro de un objeto de la clase Cola. La tercera función, Ctomar(), nos permite sacar un número (metido previamente) en un objeto de la clase Cola. Como puede verse, podemos declarar algunos miembros de la clase como PUBLICOS, esto es, accesibles por todas las funciones del programa, despues de la palabra clave public.
Usualmente, en la programacion Orientada a Objetos, los DATOS miembros de una clase (q[], sloc y rloc en este caso) son declarados como PRIVADOS, mientras que las FUNCIONES miembros (init(), Cponer() y Ctomar() en este caso) son declarados como PUBLICOS.
Nuevamente, podemos establecer una comparación entre las funciones miembros de una clase y los métodos de un objeto creado en el entorno Visual Basic. Recuérdese que en Visual Basic los métodos de un objeto son lo que nos permite cambiar los valores de cualquiera de las propiedades del objeto; los métodos de Visual Basic son funciones, y son conceptualmente la misma cosa que lo que tenemos aquí con las funciones públicas de una clase. Lo único que cambia es la terminología con la cual nos referimos a estos conceptos dependiendo del lenguaje o entorno que estemos utilizando, pero los conceptos son lo mismo; es como usar la palabra inglesa car y la palabra francesa voiture para describir un carro, el lenguaje usado es tan sólo un medio necesario para poder describir una misma realidad. O como dijera Shakespeare, ¿qué hay detrás de un nombre? Una rosa tiene la misma fragancia con cualquier otro nombre que se le dé. Entendida la abstracción (generalización) de los conceptos, podemos transplantar nuestros conocimientos de un idioma a otro sin mayores dificultades.
Resumiendo lo que hemos visto, una clase class puede contener partes tanto públicas como privadas. En forma predeterminada, todos los artículos definidos en la clase son privados, lo cual significa que no pueden ser accesados por función alguna que no pertenezca a la clase. Esta es una manera en la cual se logra la encapsulación, el acceso a ciertos artículos de datos está firmemente controlado manteniéndolos privados. Aunque no se muestra en el ejemplo que se ha dado, podemos definir también funciones privadaas que sólo podrán ser invocadas por otros miembros de la misma clase. Para hacer públicas las partes de una clase, tenemos que declararlas después de la palabra clave public. Todas las variables o funciones definidas después de public son accesibles por todas las demás funciones de un programa. Se agregará que aunque es posible tener variables públicas, filosóficamente debemos tratar de limitar o inclusive eliminar su uso, ya que ello es contrario al paradigma de la programación orientada a objetos. En lugar de ello, debemos hacer todos los datos privados y controlar el acceso a ellos a través de funciones públicas. Obsérvese que tanto la palabra clave public como la palabra clave private son seguidas por un colon (:).
Las funciones Init(), Cponer() y Ctomar() son llamadas funciones miembro porque son parte de la clase Cola. Recuérdese que un objeto establece una ligadura estrecha entre código y datos. Unicamente aquellas funciones declaradas dentro de una clase pueden tener acceso a las partes privadas de la clase; y por ello estas funciones son conocidas como funciones miembro.
Una vez que hemos definido una clase, una vez que hemos definido el esqueleto, la armazón básica, podemos crear un objeto usando el nombre dado a la clase. Pensándolo bien, el nombre de una clase es el especificador de un nuevo tipo de dato. A modo de ejemplo, la siguiene instrucción crea un objeto llamado Cola_1 del tipo Cola:
Cola Cola_1;
Podemos crear también uno o varios objetos al mismo tiempo que definimos una clase poniendo sus nombres después del corchete de cierre con el que definimos la clase, exactamente de la misma manera como lo hacemos con una estructura struct en el lenguaje C. De este modo, la sintaxis más general que se le pueda dar a la definición de una clase es la siguiente:
class nombre de la clase {
private:
datos y funciones privados
public:
datos y funciones públicas
} lista de objetos;
En la definición que se dió de la clase Cola, se usaron prototipos a las funciones miembro. Es importante tener presente aquí que cuando tenemos que decirle al compilador de una función tenemos usar la forma completa del prototipo, no se permiten abreviaturas como en los métodos más viejos y tradicionales; aquí los prototipos no son optativos.
Cuando llega el momento de elaborar el código para una función que es miembro de alguna clase, tenemos que decirle al compilador a qué clase corresponde una función calificando el nombre de la función con el nombre de la clase a la cual pertenece. A modo de ejemplo, esta es una manera de codificar la función Cponer():
void Cola::Cponer(int i) {
if (sloc == 100) {
cout << "La cola esta llena";
return;
}
sloc++;
q[sloc] = i;
}
Obsérvese el uso del operador:
::
En C++, el operador :: es conocido como el operador de resolución de alcance, y en nuestro ejemplo le dice al compilador que esta versión de la función Cponer() pertenece a la clase Cola, o poniéndolo en términos distintos, le dice que Cponer() está dentro del alcance de la clase Cola:
void Cola::Cponer(int i)
En C++, es perfectamente válido que varias clases puedan usar los mismos nombres para una función. El compilador sabe a qué clase pertenece cada función en virtud del operador de resolución de alcance y el nombre dado a la clase.
Para invocar una función miembro desde un lugar del programa que no forma parte de una clase, debemos usar el nombre del objeto seguido del operador punto. A modo de ejemplo, el siguiente fragmento invoca la función Init() para el objeto b:
Cola a, b;
b.Init();
En la primera línea creamos dos objetos de la clase Cola, a los cuales les damos los nombres a y b, y con la segunda línea invocamos la función Init() para el objeto b.
Es muy posible que el lector empiece a atar aquí más cabos, al recordar que el operador punto fué lo mismo que usamos en Visual Basic para poder tener acceso tanto a las propiedades como a los métodos de un objeto. En realidad, y en esencia, se trata del mismo operador. Y se trata del mismo operador porque los creadores de Visual Basic se inspiraron no solo en la metodología sino también en la notación usada previamente en el lenguaje C orientado a objetos. El uso del operador punto es de aplicación casi universal en virtud de su genealogía, y lo podemos ver aplicado en otras situaciones como en la elaboración de páginas Web con contenido dinámico en el lenguaje Javascript o en el lenguaje Java.
En el ejemplo que se acaba de dar, es importante tener presente que los objetos a y b son dos objetos separados e independientes. Esto significa, por ejemplo, que la inicialización del objeto b no hará de alguna manera que el objeto a también sea inicializado. La única relación que ambos objetos tienen entre sí es que son dos objetos del mismo tipo, y hasta allí llega la cosa.
Otra cosa importante a tener en cuenta es que una función miembro puede invocar directamente otra función miembro directamente sin necesidad de tener que usar el operador punto; únicamente cuando una función miembro es invocada por código que no pertenece a una misma clase que el nombre de la variable y el operador punto tienen que ser utilizados.
A continuación se presenta un programa que pone en un mismo sitio lo que se asentó acerca de la clase Cola dándole una aplicación a la clase Cola:
#include <iostream.h>
// Declaracion de la clase Cola
// Para evitar conflictos con funciones de biblioteca en las cuales se acostumbra
// empezarlas con minúscula, se acostumbra hacer mayuscula la primera letra de
// las funciones que son definidas por el programador
class Cola {
private:
int q[100];
int sloc, rloc;
public:
void Init(void);
void Cponer(int i);
int Ctomar(void);
};
// Definicion de la funcion Init() perteneciente a la clase Cola
void Cola::Init(void) {
rloc = sloc = 0; // Inicializacion de variables
}
// Definicion de la funcion Cponer() perteneciente a la clase Cola
void Cola::Cponer(int i) {
if (sloc == 100) {
cout << "La cola esta llena";
return;
}
sloc++;
q[sloc] = i;
}
// Definicion de la funcion Ctomar() perteneciente a la clase Cola
int Cola::Ctomar(void) {
if(rloc == sloc) {
cout << "Sub-flujo (underflow) de la cola";
return 0;
}
rloc++;
return q[rloc];
}
// El programa principal
main(void) {
Cola a, b;
// Creacion de dos OBJETOS de la clase Cola: el objeto "a" y el
// objeto "b"
a.Init();
// Inicializacion del objeto "a" haciendo un llamado a la funcion
// miembro de la clase Cola init()
b.Init();
// Inicializacion del objeto "b" haciendo un llamado a la funcion
// miembro de la clase Cola init()
a.Cponer(1500);
// Ponemos el entero 1500 en la cola objeto a
a.Cponer(225);
// Ponemos el entero 225 en la cola objeto a
b.Cponer(879);
// Ponemos el entero 879 en la cola objeto b
a.Cponer(436);
// Ponemos el entero 436 en la cola objeto a
b.Cponer(-567);
// Ponemos el entero -567 en la cola objeto b
b.Cponer(1);
// Ponemos el entero 1 en la cola objeto b
cout << a.Ctomar() << " ";
// Sacamos e imprimimos el primer entero que metimos en la
// cola del objeto a
cout << a.Ctomar() << " ";
// Sacamos e imprimimos el segundo entero que metimos en la
// cola del objeto a
cout << b.Ctomar() << " ";
// Sacamos e imprimimos el primer entero que metimos en la
// cola del objeto b
cout << a.Ctomar() << " ";
// Sacamos e imprimimos el ultimo entero que metimos en la
// cola del objeto a
cout << b.Ctomar() << " ";
// Sacamos e imprimimos el segundo entero que metimos en la
// cola del objeto b
cout << b.Ctomar() << "\n";
// Sacamos e imprimimos el ultimo entero que metimos en la
// cola del objeto b
return 0;
}
El programa imprime lo siguiente:
1500 225 879 436 -567 1
Repasando el programa, después de que los dos objetos (colas) a y b son inicializados con la función Init() puede verse que al accesar primero el dato privado (propiedad) del objeto a con el operador punto:
a.Cponer(1500);
se le mete el entero 1500 con la función Cponer(). Es lógico que el primer dato que se le metió a la cola a sea el primer dato que saldrá cuando se invoque la función Ctomar() usando nuevamente el operador punto:
a.Ctomar();
en la línea de impresión:
cout << a.Ctomar() << " ";
Así pues, se mete primero el entero 1500 en la cola a, y seguidamente se le mete el entero 225, con lo cual hay dos enteros 1500 y 225 en la cola a. Tras esto, se mete el entero 879 como primer dato a la cola b, y tras esto se mete el entero 436 en la cola a. De este modo, en la cola a habrá tres enteros, 1500, 225 y 879. Obsérvese que los datos no van entrando en cada cola siguiendo un orden en particular, y los datos tampoco son sacados de cada cola siguiendo un orden en particular.
Posiblemente el lector capte lo que se ha creado con la clase Cola. Se ha creado la forma fundamental de una estructura de datos conocida en informática como queue o cola, en la cual lo primero que entra (por la puerta de entrada) es lo primero que sale (por la puerta de salida), y es mejor conocida como una cola tipo FIFO (First In, First Out). La siguiente ilustración basada en el programa de ejemplo que se ha dado arriba ilustra lo que sucede y el por qué se imprimen los números en el orden mostrado:
En cada objeto de la clase Cola, lo que entra al objeto entra por arriba, y lo que sale del objeto sale por abajo.
Una manera en la cual C++ implementa el polimorfismo es a través de lo que se conoce como el sobrecargado (overloading) de funciones. A diferencia de lo que ocurre en otros lenguajes que no permiten tal cosa, aquí dos o más funciones pueden tener el mismo nombre siempre y cuando sus declaraciones de parámetros sean diferentes. En una situación así, se dice que las funciones que tienen el mismo nombre están sobrecargadas. Como ejemplo de ello, considérese el siguiente programa que nos muestra la manera en la cual podemos sobrecargar una función cubo() dándole tres significados distintos a la misma función, un significado para cada tipo de dato:
#include <iostream.h>
// La funcion cubo() esta SOBRECARGADA de tres maneras distintas
// Declaracion de los tres prototipos distintos de la funcion cubo():
int cubo(int i);
double cubo(double d);
long cubo(long l);
main(void) {
cout << cubo(5) << "\n";
cout << cubo(15.5) << "\n";
cout << cubo(357L) << "\n";
return 0;
}
int cubo(int i) {
cout << "Dentro de la funcion cubo() que utiliza ";
cout << "como argumento un entero.\n";
return i*i*i;
}
double cubo(double d) {
cout << "Dentro de la funcion cubo() que utiliza ";
cout << "como argumento un doble.\n";
return d*d*d;
}
long cubo(long l) {
cout << "Dentro de la funcion cubo() que utiliza ";
cout << "como argumento un long.\n";
return l*l*l;
}
Si se ejecuta este programa, se imprime lo siguiente:
Dentro de la funcion cubo() que utiliza como argumento un entero
125
Dentro de la funcion cubo() que utiliza como argumento un doble
3723.88
Dentro de la funcion cubo() que utiliza como argumento un long
45499293
Obsérvese lo que se ha logrado aquí con el sobrecargado de la función cubo(). Una de las desventajas del lenguaje C ordinario con su declaración y manejo estricto de tipos es que no da margen para que el usuario pueda meter números que puedan ser enteros, flotantes o dobles; si se define una función cubo() de tipo int para que el usuario meta un entero como 15, y se le ocurre al usuario escribir dicho entero como 15.0, estamos en problemas porque con el solo hecho de haber agregado un punto decimal y un dígito cero el número que mete el usuario no es un entero sino un número de tipo flotante. Podemos, desde luego, hacer maniobras y malabarismos en C para que cuando el usuario meta un número dicho número pueda ser interpretado primero como un entero, un flotante o un doble, y una vez que se ha determinado el tipo entonces se puede invocar una de tres funciones distintas que pueden ser cubo_int(), cubo_float() y cubo_float(). Sólo se usará una de ellas, desde luego, dependiendo del tipo de número que ha metido el usuario, pero al requerirse definir tres funciones distintas para algo así la carga de trabajo para el programador aumenta. Con el sobrecargado de funciones el programador no tiene por qué preocuparse por estos detalles, y puede proceder como se ha hecho arriba.
De este modo, puede afirmarse que una de las razones para el sobrecargado de funciones es que permite administrar la complejidad. La utilidad del sobrecargado de funciones es que posibilita que conjuntos relacionados de funciones puedan ser accesadas usando el mismo nombre. El sobrecargado de funciones nos permite crear un nombre genérico para alguna operación dejando que el compilador resuelva el asunto de cuál función es requerida para llevar a cabo la operación. Para el ejemplo que acabamos de ver, la opción en C es recurrir a funciones como atoi() (ASCII to integer), atof() (ASCII to float) y atol() (ASCII to long) que se encuentran definidas en muchas bibliotecas estándard del ANSI C. Colectivamente, estas funciones convierten una hilera de dígitos (en secuencia de caracteres ASCII) hacia los formatos internos de un entero, un double o un long, respectivamente. Aunque estas funciones llevan a cabo acciones casi idénticas, se requiere de tres nombres ligeramente diferentes en C para representar estas tareas, lo cual hace que la situación sea conceptualmente más compleja de lo que es. Aunque el concepto detrás de cada función es esencialmente el mismo, ello significa que el programador tiene que acordarse de tres cosas en lugar de una sola. Sin embargo, con el sobrecargado de funciones es posible usar un mismo nombre como atonum() (ASCII a número) para las tres funciones. Por lo tanto, el nombre atonum() representa la acción general que se está llevando a cabo, dejándole al compilador la tarea de escoger la versión específica correcta para una circunstancia particular, y por lo tanto el programador sólo tiene que acordarse de la acción general que se está llevando a cabo. De este modo, aplicando el polimorfismo, tres cosas a ser recordadas por el programador se han reducido a una sola. Aunque el ejemplo que se ha dado es algo trivial, si ampliamos el concepto a otras situaciones podemos ver cómo el polimorfismo nos puede ayudar a entender programas grandes y complejos.
Tanto en C como en C++ no existe ninguna función de librería que peticione al usuario a que proporcione alguna entrada, esperando una respuesta hasta que el usuario responda. Es lo que se conoce en lenguaje técnico como prompt(). A continuación se presenta otro ejemplo del sobrecargado de funciones con el cual se crean tres funciones llamadas prompt() que lleva a cabo esta tarea para datos de tipo int, double y long (obsérvese que la entrada del dato es mediante una hilera de caracteres, y por ello recurrimos al uso de punteros):
#include <iostream.h>
void prompt(char *hilera, int *i);
void prompt(char *hilera, double *d);
void prompt(char *hilera, long *l);
main(void)
{
int i;
double d;
long l;
prompt("Dame un entero: ", &i);
prompt("Dame un double: ", &d);
prompt("Dame un long: ", &l);
cout << i << " " << d << " " << l;
return 0;
}
void prompt(char *hilera, int *i)
{
cout << hilera;
cin >> *i;
}
void prompt(char *hilera, double *d)
{
cout << hilera;
cin >> *d;
}
void prompt(char *hilera, long *l)
{
cout << hilera;
cin >> *l;
}
Si se corre el programa anterior, el cual es interactivo, se puede obtener algo como lo siguiente:
Dame un entero: 125
Dame un double: 3723.8
Dame un long: 45499293
En principio, es posible usar el mismo nombre para sobrecargar funciones que no están relacionadas de ninguna manera. Podemos, por ejemplo, usar el nombre cubico() para crear funciones que nos regresan el cubo de un entero y la raíz cúbica de un flotante. Sin embargo, estas dos operaciones son fundamentalmente distintas, y usar el sobrecargado de esta manera desvirtúa por completo el espíritu y la intención del sobrecargado de funciones. Se debe tratar de usar el sobrecargado únicamente para operaciones que están relacionadas en forma estrecha.