C++ permite que los objetos puedan inicializarse a sí mismos al momento de ser creados, en virtud de que el requerimiento de inicialización es tan común. La inicialización automática de un objeto se lleva a cabo mediante una función conocida como función constructura. Se trata de una función especial que es miembro de la clase y que tiene el mismo nombre que se le dá a la clase. Usando el ejemplo de la clase Cola, a continuación podemos ver el aspecto que toma dicha clase cuando la modificamos para que use una función constructora para llevar a cabo su inicialización:
class Cola {
int q[100];
int sloc, rloc;
public:
Cola(void);
// Aqui declaramos la funcion CONSTRUCTORA Cola que permitira llevar a
// cabo la inicializacion de los objetos de la clase Cola en el momento en que
// estan siendo creados. La funcion constructora es invocada y ejecutada al
// momento de estar siendo creado un objeto
void Cponer(int i);
int Ctomar(void);
};
Obsérvese que la función constructora Cola() no tiene ningún tipo de retorno especificado. Ello se debe a que las funciones constructoras no pueden regresar valores.
La función constructora en sí puede ser codificada de una manera como la que se muestra a continuación:
Cola::Cola(void) {
rloc = sloc = 0; // Inicializacion de variables
cout << "Una cola ha sido inicializada\n";
}
El mensaje “Una cola ha sido inicializada” ha sido puesto como una manera de confirmar la ejecución de la función constructora. En los programas del mundo real, la gran mayoría de las funciones constructoras no producirán salida alguna al usuario ni tomarán entrada alguna.
La función constructora es invocada cada vez que se crea un objeto. Esto significa que es llamada cuando se ejecuta la declaración del objeto.
En C++ no solo es posible crear objetos e inicializarlos al momento de su creación. También es posible destruírlos, lo cual permite removerlos de la memoria RAM y reclamar el espacio de memoria que estaban ocupando cuando ya no son de utilidad alguna. El complemento opuesto de la función constructora es el destructor o función destructora. Hay varias situaciones y casos en los cuales se requiere que un objeto ejecute alguna acción o acciones al momento de ser destruído. Un destructor tiene el mismo nombre que un constructor, excepto que está precedido por una tilde (~),
Usando el ejemplo de la clase Cola a la cual el agregamos una función constructora, a continuación podemos ver el aspecto que toma dicha clase cuando la volvemos a modificar para que use una función destructora:
class Cola {
int q[100];
int sloc, rloc;
public:
Cola(void); // Funcion constructora
~Cola(void);
// Aqui declaramos la funcion DESTRUCTORA Cola que permitira llevar a cabo
// las acciones que sean requeridas cuando los objetos de la clase Cola son
// destruidos despues de haber sido utilizados. La funcion destructora es invocada
// y ejecutada cuando el objeto ya termino de ser utilizado.
void Cponer(int i);
int Ctomar(void);
};
La función constructora en sí puede ser codificada de una manera como la que se muestra a continuación:
Cola:: ~Cola(void) {
cout << "Una cola ha sido destruida\n";
}
La mejor manera de ver cómo trabaja lo anterior consiste en compilar y ejecutar el programa completo que se muestra a continuación:
#include <iostream.h>
// Declaracion de la clase Cola:
class Cola {
int q[100];
int sloc, rloc;
public:
Cola(void); // Funcion constructora
~Cola(void); //Funcion destructora
void Cponer(int i);
int Ctomar(void);
};
// Definicion de la funcion constructora:
Cola::Cola(void) {
rloc = sloc = 0;
cout << "Una cola ha sido inicializada\n";
}
// Definicion de la funcion destructora:
Cola:: ~Cola(void) {
cout << "Una cola ha sido destruida\n";
}
// 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".
// Ambos objetos son inicializados al momento de ser
// creados por la funcion constructora
a.Cponer(1500);
// Ponemos el entero 1500 en la cola del objeto a
a.Cponer(225);
// Ponemos el entero 225 en la cola del objeto a
b.Cponer(879);
// Ponemos el entero 879 en la cola del objeto b
a.Cponer(436);
// Ponemos el entero 436 en la cola del objeto a
b.Cponer(-567);
// Ponemos el entero -567 en la cola del objeto b
b.Cponer(1);
// Ponemos el entero 1 en la cola del objeto b
cout << a.Ctomar() << " ";
// Sacamos e imprimimos el primer entero que
// habíamos metido en la cola del objeto a
cout << a.Ctomar() << " ";
// Sacamos e imprimimos el segundo entero que
// habíamos metido en la cola del objeto a
cout << b.Ctomar() << " ";
// Sacamos e imprimimos el primer entero que
// habíamos metido en la cola del objeto b
cout << a.Ctomar() << " ";
// Sacamos e imprimimos el ultimo entero que
// habíamos metido en la cola del objeto a
cout << b.Ctomar() << " ";
// Sacamos e imprimimos el segundo entero que
// habíamos metido en la cola del objeto b
cout << b.Ctomar() << "\n";
// Sacamos e imprimimos el ultimo entero que
// habíamos metido en la cola del objeto b
return 0;
}
Si se ejecuta el programa anterior, se obtendrá la siguiente respuesta en la ventana emulada tipo DOS:
Una cola ha sido inicializada
Una cola ha sido inicializada
1500 225 879 436 -567 1
Una cola ha sido destruida
Una cola ha sido destruida
Cuando un objeto es creado, frecuentemente se vuelve necesario o deseable inicializar varios datos con valores específicos. Ya vimos arriba que usando una función constructora es posible inicializar varias variables cuando se lleva a cabo la creación del objeto. Sin embargo, el concepto de inicialización de objetos es expandido para permitir la inicialización de objetos específicos usando valores definidos por el programador. En Visual Basic, un ejemplo vendría siendo la creación de un conjunto de botones de opción dentro de una ventana, cada botón de opción tiene sus propias coordenadas de ubicación dentro de una forma (ventana) y tiene su propio título, lo cual es fijado de antemano a través de sus propiedades.
Tomando el ejemplo anterior, podemos expandir la clase Cola para aceptar un argumento que servirá las veces de número de identificación de un objeto cola, con lo cual le podemos dar una identidad única a cada objeto al momento en que el objeto está siendo creado. Esto lo podemos lograr modificando la declaración de la clase Cola haciendo que tome el siguiente aspecto:
class Cola {
int q[100];
int sloc, rloc;
int quien;
// Esta variable de tipo int sera usada para almacenar el
// numero de identificacion ID de la cola
public:
Cola(int ID);
// La declaracion de la funcion constructora recibira como argumento al
// entero ID
~Cola(void);
void Cponer(int i);
int Ctomar(void);
};
En la declaración refinada que se ha dado de la clase Cola, la variable quien será usada para contener un número de identificación ID cuando se lleve a cabo la creación de un objeto del tipo Cola.
La función constructora puede ser codificada de una manera como la que se muestra a continuación:
Cola::Cola(int ID) {
rloc = sloc = 0;
quien = ID;
cout << "La cola " << quien << " ha sido inicializada\n";
}
Para poder pasarle un argumento a una función constructora, es necesario asociar el valor o los valores que se le están pasando a un objeto cuando el objeto está siendo creado. Hay dos maneras de lograrlo. La primera manera es mediante algo como lo siguiente:
Cola a = Cola(7);
Con esta declaración se crea un objeto a de la clase Cola y se le pasa un valor 7 como argumento al momento de su creación. Esta manera no es usada con frecuencia porque la segunda manera es más breve y va más directo al grano. En la segunda manera, el argumento o los argumentos deben seguir inmediatamente al nombre del objeto al momento en que es creado, y deben estar puestos entre paréntesis. De este modo, la siguiente línea logra lo mismo que lo anterior:
Cola a(7);
Estaremos usando de aquí en delante la forma breve de pasarle argumentos a un objeto al momento de su creación en virtud de que es lo que se utiliza en casi todos los programas. Generalizando lo que acabamos de ver, la sintaxis para pasarle argumentos a una función constructora es la siguiente:
clase variable(lista de argumentos);
en donde lista de argumentos es la lista de argumentos separados con comas que le es pasada a la función constructora.
De este modo, tomando el programa anterior podemos elaborar la siguiente versión modificada que nos muestra la manera en la cual le podemos pasar argumentos a una función constructora:
#include <iostream.h>
class Cola {
int q[100];
int sloc, rloc;
int quien;
public:
Cola(int ID);
// La declaracion de la funcion constructora recibira como argumento el
// entero ID
~Cola(void);
void Cponer(int i);
int Ctomar(void);
};
// Definicion de la funcion constructora:
Cola::Cola(int ID) {
rloc = sloc = 0;
quien = ID;
cout << "La cola " << quien << " ha sido inicializada\n";
}
// Definición de la función destructora:
Cola:: ~Cola(void) { // Funcion destructora
cout << "La cola " << quien << " ha sido destruida\n";
}
void Cola::Cponer(int i) {
if (sloc == 100) {
cout << "La cola esta llena";
return;
}
sloc++;
q[sloc] = i;
}
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(1), b(2);
// Creacion de dos OBJETOS de la CLASE Cola:
// el objeto "a" y el objeto "b". El objeto a
// es identificado como la cola # 1 y el
// objeto b como la cola # 2
a.Cponer(1500);
a.Cponer(225);
b.Cponer(879);
a.Cponer(436);
b.Cponer(-567);
b.Cponer(1);
cout << a.Ctomar() << " ";
cout << a.Ctomar() << " ";
cout << b.Ctomar() << " ";
cout << a.Ctomar() << " ";
cout << b.Ctomar() << " ";
cout << b.Ctomar() << "\n";
cout << "El programa ha concluido\n";
return 0;
// Los objetos creados por el programa son
// destruidos aqui al momento de concluir
// el programa. El ultimo objeto en ser
// destruido es el primer objeto en haber
// sido creado
}
Si se ejecuta el programa anterior, el resultado en la pantalla será el siguiente:
Hemos visto un ejemplo de cómo podemos pasarle un argumento a un objeto al momento en que es creado el objeto. El siguiente programa nos muestra la creación de cuatro objetos de la clase Arg_mul a los cuales se les pasan dos argumentos al momento de su creación:
#include <iostream.h>
class Arg_mult {
int i;
int j;
public:
Arg_mult(int a, int b);
void PonerArg_mult(void);
};
// Definicion de la funcion constructora
Arg_mult::Arg_mult(int a, int b) {
i = a;
j = b;
}
// Definicion de la funcion PonerArg_mult
// perteneciente a la clase Arg_mult
void Arg_mult::PonerArg_mult(void) {
cout << "(" << i << "," << j << ")\n";
}
main(void) {
// Creacion de cuatro objetos de la clase Arg_mult
Arg_mult w(21,16), x(7,83), y(-9,44), z(0,-55);
w.PonerArg_mult();
x.PonerArg_mult();
z.PonerArg_mult();
y.PonerArg_mult();
return 0;
}
PROBLEMA: ¿Cuál será el resultado del programa anterior?
Al ejecutarse el programa anterior, se imprime en la pantalla lo siguiente:
(21,16)
(7,83)
(0,-55)
(-9,44)
En C++, las reglas de alcance (scoping rules) nos dicen no solo cuándo una variable es válida sino también cuándo un objeto es válido; esto es, cuándo son creados, cuándo son destruídos y cuándo salen fuera de alcance. El alcance de una variable o de un objeto se extiende desde el punto en el cual es definida la variable o el objeto hasta el primer corchete de cierre que hace par con el corchete de apertura más cercano antes de que la variable sea declarada (o el objeto creado). En base a esto, en un programa que se dará más abajo y en el cual aparece lo siguiente:
main(void) {
Obj A(1), B(2), C(3);
{
Obj D(4), E(5);
}
{
Obj G(6);
{
Obj H(7), I(8);
}
}
}
los objetos A(1), B(2) y C(3) están disponibles (dentro de alcance) para todos los demás objetos, mientras que los objetos H(7) e I(8) no están disponibles (fuera de alcance) para ninguno de los demás objetos.
Una cosa que podemos hacer para abreviar un poco el código es usar el hecho de que odemos definir tanto a una funcion constructora como a una funcion destructora DENTRO de la declaracion de la clase. Esto lo pondremos en práctica en el siguiente programa que además muestra las reglas de alcance tal y como aplican a los objetos (en el programa se usa el mismo fragmento que se acaba de dar arriba):
#include <iostream.h>
int conteo = 0;
// Declaracion de una variable GLOBAL de tipo int, inicializada a cero
class Obj { // Declaracion de la clase Obj
int quien;
// Variable de tipo int para almacenar la identificacion de los objetos creados
public:
Obj(int ID) { // Funcion contructora
quien = ID;
cout << "El objeto " << quien;
cout << " ha sido creado e inicializado\n";
conteo++;
cout << "Numero de objetos: ";
cout << conteo << "\n";
}
~Obj() { //Funcion destructora
cout << "El objeto " << quien;
cout << " ha sido destruido\n";
conteo--;
cout << "Numero de objetos: ";
cout << conteo << "\n";
}
// Observese que podemos definir tanto a la funcion constructora como
// a la funcion destructora DENTRO de la declaracion de la clase como
// se acaba de hacer arriba
};
// El programa principal
main(void) {
Obj A(1), B(2), C(3);
// Creacion de tres objetos de la clase Obj.
// El objeto A es identificado como el objeto # 1, el objeto B como el objeto # 2,
// y el objeto C como el objeto # 3
{
// Con esta apertura de corchete preparamos la creacion de los objetos C y D
Obj D(4), E(5);
}
// Con el corchete de cierre, los objetos D y E son destruidos y la funcion
// destructora es invocada
{
Obj G(6);
// El objeto G es creado
{
Obj H(7), I(8);
// Los objetos H e I son creados
}
// Los objetos H e I son destruidos
}
// El objeto G es destruido
}
// Los objetos A, B y C son destruidos
PROBLEMA: Determínese la salida producida por el programa anterior.
Aplicando las reglas de alcance, la salida que esperamos del programa anterior es la siguiente:
El objeto 1 ha sido creado e inicializado
Numero de objetos: 1
El objeto 2 ha sido creado e inicializado
Numero de objetos: 2
El objeto 3 ha sido creado e inicializado
Numero de objetos: 3
El objeto 4 ha sido creado e inicializado
Numero de objetos: 4
El objeto 5 ha sido creado e inicializado
Numero de objetos: 5
El objeto 5 ha sido destruido
Numero de objetos: 4
El objeto 4 ha sido destruido
Numero de objetos: 3
El objeto 6 ha sido creado e inicializado
Numero de objetos: 4
El objeto 7 ha sido creado e inicializado
Numero de objetos: 5
El objeto 8 ha sido creado e inicializado
Numero de objetos: 6
El objeto 8 ha sido destruido
Numero de objetos: 5
El objeto 7 ha sido destruido
Numero de objetos: 4
El objeto 6 ha sido destruido
NUmero de objetos: 3
El objeto 3 ha sido destruido
Numero de objetos: 2
El objeto 2 ha sido destruido
Numero de objetos: 1
El objeto 1 ha sido destruido
Numero de objetos: 0
Un problema que se puede presentar al llevar a cabo una programación orientada a objetos se suele encontrar cuando en el código usado al declarar una clase aparece en la definición de dicha la clase una mención directa a otra clase que aún no ha sido declarada con anterioridad, lo cual puede motivar un rechazo del compilador. ¿Qué podemos hacer en tal situación? Esto no es muy diferente a lo que en nuestro estudio del lenguaje C sucedió con las funciones usadas en un programa cuando son puestas después de la función principal main(), lo cual muy posiblemente provocará un rechazo del código en la mayoría de los compiladores y entornos C. El problema se podía resolver de dos maneras, ya sea poniendo la función principal main() después de la definición de las funciones, o de manera más elegante y formal poniendo al principio del programa la declaración de los prototipos de las funciones (no el código completo de cada función). Aquí la situación no es muy diferente, y se puede resolver haciendo uso de lo que se conoce como referencia frontal (forward reference). Una referencia frontal a una clase es simplemente la palabra clave class seguida del nombre de la clase y terminada con un semicolon, sin poner aún la definición de la clase. La referencia frontal es requerida cuando dentro de la declaración de una clase se está invocando otra clase que aún no ha sido declarada o definida. A continuación se muestra un ejemplo de una referencia frontal:
En el ejemplo, podemos ver que se define una clase de nombre Caja. Y dentro de la definición de la clase Caja podemos ver que aparece la definición de una función, la función mismo_color() que recibe como argumento un objeto de la clase Linea. Pero suponemos que antes de la definición de la clase Caja no se ha dado aún la definición de la clase Linea, suponemos que la definición de la clase Linea será dada después de la definición de la clase Caja. Entonces, para evitar un rechazo de parte del compilador, antes de la definición de la clase Caja hacemos una referencia frontal a la clase Linea, con lo cual el código debe poder ser compilado sin mayores problemas.
En el ejemplo que se acaba de dar sobre el uso de referencias frontales, posiblemente el lector habrá observado que la función mismo_color(), una función de tipo int, es además invocada dentro de la clase Caja como una función amiga con el uso de la palabra clave reservada friend.
¿Y qué exactamente es una función amiga, una vez que ha sido declarada como tal? En el siguiente ejemplo, la función UACJ() es declarada como una función amiga de la clase muestra:
class muestra {
.
.
.
public:
friend void UACJ(void);
.
.
.
};
Las funciones amigas son funciones que sin ser funciones miembro de ninguna clase pueden tener acceso a las partes privadas de una clase, y para ello son declaradas dentro de la clase precedidas por la palabra clave friend. Una vez hecho, las funciones amigas pueden ser definidar como cualquier otra función ordinaria del lenguaje C. En el ejemplo mostrado, la función amiga UACJ, sin ser una función miembro de la clase muestra (o de ninguna otra clase) de cualquier modo puede tener acceso a las partes privadas de la clase muestra por haber sido declarada como función amiga de dicha clase.
El fragmento de código que se dió arriba para ilustrar el uso de las referencias frontales era de hecho parte del código de un programa completo que en otros tiempos se podía compilar y ejecutar sin problema alguno en muchas computadoras personales caseras basadas en la arquitectura IBM que usaba los procesadores Intel x86 trabajando en el modo de texto (con la pantalla DOS de líneas de comandos). Como una muestra del uso del operador punto para ejecutar funciones que forman parte de un objeto (y además por completitud y por el interés que pueda haber en el programa), a continuación se proporciona el programa completo (debajo del programa se explicará la razón por la cual el programa aún si puede ser compilado a un archivo ejecutable posiblemente no funcione tal y como se espera):
#include <iostream.h>
#include <conio.h>
// Los prototipos de las funciones de biblioteca gotoxy(), textcolor(), cprintf(),
// clrscr() y getch() asi como los enteros correspondientes a los macros GREEN, CYAN
// y WHITE se encuentran en este archivo de cabecera
class Linea;
// Observese la declaracion vacia de la clase Linea puesta al principio de las
// declaraciones de las clases, requerida en virtud de que la FUNCION AMIGA
// mismo_color en la declaracion de la clase Caja hace referencia a Linea antes de que
// la clase Linea sea declarada
// Declaracion de la clase Caja
class Caja {
int color; // El color de la caja
int xsup, ysup; // Esquina izquierda superior
int xinf, yinf; // Esquina derecha inferior
public:
// Funcion amiga
friend int mismo_color(Linea l, Caja c);
void fijar_color(int c);
void definir_caja(int x1, int y1, int x2, int y2);
void mostrar_caja(void);
};
// Declaracion de la clase Linea
class Linea {
int color;
int comienzox, comienzoy;
int longitud;
public:
// Funcion amiga
friend int mismo_color(Linea l, Caja c);
void fijar_color(int c);
void definir_linea(int x, int y, int l);
void mostrar_linea();
};
// Declaracion de la funcion mismo_color(), con la cual regresamos verdadero
// (entero 1) si la linea y la caja tienen el mismo color, y falso (entero 0) si son
// de un color diferente.
// Observese que esta funcion recibe como argumentos dos OBJETOS
int mismo_color(Linea l, Caja c) {
if(l.color == c.color) return 1;
// Si la linea y la caja tienen el mismo
// color, la ejecucion de la funcion termina
// en esta linea
return 0;
}
// Definicion de las funciones relacionadas con la clase Caja
void Caja::fijar_color(int c) {
color = c;
}
void Caja::definir_caja(int x1, int y1, int x2, int y2) {
xsup = x1;
ysup = y1;
xinf = x2;
yinf = y2;
}
void Caja::mostrar_caja(void) {
int i;
textcolor(color);
// Trazado de la linea horizontal superior de la caja
gotoxy(xsup, ysup);
// Posicionamiento del cursor hacia las coordenadas de la esquina izquierda
// superior
for(i=xsup; i<=xinf; i++) cprintf("-");
// Trazado de la linea horizontal inferior de la caja
gotoxy(xsup, yinf-1);
// Posicionamiento del cursor hacia las coordenadas de la esquina izquierda
// inferior
for(i=xsup; i<=xinf; i++) cprintf("-");
// Trazado de la linea vertical izquierda de la caja
gotoxy(xsup, ysup);
for(i=ysup; i<=yinf; i++) {
cprintf("|");
gotoxy(xsup, i);
}
// Trazado de la linea vertical derecha de la caja
gotoxy(xinf, ysup);
for(i=ysup; i<=yinf; i++) {
cprintf("|");
gotoxy(xinf,i);
}
}
void Linea::fijar_color(int c) {
color = c;
}
void Linea::definir_linea(int x, int y, int l) {
comienzox = x;
comienzoy = y;
longitud = l;
}
void Linea::mostrar_linea(void) {
int i;
textcolor(color);
gotoxy(comienzox, comienzoy);
for(i=0; i<longitud; i++) cprintf("-");
}
main(void) {
clrscr();
// Limpiamos la pantalla posicionando el cursor en el extremo superior
// izquierdo
Caja c;
// Declaracion del objeto c de la clase Caja
Linea l;
// Declaracion del objeto l de la clase Linea
// Trazamos una caja color ciano cuya esquina/ superior izquierda esta
// situada en las coordenadas (10,10) y cuya esquina inferior derecha esta
// situada en las coordenadas (15,15)
c.definir_caja(10,10,15,15);
c.fijar_color(CYAN);
// Podemos reemplazar el macro CYAN por 3
c.mostrar_caja();
// Trazamos una linea horizontal de color verde desde las coordenadas (2,2)
// hasta las coordenadas (10,2)
l.definir_linea(2,2,10);
l.fijar_color(GREEN);
// Podemos reemplazar el macro GREEN por 2
l.mostrar_linea();
// Comparamos el color de las lineas de la caja con el color de la linea horizontal
if(!mismo_color(l,c))
cout << "No son del mismo color\n";
cout << "Oprimir cualquier tecla";
getch();
// Ahora hacemos la linea y la caja del mismo color
l.definir_linea(2,2,10);
l.fijar_color(CYAN);
l.mostrar_linea();
// Volvemos a comparar el color de las lineas de la caja con el color de linea
// horizontal
if(mismo_color(l,c)) {
textcolor(WHITE);
cout << "Son del mismo color \n";
}
getch();
clrscr();
// Limpiamos la pantalla posicionando el cursor en el extremo superior izquierdo
// borrando la linea blanca dejada por la ejecucion de las dos funciones anteriores
return 0;
}
En las computadoras caseras (viejitas pero no tanto) en las cuales el programa anterior se puede compilar y ejecutar, se llevan a cabo dos acciones. En la primera acción se imprime lo siguiente (la línea horizontal que precede a la primera línea de texto es trazada de color verde):
---------- No son del mismo color
Oprimir cualquier tecla
y debajo del mensaje de texto se dibuja una cajita de color ciano. En la segunda acción, al oprimir el usuario cualquier tecla, se imprime lo siguiente (en esta ocasión en lugar de ser una línea horizontal de color verde se traza una línea de color ciano, con lo cual la linea horizontal que precede a la primera línea de texto y la cajita vienen quedando del mismo color):
---------- Son del mismo color
Oprimir cualquier tecla
y debajo del mensaje se mantiene dibujada la cajita de color ciano.
Se había dicho que aún si el programa puede ser compilado a un archivo ejecutable, es probable que el programa no funcione como se espera a menos de que el lector tenga disponible una computadora añeja que corresponda a cierta época muy específica, pero no antes ni después. Ciertamente, debe corresponder a un sistema en el que la salida a través de la pantalla se lleve a cabo en un monitor a colores (aunque sea un monitor CRT de baja resolución) ya que de lo contrario no se verán los colores verde y ciano. Sin embargo, no es necesario que la computadora tenga una tarjeta de adaptador gráfico para trabajar, ya que el programa está elaborado para correr en el modo de texto que este es el modo más básico y primitivo a esperar en cualquier computadora con salida hacia un monitor. Obsérvese que funciones como gotoxy() para posicionar el cursor en la pantalla funcionan en el modo de texto sin necesidad de que haya un adaptador gráfico instalado. Las primeras computadoras caseras basadas en los procesadores Intel solo podían poner en las pantallas de los monitores monocromáticos líneas de texto, eran las computadoras que funcionaban con ventanas de líneas de comandos tipo DOS y UNIX produciendo resultados como el siguiente:
Es precisamente en este tipo de monitores en los cuales el programa que se ha dado arriba podía funcionar sin problema alguno, con la máquina aún funcionando bajo un sistema operativo como MS-DOS. Y podía funcionar porque los programas ejecutables producidos con los compiladores y entornos C del momento proporcionaban todos los programas drivers requeridos para funcionar en el modo de texto. Hasta que hicieron su aparición los sistemas operativos Windows (empezando con Windows 95) que tomaron un control completo de todo lo que era puesto en la pantalla. Y muchos programas que antes se podían ejecutar sin interferencia alguna de parte del sistema operativo dejaron de ejecutarse, y muchos de los que se podían ejecutar en una ventana especial para emular el modo de texto tipo DOS empezaron a producir efectos inesperados como el tomar posesión aparente de toda la pantalla sacando fuera la barra de tareas y el escritorio en la pantalla de Windows, y cosas por el estilo.
De cualquier modo, existen entornos IDE en C que son capaces de tomar programas como el que hemos visto arriba (como el entorno Borland C++), y son capaces de compilarlos y poder producir una emulación de la ventana DOS de graficados simulando estar en un monitor de colores CGA o EGA o VGA o lo que se requiera, Precisamente en este entorno IDE funcionando bajo un sistema operativo Windows XP con una pantalla SVGA de resolución elevada se ha compilado y ejecutado el programa anterior, y en la ventana simulada DOS generada por el entorno IDE se produce la siguiente primera ventana:
Al oprimir el usuario cualquier tecla, aparece la siguiente segunda ventana:
Esto nos dá una lección importante: aunque la tecnología vaya evolucionando y se tengan programas viejitos que aparentemente ya no se pueden ejecutar en máquinas más modernas, la disponibilidad de un buen entorno de programación IDE puede recrear mediante una emulación las condiciones previas en las cuales se ejecutaba un programa que era capaz de producir algunos efectos especiales que hoy ya no son tan especiales. Pudiendo hacer esto, una vez que se está satisfecho de que el programa viejito funciona, el programa se puede modificar actualizándolo a los nuevos parámetros de las máquinas en uso actual.
Al usar funciones parametrizadas, hay ocasiones en las cuales al invocar a la función no se le proporciona un argumento que corresponda a un parámetro que la función espera recibir, y en tales situaciones se desea que la función pueda usar un valor predeterminado cuando no se le ha pasado un argumento, un argumento default.
El lenguaje C++ permite asignar un valor predeterminado a un parámetro cuando no se ha especificado un argumento al invocar una función. El valor predeterminado es especificado de una manera que es sintácticamente similar a una inicialización de variable. A modo de ejemplo, lo siguiente declara una función estatura() que toma como argumento una variable de tipo float y declara un valor predeterminado de 1.78:
void estatura(float h = 1.78)
{
.
.
.
}
De este modo, la función se puede invocar de dos maneras diferentes:
estatura(1.95); // se le pasa un valor explicito
estatura(); // usar el valor predeterminado
La primera invocación le pasa el valor 1.95 al parámetro h, mientras que la segunda invocación le pasa automáticamente a h el valor 1.78.
Los argumentos predeterminados forman parte de C++ para darle al programador más instrumentos para poder administrar una mayor complejidad en los programas. Para poder manejar la mayor cantidad posible de situaciones, es frecuente que una función contenga más parámetros que los que se requieren para sus usos más comunes. Los argumentos predeterminados le permiten al programador utilizar y especificar únicamente los argumentos que se requieren para una situación exacta en lugar del caso general. Para dar una mejor idea del uso dado a los argumentos predeterminados, considérese la siguiente porción de código que no siempre se encuentra en las bibliotecas del lenguaje:
void salidaxy(char *hilera, int x, int y)
{
if(x==-1) x = wherex();
if(y==-1) y = wherey();
gotoxy(x, y);
cout << hilera;
}
La función salidaxy() pone en la pantalla en modo de texto (no en modo gráfico) la hilera hacia la cual se está apuntando con la variable hilera, a partir de la ubicación X y Y definida por x y y. Si no se especifica ni x ni y, entonces la hilera es colocada en la localidad actual (X,Y) del modo de texto. La definición de la función salidaxy() se apoya en las funciones de biblioteca wherexy(), wherey() y gotoxy() disponibles en muchos archivos de cabecera CONIO.H. Las primeras dos funciones regresan las coordenadas actuales (medidas en un sistema de coordenadas en la pantalla en las que el origen está puesto en la esquina superior izquierda) X y Y, respectivamente, mientras que la tercera mueve el cursor hacia las coordenadas de la localidad X y Y especificada. El siguiente programa muestra el fragmento dentro de un programa completo:
#include <iostream.h>
#include <conio.h>
void salidaxy(char *hilera, int x = -1, int y = -1);
main(void)
{
salidaxy("hola", 5, 5);
salidaxy(" mis amigos");
salidaxy("me gusta el C++", 32); // aun en la linea 5
salidaxy("esto empieza en la linea 11.\n", 1, 11);
salidaxy("esto empieza en la linea 12.\n");
salidaxy("esto empieza en la linea 13.");
return 0;
}
void salidaxy(char *hilera, int x, int y)
{
if(x==-1) x = wherex();
if(y==-1) y = wherey();
gotoxy(x, y);
cout << hilera;
}
Si se ejecuta el programa, el resultado obtenido es el siguiente:
Por lo general, los valores de los argumentos predeterminados solo pueden ser declarados una vez, en el prototipo de la función puesto al principio del programa. Tratar de repetir:
void salidaxy(char *hilera, int x = -1, int y = -1)
en la definición del código de la función posiblemente hará que el compilador proporcione un mensaje de error como el mensaje “Previously specified argument value cannot be changed” (Valor de argumento especificado previamente no puede ser cambiado).
Como lo muestra el programa, aunque en ocasiones es útil especificar el punto exacto (coordenada) a partir del cual el texto será impreso (de izquierda a derecha, desde luego), frecuentemente es deseable continuar a partir del punto en el cual ocurrió la última impresión. Con argumentos predeterminados podemos usar la misma función para ambas cosas sin necesidad de tener que codificar dos funciones distintas. Obsérvese que en la función principal main(), la función salidaxy() es invocada con uno, dos, y tres argumentos. Cuando la función es invocada con un solo argumento (el primero, o sea sea la hilera de texto), para los otros dos argumentos se usan los valores predeterminados para x y y. Cuando la función es invocada con los primeros dos argumentos, únicamente se usa el valor predeterminado de y. Cuando una función es invocada, todos los argumentos son aparejados a sus parámetros respectivos siguiendo un orden de izquierda a derecha. Una vez que los argumentos existentes (cuando los hay) han sido aparejados, cualesquier valore predeterminado que haya por usar es usado. Es importante observar que todos los parámetros que toman valores predeterminados tienen que aparecer a la derecha de aquellos que no toman valores predeterminados. Una vez que se han definido parámetros que toman valores predeterminados, no se puede especificar un parámetro que no lo toma. Esto significa que lo siguiente es incorrecto:
void salidaxy(int x = -1, int y = -1, char *str)
void Bessel(int n, float e = 2.7182, float x)
Una vez que empieza la lista de parámetros predeterminados, no pueden aparecer parámetros no predeterminados en el resto de la lista.
Podemos utilizar parámetros predeterminados en la función constructora de un objeto. A modo de ejemplo se puede dar la siguiente variante de la función constructora Cola() dada previamente:
Cola::Cola(int ID = 0) {
rloc = sloc = 0;
quien = ID;
cout << "La cola " << quien << " ha sido inicializada\n";
}
De este modo, si un objeto de la clase Cola es creado sin usar valores de inicialización, entonces ID toma el valor predeterminado de 0. De este modo:
Cola a, b(4);
crea dos objetos a y b, teniendo b un valor ID de 4 y teniendo a un valor ID de 0.