Documentos de Académico
Documentos de Profesional
Documentos de Cultura
CPP Programacion Orientada Objetos PDF
CPP Programacion Orientada Objetos PDF
UN ENFOQUE PRCTICO
QU MATERIAL SE NECESITA?
QU OBJETIVO SE PERSIGUE?
Intimida abrir cualquier revista tcnica sobre OOP por la cantidad de siglas y
jerga incomprensible que aparece en cada pgina. Encontramos con
demasiada facilidad claves como OBCS, HSC, ODS, EER, etc. de difcil
traduccin. Qu ocurre? Vive la comunidad OOP en un mundo aparte?
Bien, la verdad es que s. No existen tcnicas estndares ni en anlisis ni en
diseo, por lo que investigadores y equipos privados desarrollan
continuamente tcnicas propias, frecuentemente sin ningn nexo comn;
junto con stas, naturalmente, desarrollan tambin sus propias siglas y
terminologas. En general podemos afirmar que O representa Objeto, OO
equivale a Orientado-a-Objetos, A vale por Anlisis y D por Diseo, R
significa Requerimientos, L representa Lenguaje y C suele equivaler a Clase.
De esta forma OOAR, por ejemplo, significa Requerimientos del Anlisis
Orientado-a-Objetos. Mi consejo es, de cualquier manera, que el lector no
d por asumidos significados intuitivos a siglas o conceptos cuya
procedencia no conozca, por muy elementales que stos parezcan.
Bien. Hemos visto en el apartado anterior que la OOP intenta solucionar los
problemas originados por el "enfoque estructurado". Todo esto est muy
bien, pero estas nuevas tcnicas necesitan de lenguajes de programacin
adecuados. Surgieron, as, lenguajes de nueva creacin como Smalltalk,
Eiffel, Actor, etc. Por otro lado se intent dotar de extensiones
Orientadas-a-Objetos a los lenguajes clsicos ms importantes, como C,
Pascal, Fortran, Ada, Cobol, Lisp, etc., originando C++, Object Pascal,
CLOS, etc.
1
Lo cierto es que a estas alturas subsisten importantes diferencias de criterio
entre distintos autores a la hora de establecer los pilares en que se apoya la
Programacin Orientada-a-Objetos. El mismo autor del presente texto sostiene, por
ejemplo, que la herencia no es una caracterstica bsica del paradigma, sino ms
bien un mecanismo que permite la implementacin de jerarquas polim rficas.
Intentanto ser lo ms general posible, he reflejado, no obstante, las caractersticas
ms comunmente aceptadas por el grueso de expertos en estas reas.
SalaDeCine cineParadox;
SalaDeCine *punteroASalaDeCine;
cineParadox.empezarProyeccion();
void SalaDeCine::empezarProyeccion()
{
// apaga msica y luces y comienza proyeccin
ponerMusicaDeFondo( OFF );
ponerLucesSala( OFF );
marchaProyector( ON );
}
En este caso el mensaje empezarProyeccion podra ser enviado al objeto cineParadox por un objeto
cronmetro con un horario determinado.
marchaProyector( ON );
equivale a
this->marchaProyector( ON );
Hemos visto, pues, que el objeto cineParadox responde al mensaje de iniciar la sesin envindose
a s mismo distintos mensajes de control, que a su vez enviarn otros mensajes (a ste u otros objetos) o
manipularn sus propios datos internos. Aparece ahora claro que, en general, el envo de un mensaje a un objeto
que carezca del mtodo para responderlo, por s mismo directamente o mediante herencia o delegacin, ser
calificado, en C++, como un error en tiempo de compilacin: lo mismo que ocurrira si llamramos en C a una
funcin inexistente.
ABSTRACCIN
ENCAPSULACIN
Una class en C++ es como un struct del tipo usado en C que admite en su
cuerpo tanto variables como funciones, ms unas etiquetas que controlan el
acceso a ambas.
Class Racional {
friend Racional& operator+( Racional, Racional );
// ...
private:
int numerador;
int denominador;
// ...
void simplificaFraccion();
// ...
public:
// ...
void estableceNumerador( int );
void estableceDenominador( int );
// ...
};
Observemos que en la clase Racional, abstraccin del conjunto de los nmeros racionales, de su
representacin formal (x/y: equis dividido por y) y operaciones (a/b + c/d), aparecen encapsulados tanto los
datos internos (numerador, denominador) como las funciones miembro para el tratamiento de stos (el operador
suma de quebrados, el procedimiento para cambiar el denominador, el mtodo para simplificar fracciones, etc.).
Class SalaDeCine {
private:
int aforoSala;
char *nombreSala;
// ...
public:
void alarmaEnCasoDeIncendio();
void empezarSesion();
// ...
};
La clase SalaDeCine aglutina, segn lo expuesto, tanto los datos correspondientes a las salas de cine en
general (capacidad de la sala, nombre de la misma, etc.) como los mtodos de respuesta a los mensajes dirigidos
a los objetos "salas de cine" (alarma por incendio, etc.). O sea, la clase SalaDeCine es la abstraccin resultante
de la asimilacin intelectual de todas las posibles salas de cine, vistas o soadas: aqu tenemos la cualidad de
abstraccin. La misma clase encapsula, por otro lado, datos y funciones dentro de un lmite bien-definido: cul?
las paredes del edificio? No, vaya! El lmite es el mismo que el que separa una sala de cine de un almacn de
frutas: su propia definicin, que en este claso coincide con el bloque de la clase. La cpsula esta formada por los
brazos {} que encierran el cuerpo de la clase.
Conviene notar que los tipos de datos incorporados al compilador (int, char,
long, etc.) son realmente abstracciones encapsuladas de datos y mtodos.
Consideremos el siguiente cdigo:
2
Cuando hablo de tipos predefinidos o incorporados me refiero a los tipos de
datos que el compilador, tal y como sale de su envoltorio original, reconoce: char,
int, long, short, double, float, etc.
multiplicando1 x multiplicando2 x
multiplicando3 x multiplicando4
( ( ( multiplicando1 x multiplicando2 ) x
multiplicando3 ) x multiplicando4 )
HERENCIA
class Edificio {
// ...
protected:
int numeroDePlantas;
char* direccion;
Fecha fechaConstruccion;
// ...
public:
virtual void alarmaEnCasoDeIncendio();
// ...
};
Hemos hecho derivar la clase SalaDeCine de la clase base Edificio (en este caso se ha realizado una
derivacin pblica, significada por la notacin : public Edificio de la cabecera de la clase SalaDeCine, y que
explicaremos ms adelante). De este modo aprovechamos la abstraccin realizada en el nuevo tipo de dato
Edificio, no teniendo necesidad de volver a implementar en la clase SalaDeCine las variables y mtodos ya
adscritos a la clase base (efectivamente, la clase derivada puede hacer uso, nicamente restringido por las
etiquetas de acceso, de las funciones y los datos de la clase base sin necesidad de redeclararlos). Esto quiere
decir, en definitiva, que, aunque no lo hayamos declarado expresamente, la clase SalaDeCine encapsula,
tambin, datos como el numeroDePlantas o la fechaConstruccion. Es como si, al derivar la clase SalaDeCine de la
clase Edificio, se aadiera de alguna forma todo lo que hemos declarado en la clase base a la clase derivada. O
sea, que al declarar esta derivacin lo que estamos haciendo es aadir un pegote de la clase "padre" a nuestra
clase "hija". Bueno, la verdad es que fsicamente ocurre ni ms ni menos que esto (con la excepcin de las clases
bases virtuales, de las que hablaremos bastante ms adelante): la porcin del cdigo de la clase base se aade
en el mdulo objeto a la codificacin propia de la clase derivada. Pero, atencin, esto no quiere decir que
podamos usar indiscriminadamente cualquier dato o funcin de la clase base. Vaya! Y por qu no? -preguntar
el lector-: Para qu queremos aadir un "pegote" que luego no podamos utilizar? Bueno, esto tiene que ver con
las "etiquetas" de que hemos hablado antes, y tambin con el tipo de derivacin utilizado. Entonces, hay varios
tipos de derivacin?. S. Existen tres tipos de derivacin: pblica, privada y protegida, como antiguamente
decase que existan tres tipos de descendencia humana: legtima, secreta y bastarda (o natural). Pero, bueno,
contestando a la pregunta podra decirse que el bloque del "padre" se traspasa ntegro y luego, dependiendo del
tipo de derivacin y de las caractersticas de las funciones y datos de la clase base, as como de cmo y quin
pretenda usarlas, el sistema dir: "Puede usarse" o "NO puede usarse". Adems, es bueno que el padre, o clase
base, pueda restringir el acceso a ciertas cosas (pinsese, si no, en el dormitorio conyugal en la vida marital real).
POLIMORFISMO
Esta propiedad, como su mismo nombre sugiere (slo para los que se
manejen en griego: mltiples formas), se refiere a la posibilidad de acceder
a un variado rango de funciones distintas a travs del mismo interfaz. O sea,
que, en la prctica, un mismo identificador puede tener distintas formas
(distintos cuerpos de funcin, distintos comportamientos) dependiendo, en
general, del contexto en el que se halle inserto.
cmo sabr el sistema cul de las funciones suma tiene que ejecutar?
Bueno, en sucesivos captulos veremos que el sistema aplica a rajatabla unas
determinadas reglas en un orden muy preciso, y ayudndose de stas
determina (si puede) a qu funcin concreta debe llamar.
class Racional {
friend Racional& operator+( Racional, Racional );
// ...
};
class Real {
friend Real& operator+( Real, Real );
// ...
};
Edificio::alarmaEnCasoDeIncendio()
{
virtual llamarEstacionDeBomberos();
virtual llamarEstacionDePolicia();
}
SalaDeCine::alarmaEnCasoDeIncendio()
{
lucesSala( ON );
marchaProyector( OFF );
llamarEstacionDeBomberos();
llamarEstacionDePolicia();
abrirPuertasEmergencia();
// ...
}
pues en C++ una lista de argumentos vaca significa "sin argumentos". Para
asegurar la compatibilidad se aconseja en un primer estadio, aunque en
C++ es un anacronismo, el uso de la notacin funcion(void) para indicar
funciones sin argumentos tanto en C como en C++.
De acuerdo con lo anterior, la funcin main(), en aras de una homogeneidad conceptual, ser
siempre codificada as:
void partidaDeBingoDeSupersticiosos()
{
numero = extraeBola();
if ( numero < 1 || numero > 99 )
goto Fin; //Error en C++
for ( int contador = numero; contador < 99; contador++) {
switch ( contador ) {
case 13:
cout << "La empresa es supersticiosa.";
Fin: cout << "\nFin de partida.";
return;
default:
cout << "Ha salido el nmero: "
<< numero << "\n";
break;
}
}
}
Vemos, a la vez, extraas construcciones del tipo cout << X, que responden a la
interpretacin en C++ del grupo printf(...) de C. Digamos, por el momento, que cout es un objeto predefinido
para el direccionamiento de objetos al dispositivo de salida estndar, << es el operador de insercin (recordemos
aqu lo ya dicho sobre los operadores), mientras que X es el objeto a insertar. En definitiva, tal sintaxis resultar
en la impresin del objeto X, tal y como, si de manera informal, escribiramos printf("%x", X). Sigamos.
es equivalente a
// ...
char* dato = "hola"
void* punteroADato = &dato
int* punteroAEntero = punteroADato; // error en C++
// ...
int resultado = 100 * (*punteroAEntero) // INDEFINIDO!
// ...
as que una asignacin de NULL a un puntero a cualquier tipo, como por ejemplo
// error en C++
int* punteroAInt = NULL;//equivale a punteroAInt = (void*)0
#ifdef __cpluscplus
# define NULL 0
#else
# define NULL (void*)0
#endif /*__cplusplus */
3
Incidentalmente, y como corolario del espinoso tema de la definicin de NULL,
debemos notar que la portabilidad del cdigo podra verse truncada, tambin, por
declaraciones como la siguiente, encontrada en ficheros de cabecera DOS para el
modelo de memoria large:
#define NULL 0L
Las variables globales -declaradas fuera del mbito de cualquier funcin- con
tipo const se consideran en ANSI C con tipo de enlace extern, mientras
que en C++ se enlazaran como static.
o simplemente
extern "C" {
extern size_t strlen( char* );
// ...
}
o an
extern "C" {
# include <stdio.h>
// ...
}
4
Hay que notar que el lenguaje C++ nicamente asegura la validez de los
cdigos "C++" y "C", siendo los restantes ("FORTRAN", por ejemplo)
dependientes de la implementacin especfica del compilador.
No se permite, por otra parte, el uso de tal directiva de enlace dentro del
cuerpo de definicin de una funcin, as como su uso de ninguna manera
implica la conversin de tipos entre lenguajes de programacin (strings en
FORTRAN a strings en C++, etc.), siendo responsable del explcito ajuste el
programador. O sea, que esta directiva, como ya ha sido dicho, nicamente
afecta al name mangling de los identificadores. Nada ms. Y nada menos.
Pensar que una simple clave podra "transformar" cdigo de un lenguaje a
otro, sin ms, es pensar demasiado. Bajemos a planeta Tierra (y digamos,
de paso, que existen herramientas comerciales para llevar a cabo esta tarea,
aunque ste no es nuestro tema).
Es interesante notar que la declaracin extern "C" aplicada a la definicin de una funcin
contenida en un mdulo C++ funcionar correctamente, causando que tal funcin (no sobrecargada, en
previsin de pausibles errores de compilacin) no sea internamente codificada por el esquema de enlace de
tipo-seguro. Tendramos, as, una funcin compilada en C++ con tipo de enlace C. Esta tcnica puede ser
empleada, por ejemplo, para la llamada desde C a funciones miembro de clases en C++.
[@NombreClase][@nombreFuncion$qArgumentos || @datoMiembro]
MBITO DE STRUCT'S
struct estadoImpresora {
enum { APAGADA, ENCENDIDA, ESPERA } status;
// ...
}
char* ESPERA = "Impresora en espera";
En C++, sin embargo, struct define su propio mbito, por lo que las
enumeraciones declaradas en su bloque son locales a ste. El cdigo
anterior es legal, pues, en C++. O sea que, ojo a lo que contienen los
structs en nuestro cdigo C. Una buena forma de conocer los problemas es
intentar compilar como C++ lo codificado en C, e ir revisando los errores y
warnings uno a uno.
En ANSI C una constante literal de carcter posee tipo int. Esto es,
En C++, sin embargo, una constante literal de carcter posee tipo char:
5
Cuando decimos que algo no es necesariamente cierto en C++, esto significa
que ese "algo" en cuestin depende de la implementacin C++. Es decir, en un
sistema dado un char puede ocupar los mismos bits que un int, mientras que en
otro estos dos tipos pueden tener tamaos distintos. La estandarizacin de C++
nicamente impone que un determinado tipo debe tener un tamao igual o mayor
las variables soyGuapo y tengoDinero seran de tipo int en ANSI C, mientras que en C++ seran de tipo
boolean.
MACRO __cplusplus
#ifdef __cplusplus
const limiteVector = 10;
#endif /* __cplusplus */
#ifndef __cplusplus
#define limiteVector 10
#endif /* __cplusplus */
DEFINICIONES MLTIPLES
char* cineParadox;
char* cineParadox; //error en C++: mltiple inicializacin
// ...
int main( int, char** )
{
char* cinePalafox;
char* cinePalafox; // error en ANSI C y en C++
// ...
return 0;
}
class Float {
private:
float numero;
// ...
};
La sentencia
Float numeroConComaFlotante;
float numeroConComaFlotante;
int direccion = 1;
void introduceDatosClientes() {
struct direccion {
char* tipoViaPublica;
char* calle;
int numeroDePolicia;
char* codigoPostal;
char* ciudad;
char* provincia;
};
// la siguiente sentencia imprimir el nmero 1
// en el dispositivo de salida estndar
// si este cdigo se compila en ANSI C,
// procurando, sin embargo, un error en C++
// por uso impropio del identificador
printf( "%i\n", direccion + 0 ); //error en C++
// la siguiente sentencia equivale, en ANSI C, a
// sizeof( int )
printf( "%i", sizeof( direccion ) );
// ....
}
class SalaDeCine {
// ...
public:
SalaDeCine() {};
SalaDeCine( char* );
// ...
};
SalaDeCine cineParadox = SalaDeCine( "Cine Paradox" );
/*
Los delimitadores de este comentario
corresponden al viejo estilo C.
*/
Estos delimitadores, como es bien sabido, son usados para encerrar varias
lneas de cdigo y no se pueden anidar: esto es, el cdigo
f = m * a * a */;
/*
void solicitaClaveDePaso()
{
cout << "Introduzca clave de acceso: ";
cin >> clave; /* mi clave: LOGOS */
validaClave( clave );
}
*/
validaClave( clave ); } */
f = m * a; / * frmula de la fuerza * /
v = e / t; / / frmula de la velocidad
double v = e;
double v = e/t;
Este problema puede evitarse insertando un espacio tras el operador '/' as:
int superficieHabitable[ 7
Bien, queda por considerar la cuestin: cundo se debe usar uno u otro
delimitador? En teora el estilo C debera utilizarse para encerrar bloques de
varias lneas, mientras que los nuevos delimitadores parecen ms apropiados
para comentarios puntuales de lneas concretas de cdigo. Qu ocurre, sin
embargo, en el planeta Tierra? -como dira Allen-. Lo cierto es que si
examinamos un archivo de cabecera tpico de C++ encontraremos una
letana interminable de '//' situadas al inicio de cada lnea, donde pudiera
parecer mucho ms apropiado y requerira muchas menos pulsaciones el
viejo estilo C: o sea, en el fondo a los desarrolladores de C++, como
comentbamos al principio, nos gusta diferenciarnos y, demonios!, por qu
no? Para el principiante puede significar tambin un aliciente pensar que, al
comentar de esta nueva forma su primera lnea de cdigo, ya est
"desarrollando en C++". Bendita salvedad!
El operador '::' posee dos usos en C++. Por un lado puede utilizarse para
acceder a una variable global cuya visibilidad ha sido ocultada por una
variable local. Vemoslo en el siguiente ejemplo:
// ...
// la siguiente funcin fue definida anteriormente
int edad = calculaEdad( fechaNacimiento);
const int edadMinima = 18; // siempre mejor que usar #define
void intentaFranquearAccesoABar( int edad )
{
// slo modificar la variable local edad
if ( ::edad < edadMinima ) // chequea la variable global
edad = 16; // cambia el valor de la variable local
cout << "Djeme pasar al bar: ya tengo "
<< edad << " aos.";
// ::edad no ha cambiado en ningn caso
}
Bueno, el ejemplo est un poco "trado por los pelos", pero nos permite
recordar que debe tenerse en cuenta que el mbito del parmetro formal de
una funcin se limita al mbito local delimitado por el bloque constituido por
sta. La notacin ::edad se refiere a la variable global edad de tipo
constante int, mientras que la variable local edad contenida en el mbito
local de la funcin oculta la visibilidad de aqulla.
El operador '::' se usa tambin en C++ cuando una funcin miembro de una
clase se define fuera del bloque en que sta se declar. Bueno, ya sali: ni
habindolo anunciado en el prembulo podemos libranos de las clases: al fin
y al cabo estamos en C++. Intentar explicarlo mediante un ejemplo:
class Racional {
private: // etiqueta cualificadora de acceso privado
int numerador; // dato miembro de la clase
// ...
public: // etiqueta cualificadora de acceso pblico
// sigue una declaracin de una funcin miembro
void estableceNumerador( int );
// ...
};
Hay que notar que el mbito de una variable declarada dentro de una
expresin no se limita al bloque que sta define, sino que se extiende desde
el lugar de declaracin hasta el final del bloque que contiene a la expresin.
Vemoslo en el siguiente fragmento de cdigo:
// variables globales
const float proporcion = 1;
int coordenadaX = 20;
// parmetros por defecto en declaracin funcin
double transformacionEscala( double, double = 1.0 );
// sigue declaracin "normal"
void solicitaConfirmacion( int, int );
// ...
double transformacionEscala( double parametro, double escala )
{
return parametro * escala; // escala vale 1.0 por
// defecto
// (en declaracin funcin)
}
// parmetros por defecto en definicin
// y declaracin coincidentes
float escalaProporcion( float medidaReal, float medidaPlano,
float coeficiente = proporcion )
{
return coeficiente * medidaReal / medidaPlano;
}
// parmetros por defecto en definicin funcin
void solicitaConfirmacion( int x = coordenadaX, int y = 40 );
{
CajaSiNo::dialogo( this, x, y ); // 'this' es un
// puntero implcito
// al objeto
}
void imprimeNodos()
{
int miOrden = 1;
// la siguiente definicin de funcin incurre en ERROR
// en C++ 3.0, compilando, sin embargo, correctamente
// bajo C++ 2.0 y anteriores.
void recorreArbolBinario( Arbol* miArbol,
orden = miOrden );
}
return 0;
}
Observamos, pues, que una funcin dada podr ser redeclarada para
aadirle parmetros por defecto, con las nicas restricciones de no incurrir
en la redeclaracin de asignaciones de valores por defecto y no vulnerar la
continuidad de tales asignaciones desde su aplicacin en un parmetro hasta
el parmetro final de la funcin. Veamos algunos ejemplos, comentando
bajo cada lnea su correctitud y, en su caso, un ejemplo de aplicacin:
int& numeroCabalistico = 7;
int enteroTemporal = 7;
int& numeroCabalistico = enteroTemporal;
equivale en la prctica a
Las referencias son usadas como parmetros formales de funciones por las
mismas razones que se usan los punteros: para permitir la modificacin de
los datos en el mbito local de las funciones y para evitar la penalizacin en
tiempo de ejecucin que supone la copia de los argumentos en las llamadas
a funciones con argumentos pasados por valor. Examinemos el siguiente
ejemplo:
EL ESPECIFICADOR "INLINE"
Hay que notar, ant todo, que una funcin inline no es igual a una macro del
preprocesador, pues en stas no se produce chequeo alguno de tipos. Se
obtiene, pues, "lo mejor de dos mundos". Conviene destacar, por otro lado,
que el especificador no surtira efecto sobre una declaracin de funcin, pues
no habra cdigo que sustituir.
SOBRECARGA DE FUNCIONES
6
Atencin a esta caracterstica! A pesar que la afirmacin es rigurosamente
exacta, el siguiente cdigo (y otros parecidos) podr ser compilado sin problemas
con Borland C++ 3.X y 4.0, mientras que AT&T C++ 3.0 -de acuerdo con lo
establecido en ARM- flagelara como errores las dos ltimas lneas:
sea declarada como error, pues encaja con cualquiera de los dos prototipos.
Aqu surge la cuestin sobre la idoneidad de tal sobrecarga, resultando as
que o bien se aade el tercer argumento al prototipo de la funcin original,
Se trata aqu de una cuestin de adaptacin del compilador a los estndares del
lenguaje. Debemos recordar que, aun siendo Borland C++ 4.0 la implementacin de
C++ para PC's ms ajustada a los "estndares", sta se basa slo parcialmente en
AT&T C++ 3.0 y en el borrador del estndar de C++ proveniente de X3J16. Deberemos
esperar a futuras versiones para ver si se solucionan estas "curiosidades". Mi
consejo: el lector deber evitar las construcciones del tipo expuesto, pues de otra
manera su cdigo en el futuro podra no ser portable.
1) Conversin trivial: tipo pasa a tipo&, tipo& pasa a const tipo&, tipo&
pasa a volatile tipo&, tipo* pasa a const tipo* y tipo* pasa a volatile tipo*.
2) Promocin: los tipos char, unsigned char, short int e int son
promocionados a int si int puede contenerlos, o a unsigned int de otra
manera; float pasa a double y double pasa a long double.
Hay que notar que si bien los intentos de ajustar un valor a un tipo de
argumento formal se dan mediante la aplicacin ordenada de los diferentes
formatos de conversin, no existe prevalencia de conversin dentro de cada
uno de stos, de forma que debemos desechar la idea intuitiva de que el
compilador efectuar la conversin que requiera menos esfuerzo o tiempo:
en realidad el compilador probar todas las posibilidades dentro de cada uno
de los cinco tipos de conversin, declarando un error de ambigedad cuando
sea posible realizar ms de una correspondencia entre valor y tipo de
argumento. No existe, tampoco, precedencia en la conversin en razn del
orden de declaracin de las funciones sobrecargadas. As, por ejemplo,
dadas las siguientes declaraciones:
en el siguiente cdigo:
7
Surge aqu de nuevo lo ya apuntado en la nota anterior: esta lnea compilar
sin error con Borland C++ 3.X, aunque s fallar en el cfront 3.0 de AT&T.
Afortunadamente Borland C++ 4.0 ha subsanado este desajuste, y correctamente
origina un error en compilacin por ambigedad en tal lnea.
8
El adjetivo estndar relativo al lenguaje C++, como se ha advertido varias
veces, se refiere a las caracters ticas derivadas de las implementaciones de AT&T.
En el presente caso la sobrecarga del operador ::new aparece predefinida en el
archivo "new.h", mientras que en otras imple mentaciones pudiera no existir tal
declaracin, debido, entre otras cosas, a la facilidad con que tal sobrecarga puede
ser implementada por el propio desarrollador.
Hasta ahora hemos visto la creacin de nuevos objetos por medio de los
constructores por defecto de las clases de las que seran instanciaciones. La
creacin de nuevos objetos usando otros constructores puede realizarse
mediante la sintaxis:
ClaseEjemplo* punteroAObjetoClaseEjemplo =
new ClaseEjemplo( argumento1, ... );
set_new_handler( errorPorMemoriaLibreAgotada );
delete punteroAObjetoDeClaseEjemplo;
delete[] punteroAArrayDeObjetosClaseEjemplo;
delete [ 6 ] punteroAArrayDeObjetosClaseEjemplo;
Class ArrayDeClasesEjemplo {
private:
ClaseEjemplo** punteroAArrayDeClasesEjemplo;
//...
}
ClassArrayDeClasesEjemplo* punteroAObjetoArray;
punteroAObjetoArray = new ClassArrayDeClasesEjemplo;
delete punteroAObjetoArray;
delete punteroAArrayDeObjetosClaseEjemplo;
Por supuesto los operadores globales son siempre accesibles por medio del
operador ::, de forma que pueden de esta manera ser invocados en
sustitucin del posible operador sobrecargado que en razn de las reglas de
mbito conviniera aplicar.
9
Con la nica excepcin de arrays unidimensionales (vectores) que carezcan
del operador delete y de destructor, como, por ejemplo, los vectores de tipos
incorporados (int, float, etc.)
switch ( respuesta ) {
case NO:
cout << "La respuesta es NO";
default:
cout << "No sabe. No contesta";
}
int
main( int, char** ) {}
podra inducir a pensar que se estn declarando dos punteros a char (los
espacios no son considerados por el analizador lxico del compilador).
10
Pinsese que el compilador ignora, a efectos de optimizacin del cdigo, el
hecho de que unas determinadas variables hayan sido declaradas o no en la
misma lnea. La mana economizadora no proporciona, pues, a excepcin de en
algunos ejemplos triviales, ninguna ventaja apreciable, procurando, en la mayora
de los casos, una dificultad adicional para el lector. Mi consejo: eviten las
multi-declaraciones en una lnea! Reserven el ingenio para la concisin de los
algoritmos!
double calculaTasaInternaDeRetorno()
{
// aqu viene el cdigo
}
if ( condicion ) hazAlgo();
if ( condicion )
hazAlgo();
Bien: esto es todo. Dumas sola decir: "Prefiero los malvados a los imbciles;
aqullos, por lo menos, descansan". Y es que pocas cosas hay tan
insoportables como la gratuita e inmisericorde pesadez. Sepa de cualquier
manera el lector que existen textos enteros consagrados a esta materia
(como el de Indian Hill para C), as como manuales de corporaciones con
sus correspondientes normativas estilsticas "de empresa". Vayamos, por fin,
a C++ y a las clases.
class ClaseVacia { // 1
}; //atencin al punto y coma
// tras la declaracin de la clase
class ClaseSinDatos { // 5
char* miClaveDeAcceso();//acceso PRIVADO por defecto // 6
public: //cambio a acceso PBLICO
int suma( int a, int b) { return a + b; } // 7
private: //acceso PRIVADO de nuevo
long edadDeLolaFlores;
};
class ClaseConDatosYMetodos { // 8
private:
long numeroDNI;
char letraDNI;
public:
char calculaLetraDNI( long );
ClaseSinMetodos miObjetoDeDatos;
miObjetoDeDatos.numeroDNI = 21428748; // OK
miObjetoDeDatos.letraDNI = 'Q'; // OK
miObjetoDeDatos.nombreCliente = "Landr" // ERROR: el acceso a
// nombreCliente es PRIVATE
// ...
claseSinMetodos objetoSinMetodos;
cout << objetoSinMetodos.numeroDNI; // Ok: legal;
cout << objetoSinMetodos.nombreCliente; // ERROR: acceso a
// miembro PRIVATE
6. En una clase el acceso es private por defecto. Tal circunstancia puede ser
modificada por cualquier otra etiqueta expresa de cualificacin de acceso:
public o protected (un tipo especial de restriccin de acceso que
estudiaremos cuando veamos la derivacin de clases, y que por ahora
asimilaremos como que surte los mismos efectos que private), que pueden
aparecer sin restriccin de cantidad en cualquier seccin del cdigo de
descripcin de la clase.
7. La funcin suma(int,int) se ha definido dentro del cuerpo de descripcin de la clase. Esto supone que,
automticamente, tal funcin ser considerada inline y, por tanto, convenientemente macro-expandida
(observando, naturalmente, las restricciones que a tal respecto pueda imponer el compilador, segn establecimos
en el captulo anterior).
8. Una clase con mtodos y datos supone el modelo ms general, del que
los casos anteriores se constituyen en particularizaciones slo prcticas a
efectos didcticos, pero sin ningn valor formal (no existen por tanto, ahora
lo podemos decir, esos "tipos especiales" de clases, aunque s es posible que
se den clases sin datos, etc. O sea, existen mujeres fatales, pero esto no
quiere decir que las mujeres se dividan morfolgicamente en normales y
fatales, aunque otra cosa pensara Jardiel). Pensemos, as pues, en las clases
como colecciones de atributos y servicios comunes a bien-definidos
conjuntos arbitrarios de objetos, de forma que, en un nivel extensivo,
podran tambin verse como unas particulares colecciones conceptuales de
objetos. Recordemos que estamos hablando de tipos de datos sin lmite
cualitativo virtual, por lo que podran aplicarse a cualquier conjunto que
admitiera una definicin comprehensiva. Tomemos, por ejemplo, un
conjunto arbitrario de personas asistiendo a un concierto. El subconjunto de
tales espectadores que portara una camisa blanca podra ser identificado y
class Persona {
public:
char sexo; // 'M' por masculino, 'F' por femenino
// resto de la descripcin de la clase
};
// la siguiente asignacin es errnea, porque sugiere que
// el tipo abstracto 'Persona' posee siempre sexo masculino,
// lo cual es mentira, a pesar de lo que puedan pensar algunos.
Persona::sexo='M'; // ERROR
// la prxima asignacin es correcta, puesto que establece que
// el objeto Luis de tipo Persona es de sexo masculino.
Persona Luis;
Luis.sexo = 'M';
Puede darse el caso, sin embargo, que deseemos que todos los objetos
compartan una misma variable o constante: entonces deberamos usar un
miembro esttico (static) comn a todos ellos y cuya nocin, como es
costumbre, desarrollaremos ms adelante.
class Cliente {
// cuerpo de descripcin de la clase
};
Cliente fulano, mengano, zutano; // definicin de objetos
// de tipo "Cliente"
que equivale a:
Vemos, as, que se han declarado variables de tipo long y punteros a char,
junto con variables de tipo Persona y punteros a objetos de tipo Persona,
que muy bien pudieran corresponderse a la clase ejemplificada poco antes.
Hemos visto que los datos miembros se han declarado indistintamente como
pblicos o privados en las clases anteriores. En realidad la OOP enfatiza una
propiedad que se denomina ocultacin de la informacin y que,
bsicamente, consiste en que los datos no podrn ser accedidos
directamente, sino que tal acceso ser realizado a travs de funciones
especficas. Qu se pretende con esto? Pues que el usuario (por
desarrollador) no tenga acceso a la representacin interna de la clase.
Imaginemos una clase en la que los datos internos estn dispuestos en
ficheros secuenciales. Si se pudiera acceder sin restriccin a estos datos
directamente, un programador podra generar cdigo basado en la
disposicin fsica secuencial, de forma que si en un momento dado
decidiramos cambiar la disposicin interna de los datos a una lista
enlazada, aqel cdigo debera ser totalmente re-escrito. Solucin?
Mantener los datos como privados e implementar una o varias funciones
pblicas de acceso a los datos, de manera que el programador "ignorar" la
disposicin interna de los datos y manejar nicamente funciones del tipo
buscaClaveCliente(long) y listaClientesEntreCodigos(long, long). Si con tal disposicin deseramos cambiar la
representacin interna de una lista enlazada a una base de datos relacional, efectuaramos los pertinentes
cambios en la definicin de la clase y, por supuesto, en las funciones de acceso, pero el cdigo ya escrito (que
usara nicamente un prototipo inalterable de funcin miembro) no tendra que ser retocado. Bien: sta es la
idea. Para llevarla a buen fin C++ proporciona un sistema de cualificacin de acceso finamente granulado a
travs de etiquetas, como ya hemos podido ver, y que consiste en las siguientes:
class Cliente {
private:
char* nombre;
// etc.
};
Cliente miClienteDePrueba;
sino que tendramos que definir una funcin "especial" que realizara tal
cometido. As, podramos por ejemplo declarar en la clase Cliente una funcin
imprimerNombre() que realizara la tarea. Vemoslo:
class Cliente {
public:
void imprimeNombre()
{
cout << nombre;
}
private:
char* nombre;
// etc., etc.
};
miClienteDePrueba.imprimeNombre();
class Persona {
public:
void imprimeDNI(); // funcin sin argumentos
void imprimeNombre();
long leeDNI();
char* leeNombre();
int calculaEdad( Fecha ); // argumento: un objeto
// del tipo Fecha
Ciudad& leeCiudad(); // devuelve una referencia a
// objeto del tipo Ciudad
// ...
private:
long numeroDNI;
Fecha* nacimiento;
char* nombre;
// ...
};
class Proveedor {
public:
void imprimeNombre();
void imprimeDireccion();
void imprimeFichaProveedor();
// ...
private:
char* nombre;
char* direccion;
// ...
};
void Proveedor::imprimeNombre()
{
cout << nombre << "\n";
}
void Proveedor::imprimeDireccion()
{
cout << direccion << "\n";
}
void Proveedor::imprimeFichaProveedor()
{
imprimeNombre();
Proveedor proveedorDescargandoEnElMuelle;
proveedorDescargandoEnElMuelle.imprimeNombre();
la funcin miembro imprimeNombre() conocer en este caso con exactitud a qu objeto ha de ser
aplicada, evidentemente. Asi mismo, cuando codificamos
proveedorDescargandoEnElMuelle.imprimeFichaProveedor();
este nuevo mensaje o funcin miembro tambin sabr que el objeto a que
debe aplicarse, al igual que en el ejemplo anterior, es el proveedor-
DescargandoEnElMuelle. Pero, qu pasa con las funciones miembros que se encuentran, sin referencia directa
alguna, en el cuerpo de esta ltima funcin? O sea, a qu objeto se aplicarn los mensajes imprimeNombre() e
imprimeDireccion(), contenidos en la funcin imprimeFichaProveedor(), toda vez que la sintaxis no es explcita
como en los casos anteriores? Bien, en realidad todas las llamadas a miembros de una clase (tanto variables como
fuinciones) desde el protocolo de descripcin de sta se direccionan a un puntero implcito, denominado this, que
contiene la direccin del objeto de esa clase por medio del que se ha producido la llamada. Esto es, si explicitamos
tal puntero, podramos perfectamente escribir:
void Proveedor::imprimeNombre()
{
cout << this->nombre << "\n";
}
void Proveedor::imprimeFichaProveedor()
{
this->imprimeNombre();
this->imprimeDireccion();
}
De esta forma, en nuestro caso, ya podemos inferir que los mensajes del
cuerpo de la funcin imprimeFichaProveedor() sern direccionados al objeto
proveedorDescargandoEnElMuelle, como era intuitivo suponer. Naturalmente que el usuario puede explicitar el
puntero this en los cuerpos de todas las funciones miembros, aunque esto, lejos de aclarar, oscurece ms el
cdigo y, en la prctica, simplemente no se hace.
class Proveedor {
public:
Proveedor imprimeNombre();
Proveedor imprimeDireccion();
void imprimeFichaProveedor();
// ...
private:
char* nombre;
char* direccion;
// ...
};
Proveedor Proveedor::imprimeNombre()
{
cout << nombre << "\n";
return *this;
}
Proveedor Proveedor::imprimeDireccion()
{
cout << direccion << "\n";
return *this;
}
void Proveedor::imprimeFichaProveedor()
{
imprimeNombre().imprimeDireccion();
}
class Proveedor{
public:
Proveedor imprimeNombre() const;
// ...
};
Todo lo visto hasta ahora parece que viene en afirmar que una funcin
miembro declarada constante no puede modificar un objeto, pero lo cierto
es que s puede. Demonios!, pensar el lector: Ahora s que no se entiende
nada!. Bueno, como tantas veces suele ocurrir en C++, el lenguaje
proporciona fuertes mecanismos por defecto que sin embargo pueden ser
totalmente obviados por el desarrollador: pensemos, por ejemplo, en el
"fuerte chequeo de tipos", que puede ser totalmente anulado mediante la
tcnica llamada de "constructores virtuales". Bien, volviendo a las funciones
const, debo decir que esta etiqueta se refiere a lo que se denomina
"constancia lgica", en contraposicin a la constancia fsica, indicando que tal
caracterstica es, en definitiva, una mera apariencia, que el usuario puede
soslayar simplemente realizando un cast expreso de la siguiente forma:
INTERFAZ E IMPLEMENTACIN
En las clases istream y ostream se sobrecargan11, respectivamente, los operadores para la ejecucin de las
operaciones de insercin o entrada de datos (<<) y extraccin (>>) o salida de datos. De esta forma la
representacin codificada de I/O podra ser:
#include <iostream.h>
int main( int, char** ) {
cout << "Hola, C++";
return 0;
}
Bueno, pero por qu utilizar los operadores '<<' y '>>' en lugar de otros?
Y por qu, en todo caso, usar dos operadores distintos cuando la distincin
en s de las operaciones fcilmente se localiza en los objetos predefinidos?
En primer lugar tenemos que no podemos "crear" nuevos operadores:
tenemos que conformarnos con los existentes. Seguidamente, en efecto,
cabra preguntarse: cul? quiz el operador '='? Lo cierto es que las
expresiones (sobre todo al encadenarse) pueden complicarse sobremanera, y
una no muy intuitiva adecuacin del operador a la operacin dada podra
dificultar, a veces de forma insuperable, la legibilidad del cdigo. Y es tal
inteligibilidad la que se pretende al elegir dos operadores asimtricos que
por su forma ya sugieren un direccionamiento (de datos, de objetos, etc.).
Realice el amable lector una sencilla prueba: tras terminar el presente
11
Recuerde el lector lo ya dicho sobre sobrecarga de funciones (de las que los
operadores son un caso especial): el mismo interfaz con distintas implementaciones
contextuales. De esta manera los operadores normalmente usados para
desplazamiento de bits son "redefinidos" para actuar como "insertores" y
"extractores" en sus relaciones con streams.
cout << "El nmero " << 7 << " es cabalstico." << "\n";
// la siguiente lnea es equivalente a la anterior
cout << "El nmero 7 es cabalstico\n";
La idea intuitiva del "funcionamiento" del objeto cout, por ejemplo, es que
los datos se van insertando en un buffer interno que una vez lleno, o forzado
por un manipulador o una funcin miembro, direcciona su volcado hacia un
dispositivo de salida preestablecido, que por defecto (igual para entrada que
para salida) es la pantalla del terminal, aunque, como es fcil suponer, tal
circunstancia puede ser variada direccionando el volcado a otro dispositivo.
12
No olvidemos que C++ es un lenguaje que permite al desarrollador una gran
flexibilidad: el lector disconforme con la eleccin de operadores podra, por ejemplo,
derivar sus propias clases de istream y ostream aadiendo a stas, seguidamente
y tras lo que puede ser un arduo trabajo, un operador distinto que, caracterizado
como inline, sustituyera a aqullos, creando a la vez unos nuevos streams micout
y micin. Tal posibilidad existe, como tambin la de automutilarnos, pero esto no
dice nada de la conveniencia de su aplicacin.
cout << 8;
cout << "hola";
ORIENTADA-A-OBJETOS FUNCIONAL
Ahora lo entiendo! -podra suspirar aqu el paciente lector-: entonces si, por
ejemplo, deseara imprimir un objeto de un tipo definido-por-el-usuario a
travs de una clase, lo nico que tendra que hacer es descomponer el
objeto en datos que seran insertados individualmente en el objeto cout. O no es
as? Bien, bsicamente s es as. De esta manera podramos codificar:
class Recluta {
public:
void imprimeDatos()
{ // funcin inline
cout << "Datos recluta Ejercito de Tierra:\n";
cout << numeroDNI << endl;
cout << apodo << "\n";
}
private:
long numeroDNI;
miRecluta.imprimeDatos();
Pero -dirn ustedes-, se supone que la clase ostream no posee una funcin
miembro con argumento del tipo Recluta, pues el implementador de tal clase
no saba -ni afortunadamente sabr nunca- nada sobre esta nuestra clase.
Qu pasa aqu? Bien, es muy sencillo: nicamente tenemos que
sobrecargar, en la clase Recluta, los operadores de insercin y extraccin,
para que respondan a argumentos del tipo Recluta. Qu cmo se hace
esto? Ms adelante lo veremos.
cout << "La respuesta es " << flecha << "NO" << campana << endl;
donde endl es una operacin predefinida en ostream que inserta una nueva
lnea y, a la vez, vaca el buffer. A este ltimo y nico fin tambin podran
utilizarse las siguientes expresiones:
cout
cin
equivale a
cout.flush();
O sea, la insercin del objeto flush en el objeto cout equivale a (o provoca) la llamada de la funcin
miembro flush() en este ltimo.
Esta tcnica, ideada por Andrew Koenig, permite una sintaxis ms clara y
una mayor flexibilidad, pues las operaciones de entrada y salida no tienen
que ser interrumpidas para la aplicacin de determinadas funciones.
Hemos visto que ciertos manipuladores (como por ejemplo hex y setprecision(int))
pueden afectar el formato de las operaciones de entrada/salida. De hecho los manipuladores sin argumentos se
definen en la clase ios, que es una clase base (superclase) de las clases istream y ostream. Esta clase dispone de
distintas funciones miembros que pueden ser aplicadas a los stream predefinidos. Alto aqu! Un objeto va a usar
funciones miembros de otra clase como si fueran suyas? Efectivamente! Mediante la derivacin de clases, los
objetos de las clases derivadas pblicamente de una clase base (cual es ios en este caso) pueden llamar
directamente a las funciones miembros declaradas en la seccin public de tal clase base. Bien, veamos algunas de
tales funciones, aplicables tanto a objetos ostream como istream:
cout.setf( ios::FLAG );
donde FLAG habr de ser sustituido por uno de los siguientes identificadores:
donde el primer argumento puede ser una flag de las antes detalladas o un
campo de los siguientes,
de manera que cada campo (Format Bit Field) se aplica a unas determinadas
seales (Format Flags). As
tenemos que al aplicar dos distintas bases sobre el campo ios::basefield, ste vuelve
a su valor por defecto: la base decimal. Cmo se soluciona esto? Bien, la versin sobrecargada primero resetea
el campo de formato a 0 y seguidamente establece en l el valor del flag. Y si quiramos conservar el antiguo
valor del campo? Precisamente long setf( long, long ) devuelve tal valor, por lo que podr ser almacenado y
recuperado posteriormente.
#include <iostream.h>
int main( int, char** )
{
// inserciones // salida a dispositivo estndar
// y comentarios
cout << hex << 10; // A
cout << oct << 9; // 11
cout << oct << 9
<< hex << 10; // 11 A
cout << char( 67 ); // 'C' en un PC
cout << 'C'; // C
cout << int( 'C' ); // 67 en un PC ( CHAR es distinto
// de INT en C++)
cout.precision( 2 ); // establece la precisin SLO
// para la prxima insercin
cout << 1.23234; // 1.23
cout << 1.23234; // 1.23234
cout.width( 5 ); // establece la longitud de
// la prxima insercin
cout << 2; // 2
cout << 2; // 2
cout .width( 8 ); // establece la longitud SLO
// de la prxima insercin
cout.fill( '0' ); // establece el carcter de
// relleno para completar "width"
cout.precision( 3 );
cout << 1.28317; // 0001.283
cout.width( 9 );
cout << 1.28317; // 001.28317
cout.fill( ' ' ); // restablece el caracter de
// relleno a Blanco
return 0;
}
MANEJO DE FICHEROS
ofstream miFicheroEnModoEscritura;
miFicheroEnModoEscritura.open( "fichero.txt", ios::out );
O en un solo paso:
miFicheroEnModoEscritura.close();
fstream miFichero;
// apertura de un fichero (los modos se pueden disyuntar)
miFichero.open( "fichero.txt", ios::in | ios::out );
// coloca el puntero en una posicin determinada del fichero,
// donde POS sera sustituido por beg (inicio: POR DEFECTO),
// cur (posicin actual) o end (final fichero)
miFichero.seekg( long posicion, ios::POS );
// devuelve la posicin actual en un fichero
// desde el inicio del mismo
miFichero.tellg();
// cierra fichero
miFichero.close();
13
En realidad la funcin miembro "open" consta de un tercer argumento, que
podramos calificar como de acceso, y que mantiene una correspondencia con los
conocidos atributos de los ficheros: normal, read-only, hidden y system. Por
defecto tal argumento est establecido en la posicin de "normal".
INICIALIZACIN DE OBJETOS
class Direccion {
private:
char* calle;
long cp;
char* ciudad;
// ...
};
class Persona {
private:
char* nombre;
Direccion* direccion;
// ...
};
Tenemos que la clase Persona contiene un puntero a un objeto de la clase Direccion. Examinemos el
funcionamiento de una variable de tipo predefinido: si en un bloque local codificamos
float objetoDeTipoPredefinido;
class Direccion {
public:
void estableceCalle( const char* miCalle ) {
calle = new char[ strlen( miCalle) + 1 ];
strcpy( calle, miCalle );
}
void estableceCp( const long miCp) {
cp = miCp;
}
void estableceCiudad( const char* miCiudad ) {
ciudad = new char[ strlen( miCiudad ) + 1 ];
strcpy( ciudad, miCiudad );
}
// ...
};
class Persona {
public:
void estableceNombre( const char* miNombre ) {
Direccion miDireccion;
miDireccion.estableceCalle( "Orense, 36" );
miDireccion.estableceCp( 28020 );
miDireccion.estableceCiudad( "Madrid" );
Persona miPersona;
miPersona.estableceNombre( "John Doe" );
miPersona.estableceDireccion( &miDireccion );
class Direccion {
public:
void init( const char* miCalle,
const long miCp,
const char* miCiudad)
{
calle = new char[ strlen( miCalle) + 1 ];
strcpy( calle, miCalle );
cp = miCp;
ciudad = new char[ strlen( miCiudad ) + 1 ];
strcpy( ciudad, miCiudad );
}
// ...
};
class Persona {
void init( const char* miNombre = "",
const char* miCalle = "",
const long miCp = 0,
const char* miCiudad = "" )
{
nombre = new char[ strlen( miNombre ) + 1 ];
strcpy( nombre, miNombre );
Direccion miDireccion;
void evaluaBushGate()
{
Persona candidatoDemocrata;
// en la siguiente lnea el resto de argumentos
// se aplica por defecto
candidatoDemocrata.init( "Bill Clinton" );
// realiza algn tipo de proceso
}
void hazNoSeQue()
{
char[ 16 ] nombrePresidente; // inicializacin
nombrePresidente = "Felipe Gonzlez" // asignacin
// ...
} // fin del mbito local: desinicializacin
// o destruccin de las variables locales
class Evento {
private:
long claveEstadistica;
Racional probabilidad;
// ...
};
class Racional {
private:
int numerador, denominador;
// ...
};
la siguiente codificacin
Evento tiradaDeDado;
class ControlMilitar {
ControlMilitar::ControlMilitar()
{
sexo = 'V';
edad= 18;
}
ControlMilitar ejercicioActual;
donde se define un objeto de tipo ControlMilitar, origina que se produzca una llamada al
constructor implementado por nosotros, de manera que los datos miembros del objeto ejercicioActual se inicializan
a 'V' y '18'. Hemos sustituido, de hecho, al constructor "implcito" del compilador para esta clase. Pero la situacin
sigue sin ser totalmente satisfactoria: y si deseramos parametrizar la inicializacin de nuestro objeto? por qu
limitarnos a un rango de valores por defecto? Bien, es razonable suponer que, dado que los constructores son en
realidad funciones miembros, admitirn argumentos en tipo y nmero arbitrarios. Recodifiquemos, a efectos de
ejemplo prctico, la clase anterior:
class ControlMilitar {
private:
char sexo;
int edad;
public:
ControlMilitar( char miSexo, int miEdad )
{
sexo = miSexo;
edad = miEdad;
}
};
ControlMilitar ejercicioActual;
ControlMilitar arrayDeControlesMilitares[ 10 ];
class ControlMilitar {
public:
ControlMilitar() {};
// ...
};
class ControlMilitar {
public:
ControlMilitar( char = 'V', int = 18 );
private:
char sexo;
int edad;
// ...
};
Los dos puntos tras el constructor indican que seguir, antes de que
comience el cuerpo del mismo, una lista de inicializadores de los datos
miembros de la clase (y tambin de las clases base, en su caso). Vemos,
tambin, que ste constructor posee un cuerpo vaco. Naturalmente! Este
cdigo indica expresamente cmo deben ser construidos los objetos
miembros, a la vez que, en la misma operacin, les asigna los pertinentes
valores, an antes de comenzar la ejecucin del cuerpo de la funcin
constructora que, en este caso, no aportara nada nuevo. En lo posible
debemos usar la sintaxis de inicializacin de miembros en lugar de la de
asignacin en constructores, aunque en algunas ocasiones esto ser
obligatorio: los datos miembros referencias o const no se pueden asignar,
sino slo inicializar.
Las dos primeras lneas construyen dos objetos de tipo char y, en el mismo
acto, copian el valor situado a la derecha del signo '=' a cada uno de ellos.
En la primera lnea, en puridad, se construye primero un objeto temporal de
tipo char a partir del carcter '', y seguidamente se copia tal objeto en el
objeto de nueva construccin con identificador tuLetra. En la segunda lnea
se produce, sin ms, una construccin con copia simultnea. A este tipo de
constructores se les denomina, vctimas de una imaginacin lxica
sorprendente, constructores de copia. Examinemos ahora la ltima lnea de
cdigo: se sustituye la representacin interna del objeto a la izquierda del
operador '=' con la del objeto a la derecha de ste. En definitiva, un objeto
se asigna a otro pero, como el atento lector ya habr notado, no se
construye ningn objeto, sino que se trata de una operacin entre objetos
"ya construidos"; una operacin correspondiente al operador de asignacin
(operator=). De hecho, para distinguir entre asignacin e inicializacin
debemos preguntarnos: se ha construido algn objeto? En caso negativo
podramos inferir que se trata de una asignacin.
class Persona {
public:
Persona( char* miNombre = 0, int miEdad = 0 )
: edad( miEdad )
{
if ( miNombre ) {
nombre = new char[ strlen( miNombre ) + 1 ];
strcpy( nombre, miNombre );
} else {
nombre = new char[ 1];
*nombre = '\0';
}
}
private:
int edad;
char* nombre;
// ...
};
class Persona {
public:
Persona( const Persona& miPersona )
{
edad = miPersona.edad;
nombre = new char[ strlen( miPersona.nombre ) + 1 ];
strcpy( nombre, miPersona.nombre );
}
Pesona& operator=( const Persona& miPersona )
{
if ( this == &miPersona )//chequea autoasignacin
Persona* punteroAPersona;
punteroAPersona = new Persona( "Roque", 52 );
DESTRUCTORES
class Persona {
public:
~Persona()
{
delete [] nombre;
Pero, entonces, cundo debe ser usada una funcin "destructora"? Bien, lo
cierto es que raras veces es necesario llamar expresamente a un destructor.
La inmensa mayora de las veces ste es usado de forma implcita por el
compilador, producindose una llamada automtica al mismo en cualquier
momento desde la ltima aparicin de un objeto y el fin del mbito del
mismo.
UN SENCILLO CONTROL
En fin, hasta ahora hemos visto bastante de teora y muy poco de cdigo
prctico. Y es hora de que pongamos las manos en la masa. Por eso ste
prembulo ser excepcionalmente corto. Tan corto que ya se acab.
class Racional {
private:
int numerador;
int denominador;
Hemos declarado los datos internos como privados, siguiendo los esquemas
de encapsulacin y ocultacin de la informacin preconizados por la OOP.
En general los datos miembros de una clase se declararn private, dejando
el interfaz de cliente (la parte public) nicamente para funciones miembros.
El cliente de la clase14 no tendr as dudas, entre otras cosas, sobre aadir o
no parntesis funcionales a los miembros del interfaz pblico (situacin
resuelta, por otro lado, en el lenguaje Eiffel por la no diferenciacin
sintctica entre datos y funciones miembros). Pensemos tambin que
podramos cambiar, por ejemplo, el tipo de estos datos (pasndolos a long),
o aadir un nuevo dato miembro: su acceso exclusivo a travs de funciones
miembros pblicas nos asegura que en caso de tales cambios (que sern
oportunamente reflejados en la implementacin de tales funciones) el cdigo
de los clientes de la clase no deber ser en absoluto variado.
Racional dosQuintos( 2, 5 );
Racional ochoTercios( 8, 3 );
Racional nueve( 9, 1 ); // Todos los enteros son Racionales!
Racional cero( 0, 1 );
class Racional {
public:
Racional();
15
En el desarrollo de sistemas software en C++ es frecuente la aplicacin de la
mxima "analizar un poco, disear un poco, codificar un poco", de tal forma
que el proceso de creacin se convierte en un iterativo refinamiento de cada etapa
en sucesivos ciclos, involucionndose stos de manera que se quiebra el tradicional
esquema de "cascada" de fases.
Racional::Racional() { ; }
Tenemos, pues, dos constructores: uno con dos argumentos y otro por
defecto. Pero examinemos el constructor por defecto: en realidad inicializa
los datos miembros de la misma forma que lo hara el constructor con
argumentos, lo nico que con unos valores predefinidos. Por qu no unir
estos dos constructores en uno solo con dos argumentos a los que se
asignaran parmetros por defecto? Nuestra clase quedara ahora as:
class Racional {
public:
Racional( int = 0, int = 1 );
~Racional() {};
private:
int numerador, denominador;
};
Racional unQuinto( 1, 5 );
Racional miFraccion; // equivale a miFraccion( 0, 1 )
Racional nueve( 9 ); // equivale a nueve( 9, 1 )
Por supuesto que otra posible solucin hubiera sido declarar parmetros por
defecto en el constructor con dos argumentos y mantener, a la vez, el
16
Quiz lo ms prctico, en este y otros casos de desajuste entre ARM y
nuestra implementacin concreta de C++, sea encerrar el cdigo
desafortunadamente generado entre condicionales del preprocesador, como por
ejemplo:
class Racional {
public:
#ifdef CPP_NOCOMP
Racional() {}
#endif /* CPP_NOCOMP */
// sigue resto clase
};
SOBRECARGA DE OPERADORES
17
De hecho, lo contrario puede afirmarse como regla: "siempre que una clase
haga uso de la memoria de almacenamiento libre (normalmente mediante el
operador new), el desarrollador deber proveerla de sus propias versiones del
constructor de copia y de la sobrecarga del operador de asignacin".
class Racional {
public:
boolean esIgualA( const Racional& ) const;
boolean esMayorQue( const Racional& ) const;
Racional suma( const Racional& );
// etc., etc.
};
Pero no, esto no funciona. Pensemos que para, por ejemplo, comparar dos
objetos de tipo Racional deberamos codificar, con arreglo a lo expuesto, lo
siguiente:
class Racional {
public:
boolean operator ==( const Racional& );
// sigue resto descripcin clase
};
pero siempre teniendo en cuenta que el uso infijo del operador equivale a la
siguiente notacin funcional:
if ( miFraccion.operator==( tuFraccion ) )
hazAlgunaCosa();
Probemos ahora con una operacin algebrica, como por ejemplo la suma
de Racionales. Veamos la declaracin de la sobrecarga:
class Racional {
public:
Racional operator+( const Racional& );
// sigue resto descripcin clase
};
obtengamos un valor interno para fraccionSuma de '7/6', lo cual es correcto. El objeto dosTercios
sigue representando el valor '2/3'. Pero, qu pasa con el objeto unMedio? Pues que su valor interno ha pasado
a ser '7/6'! Repasemos la secuencia: en primer lugar la aplicacin del operador suma equivale a la siguiente
notacin funcional:
unMedio.operator+( dosTercios );
Esto es otra cosa: ahora se crea un objeto temporal de tipo Racional, distinto
de los sumandos, utilizando el constructor de la clase con los argumentos
del resultado de la suma. Si volviramos al cdigo de aplicacin
inmediatamente anterior veramos que el objeto fraccionSuma resulta en valer '7/6',
mientras que el objeto unMedio no ha cambiado su representacin interna. Pero, por qu devolver un objeto
Racional en lugar de una referencia a un objeto Racional? No parece esto ltimo, de acuerdo con lo expuesto
anteriormente, mucho ms eficiente? Bien, es una buena pregunta. Intentar responderla. Si cambiamos el tipo
de retorno a Racional&, el cdigo anterior podra quedar, salvo eso, invariante: al ejecutar la ltima lnea de la
funcin se devolvera una referencia al objeto temp. Pero este ltimo objeto es local al cuerpo de la funcin, por
lo que al devolver una referencia al mismo estamos preludiando el desastre: tras ejecutar la sentencia de
retorno, devolviendo en este hipottico caso una referencia, el objeto temp ser desinicializado por el destructor
de su clase, de forma que la referencia, antes siquiera que podamos utilizarla, "apuntar" a un objeto que ya no
Aqu uno se suma a cero, devolviendo una referencia a un objeto Racional temporal, al que se sumar el
objeto dos, devolviendo una referencia a un nuevo objeto tambin temporal, con el que se construir el objeto
suma. Se han creado, pues, dos objetos temporales mediante el operador new, pero lo cierto es que por ser
innominados no podemos acceder a ellos (a no ser que, con una tozudez cercana a la de Icaro, despachemos,
extendidas por todo el cdigo, declaraciones y asignaciones de identificadores que permitan nominar y tomar las
direcciones de estos objetos temporales) , por lo que, en consecuencia, no podremos aplicarles el operador
delete, lo que originar que, poco a poco, ineludiblemente, contruibuiremos a gastar la memoria disponible por la
aplicacin, hasta el punto de poder llegar al fatdico mensaje de falta de memoria. Vemos, pues, que la mejor
solucin es la originalmente propuesta de devolucin de un objeto en lugar de una referencia.
19
Como ya se ha sealado repetidas veces en el texto, el paso por valor o
copia de un objeto se realiza mediante la aplicacin del constructor de copia , bien
implcito bien expreso, con el que cuentan todas las clases, como tambin ha
quedado suficientemente explicado.
:: . .*
Por qu? Pues porque ya tienen un significado preciso y, tras conocer los
mecanismos del lenguaje, intuitivo para el lector: tales operadores, entre
todos los dems, se aplican ya, con carcter predefinido, sobre clases y sus
instanciaciones. No se pueden sobrecargar, as mismo, los siguientes
operadores:
?: # ##
+ - * / % ^ & | ~ ->* =
< > += -= *= /= %= ^= &= |= <<
>> >>== <<== == != <= >= && || , ->
! ++ -- () [] new delete
+ - * &
, || &&
Operadores Descripcin A
:: cualificador de acceso a mbito global D
:: cualificador de acceso a mbito clase I
|| operador OR lgico I
, operador coma I
class Racional {
public:
Racional operator^( const Racional& );
Racional operator+( const Racional& );
// sigue descripcin de clase
};
Cul es el valor interno del objeto resultado? Si aplicamos las reglas de precedencia de
operaciones que hemos aprendido en el grado escolar elemental, donde la exponenciacin precede a la suma,
tendremos que resultado contiene la fraccin '4/1'. Correcto? No! Estamos en C++, y la precedencia (y la
asociatividad, en su caso) viene dada por la tabla anterior! Y en tal tabla podemos apreciar que la precedencia
del operador '^' est varios niveles por debajo del la del operador '+'. Qu ocurre, entonces, con nuestra
expresin? Pues que resultado contendr la fraccin '1/1'! O sea, el uso del operador ms intuitivo para una
operacin nos ha llevado a un antinatural orden de evaluacin de la misma. Conclusin? Debemos evitar esta
sobrecarga. No veo por qu! -podra afirmar un lector-: slo bastara con explicitar la precedencia deseada de la
siguiente forma:
20
Este documento de Matthew H. Austern, titulado "Una propuesta para
aadir un operador de exponenciacin al lenguaje C++", X3J16/92-0099,
WG21/N0176, propone la adopcin por el lenguaje de dos nuevos operadores
estndar: *^ y *^=, donde el primero sera un operador binario de exponencia -
cin y el segundo uno del conocido tipo operador=, con un nivel especfico de
precedencia y pudiendo ser normalmente sobrecargados.
Qu pasa, por otra parte, con los operadores unarios? Pues simplemente
que la funcin operadora miembro correspondiente no dispondr de
argumentos (recordemos que el objeto que llama a tal funcin sera su nico
operando). El problema, por llamarlo de alguna forma, podra aparecer con
los operadores de autoincremento y autodecremento (++, --) porque, en su
mbito de actuacin predefinido, operarn de forma distinta segn prefijen o
postfijen a su operando, y debemos encontrar alguna manera de expresar
esta caracterstica al sobrecargarlos. Realmente hasta AT&T C++ 2.1 no
haba posibilidad de diferenciar el uso prefijo o postijo de estos operadores:
siempre operaban como prefijos. AT&T C++ 3.0 ha cambiado el panorama,
aun a costa de un truco algo artificioso: el operador postfijo (para
distinguirlo del prefijo) incorpora un argumento adicional de tipo int,
ocupndose el compilador de proporcionarle un valor por defecto (que no
nos interesa por su inutilidad prctica):
class Racional {
public:
Racional& operator++(); // PREFIJO
Racional operator++( int ); // POSTFIJO
// sigue descripcin de clase
};
Parece que se han sentado las bases para la descripcin completa de nuestra
clase, que quedara, ms o menos, de la siguiente guisa:
class Racional {
public:
boolean operator==( const Racional& );
boolean operator<( const Racional& );
// ...
Racional operator+( const Racional& );
Racional operator-( const Racional& );
Racional operator*( const Racional& );
// ...
Racional( int = 0, int = 1 );
~Racional();
private:
int numerador, denominador;
};
suma = uno.operator+( 7 );
El objeto suma pasar, as, a detentar un valor interno de '8/1'. Ahora, circunstancialmente, podemos
apreciar la ventaja de haber declarado parmetros por defecto en nuestro constructor. Si no hubiera sido as no
tendramos forma de "convertir" un entero en un objeto Racional. Observamos, tambin, la conveniencia de
haber significado por la unidad el segundo argumento por defecto, pues la representacin interna del objeto
construido a partir de un solo entero se corresponde as con el concepto matemtico que subyace tras tal
representacin. Veamos, sin embargo, qu ocurre con la siguiente expresin:
int siete = 7;
Racional unMedio( 1, 2 ), suma;
suma = siete + unMedio;
int varint;
Prueba uno;
const Prueba dos,tres;
dos + tres; // funcin global
dos + uno; // funcin global
uno + tres; // funcin miembro
uno + dos; // funcin miembro
uno.operator+(dos); // funcin miembro
uno + varint; // funcin miembro
varint + dos; // funcin global
uno + 1; // funcin miembro
2 + dos; // funcin global
// funcin global
Racional operator+( const Racional&, const Racional& );
clase Racional {
public:
// funcin miembro
Racional operator+( const Racional& );
};
Pero no perdamos el hilo del relato: toda esta explicacin ha venido a cuento
de la necesidad de definir una funcin global para evitar algunas de las
incoveniencias de las funciones miembros operadores. Notamos que pueden
coexistir ambas versiones, pero que esto no tiene razn de ser y, si no
afinamos mucho, puede conducirnos a problemas de ambigedad. Qu
hacemos entonces? Pues suprimir la funcin miembro y dejar nicamente la
funcin global, naturalmente.
class Racional {
public:
int num() const { return numerador; }
int denom() const { return denominador; }
private:
int numerador, denominador;
// sigue resto descripcin clase
};
class Racional {
private:
Racional& simplificar();
// sigue descripcin de clase
};
class Racional {
friend Racional operator+( const Racional&,
const Racional& );
friend boolean operator<=( const Racional&,
const Racional& );
// sigue resto descripcin de clase
};
class Racional {
public:
ostream& operator<<( ostream& );
// ...
};
class Racional {
friend ostream& operator<<( ostream&, const Racional& );
friend istream& operator>>( istream&, const Racional& );
// sigue resto descripcin clase
};
OPERADORES DE CONVERSIN
Racional unoRacional( 1 );
float unoFloat = unoRacional; // error
class Racional {
public:
operator float() {
return float( numerador / denominador );
}
// sigue resto descripcin de clase
};
Hemos definido una nueva funcin miembro de nuestra clase, con la especial
sintaxis de los operadores, sin argumentos y sin tipo alguno de retorno, lo
cual es lgico si pensamos que, como operador de conversin, habr de
convertir un objeto Racional devolviendo, al fin del proceso, un float, por lo
que ya se anuncia el tipo de retorno. De esta manera ya podramos compilar
sin problemas el anterior cdigo.
#include <complex.h>
class Racional {
public:
operator float();
operator complex();
// ...
};
Racional unMedio( 1, 2 );
2.5 + unMedio;
class Racional {
friend Racional operator+( float, const Racional& );
friend Racional operator+( const Racional&, float );
friend Racional operator+( const Racional&,
const Racional& );
// ...
public:
operator float();
// ...
};
long prueba;
Racional dosTercios;
class Racional {
public:
int comoInt() {
return int( numerador / denominador );
}
int comoFloat() {
return float( numerador / denominador );
}
// etc, etc.
};
class Racional {
private:
int numerador, denominador;
Racional& simplificar();
static int numeroDeRacionales;
public:
Racional( int = 0, int = 1 );
// ...
};
int Racional::numeroDeRacionales = 0;
Bueno, este cdigo requiere algn comentario. En primer lugar vemos que
es necesario aplicar a nuestra variable el operador de resolucin de mbito,
lo cual es lgico, pues la visibilidad de sta se limita al mbito de la clase.
Podramos preguntarnos seguidamente: qu pasa con el nivel de acceso?
Esta variable es privada y, sin embargo la hemos inicializado con mbito
global de fichero. En realidad esta es una particularidad de los miembros
estticos de clases: el nivel de proteccin se refiere a las operaciones de
acceso y modificacin de los mismos, pero no afecta a su inicializacin o
definicin.
Racional::~Racional()
{
Racional::numeroDeRacionales -= 1;
}
class Racional {
public:
static numeroDeObjetosRacionales() {
return Racional::numeroDeRacionales;
}
// acceso directo
cout << Racional::numeroDeObjetosRacionales();
Racional cualquiera;
// funcin miembro
cout << cualquiera.numeroDeObjetosRacionales();
// funcin global
Racional operator+( float, const Racional& );
class Racional {
public:
Racional operator+( const Racional&, const Racional& );
// ...
};
#ifndef RACIONAL_H
// fichero: RACIONAL.H
// contenido: interfaz de la clase "Racional",
// representacion del conjunto matematico
// de los numeros racionales.
// autor: Ricardo Devis
// derechos: (C) INFO+ (1993)
// Este codigo puede copiarse libremente,
// con la unica restriccion de respetar
// el aviso de copyright.
// last mod: 03-06-93
#include <iostream.h>
class Racional {
// operadores de entrada/salida
friend ostream& operator<<( ostream& os,
const Racional& );
friend istream& operator>>( istream& is, Racional& );
// operadores artimeticos
friend Racional operator+( const Racional&,
const Racional& );
friend Racional operator-( const Racional&,
const Racional& );
friend Racional operator*( const Racional&,
const Racional& );
friend Racional operator/( const Racional&,
const Racional& );
// operadores de comparacion
friend boolean operator==( const Racional&,
const Racional& );
friend boolean operator!=( const Racional&,
const Racional& );
friend boolean operator>( const Racional&,
const Racional& );
friend boolean operator<( const Racional&,
const Racional& );
friend boolean operator<=( const Racional&,
const Racional& );
friend boolean operator>=( const Racional&,
const Racional& );
public:
// constructor y destructor inline
Racional( long num = 0, long denom = 1 )
: numerador( num ), denominador( denom )
{
simplificar();
}
~Racional() {};
private:
long numerador;
long denominador;
// funcion interna para simplificar fracciones
Racional& simplificar();
};
#endif /* RACIONAL_H */
// fichero: RACIONAL.CPP
// contenido: implementacion de la clase "Racional",
// representacion del conjunto matematico
// de los numeros racionales.
// autor: Ricardo Devis
// derechos: (C) INFO+ (1993)
// Este codigo puede copiarse libremente,
// con la unica restriccion de respetar
// el aviso de copyright.
// last mod: 03-06-93
Racional Racional::operator-()
{
Racional temp( -numerador, denominador );
return temp;
}
Racional& Racional::operator+()
{
return *this;
}
Racional& Racional::operator++()
{
numerador += denominador;
return *this;
}
Racional& Racional::operator--()
{
numerador -= denominador;
return *this;
}
float Racional::comoFloat()
double Racional::comoDouble()
{
return (double)numerador / (double)denominador;
}
long Racional::comoLong()
{
return numerador / denominador;
}
int Racional::comoInt()
{
return (int)numerador / (int)denominador;
}
Racional& Racional::simplificar()
{
int minimo;
int mcd = 1; // maximo comun divisor
if ( numerador > denominador )
minimo = denominador;
else
minimo = numerador;
for ( int i = 1; i <= minimo; i++ )
if ( ( numerador % i == 0 )
&& ( denominador % i == 0 ) )
mcd = i;
numerador /= mcd;
denominador /= mcd;
return *this;
}
// fichero: RACIOTST.CPP
// contenido: ejemplos de uso de la clase "Racional",
// autor: Ricardo Devis
// derechos: (C) INFO+ (1992)
// Este codigo puede copiarse libremente,
// con la unica restriccion de respetar
// el aviso de copyright.
// last mod: 03-06-93
#include "racional.cpp"
#define nl '\n'
return 0;
}
A estas alturas ya hemos revisado muchos de los tpicos del lenguaje C++
que nos habrn de permitir disear, al principio con inevitable candidez,
nuestras primeras clases. De hecho, como ya vimos en la construccin de la
clase Racional, la tradicional metodologa de programacin ha sufrido un
vuelco (otra cuestin sera si hemos derramado algo realmente importante).
Quiere esto decir, pues, que ya estamos programando con tcnicas de
orientacin-a-objetos? Bueno, un descorazonador halito de honradez me
impide decir, en puridad, que s. La verdad es que, si recordamos bien, las
propiedades (o, ms bien, facilidades) que caracterizan a un OOPL son:
abstraccin, encapsulacin, herencia y polimorfismo. En la mera
construccin de clases hemos involucrado solamente los conceptos de
abstraccin y encapsulacin, as como una muy rudimentaria aproximacin
al polimorfismo (mediante la sobrecarga de operadores y funciones). En
C++, sin embargo, los mecanismos de OOP tienen su principal apoyo en la
caracterstica denominada "herencia" y que en C++ se implementa mediante
la derivacin de clases: en ella se basa el mecanismo polimrfico del
lenguaje, constituido por las funciones virtuales; en ella se realzan y tienen
pleno sentido, tambin, las propiedades de abstraccin y encapsulacin.
Apreciamos, as, que la herencia es el ojo del huracn de la OOP con
respecto de C++ . No debemos olvidar, con todo, que, adems de C++ y
por raro que parezca, existen otros OOPL's en los que no todo tiene su
exacta correspondencia: en el lenguaje SELF, por ejemplo, la herencia se
sustituye por la delegacin, una interesantsima propiedad (de posible
implementacin "manual" en C++), cuya explicacin en detalle sobrepasa
(cmo no?) los lmites de esta introduccin. Bueno, derivemos a lo prctico
y heredaremos facilidad de codificacin.
21
Volvemos aqu al gastado tema de la prevalencia al considerar los posibles
estndares del lenguaje. C++ naci, de hecho, como una implementacin
particular de AT&T, independientemente de que ARM sirviera despus como
documento base para el comit ANSI C++. No cansar, pues, ms al lector.
22
Oops! De hecho la afirmacin contraria se acerca muchsimo ms a la
realidad prctica: los mecanismos de ligazn dinmica y orientacin-a-objetos de
C++ se asimilan sobremanera con la derivacin pblica. En la seccin 11.2 de ARM
se afirma, refirindose a la derivacin de clases: "Probablemente fue un error
definir un especificador de acceso por defecto.". Bien, el lector lo tiene claro: para
evitar problemas en el presente y en el futuro, codifique siempre expresamente la
cualificacin de acceso en derivacin.
23
De hecho, en la programacin efectiva con C++, lo ms frecuente es que,
habiendo reunido ms de una clase con caractersticas comunes, se haga
abstraccin de stas y se cree una superclase o clase base de las mismas y que
se insertara con naturalidad en la posible jerarqua de clases ya existente.
Naturalmente un diseo adecuado identificara apropiadamente las relaciones
iniciales de derivacin.
class Persona {
protected:
Persona( char* unNombre ) {
nombre_ = unNombre; // peligro!!
}
// ...
};
char* maestro = "Ferrer Guardia";
void Profesional::algunaFuncion()
{
Persona miIdolo( maestro );
// ...
cout << miIdolo.nombre(); // imprime 'Ferrer Guardia'
// con la siguiente lnea vulneramos
// nuestro cuidado control de acceso
*maestro = "Friedrich Nietzsche";
cout << miIdolo.nombre(); // imprime 'Friedrich Nietzsche'
}
24
En realidad esto es una mera regla mnemotcnica que el lector no debe
tomar al pie de la letra, pues aparte del nivel de acceso habra que considerar las
cuestiones de prevalencia y solapamiento de miembros (pensemos, por ejemplo,
en un miembro de una clase base y otro de una clase derivada de sta que
compartan el mismo identificador: problema!).
Profesional::Profesional() : anosEjercicio( 0 )
{
cout << "Constructor por defecto de Profesional" << '\n';
}
Solucionado todo? Ms bien no! Veamos la salida que arroja, con esta
nueva adicin, la siguiente variacin de nuestro simptico programita:
He aqu el resultado:
25
Dada la apreciable segmentacin en el uso del lenguaje, se han llegado a
establecer tres diferentes niveles de uso de C++: "mejor C" o "C con chequeo de
tipos", que utiliza todas las caractersticas de C++ no relacionadas con las clases;
"C++ basado en Objetos", que usa de las clases, aunque no de la derivacin; y,
por ltimo, "C++ completo", que abarca la totalidad del lenguaje.
class Base {
public:
void pruebaAcceso();
void pubBf() {}
protected:
void proBf() {}
private:
void priBf() {}
};
class FriendPublicDerived {
public:
void pruebaAcceso();
PublicDerived objetoPublicDerived;
};
class FriendProtectedDerived {
public:
void pruebaAcceso();
ProtectedDerived objetoProtectedDerived;
};
class FriendPrivateDerived {
public:
void pruebaAcceso();
PrivateDerived objetoPrivateDerived;
};
void Base::pruebaAcceso()
{
pubBf();
proBf();
priBf();
}
void PublicDerived::pruebaAcceso()
{
pubBf();
proBf();
priBf(); // error
}
void ProtectedDerived::pruebaAcceso()
{
pubBf();
proBf();
priBf(); // error
}
void PrivateDerived::pruebaAcceso()
{
pubBf();
proBf();
priBf(); // error
}
void FriendPublicDerived::pruebaAcceso()
{
PublicDerived objetoPublicDerived;
objetoPublicDerived.pubBf();
objetoPublicDerived.proBf();
objetoPublicDerived.priBf(); // error
void FriendProtectedDerived::pruebaAcceso()
{
ProtectedDerived objetoProtectedDerived;
objetoProtectedDerived.pubBf();
objetoProtectedDerived.proBf();
objetoProtectedDerived.priBf(); // error
objetoProtectedDerived.pubPRODf();
objetoProtectedDerived.proPRODf();
objetoProtectedDerived.priPRODf();
}
void FriendPrivateDerived::pruebaAcceso()
{
PrivateDerived objetoPrivateDerived;
objetoPrivateDerived.pubBf();
objetoPrivateDerived.proBf();
objetoPrivateDerived.priBf(); // error
objetoPrivateDerived.pubPRIDf();
objetoPrivateDerived.proPRIDf();
objetoPrivateDerived.priPRIDf();
}
PublicDerived objetoPublicDerived;
objetoPublicDerived.pubBf();
objetoPublicDerived.proBf(); // error
objetoPublicDerived.priBf(); // error
ProtectedDerived objetoProtectedDerived;
objetoProtectedDerived.pubBf(); // error
objetoProtectedDerived.proBf(); // error
objetoProtectedDerived.priBf(); // error
PrivateDerived objetoPrivateDerived;
objetoPrivateDerived.pubBf(); // error
objetoPrivateDerived.proBf(); // error
objetoPrivateDerived.priBf(); // error
FriendPublicDerived objetoFriendPublicDerived;
objetoFriendPublicDerived.
objetoPublicDerived.pubBf();
objetoFriendPublicDerived.
FriendProtectedDerived objetoFriendProtectedDerived;
objetoFriendProtectedDerived.
objetoProtectedDerived.pubBf(); // error
objetoFriendProtectedDerived.
objetoProtectedDerived.proBf(); // error
objetoFriendProtectedDerived.
objetoProtectedDerived.priBf(); // error
objetoFriendProtectedDerived.
objetoProtectedDerived.pubPRODf();
objetoFriendProtectedDerived.
objetoProtectedDerived.proPRODf(); // error
objetoFriendProtectedDerived.
objetoProtectedDerived.priPRODf(); // error
FriendPrivateDerived objetoFriendPrivateDerived;
objetoFriendPrivateDerived.
objetoPrivateDerived.pubBf(); // error
objetoFriendPrivateDerived.
objetoPrivateDerived.proBf(); // error
objetoFriendPrivateDerived.
objetoPrivateDerived.priBf(); // error
objetoFriendPrivateDerived.
objetoPrivateDerived.pubPRIDf();
objetoFriendPrivateDerived.
objetoPrivateDerived.proPRIDf(); // error
objetoFriendPrivateDerived.
objetoPrivateDerived.priPRIDf(); // error
return 0;
}
punteroAProcionPersonaDeDirectivo->imprimeDatos(); // OK
punteroADirectivo->imprimeDatos(); // OK
punteroAPorcionProfesionalDeDirectivo->imprimeDatos(); // OK
Note el lector que aqu no aparecen casts, pues con arreglo a lo expuesto las
conversiones codificadas se pueden producir de forma implcita.
26
Entramos de nuevo en el espinoso tema de las implementaciones
particulares de C++: aun cuando lo expuesto es correcto y lgicamente impecable,
si intentamos compilar los ejemplos con, verbigratia, Borland C++ 4.0, algunas
lneas sern flageladas como errores del tipo "No se puede convertir X* a Y*".
Concretamente el compilador encontrar problemas, normalmente, con los niveles
de acceso a las clases base indirectas. Estamos, simplemente, ante una limitacin
del compilador.
class Persona;
Por otro lado tenemos el fichero en el que definimos nuestra clase Persona:
class Persona {
public:
char* nombre() const {
return rip->nombre_;
}
protected:
Persona() {
rip = new RepInternaPersona;
}
Persona( char* unNombre ) {
rip = new RepInternaPersona( unNombre );
}
~Persona() {
delete rip;
}
private:
RepInternaPersona* rip; // la cola del gato de Cheshire
};
class Empleado {
public:
int antiguedadEnLaEmpresa()
{
return antiguedad;
}
void estableceAntiguedad( int anos )
{
antiguedad = anos;
}
private:
int antiguedad;
// ...
};
class JefeDeSeccion : public Empleado { /* ... */ };
class Administrativo : public Empleado { /* ... */ };
class DirectorGeneral : public Empleado { /* ... */ };
// etc., etc.
JefeDeSeccion* pFranklin;
Administrativo* pEleanor;
DirectorGeneral* pFrank;
// ...
imprimeAntiguedad( pFranklin ); // OK
imprimeAntiguedad( pEleanor ); // OK
imprimeAntiguedad( pFrank ); // OK
DirectorGeneral miJefe;
miJefe.estableceAntiguedad( 5 );
miJefe.antiguedadEnLaEmpresa( 17 ); // OK: devuelve '5'
miJefe.antiguedadEnLaEmpresa(12 ); // OK: devuelve '-1'
// seguidamente intentamos acceder a la
// funcin heredada de la clase base
miJefe.antiguedadEnLaEmpresa(); // ERROR
SELECTORES DE TIPO
class Empleado {
public:
enum tipoEmpleado{ JEFESEC, ADMTVO, DTORGEN }
tipoDeEmpleado() {
return valorTipoEmpleado;
}
char* lema() const
{ // lemas de las distintas categoras de empleados
// para ser particularizados en las clases derivadas
return "Los empleados son el alma de la empresa";
}
// ...
private:
tipoEmpleado valorTipoEmpleado;
// ...
};
Empleado* juanNadie;
Administrativo* ruperez;
JefeDeSeccion* carvajal;
DirectorGeneral* deLasHeras;
// ...
FUNCIONES VIRTUALES
El mecanismo que usa C++ para procurar tal servicio es la funcin virtual.
En definitiva se trata de lo siguiente: si sabemos que una funcin de una
clase base va a ser particularizada o redefinida en una o ms clases
derivadas, esto se lo haremos saber al compilador notndole que se trata de
una funcin virtual. Hacindolo as, el compilador generar para la clase
cdigo adicional (algo as como el selector de tipo visto anteriormente) que
27
En el diseo del lenguaje C++ se sopes sobremanera la posibilidad de incluir
una tal identificacin en las clases, pero se estim que tal posibilidad abocara con
facilidad las codificaciones hacia estructuras de tipo switch, perjudicando la
modularidad y mantenibilidad de los programas. De hecho, las deficiencias
observadas en el lenguaje SIMULA derivadas de tal identificacin de tipos, que este
lenguaje s contempla, influyeron decisivamente en la decisin de no incluirlos en
C++.
class Empleado {
public:
virtual char* lema() const
{
return "Los empleados son el alma de la empresa";
}
// ...
};
Con esta simple adicin (y una vez suprimido el cdigo generado por los
campos selectores de tipo), nuestra funcin "genrica" podra quedar
simplemente as:
Empleado* juanNadie;
Administrativo* ruperez;
JefeDeSeccion* carvajal;
DirectorGeneral* deLasHeras;
// ...
imprimeIdeario( juanNadie );
imprimeIdeario( ruperez );
imprimeIdeario( carvajal );
imprimeIdeario( deLasHeras );
Mensajero* juan;
// ...
imprimeIdeario( juan );
Notamos, en principio, que al declarar una funcin de una clase base como
virtual, automticamente las funciones con la misma signatura en las clases
derivadas pasan a ser virtuales. De esta forma la mantenibilidad del cdigo
se simplifica sobremanera.
En resumen: una funcin virtual es una funcin miembro (no puede ser
global) antecedida por la clave virtual en el protocolo de descripcin de su
clase, y tiene sentido cuando de la clase en la que se declara se derivarn
otras. La palabra clave virtual puede usarse en la declaracin de las
funciones virtuales en las clases derivadas, pero es redundante. Si una
funcin virtual no se redefine en una clase derivada, se asumir para sta la
definicin de tal funcin en su clase base. Una funcin virtual puede ser
declarada inline, pero esta declaracin nicamente deber tener efecto
cuando la llamada a tal funcin se resuelva, como es lgico, en tiempo de
compilacin.
class Empleado {
public:
virtual char* lema() const {
// cuerpo de la funcin
}
// ...
};
Empleado* cualquiera;
Empleado* unEmpleado = new Escribiente;
Escribiente* unEscribiente = new Escribiente;
// ...
imprimeIdeario( cualquiera ); // salida: Los Empleados son
// el alma de la empresa
imprimeIdeario( unEmpleado ); // salida: Los Empleados son
// el alma de la empresa
//
imprimeIdeario( unEscribiente ); // salida: Los Empleados son
// el alma de la empresa
TipoDeLetra courier;
// ...
unEscribiente->lema( courier ); // salida: Los Escribientes
// son la letra de la empresa
unEmpleado->lema( courier ); // error: no existe tal funcin
// en la clase Empleado
#define nl '\n'
#include <iostream.h>
class Base {
public:
virtual void f()
{
cout << "Base" << nl;
}
};
g( pBase );
g( pBaseMedia );
g( pBaseMediaDerivada );
g( pMedia );
g( pMediaDerivada );
g( pDerivada );
return 0;
}
Base
Base
Derivada
Base
Derivada
Derivada
Queda establecido, pues, que las funciones virtuales redefinidas en las clases
derivadas de aqulla en la que se declara como virtual la primera, deben
compartir exactamente la misma signatura, lo que incluye el nombre de la
funcin, el nmero, tipo y orden de sus argumentos, y su tipo de retorno.
Queda entendido, tambin, que una modificacin de la signatura no
constituye una sobrecarga de la funcin virtual. Pero, y si lo que se cambia
es nicamente el tipo de retorno de la funcin virtual? Simplemente se
genera un error en compilacin. Algo s como:
DESTRUCTORES VIRTUALES
class Empleado {
public:
Empleado() { /* ... */ } // constructor
virtual void f() { /* ... */ }
// ...
Hay un caso, sin embargo, en el que los destructores virtuales no nos sern
de mucha utilidad: no podemos destruir un array de objetos de una clase
derivada mediante un puntero a su clase base, pues el compilador no tiene
forma de conocer el tamao de los objetos de la clase derivada, necesario
para calcular la situacin secuencial de los punteros this pasados al
destructor. Pero, bueno, no todo el monte es organo.
class Base {
public:
virtual void v() {
cout << "funcin Base::v()";
void Derivada::unaFuncionMiembroCualquiera()
{
Base::funcionVirtual();
}
Como el lector habr podido apreciar, las funciones virtuales permiten aislar,
en la clase base de una jerarqua, el interfaz genrico comn a las clases
derivadas de sta. Volviendo a nuestro ejemplo primero y acercndonos ms
a la vida real, la clase base de nuestra jerarqua podra aparecer ms o
menos as:
class Empleado {
public:
Empleado();
Empleado( char* nombre );
virtual int nivelDeResponsabilidad();
virtual ~Empleado();
// ...
};
Empleado unEmpleado;
class Empleado {
public:
virtual int nivelDeResponsabilidad() = 0;
// ...
};
Cabra inquirir aqu, dada la peculiar sintaxis vista, si las funciones virtuales
puras estn dispensadas de ofrecer una definicin. Bueno, en efecto lo estn,
dado que, como he explicado, cada clase con posibles instanciaciones
proveer su propio definicin. Podemos, sin embargo, dotar de definicin a
nuestra funcin en la clase base, de la misma forma que lo haramos con
una funcin miembro ordinaria:
class Secretario {
public:
int nivelDeResponsabilidad()
{ // redefinicin de la funcin virtual pura
return 13;
}
// ...
};
class Administrativo {
public:
int nivelDeResponsabilidad()
{ // uso de la definicin de la funcin virtual
// pura provista en la clase Base
Empleado::nivelDeResponsabilidad();
};
};
Tambin podra accederse tal definicin, usando del operador ::, a travs de
un objeto de clases derivadas, como por ejemplo:
Secretario secretarioDireccion;
secretarioDireccion.Empleado::nivelDeResponsabilidad(); //resolucin esttica
Antes hemos visto que los destructores pueden ser declarados virtuales, y es
muy posible que el lector haya notado que nada a este respecto se dice de
los constructores. Veamos primero qu quiere expresar la idea de un
"constructor virtual". En OOP, y por ende en C++, debemos acostumbrarnos
a que los objetos sean autosuficientes: si mandamos un mensaje, se
ejecutar el mtodo apropiado de la clase apropiada al objeto. Las funciones
virtuales proveen buena parte de este "sabor polimrfico", estableciendo
procedimientos efectivos de identificacin, en tiempo de ejecucin, de las
funciones apropiadas a los mensajes lanzados en una jerarqua de clases.
Los destructores virtuales permiten, por otro lado, despreocuparnos de
codificaciones explcitas de desinicializacin de objetos. En perfecta secuencia
lgica, las funciones de carcter constructor y esencia polimrfica deberan
liberarnos de cdificar expresamente el tipo de objeto a construir. Es decir,
dado un objeto a ser construido en una determinada porcin de programa,
un constructor virtual de una jerarqua de clases permitira aplicar la funcin
constructora apropiada al objeto en cada caso a partir de una codificacin
general. Es como si dijramos: quien ms sabe de un objeto es el objeto en
s, as que ... constryete! Bien, esto es francamente interesante y suena casi
a mgico, pero volvamos a la Tierra: es curioso pensar que un lenguaje con
un muy fuerte chequeo de tipos, cual es C++, permita la creacin de una
codificacin con una absoluta relajacin de tal chequeo (pinsese que el tipo
de los objetos sera determinado en tiempo de ejecucin). Las posibilidades,
de cualquier forma, son indudablemente atractivas. Pero vayamos al grano:
C++ NO admite "constructores virtuales" (y aqu la comunidad Smalltalk
podra aumentar el tono y denostar, como es habitual, la muy cacareada
hibridez de C++ como OOP). Seamos ms precisos: el lenguaje C++ en s
no permite que los constructores puedan ser virtuales, pero es fcil construir
un esquema que pueda simular tal efecto. Bueno, puntualizando, es
Stroustrup quien dice que es fcil. Bsicamente la tcnica ms utilizada
consiste en disear una funcin (o modificar el cuerpo del constructor en la
clase base) que permita discernir el tipo de argumento para aplicar uno u
otro constructor y devolver un objeto apropiado. A un programador
tradicional esta tcnica le sugerira una estructura de tipo switch, aunque en
C++ lo suyo es resolverlo mediante funciones virtuales. No detallar ahora,
con todo, tales mecanismos, que el lector podr encontrar en el mismo
Stroustrup y mayormente en Coplien.
Bien, hasta aqu hemos revisado distintos aspectos del lenguaje C++ imbri-
cndolos con la programacin orientada-a-objetos, hasta llegar a lo que
parece su ncleo operativo: las funciones virtuales. Y es precisamente aqu
donde hemos de terminar esta introduccin. Por qu? Bien, precisamente
debido a ese carcter deliberadamente introductorio del texto. A partir de
este punto habra que considerar, al menos, los siguientes temas:
- herencia mltiple
- derivacin virtual
- plantillas ("templates")
- manejo de excepciones
Bien, existen dos tipos bsicos de libreras: por un lado estn las que parten
de una nica clase (libreras csmicas), normalmente denominada "Object",
y que suelen constituirse en los marcos de aplicacin que veremos un poco
ms adelante; por otro lado estn las libreras basadas en distintas clases. En
este apartado nos centraremos en libreras del ltimo tipo.
MARCOS DE APLICACIN
Como quiera que parece que es un producto de este tipo el que ms pudiera
interesar, por la facilidad que aporta en el desarrollo de aplicaciones
Windows, a un lector de RMP, me extender suficientemente detallndolo.
VObject
VAccelerator
Vassoc
VIntAssoc
VBitBltr
VBool
VBrush
VClipBoard
VCollection
VOrdCollect
VStack
VSet
VDictionary
VTokens
VContainer
VIntegerSet
VObjArray
VPointArray
VFont
VFrame
VGlobal
VClassTable
VMemory
VNotifier
VIcon
VIterator
VLocation
VMenu
VPopupMenu
VSysMenu
HelloCPPView()
~HelloCPPView()
boolean free()
boolean HelloCPPView::free()
{
delete this;
return( TRUE );
}
y cuyo cometido es, con cierta lgica, establecer el ttulo de la vista a que se
aplica en la cadena apuntada por 's'; otra opcin es seleccionar, dentro del
men "Classes", la opcin "Inherits...", lo que provoca la aparicin de una
ventana de dilogo conteniendo una lista con todas las funciones miembros
heredadas por la clase actual (en nuestro caso, "HelloCPPView"), debiendo
buscar seguidamente una funcin del tipo necesitado (esto es, algo as como
"setWindowTitle" o "setViewTitle" o, definitivamente, "setTitle"), y
encontrando el mtodo anteriormente expuesto. Ahora lo nico que
tenemos que hacer es incluir tal mtodo en el constructor de nuestra clase,
de la siguiente forma:
HelloCPPView::HelloCPPView()
{
setTitle( "Ventana de saludo" );
}
cuyo cometido es escribir una cadena de tipo 'C' dentro del rea de cliente de
una ventana de tipo "VWindow" en la posicin (x,y). Parece que lo tenemos
claro: incluimos una llamada a esta funcin en nuestro anterior constructor y
... voil! c'est tout!. Diantre, no!! Recapacitemos ligeramente. Si incluimos
en el constructor este mtodo, cuando nuestra ventana se construya en ella
efectivamente aparecer la frase deseada, pero qu pasar cuando esta
ventana sea modificada o cubierta por otra y posteriormente descubierta? Y
aqu el lector podra pensar: "Pues que la frase seguir donde estaba, pues
de eso tiene que ocuparse el sistema". Bueno, algo de razn tiene el
confiado lector, pero tambin, en una buena medida, bastante de lgica le
falta: el sistema no tiene por qu saber si deseamos que tal frase
permanezca tras una modificacin de la ventana; o, ni siquiera, saber si debe
redimensionar el tipo de letra para adaptarlo al nuevo tamao de la ventana.
Bien, parece que antes de seguir debemos revisar el paradigma MVC en que
se basa esta librera.
boolean VWindow::paint()
{
return ( FALSE );
}
boolean
HelloCPPView::paint()
{
wrtText( "Hola C++", 20, 20 );
return ( TRUE );
}
// -------------------------------------------------------------
// HOLACPP.CPP
// -------------------------------------------------------------
#include "notifier.h"
#include "hllcppvw.h"
#ifdef TURBO
int i;
#endif
// -------------------------------------------------------------
// HLLCPPVW.H
// -------------------------------------------------------------
#ifndef hllcppvw_h
#define hllcppvw_h
#include "appview.h"
// -------------------------------------------------------------
// HLLCPPVW.CPP
// -------------------------------------------------------------
#include "hllcppvw.h"
defineClass(HelloCPPView,VAppView)
HelloCPPView::HelloCPPView()
{
setTitle( "Ventana de Saludo" );
}
HelloCPPView::~HelloCPPView()
{
;
}
boolean HelloCPPView::free()
{
delete this;
return( TRUE );
}
boolean
HelloCPPView::paint()
{
wrtText(
"Hola C++", 20, 20 );
return( TRUE );
}
#include "d4data.h"
Aqu, por supuesto, vale lo dicho con respecto a la librera de Rogue Wave:
una vez decididos a usar CodeBase++, debe crearse una clase interfaz nueva
(quizs CIDBFM: Caos Informtico DataBase Files Manager) que encapsule
los mtodos de gestin necesitados: abrir, cerrar, indexar, filtrar, etc.
La sintaxis es transparente:
de tal forma que esta simple adicin asegura que la nueva clase dispondr,
entre otras cosas, de un mtodo "store()" para el archivo expreso de sus
instanciaciones.
PtBase baseDeObjetos;
// ...
// el preprocesador ha creado un nuevo constructor que toma
// por argumento un objeto PtBase
MiClase* objetoDeMiClase = new MiClase( baseDeObjetos );
objetoDeMiClase->store(); // archivo en la base de objetos
// ...
delete objetoDeMiClase;
The C++ Workbook, por Richard S. Wiener & Lewis J. Pinson, 1990,
Addison-Wesley, 0-201-50930-X, 349 pg.
Nos encontramos ante lo que podra ser calificado como el texto estndar
(por oficial) de introduccin a C++. En l se detalla la especificacin AT&T
C++ 3.0 (y, por tanto, plantillas y clases anidadas) de una forma
extremadamente rigurosa y pulida: los escassimos errores o carencias del
texto se han debido, como es posible apreciar en comp.lang.c++, a
circunstancias ajenas al autor. No se le supone al lector conocimiento previo
alguno de C++ (ni siquiera una media experiencia en C), y la exposicin
gradual de las caractersticas del lenguaje est perfectamente modulada. O
sea, se trata del libro ideal para iniciarse, sin la ayuda externa de un profesor
(aunque sta es, en general, de valiossima consideracin), en los tpicos y
recovecos del lenguaje. Lippman empieza muy pronto en la obra con la
codificacin de la clase "Array", y el lector puede asistir al proceso de su
refinamiento, mediante la aparicin en el texto y acertado comentario de
nuevas caractersticas que se le van aadiendo, involucrndose y
comprometindose poco a poco en el proceso. El libro est repleto de
instructivos ejercicios, de factible resolucin, a la vez que de ejemplos
"vivos": el lector ve, sorprendido, cmo stos van incorporando con extrema
facilidad nuevos detalles conforme avanza el relato. Todas las secciones del
libro muestran en la prctica, pues, los conceptos tericos que se
desprenden del lenguaje. Se exponen, por otro lado, una buena cantidad de
interesantes y bien probados trucos de programacin, que el lector podr
apreciar "en su salsa". Lippman dedica tambin un captulo a OOD (Diseo
Orientado-a-Objetos), sustanciado, como es habitual en el resto del libro, en
un ejemplo apropiado. La tcnica de OOD empleada es simplsima: se trata
de identificar las clases necesarias a nuestra aplicacin para dotarlas
seguidamente de los interfaces apropiados, y establecer las relaciones entre
ellas. Se trata, pues, ms que de una exposicin de OOD, una indicacin de
la importancia que se le debe conceder al uso, expreso o no, de los
conceptos de OOD a la codificacin en C++. En definitiva: este libro es
fundamental para el principiante, mientras que para el experto se convierte
en paradigma de cmo debe escribirse un buen texto de introduccin.
Esta es la segunda edicin del texto que el Dr. Stroustrup public en 1.986
detallando el lenguaje C++, en su calidad de creador del mismo. Se trata de
un tutorial del lenguaje en el que, a diferencia del texto de Lippman, se
enfatizan los aspectos claves de uso del mismo. Se asume que el lector tiene
experiencia previa en programacin en C, y se detalla la especificacin AT&T
C++ 3.0 partiendo "de cero". El estilo del texto es enormemente sinttico,
de manera que la cantidad de tpicos revisados es netamente superior a la
del texto de Lippman. Comparada con la primera edicin, la obra ha crecido
tambin considerablemente en nmero de pginas. Precisamente ahora,
C++ Programming Style, por Tom Cargill, 1992, Prentice Hall, 0-201-
56365-7
Los que experimenten cierta aversin por las colecciones de reglas y los
manuales de estilo, encontrarn en este libro el arquetipo de sus pesadillas.
Al menos en el formato. Se trata, en efecto, de secciones codificadas que
observan el siguiente esquema: TPICO (por ejemplo, 6.06 virtual_fct -
funciones virtuales), REGLA (descripcin de las lneas maestras de estilo
correspondientes al tema del ttulo), EJEMPLO (demostracin prctica de la
problemtica contextual en que se desarrolla la regla), JUSTIFICACIN
(argumentacin de apoyo, tanto mediante razonamiento directo como a
travs de informes y textos externos, de la regla expuesta), ALTERNATIVAS
(opciones de la regla consideradas menores, o aun impracticables),
NOTAS LOCALES (media pgina al menos, y normalmente pgina y media,
en blanco para el supuesto apunte de notas por el lector). Bien, decamos
que una tal rgida esquematizacin pudiera resultar demasiado restrictiva. El
presente texto es, sin embargo, enormemente provechoso. Muy en la lnea
de un anterior libro de los mismos autores ("C Programming Guidelines"),
sus distintas secciones tienden a reforzar una idea que los principiantes en
C++ rpidamente relegan al olvido: C++ no es C. C++ no es, tampoco,
Smalltalk o Eiffel. Se trata de sistematizar las soluciones a los cuellos de
botella en la gestin de proyectos en C++, fundamentalmente cuando
intervienen equipos de ms de dos personas: a este fin se explicitan
convenciones para nominar identificadores, facilidades para comentar
cdigo, gestin de sobrecargas, etc. El libro es, en definitiva, un compaero
a tener muy en cuenta ante el diseo efectivo de clases realmente cohesivas
y portables.
Este texto est basado en el estudio realizado por los autores para proveer
de optimizaciones software a los sistemas UNIX de los Institutos Nacionales
de Salud (NIH) en USA. Se trata, en esencia, de una librera de clases
envolvente (conocida como librera NIH) modelada a imagen y semejanza
del entorno Smalltalk-80 y prescindiendo de las capacidades grficas de ste.
El libro exige un solvente conocimiento previo de C++ y se apoya en AT&T
C++ 2.0. Usando la terminologa de Coplien, podramos decir que aqu se
genera un "idioma tipo Smalltalk", y lo cierto es que lo que en el texto se
expone ha tenido una extraordinaria y reconocida influencia en el desarrollo
de un gran conjunto de entornos de aplicacin y libreras de clases
genricas. El provecho que los programadores de C++ pueden extraer de la
obra es evidente, pues sta est montada como un tutorial, explicando
detalles y decisiones de diseo de las clases (algunas de ellas inapreciables
para la adecuada construccin de componentes reutilizables, una de las
expectativas ms cacareadas y, a la vez, ms difciles de C++) y, tras esto,
directamente utiliza el entorno creado como herramienta de desarrollo,
explicando la posible extensin del sistema. La librera NIH incluye clases
como Iterador, Bolsa, Diccionario, Tiempo, Vector, Fecha, Coleccin, Pila,
Objeto (la raz de la librera "csmica", pues tal es el nombre por el que se
conocen las libreras con una nica clase base inicial). Como el lector puede
fcilmente intuir, este tipo de libreras propende al usuario a practicar la
derivacin mltiple, y dadas las dificultades no siempre evidentes que esto
entraa, el libro dedica un largo captulo a este respecto. Se detalla, incluso,
un ejemplo de aplicacin de base de datos usando la librera. Mi
sugerencia? Bien, el fuerte entroncamiento de los conceptos de OOD con el
acertado uso de C++ procuran un excelente texto de uso referencial, de
valiossima lectura para cualquier programador de C++ y aun de los
estudiosos de OOP; las ventajas y defectos del enfoque tipo Smalltalk
adoptado, por otro lado, disgustarn o deleitarn al lector, dependiendo en
buena medida de su estilo y costumbres, pero esto aguzar,
alternativamente, el ojo crtico o el gozo del lector. Lanlo!
Como el mismo ttulo indica, Bob Murray, editor durante muchos aos de
C++ Report, ha querido mostrar, como en ajedrez, las estrategias y compo-
nendas que trascienden los meros movimientos de las piezas del juego. No
es exactamente, en general, un libro "avanzado" sobre el lenguaje, sino que
ms bien ocupa ese estadio intermedio entre los libros de estilo y la
practicidad del cdigo realmente efectivo. En este sentido los dos captulos
dedicados a las plantillas ("templates") justificaran, por s solos, la lectura
Estamos ante una clara continuacin del anterior libro de los autores sobre
OOA, que usa de una extensin apropiada a OOD de la herramienta de
anlisis OOATool de los autores. Tras una leve introduccin (pues el texto,
como el anterior, es singularmente corto), se repasa la metodologa de
Anlisis orientado-a-objetos de Peter Coad: un modelo multicapa (sujeto,
clase-objeto, structura, atributos y servicios) y multicomponente (dominio
del problema, interaccin humana, gestin de tareas y gestin de datos),
con una notacin especfica de aplicacin. Los autores intentan, con cierto
xito, integrar de forma incruenta las tcnicas de OOD con las del proceso
de OOA, para pasar despus a exposiciones sobre sectores de parcial
inters, como el de las herramientas CASE o los distintos lenguajes de
programacin. El texto, con las mismas salvedades de la obra anterior, es
totalmente recomendable como introduccin no reglada al campo del OOD.
Object Lifecycles: Modeling the World in States, por Sally Shlaer &
Stephen J. Mellor, 1991, Prentice Hall, 0-13-629940-7.
SOFTWARE ORIENTADO-A-OBJETOS
"Un objeto es algo que existe en el tiempo y en el espacio y que puede ser
afectado por la actividad de otros objetos" (Grady Booch)
James J.Odell ha afirmado que el OOA "no debiera modelar la realidad, sino
ms bien la forma en que la realidad es comprendida por la gente". Pero,
qu se entiende aqu por "modelo"? Segn Michael Blaha, "una abstraccin
de algo con el propsito de comprenderlo antes de construirlo. Los modelos
incluyen slo aquellos aspectos relevantes a la solucin de un problema; los
detalles extraos son ignorados".
Como bien se puede notar en las fases expuestas, una demasiado simple
esquematizacin intuitiva de todas ellas podra resultar en la siguiente
secuencia involutiva: descripcin textual del problema, identificacin y
descripcin de objetos, establecimiento y descripcin de relaciones y
comunalidades entre objetos, y descripcin de los procesos temporales de
cambios de estados de objetos.
MODELADO DE JERARQUAS
REFERENCIAS
FUNDAMENTOS CONCEPTUALES
IMPULSIN DE PERSISTENCIA
En realidad, el hecho de tener que variar las clases de los pmgs cada vez que
se deseara dotar de persistencia a un objeto (obviaremos aqu las facilidades
de derivacin) atenta contra el ncleo terico de la OOP, generando una
fcilmente monstruosa macro-clase virtualmente no-reutilizable. As, en la
SISTEMAS PERSISTENTES
persistent<COS:psg>POSS
donde objN representa la instanciacin de una determinada clase y objPN un objeto dotado de persistencia. Si
dirigimos la impulsin de persistencia a, por ejemplo, el objeto objP4, ste distribuir tal mensaje a travs de la
estructura (para simplificar obviaremos en este estadio las referencias cclicas y la redundancia de objetos),
causando el archivo persistente de, verbigratia, los objetos objP1, objP3 y objP9. Representaremos el
subsistema afectado SOBS con la siguiente notacin:
y calificaremos a objP4 como un objeto spg generador del mismo. Denominaremos orden de un
subsistema persistente, por otro lado, al nmero de objetos generadores del mismo, y lo notaremos como
o(persistent<COS:iob>POSS)
persistent<OBS>CPOSS
persistent<objP4:OBS>POSS == persistent<objP9:OBS>POSS
LA FUNCIN ESCRITORA
MiClaseSospechosa objetoSospechoso;
// la prxima lnea ocasionar un error por ambigedad,
// pues el objetoSospechoso posee al menos dos
// porciones TStreamable: la obtenida por derivacin
// de MiClaseBase y la derivada de MiOtraClaseBase.
// Cul emplear?
ops << objetoSospechoso; // ERROR
// el siguiente cast resolvera la ambigedad
ops << (MiOtraClaseBase)objetoSospechoso;
class MiPrimeraClaseBase :
[claseBaseNoPersistente,]* virtual public TStreamable
{ /* ... */ };
class MiSegundaClaseBase :
[otraClaseBaseNoPersistente,]* public TStreamable
{ /* ... */ };
class MiClasePersistente : public MiPrimeraClaseBase,
const MAXFILA = 2;
const MAXCOL = 4;
class PCliente;
class ArrayPersistente;
FUNCION LECTORA
class TStreamable {
protected:
void write( opstream& ops ) { persist( ops ); }
void* read( ipstream& ips ) { persist(ips);return this; }
virtual void persist( pstream& ps ) = 0;
// ...
class MiNuevaClasePersistente:
public MiClaseBase, public TStreamable { /* ... */ };
void MiNuevaClasePersistente::persist( pstream& ps )
{
MiClaseBase::persist( ps );
ps[ objetoDeTipoIncorporado ];
ps[ objetoDeOtraClasePersistente ];
ps[ punteroAObjetoDeOtraClasePersistente ];
}
Se procurar, as, una sobrecarga de tal operador para cada una de las
sobrecargas existentes en tales clases para los operadores de insercin y
extraccin. Estamos obviando, empero, (y de aqu el cuasi en la pretendida
repeticin formal de cdigo) la especial codificacin necesitada para el
archivo y recuperacin de objetos de tipo agregado-definido-por-el-usuario.
Ms adelante concretaremos en detalle esta idea.
[CID1itdv11|itdv12[CID2itdv21]itdv13[CID3]
[CID4itdv41itdv42itdv43]itdv14]
CD3::CD3() : MiWindowEstandar()
{
// inicializacin de variables diversas
iniciaDialogoModal();
}
TStreamable* MiClasePersistente::build()
{
return new MiClasePersistente( streamableInit );
}
Pero, puede un objeto crear otro objeto u operar de alguna manera antes
de ser l mismo creado?. Evidentemente no. Necesitamos aqu, as, una
operacin de creacin no encapsulada en un objeto particular, pero
restringida al mbito de la descripcin de tipo del mismo: esto es, una
funcin miembro esttica. La funcin build() sera declarada, pues, como
static en el protocolo de descripcin de la clase afectada, siendo accedida a
travs de un puntero, accesible a su vez en la base de datos soporte del
registro de clases por medio del identificador devuelto por
streamableName(). Recapitulemos: como ya vimos cuando detallbamos la
TStreamableClass RMiClasePersistente(
MiClasePersistente::name,
MiClasePersistente::build,
__DELTA( MiClasePersistente ) );
public:TStreamableClass::TStreamableClass(
const char* identificadorDeClase,
TStreamable* ( _FAR *punteroAFuncionBuild )(),
int offsetTStreamable );
__link(RegIdentificadorDeMiClasePersistenteExternaAEsteModulo)
Deben ser aadidas, antes de nada, las siguientes modificaciones a las clases
que se notan, sealadas en negrita:
class TStreamable {
friend class pstream;
private:
persist_mode persistStatus;
class pstream {
public:
virtual pstream& operator [] ( signed char& ) = 0;
virtual pstream& operator [] ( unsigned char& ) = 0;
virtual pstream& operator [] ( signed short& ) = 0;
virtual pstream& operator [] ( unsigned short& ) = 0;
virtual pstream& operator [] ( signed int& ) = 0;
virtual pstream& operator [] ( unsigned int& ) = 0;
virtual pstream& operator [] ( signed long& ) = 0;
virtual pstream& operator [] ( unsigned long& ) = 0;
virtual pstream& operator [] ( float& ) = 0;
virtual pstream& operator [] ( double& ) = 0;
virtual pstream& operator [] ( long double& ) = 0;
virtual pstream& operator [] ( TStreamable& ) = 0;
virtual pstream& operator [] ( void* ) = 0;
// ...
};
#ifdef PERSIST
#ifndef PERSIST_FLAG
#define PERSIST_FLAG
# define Uses_pstream
__link( RegClaseBase )
__link( RegNombreClase1 )
__link( RegNombreClase2 )
__link( RegNombreClase3 )
#ifdef PERSIST
inline ipstream& operator >> ( ipstream& ips, NombreClase& nc )
{ return ips >> (TStreamable&)nc; }
inline ipstream& operator >> ( ipstream& ips, NombreClase*& nc )
{ return ips >> (void *&)nc; }
inline opstream& operator << ( opstream& ops, NombreClase& nc )
{ return ops << (TStreamable&)nc; }
inline opstream& operator << ( opstream& ops,NombreClase*& nc )
{ return ops << (void *&)nc; }
#endif /* PERSIST */
Tal preprocesador -al que en adelante denominaremos CP5 (C Plus Plus Persistence Pre-Processor)- deber
registrar, adicionalmente, la descripcin de cada clase en una database aparte. Para qu? Bien: imaginemos que
la clase de un determinado objeto sufre una variacin tras el archivo en un pstream del mismo. CP5 habr de ser
instruido para comparar cada clase con la descripcin registrada de la misma, y si aprecia algn cambio con
respecto a sta, buscar en el cdigo una funcin constructo-conversora del tipo:
NuevaClaseModificada( AntiguaDescripcionClase
objetoAntiguaClase );
la cual, bsicamente, explicitara la transferencia de identidades desde una descripcin a otra (la no existencia
expresa de tal mtodo obligara a CP5 a suplir un conversor por defecto, no siempre apropiado). Tal conversor
podra ser utilizado bien para la actualizacin de los pstreams (mediante la compilacin temporal de ambas
descripciones) bien para su inclusin en un sistema de versioning de clases, similar al usado en ODBMSs.
La construccin en C++ del preprocesador CP5, usando quiz de la tcnica llamada de "constuctores virtuales"
notada en [Copli92] y [Eckel92], excede el objetivo marcado en la redaccin de este anexo, cual inicialmente fue
la exposicin, a un nivel intermedio, de determinadas tcnicas no complejas de implementacin de la persistencia
en sistemas de objetos. La codificacin de CP5 pudiera, empero, merecer por su inters (pinsese en
analizadores lxicos, scanners, etc.) un prximo trabajo detallado.
LA EVOLUCIN DE LA PERSISTENCIA
Bien, lo que aqu mayormente se ha detallado es el esquema de persistencia de un compilador concreto: Borland
C++ 3.1. Pero, se trata de una estructuracin inamovible? Ciertamente no. En la versin 4.0 de este
compilador, una decisin de diseo, cual es el cambio de las libreras de clases de contenedores desde una
jerarqua csmica (tipo Smalltalk) a una parametrizacin completa mediante plantillas, repercute grandemente
sobre la implementacin de la persistencia, aunque conserva en buena medida su interfaz. As, por ejemplo,
desaparecen las sobrecargas de los operadores:
a la vez que se solventa la cualificacin de clases persistentes mediante el uso de las macros
DECLARE_STREAMABLE e IMPLEMENT_STREAMABLEX, donde X es un nmero que depende de las clases bases
inmediatas y de las clases bases virtuales de que deriva nuestra clase candidata. De alguna manera este cambio
en la implementacin afecta de manera menor a nuestro cdigo. Bien: esto es OOP. Hay que sealar, por ltimo,
que el esquema detallado en el presente anexo es, con las lgicas variaciones, el ms utilizado en las libreras
comerciales de clases.
[Atwoo91] Thomas Atwood, At last! A distributed database for Windows 3.0, Object Magazine, 1(1),
1991.
[Booch92] Grady Booch & Michael Vilot, Physical design: persistence, C++ Report, 4(3), 1992.
[Borla91] Borland International Inc, Borland C++ 3.1 Programmer's Guide, TurboVision for C++, ObjectWindows
for C++, 1992.
[Camm91] Stephanie J. Cammarata & Christopher Burdof, PSE: and object-oriented simulation
environment supporting persistence, Journal of Object-Oriented Programming, 4(6), 1991.
[Copli92] James O. Coplien, Advanced C++ Programming Styles and Idioms, Addison-Wesley, 1992.
[Gorle90] Keith Gorlen, S. Orlow & P. Plexico, Data Abstraction and Object-Oriented Programming in C++, John
Wiley, New York, 1990.
[Rumba91] James Rumbaugh, Michael Blaha, William Premerlani, Frederick Eddy & William Lorensen,
Object-Oriented Modeling and Design, Prentice-Hall, 1991.
[Urloc91] Zack Urlocker, From applications to frameworks, Hotline on Object-Oriented Technology, 2(11), 1991.