C++ Estándar: Apuntes de Informática Industrial y Comunicaciones
C++ Estándar: Apuntes de Informática Industrial y Comunicaciones
C++ estándar
Apuntes de Informática Industrial y Comunicaciones.
______________________________________________
Steve Jobs.
Presentación
Por último, cualquier errata detectada por el lector o sugerencia constructiva que se
considere oportuna agradecería que me fuera transmitida por correo electrónico a
miguel.hernando@ upm.es.
PRESENTACIÓN .......................................................................................................... 2
ÍNDICE DE CONTENIDOS............................................................................................ 5
1. INTRODUCCIÓN A C++ ..................................................................................... 13
1.1. HISTORIA DE C++ ......................................................................................... 18
Funciones sobrecargadas................................................................................... 50
El destructor ........................................................................................................ 90
4. LA HERENCIA.................................................................................................. 121
4.1. DEFINICIÓN DE HERENCIA ............................................................................ 122
Sistemas Operativos para pequeños dispositivos: CE, Mobile yPhone ........... 297
EL CLIENTE...................................................................................................... 350
Sin embargo, para lograr estos resultados es necesario un esfuerzo del programador
en las fases anteriores a la escritura del programa propiamente dicho. Si así no fuera, los
resultados pueden ser francamente decepcionantes. Así, para no llevar a engaño, y con la
idea de ameneizar ligeramente unos apuntes que de por si prometen ser densos, se incluye a
continuación una entrevista ‐ficticia‐ que durante un tiempo circulo por los foros de
programación:
Int: Bien, hace unos pocos años que cambio el mundo del diseño de software, ¿como se siente
mirando atrás?
BS: En este momento estaba pensando en aquella época, justo antes de que llegase. ¿La
recuerdas? Todo el mundo escribía en C y el problema era que eran demasiado buenos... Las
Universidades eran demasiado buenas enseñándolo también. Se estaban graduando programadores
competentes a una velocidad de vértigo. Esa era la causa del problema.
Int: ¿Problema?
BS: Bien, al principio, esos tipos eran como semidioses. Sus salarios eran altos, y eran tratados
como la realeza...
BS: Exacto. Pero, ¿que paso? IBM se canso de ello, e invirtió millones en entrenar a
programadores, hasta el punto que podías comprar una docena por medio dólar...
Int: Eso es por lo que me fui. Los salarios bajaron en un año hasta el punto de que el trabajo de
periodista esta mejor pagado.
BS: Bien, un día, mientras estaba sentado en la oficina, pensaba en este pequeño esquema, que
podría inclinar la balanza un poquito. Pensé 'Que ocurriría si existiese un lenguaje tan complicado,
tan difícil de aprender, que nadie fuese capaz de inundar el mercado de programadores?' Empecé
cogiendo varias ideas del X10, ya sabes, X windows. Es una autentica pesadilla de sistemas
gráficos, que solo se ejecutaba en aquellas cosas Sun 3/60... tenía todos los ingredientes que yo
buscaba. Una sintaxis ridículamente compleja, funciones oscuras y estructuras pseudo-OO. Incluso
ahora nadie escribe en código nativo para las X-Windows. Motif es el único camino a seguir si
quieres mantener la cordura.
BS: Ni un pelo. De hecho, existe otro problema... Unix esta escrito en C, Lo que significa que un
programador en C puede convertirse fácilmente en un programador de sistemas. Recuerdas el
dinero que un programador de sistemas solía conseguir?
BS: Ok, por lo tanto, este nuevo lenguaje tenía que divorciarse por sí mismo de Unix, ocultando
las llamadas al sistema. Esto podría permitir a tipos que solo conocían el DOS ganarse la vida
decentemente...
BS: Bueno, ha llovido mucho desde entonces. Ahora creo que la mayoría de la gente se habrá
figurado que C++ es una perdida de tiempo, pero debo decir que han tardado más en darse cuenta
de lo que pensaba.
BS: Se suponía que tenía que ser una broma, nunca pensé que la gente se tomase el libro en
serio. Cualquiera con dos dedos de frente puede ver que la programación orientada a objetos es anti
intuitiva, ilógica e ineficiente...
Int: Qué?!?!
BS: Y como el código reutilizable... cuando has oído de una compañía que reutilice su código?
BS: Entonces estas de acuerdo. Recuerda, algunos lo intentaron al principio. Había esa
compañía de Oregon, creo que se llamaba Mentor Graphics, que reventó intentando reescribir todo
en C++ en el 90 o 91. Lo siento realmente por ellos, pero pensé que los demás aprenderían de sus
errores.
BS: Ni lo más mínimo. El problema es que la mayoría de las empresas se callaron sus mayores
disparates, y explicar 30 millones de dólares de perdidas a los accionistas podría haber sido dificil...
Démosles el reconocimiento que merecen, finalmente consiguieron hacer que funcionase
BS: Casi. El ejecutable era tan gigantesco que tardaba unos cinco minutos en cargar en una
estación de trabajo de HP con 128 MB de RAM. Iba tan rápido como un triciclo. Creí que sería un
escollo insalvable pero nadie se preocupo. SUN y HP estaban demasiado alegres de vender
enormes y poderosas maquinas con gigantescos recursos para ejecutar programas triviales. Ya
sabes, cuando hicimos nuestro primer compilador de C++, en AT&T, compile el clasico 'Hello World',
y no me podía creer el tamaño del ejecutable. 2.1 MB.
Int: Qué ?!?!. Bueno, los compiladores han mejorado mucho desde entonces...
BS: Lo han hecho? Inténtalo en la última versión de C++, la diferencia no será mayor que medio
mega. Además existen multitud de ejemplos actuales en todo el mundo. British Telecom tuvo un
desastre mayor en sus manos, pero, afortunadamente, se deshicieron de ello y comenzaron de
nuevo. Tuvieron más suerte que Australian Telecom. Ahora he oído que Siemens está construyendo
un dinosaurio y se empiezan a preocupar porque los recursos hardware no hacen más que crecer
para hacer funcionar ejecutables típicos. No es una delicia la herencia múltiple?
BS: Realmente crees eso ?!?!?! Te has sentado alguna vez y te has puesto a trabajar en un
proyecto C++? Esto es lo que sucede: Primero he puesto las suficientes trampas para asegurarme
de que solo los proyectos más triviales funcionen a la primera. Coge la sobrecarga de operadores. Al
final del proyecto casi todos los módulos lo tienen, normalmente los programadores sienten que
deberían hacerlo así porque es como les enseñaron en sus cursos de aprendizaje. El mismo
operador entonces significa cosas diferentes en cada modulo. Intenta poner unos cuantos juntos,
cuando tengas unos cientos de módulos. Y para la ocultación de datos. Dios, a veces no puedo parar
de reírme cuando oigo los problemas que algunas empresas han tenido al hacer a sus módulos
comunicarse entre sí. Creo que el término 'sinergetico' fue especialmente creado para retorcer un
cuchillo en las costillas del director de proyecto...
Int: Tengo que decir que me siento bastante pasmado por todo esto. Dice que consiguió subir el
salario de los programadores? Eso es inmoral.
BS: No del todo. Cada uno tiene su opción. Yo no esperaba que la cosa se me fuese tanto de las
manos. De cualquier forma acerté. C++ se esta muriendo ahora, pero los programadores todavía
conservan sus sueldos altos. Especialmente esos pobres diablos que tienen que mantener toda esta
majadería. Comprendes que es imposible mantener un gran modulo en C++ si no lo has escrito tu
mismo?
Int: Como?
BS: Recuerdas cuanto tiempo se perdía buscando a tientas en las cabeceras sola para darse
cuenta de que 'RoofRaised' era un numero de doble precisión? Bien, imagina el tiempo que te
puedes tirar para encontrar todos los typedefs implícitos en todas las clases en un gran proyecto.
BS: Te acuerdas de la duración media de un proyecto en C?. Unos 6 meses. No mucho para
que un tipo con una mujer e hijos pueda conseguir un nivel de vida decente. Coge el mismo
proyecto, realízalo en C++ y que obtienes? Te lo diré. Uno o dos años. No es grandioso? Mucha más
seguridad laboral solo por un error de juicio. Y una cosa más. Las universidades no han estado
enseñando C desde hace mucho tiempo, lo que produce un descenso del número de buenos
programadores en C. Especialmente de los que saben acerca de la programación en sistemas Unix.
Cuantos tipos sabrían que hacer con un 'malloc', cuando han estado usando 'new' durante estos
años y nunca se han preocupado de de chequear el código de retorno?. De hecho la mayoría de los
programadores en C++ pasan de los codigos que les devuelven las funciones. Que paso con el '-1'?
Al menos sabías que tenías un error, sin enredarte con 'throw', 'catch', 'try'...
BS: Lo hace? Te has fijado en la diferencia entre un proyecto en C y el mismo en C++? La etapa
en la que se desarrolla un plan en un proyecto en C++ es tres veces superior. Precisamente para
asegurarse de que todo lo que deba heredarse, lo hace, lo que no, no. Y aun así sigue dando fallos.
Quien ha oído hablar de la pérdida de memoria en un programa en C? Ahora se ha creado una
autentica industria especializada en encontrarlas. Muchas empresas se rinden y sacan el producto,
sabiendo que pierde como un colador, simplemente para reducir el gasto de buscar todas esas fugas
de memoria.
BS: Lo dudo. Como dije, C++ esta en su fase descendente ahora y ninguna compañía en su
sano juicio comenzaría un proyecto en C++ sin una prueba piloto. Eso debería convencerles de que
es un camino al desastre. Si no lo hace, entonces se merecen todo lo que les pase. Ya sabes?, yo
intente convencer a Dennis Ritchie a reescribir Unix en C++...
BS: Afortunadamente tiene un buen sentido del humor. Creo que tanto él cómo Brian se
figuraban lo que estaba haciendo en aquellos días, y nunca empezaron el proyecto. Me dijo que me
ayudaría a escribir una versión en C++ de DOS, si estaba interesado...
Int: Lo estaba?
BS: De hecho ya he escrito DOS en C++, te pasare una demo cuando pueda. Lo tengo
ejecutándose en una Sparc 20 en la sala de ordenadores. Va como un cohete en 4 CPUs, y solo
ocupa 70 megas de disco...
BS: Ahora estas bromeando. No has visto Windows '95? Creo que es mi mayor éxito. Casi
acaba con la partida antes de que estuviese preparado
Int: Ya sabes, la idea de Unix++ me ha hecho pensar. Quizás haya alguien ahí fuera
intentándolo.
BS: Pero es la historia del siglo. Solo quiero ser recordado por mis compañeros programadores,
por lo que he hecho por ellos. ¿Sabes cuanto puede conseguir un programador de C++ hoy día?
18 PROGRAMACIÓN C++ Y COMUNICACIONES.
Int: Lo ultimo que oí fue algo como unos $70 - $80 la hora para uno realmente bueno...
BS: ¿Lo ves? Y se los gana a pulso. Seguir la pista de todo lo que he puesto en C++ no es fácil.
Y como dije anteriormente, todo programador en C++ se siente impulsado por alguna promesa
mistica a usar todos los elementos del lenguaje en cada proyecto. Eso ciertamente me molesta a
veces, aunque sirva a mi propósito original. Casi me ha acabado gustando el lenguaje tras todo este
tiempo.
BS: Lo odiaba. Parece extraño, ¿no estas de acuerdo? Pero cuando los beneficios del libro
empezaron a llegar... bien, te haces una idea...
Int: Solo un minuto. ¿Que hay de las referencias?. Debe admitir que mejoro los punteros de C...
BS: Hmm... Siempre me he preguntado por eso. Originalmente creí que lo había hecho.
Entonces, un día estaba discutiendo esto con un tipo que escribe en C++ desde el principio. Dijo que
no podía recordar cuales de sus variables estaban o no referenciadas, por lo que siempre usaba
punteros. Dijo que el pequeño asterisco se lo recordaba.
Int: Bien, llegados a este punto suelo decir 'muchas gracias' pero hoy no parece muy adecuado.
BS: Prométeme que publicaras esto. Mi conciencia esta dando lo mejor de mi mismo en estos
momentos.
BS: ¿Quien se lo creería de todas formas?... De todos modos, ¿puedes enviarme una copia de
la cinta?.
¿Sorprendido?. No deja de ser más que una de las gracias que circulan por la red de
cuando en cuando. Sin embargo, si que hay algo de verdad en la entrevista por lo que se
anima a que al finalizar el curso el lector vuelva a leerla para que pueda comprender mejor los
riesgos de una programación orientada a objetos mal entendida, o de las posibles
consecuencias que el mal uso de las potentes herramientas de C++ pueden generar.
1
Del nombre inglés C with classes.
2
American Nacional Standards Institute.
3
En su traducción al castellano del término library, la palabra más correcta es biblioteca. Sin
embargo la mayoría de los programadores la han traducido por librería, por lo que en estos
apuntes se adoptará esta última.
20 PROGRAMACIÓN C++ Y COMUNICACIONES.
al nombre del lenguaje. De esta forma, al estándar resultante de la norma ISO/IEC 14882
publicada en 1998, se le denomina como C++98.
Posteriormente existe una nueva especificación denominada como C++03, como
consecuencia de la aprobación de la revisión INCITS/ISO/IEC 14882‐2003. En esta, no se
modifica la sintaxis y aspecto del lenguaje pero si se especifican aspectos necesarios para el
desarrollo de compiladores, y librerías para el estándar.
En donde si se produce una variación del lenguaje que los compiladores han
comenzado a integrar es en el estándar C++11. Aprobado y publicado en Agosto de 2011
como el nuevo estándar. En este caso si que se incluyen variaciones importantes en el
lenguaje, pero conservando casi prácticamente la compatibilidad total con el estándar C++98.
Los tres índices que más se utilizan para medir la popularidad de un lenguaje en la
actualidad son el TIOBE Programming Community Index, el Languaje Popularity Index, y el
PYPL, este último soportado por el análisis realizado por Google Trends.
En el siguiente gráfico se incluye una captura del índice TIOBE con fecha Enero de
2015. Este índice está pensado para decidir que lenguaje debe saber un programador a fecha
actual, y se calcula no por el número de líneas de código sino más bien por el número de
programadores y estudiantes de programación que en la actualidad siguen, programan o
aprenden un lenguaje.
Es importante destacar que Objective‐C es un lenguaje análogo a C++ pero pensado
para desarrollar aplicaciones para dispositivos móviles e impulsado por Apple como medio de
programación de sus dispositivos. Sin embargo esta misma empresa comienza a promocionar
Swift como lenguaje básico y como consecuencia, al estar tan ligado a una plataforma, tras un
fuerte boom en torno a 2012, la tendencia ahora es a la baja frente a C++. En cualquier caso el
mundo de C, C++, C# y Objective‐C constituyen casi el 40% del índice. Si a eso añadimos que
La siguiente gráfica muestra la tendencia que estos lenguajes han seguido según este
índice durante los últimos 10 años:
22 PROGRAMACIÓN C++ Y COMUNICACIONES.
Estos datos discrepan ligeramente con el mostrado por PYPL. En cualquier caso, de
nuevo, el trío C, C++, C# junto con Java son predominantes. La diferencia entre TIOBE y PYPL
básicamente se debe a que TIOBE cuenta el número de paginas web que hablan del lenguaje
(simplificando mucho) mientras que PYPL se centra en el número de visitas y búsquedas
arrojadas por GoogleTrends. Un caso curioso es el de Objective‐C que tiene unos 20 millones
páginas en la web frente a los 11 millones de C, sin embargo las lecturas y búsquedas de C son
actualmente 30 veces más en C que en Objective‐C.
C#: (c‐sharp) es un lenguaje desarrollado por Microsoft que intenta combinar los
principios de C, C++ y Java, y se usa básicamente en el desarrollo de programas para
Windows. Su mayor inconveniente es precisamente el constituir un lenguaje
asociado una plataforma.
Así podríamos pensar en una librería en donde los datos corresponden a los libros, y
el encargado de ordenarlos forrarlos, destruirlos, etc. es el librero con todos sus ayudantes. Es
claro que ni el local, ni las estanterías, ni los libros y hojas son responsables de decidir en
donde han de situarse ni de cuál es su precio etc. a pesar de que son los contenedores de la
información que determina estas características. Será el librero el que al observarlo y
examinarlo decidirá su ubicación, clasificación, precio, el forro, etc.
28 PROGRAMACIÓN C++ Y COMUNICACIONES.
Objetos. Un objeto es una entidad que tiene unos atributos particulares (datos)
y unas formas de operar sobre ellos (los métodos o funciones miembro). Es
decir, un objeto incluye, por una parte una serie de operaciones que definen su
comportamiento, y una serie de variables manipuladas por esas funciones que
definen su estado. Por ejemplo, una ventana Windows contendrá operaciones
como “maximizar” y variables como “ancho” y “alto” de la ventana.
En C++ los ficheros de código se terminan con la extensión cpp de forma que el
entorno de desarrollo puede decidir que compilador va a utilizar.
4
Case sensitive
se han añadido las 42 siguientes, por lo que tampoco podrán ser utilizadas como
identificadores en el código:
36 PROGRAMACIÓN C++ Y COMUNICACIONES.
Operador Significado
:: Operador de resolución de ámbito de una variable o función miembro de una
clase. Denominado como operador scope.
this Toma el valor de la dirección del elemento desde el que ha sido invocado.
& Adicionalmente a los significados en C, se utilizará para definir referencias y
alias.
new Crea un objeto del tipo indicado a continuación. Modo habitual de reserva
dinámica de memoria en C++
delete Destruye un objeto creado dinámicamente con el operador new
.* Accede al miembro de una clase cuando el miembro es referenciado por un
puntero.
->* Accede al miembro de una clase cuando tanto el miembro como la instancia de
la clase están referenciados por un puntero.
typeid Identificación de tipo
???_cast Conversiones forzadas del tipo de una expresión.
Los dos modos de acceso nuevos (.* y ‐>*), no aparecerán más en estos apuntes por
lo que se incluye a continuación un breve ejemplo y explicación de los mismos.
#include <cstdio>
struct Complex{
float real;
float imag;
};
void main()
{
struct Complex a={0,0}, *b; //creo dos variables de tipo Complex
float Complex::*valor; //defino un puntero a floats de Complex
b=&a;
//le asigno a valor la direccion relativa de la parte real
valor= &Complex::real;
a.*valor=5.0F;
printf("\n%f + %fj",a.real, a.imag);
//le asigno a valor la direccion relativa de la parte imag
valor = &Complex::imag;
b->*valor=3.0F;
printf("\n%f + %fj",a.real, a.imag);
}
Cuando dos operadores tienen la misma asociatividad (por ejemplo por que fueran el
mismo) entonces se sigue la regla de asociatividad, que es de izquierda a derecha o de
derecha a izquierda: 3+5+2‐4+8 lo interpretaremos como (((3+5)+2)‐4)+8 es decir de
izquiera a derecha.
1 ::
2 () [] . -> v++ v-- ??? cast
>>
typeid
3 -a +a ~ ! & * ++v –v sizeof new
<<
delete (tipo)
4 ->* .* >>
5 a*b a/b a%b >>
6 a+b a-b >>
7 << >> >>
8 < <= > >= >>
9 == != >>
10 a&b >>
11 a^b >>
12 a|b >>
13 && >>
14 || >>
15 a?b:c <<
16 = *= /= %= += -= <<= >>= &= |=
<<
^=
17 , >>
Las únicas excepciones son para el operador ternario, y en los operadores lógicos &&
y || (siendo el orden de izquierda a derecha). Todo este tema es un poco más complejo dado
que además hay que analizar si los efectos de la expresión se evalúan antes o después, pero
para el punto en el que nos encontramos es suficiente con estas nociones.
2.4. Comentarios
En el C original, la inclusión de comentarios en los ficheros de código venía
delimitado por una combinación de caracteres de comienzo (/*) y otra de finalización de
comentario (*/). De tal forma que todo el texto contenido entre estos dos delimitadores era
eliminado por el preprocesador antes de proceder a realizar cualquier operación de
interpretación.
Tenga en cuenta que los bool y los int son tipos distintos. Sin embargo, cuando es
necesario, el compilador realiza una conversión automática de forma que pueden utilizarse
libremente valores bool (true y false) junto con valores int sin utilizar una conversión explícita.
int x = 0, y = 5;
if (x == false) printf("x falso.\n"); // Ok.
if (y == true) printf("y cierto.\n"); // Ok.
if ((bool) x == false) printf("x falso.\n"); // cast innecesario
Estas conversiones entre tipos lógicos y numéricos, son simplemente una concesión
del C++ para poder aprovechar la gran cantidad de código C existente. Tradicionalmente en C
se hacía corresponder falso con el valor cero y cierto con el valor uno (o distinto de cero).
Para convertir tipos aritméticos (enumeraciones, punteros o punteros a miembros de
clases) a un tipo bool, la regla es que cualquier valor cero; puntero nulo, o puntero a miembro
de clase nulo, se convierte a false. Cualquier otro valor se convierte a true. En principio el
estándar establece que solo los valores enteros son convertidos automáticamente siendo
necesario una conversión explícita en otro caso, sin embargo, a menudo, los compiladores no
requieren de este cast por comodidad.
void main()
{
/*petición de variables*/
int j;
struct complejo micomplejo;
/*código*/
j=sizeof(struct complejo);
}
void main()
{
//petición de variables
int j;
complejo micomplejo;
//código
j=sizeof(complejo);
}
Se observa como desde el punto de vista del código, queda más claro y cómodo.
Por otro lado, al igual que ocurría con las estructuras y las uniones, en las
enumeraciones se simplifica la indicación del tipo durante su uso prescindiendo de la palabra
enum.
entero asignado a cada uno de los posibles valores de una variable enum van por orden
comenzando desde 0. De esta forma, suspenso vale 0, aprobado vale 1, bien vale 2, etc. Sin
embargo, el programador puede establecer estos valores en la definición de la enumeración.
El siguiente código muestra estas dos características:
enum colores {rojo=3,verde=5, azul, amarillo=8};
int i = verde;
En este caso, rojo vale 3, verde vale 5, azul vale 6 y amarillo vale 8. Es decir, cuando
explícitamente no se da un valor, se sigue la numeración del valor anterior.
Para ayudar a entender el nivel de fuerza que este tipo de datos tiene, las siguientes
propiedades se cumplen a la hora de evaluar expresiones con tipos enum:
No habrá error si a una variable de tipo enum le asigno (indicando la conversión) un
valor no definido: nota2=notas(100) sería una expresión válida.
Uniones anónimas
Aunque su aplicación más inmediata se da en el interior de estructuras más
complejas, es conveniente mencionar ya que C++ admite la definición de uniones anónimas.
Su finalidad es la de definir un conjunto de campos (miembros en el caso de clases) ubicados
en la misma zona de memoria. Simplifica el acceso a estos campos evitando el tener que
utilizar, como ocurre en C, el indicador del tipo de acceso sobre la unión. Para visualizar la
variación, se mostrará el mismo segmento de programa escrito en C y en C++:
El siguiente programa en C:
struct datos /*definición del patrón de estructuras*/
{
int tipo;
union _dato /*union dentro de la estructura*/
{
char caracter;
int entero;
float decimal;
}dato;
};
int main()
{
struct datos midato;
midato.tipo=2;
midato.dato.entero=5;
}
for(int j=0;j<i;j++)
{
printf(“Hola mundo”);
int k=3;
printf(“valor de k %d”,k);
}
if(--i==0)return;
}
En C quedaba claro cual era el ámbito de una variable, puesto que una variable local
era visible desde todo el cuerpo de la función en la que estaba definida, y una variable global
lo era desde el punto en el que estaba en el código en adelante.
En C++, el ámbito de una variable es desde el punto en el que esta está declarada
hasta el final del bloque en el que se encuentra.
Una variable local oculta al igual que en C a una variable más global relativamente.
Es decir, bajo un mismo nombre, las variable que se considerará será la más local
entre las visibles.
C++ dispone del operador (::), llamado operador de resolución de visibilidad. Este
operador, antepuesto al nombre de una variable global que está oculta por una variable local
del mismo nombre, permite acceder al valor de la variable global. No es posible, sin embargo,
acceder a una variable local que esté oculta por otra variable local. El siguiente ejemplo
muestra el modo de uso de el operador scope.
int i=0;
void main()
{
int i;
while(::i<10)
{
++::i;
for(i=0;i<10;i++)
printf(“%d : %d\n”,::i,i);
}
return 1;
}
En este programa se ha realizado la anidación de dos bucles con dos iteradores del
mismo nombre, uno global y otro local.
Por útimo, la duración de las variables locales (auto) será el bloque en el que están
definidas. Es decir, que la variable es destruida en el momento en que se finaliza la ejecución
del bloque en el que se encuentra su declaración. Es posible definir variables estáticas al igual
que en C, de forma que una variable local definida como estática (static) no es destruida
hasta el final del programa, y por tanto sólo es creada la primera vez que se ejecuta el bloque
en el que se encuentra su declaración .
int main()
{
int i=2;
float v;
switch (i)
{
case 3:
int i=5;
break;
case 2:
v=3.5F;
for(int i=0;i<10;i++)v=1.0F;
default:
v=3.0F;
break;
46 PROGRAMACIÓN C++ Y COMUNICACIONES.
}
}
5
Este apartado ha sido extraido y reformateado de la siguiente página web:
http://c.conclase.net/curso/index.php. Los cursos de C y C++ con clase son altamente
recomendados por lo bien que están explicados y el carácter docente de esta página
web.
Almacenamiento automático: Por defecto las variables locales son auto. Estas
variables se crean durante la ejecución, y se elige el tipo de memoria a utilizar en función del
ámbito temporal de la variable. Una vez cumplido el ámbito, la variable es destruida. Es decir,
una variable automática local de una función se creará cuando sea declarada, y se destruirá al
terminar la función. Una variable local automática de un bucle será destruida cuando el bucle
termine.
Almacenamiento estático: El especificador static se utiliza tanto para variables como
para funciones:
static <tipo> <nombre_variable>;
static <tipo> <nombre_de_función>(<lista_parámetros>);
Cuando se usa en la petición de una variable, este especificador hace que se asigne
una dirección de memoria fija para el objeto mientras el programa se esté ejecutando. Es
decir, su duración es la del programa. En cuanto la visibilidad conserva el que le corresponde
según el punto del código en que aparezca la declaración. Debido a que tiene una posición de
memoria fija, su valor permanece, aunque se trate de una variable declarada de forma local,
entre distintas reentradas en el ámbito del objeto. Los objetos estáticos no inicializados
toman por defecto un valor nulo, aunque conviene siempre indicar el inicializador com buena
practica a la hora de programar.
Esta inicialización sólo afectará al momento de creación de la variable.
Este especificador se usa para indicar que el almacenamiento y valor de una variable
o la definición de una función están definidos en otro módulo o fichero fuente. Las funciones
declaradas con extern son visibles por todos los ficheros fuente del programa, salvo que se
defina la función como static.
El especificador extern sólo puede usarse con objetos y funciones globales.
48 PROGRAMACIÓN C++ Y COMUNICACIONES.
Las declaraciones de prototipos son declaraciones externas por defecto, de ahí que al
igual que ocurría con la especificación auto, normalmente no veamos esta palabra clave en las
declaraciones de funciones. Se puede usar extern "c" con el fin de prevenir que algún
nombre de función escrita en C pueda ser ocultado por funciones de programas C++. Este
especificador no se refiere al tipo de almacenamiento, ya que sabemos que en el caso de
prototipos de funciones es el especificador por defecto. En realidad es una directiva que está
destinada al enlazador, y le instruye para que haga un enlazado "C", distinto del que se usa
para funciones en C++.
Lógicamente, este especificador lo usaremos con programas que usen varios ficheros
fuente, que será lo más normal con aplicaciones que no sean ejemplos o aplicaciones simples.
Almacenamiento en registro: Para especificar este tipo de almacenamiento se usa el
especificador register.
Funciones inline
C++ permite introducir el modificador inline en la declaración de las funciones. Con
este modificador indicamos al compilador que consideramos conveniente que las llamadas
realizadas a esta función sean sustituidas por el cuerpo de código de la función. El efecto
respecto de la funcionalidad del programa es el mismo en ambos casos, pero en caso de
realizarse esta sustitución, el programa resultante es más rápido, al evitarse el salto a la
función, a costa de un código ejecutable más extenso.
Es recomendable utilizar el modificador inline en funciones pequeñas, que son
llamadas en pocos lugares (que es diferente a que sean llamadas pocas veces).
Para poder asignar el modificador inline a una función, dicha función debe estar
definida antes de que sea invocada, de lo contrario el compilador no lo tendrá en cuenta. Por
este motivo, las funciones inline son normalmente definidas en los ficheros de cabecera.
Modificar una función para que sea de este tipo implica anteponer inline al tipo del valor
retornado por la función.
max=MAXIMO(++a,--b);
printf(“el máximo de %d y %d es %d”,a,b,max);
}
El máximo de 3 y 2 es 2
Esto es debido a que a ha sido incrementada una vez y b dos veces, una en la
evaluación de la expresión de la condición, y otra en la evaluación de la expresión del
resultado. Este tipo de errores, así como las precauciones clásicas tomadas por medio de los
paréntesis quedan solventadas por medio del uso de las funciones inline.
Funciones sobrecargadas
La sobrecarga de funciones es una característica de C++ que hace que los programas
sean más legibles.
Consiste en declarar y definir varias funciones distintas que tienen un mismo
nombre. En el momento de la compilación se decide si se llama a una u otra función
dependiendo del número y/o tipo de los argumentos actuales de la llamada a la función.
La sobrecarga de funciones no admite funciones que difieran sólo en el tipo del valor
de retorno, pero con el mismo número y tipo de argumentos.
El siguiente ejemplo muestra como sobrecargar la función suma para números reales
y números complejos:
struct complejo
{
float real, imaginario;
}
float suma(float a, float b)
{
return (a+b);
}
complejo suma(complejo a, complejo b)
{
complejo c;
c.real=a.real+b.real;
c.imaginario=a.imaginario+b.imaginario;
return c;
}
void main()
{
complejo a={1.0F,1.5F},b={0.0F,1.1F},c;
float d=3.0F,e=8.2F,f;
c=suma(a,b); //utiliza la funcion suma de complejos
f=suma(d,e); //utiliza la funcion suma de enteros
}
En caso de que pueda darse esta ambigüedad por ser los tipos de datos
promocionables o equivalentes se puede romper la ambigüedad mediante una conversión
explícita. Por ejemplo:
void imprime(double a);
void imprime(long a);
void main()
{
int b=8;
imprime(b);
imprime(static_cast<long>(b));
imprime((double)b);
}
En este ejemplo, la primera llamada a la función imprime es ambigua puesto que b
puede ser promocionado tanto a double como a long. Esta ambigüedad es resuelta en los dos
casos siguientes. En la segunda llamada se ha utilizado uno de los nuevos operadores de C++
que es la conversión estática al tipo indicado entre llaves. Este nuevo operador aparece
especialmente por la posibilidad de realizar conversiones con verificación de tipo durante la
52 PROGRAMACIÓN C++ Y COMUNICACIONES.
ejecución por medio de su dual dynamic_cast. El matiz entre los ditintos tipos de conversión
se verá más adelante, baste ahora con mencionarlos.
Es importante destacar que una vez omitido un argumento en una llamada, hay que
omitir todos los posteriors. Es decir, no es posible escoger omitir un parámetro sí y otro no,
sólo es posible omitir los parámetros desde un punto determinado.
Otro aspecto interesante a tener en cuenta para evitar errores en la compilación, es
que en caso de existir un prototipo de definición de la función (bien en el fichero de código o
en una cabecera), los parámetros por defecto deben situarse en el prototipo y no en la
definición. Así, el ejemplo anterior quedaría como sigue:
void imprimeVector(float v[], int tam=3,bool linea=false);
void main()
{
float vector[5]={1.0F,2.0F,3.0F,4.0F,5.0F};
imprimeVector(vector);
imprimeVector(vector,5);
imprimeVector(vector,5,true);
}
void imprimeVector(float v[], int tam,bool linea)
{
printf(“(“);
for(int i=0;i<tam;i++)printf(“ %f”,v[i]);
if(linea)printf(“ )\n”);
else printf(“ ) ”);
}
Antes de comenzar sin embargo con todas las posibles aplicaciones y peculiaridades
de este nuevo “tipo de variables”, la regla práctica para no terminar liándose es la ya
expuesta antes. Una referencia no es un puntero o algo externo a la variable referenciada… es
la misma variable con un nombre distinto.
Se verá ahora la aplicación más sencilla pero con menos utilidad: la creación de un
alias de una variable.
La forma de declarar una referencia a un objeto en general es:
tipo &alias = variable
Al igual que en los punteros, en caso de tener varias declaraciones seguidas, se
adjuntará el & al identificador de la referencia en vez de al tipo básico.
int a,b;
int &c=a,d,&e=b;
54 PROGRAMACIÓN C++ Y COMUNICACIONES.
Toda referencia, excepto las declaradas como parámetros formales en una función,
debe ser siempre inicializada. Además una referencia no puede alterar el objeto al que se
refiere una vez inicializada de igual forma que no es posible mover una variable de un sitio a
otro de la memoria, sino que una vez creada permanece en el mismo sitio.
El siguiente ejemplo muestra la creación de un alias de otra variable:
void main()
{
int i;
int& j=i; //j es un alias de i
for(i=0;i<3;i++)printf(“%d”,j); //imprime 012
for(j=0;j<3;j++)printf(“%d”,i); //imprime 012
for(j=0;i<3;j++)printf(“%d”,j); //imprime 012
}
El efecto es el mismo que si sustituimos todas las j por i, ¡son la misma variable!, y
por tanto los tres bucles for hacen exactamente lo mismo. Se observa por tanto, que la
aplicación de las referencias dentro de un mismo bloque tiene poca utilidad. El hecho de que
se haya mencionado esta aplicación es que ayuda al entendimiento del modo de proceder de
las referencias.
c=*a;
*a=*b;
*b=c;
}
void main()
{
int x=1,y=2;
printf(“contenido de x e y: %d, %d\n”,x,y); /*imprime 1, 2 */
permutar(&x,&y); /*llamada a la función con la dirección de
las variables x e y*/
printf(“contenido de x e y: %d, %d\n”,x,y); /*imprime 2, 1 */
}
Aunque desde el punto de vista del modo de funcionamiento real del ordenador, es
mucho más correcto el modo en que este código se escribe en C, en C++ las referencias
permiten realizar esta operación mediante la creación de alias de los argumentos actuales,
consiguiendo desde el punto de vista del programador una notación mucho más cómoda:
void permutar(int &a, int &b)
{
int c;
c=a;
a=b;
b=c;
}
void main()
{
int x=1,y=2;
printf(“contenido de x e y: %d, %d\n”,x,y); //imprime 1, 2
permutar(x,y); //llamada a la función directamente con
//las variables x e y
printf(“contenido de x e y: %d, %d\n”,x,y); /*imprime 2, 1 */
}
Pues bien, en C++ se pueden utilizar las referencias como valor de retorno para
realizar anidaciones de este tipo con notación más cómoda, o incluso ¡para poder realizar
asignaciones a una variable retornada por referencia tras la llamada de la función!. Esto ya
comienza a sonar a chino para un experto programador de C, pero la idea es la misma que
antes… con una referencia podemos utilizar un nombre distinto para una variable. Ahora ese
nombre distinto será la llamada a la función.
vector2D& normaliza(vector2D& a)
{
double modulo;
modulo=sqrt((a.x)*(a.x)+(a.y)*(a.y));
a.x/=modulo;
a.y/=modulo;
return a;
}
double& componenteX(vector2D& a)
{
return (a.x);
}
void main()
{
vector2D vector={3.0,4.0};
printf(“la x normalizada es %d”,normaliza(vector).x);
componenteX(vector)=8.0;
...
}
Nótese que gracias a las referencias, ahora es posible utilizar la llamada a una
función en el lado izquierdo de una asignación. No asignamos a la función el valor situado a la
derecha, sino que asignamos dicho valor a la variable retornada por referencia por la función,
puesto que la expresión de llamar a la función se ha convertido en un alias de la variable
retornada.
Es importante considerar que sólo es posible retornar referencias a variables cuya
vida supere a la duración de la función, por el mismo motivo que en C no se retornan
punteros a variables declaradas en el interior de la función: cuando se termina la ejecución de
la función, las variables auto definidas en su interior son destruidas.
El siguiente código sería por tanto erróneo:
vector2D& normaliza(vector2D a)
{
double modulo;
modulo=sqrt((a.x)*(a.x)+(a.y)*(a.y));
a.x/=modulo;
58 PROGRAMACIÓN C++ Y COMUNICACIONES.
a.y/=modulo;
return a;
}
void main()
{
vector2D vector={3.0,4.0;
printf(“la x normalizada es %d”,normaliza(vector).x);
}
#include <stdlib.h>
#include <stdio.h>
#include <memory.h> /*necesario para usar memcpy*/
int main()
{
long tamanio=0,capacidad=10,i;
int valor;
int *vector,*aux;
vector=(int *)malloc(capacidad*sizeof(int));
while(scanf("%d",&valor)&&(valor!=0))
{
vector[tamanio++]=valor;
if(tamanio==capacidad)
{
capacidad+=10;
aux=(int *)malloc(capacidad*sizeof(int));
if(aux==NULL)
{
printf("Error en la reserva de memoria");
free(vector);
return -1;
}
memcpy(aux,vector,tamanio*sizeof(int));
free(vector);
vector=aux;
}
}
printf("número de números almacenado: %d\n",tamanio);
for(i=0;i<tamanio;i++)printf("%d, ",vector[i]);
free(vector);
return 1;
}
Ciertamente este ejemplo no es muy didáctico, puesto que se ha optado por una
programación compacta. La finalidad del mismo es la de concienciar al lector de que a partir
de ahora se requerirán al menos los conocimientos de C reflejados en este ejercicio. Si no
fuera el caso, se recomienda repasar C antes de continuar. Por tanto:
El operador new.
El operador new es semejante a la función malloc aunque como se verá a lo largo del
curso no se limita exclusivamente a realizar la reserva de memoria. Por ahora se considerará
que permite asignar memoria perteneciente al área de almacenamiento libre para un objeto
o para una matriz de objetos –de momento entiéndase por objeto una variable.
El ejemplo muestra dos posibles sintaxis para la misma operación. Ambas son
equivalentes. Es posible, como se ha visto, realizar la declaración de un puntero y la reserva
de memoria en la misma sentencia:
int *a = new int;
El operador delete
El operador delete destruye un objeto creado por el operador new, liuberando el
sistema operativo la memoria ocupada por dicho objeto. A diferencia de lo que ocurría en C
con la función free, delete puede ser utilizado sobre un puntero nulo (apunta a cero) en cuyo
caso no realiza ninguna operación. Es importante recordar que nunca se puede pedir la
liberación de una memoria que no ha sido reservada dinámicamente. O como regla práctica,
El operador delete solo puede ser aplicado a zonas de memoria reservadas mediante
un new.
Según lo indicado, las operaciones que hay que realizar para eliminar los objetos
reservados para el anterior ejemplo son:
1 delete a;
3 delete [] b;
4 delete [] c; //observese que se considera c como vector
5 for(i=0;i<3;i++)delete [] d[i]; //para cada new un delete
ámbito, siempre que indique "Pepe" se hará referencia implícita al "Pepe" de dicho ámbito,
pero desde fuera siempre se podrá decir "Pepe el de Almería" o "Pepe el de Toledo".
Además se pude indicar, por medio de la palabra clave using el uso de un ámbito
externo por defecto. Esto nos permite subdibidir el espacio globald e nombres en espacios
personalizados evitando las redefiniciones o el agotacmiento de identificadores en proyectos
grandes.
Por último, es interesante aclarar que puedo añadir elementos a un espacio de
nombres en cualquier parte, y en muchos ficheros distintos. Lo habitual es que una librería en
C++ por ejemplo tenga todos sus elementos definidos en un namespace específico.
namespace Almeria{
int Pepe;
void foo(){
Pepe=73;
}
}
namespace Toledo{
int Pepe;
}
void foo(){
Almeria::Pepe=3;
Toledo::Pepe=5;
}
void foo2()
{
using namespace Almeria;
Pepe=8;
Toledo::Pepe=3;
Almeria::foo(); //hay foo() y Almería::foo() es necesario
}
la pantalla (mostrar un mensaje o unos valores) y un flujo de entrada puede ser el conjunto
de teclas pulsadas en un teclado.
Por tanto, un flujo es un objeto que hace de intermedio entre el programa y el
destino u origen de la información. En la mayoría de los casos no es relevante para el
programa la naturaleza física del sistema que envía o recibe los datos, sino que lo que
interesa es el carácter secuencial de los datos enviados o recibidos.
De hecho, al igual que en C, las operaciones de entrada y salida (lectura y escritura)
siguen en general siempre el mismo esquema: inicialmente se abre el canal de comunicación
o flujo (apertura de un fichero o un puerto), se envía o recibe información durante el tiempo
que sea necesario, y finalmente, cuando ya no es necesario este canal, se cierra para no
sobrecargar al sistema operativo.
Se puede objetar a lo anterior que para el uso de printf no ha sido nunca necesario
realizar la apertura del canal. Es cierto, pero es que en C, todas las operaciones de entrada y
salida estándar se realizan por medio de ficheros que en algunos casos están ocultos. Al
comenzar cualquier programa en C se produce la apertura automática de tres ficheros: stdin,
stdout, y stderr, que constituyen respectivamente la entrada estándar de datos, la salida
estándar y la salida para mensajes de error estándar. De forma que es el sistema el que
realiza la apertura de ficheros por nosotros. Cuando uno escribe una intrucción printf(…)
realmente está escribiendo fprintf(stdout,…).
Volviendo entonces a C++, el comportamiento es análogo, pero mucho más cómodo.
Cuando un programa en C++ se ejecuta, se crean automáticamente tres flujos identificados
por los siguientes objetos:
1. Un flujo de entrada estándar (normalmente el teclado): cin.
El siguiente ejemplo ilustra como se imprimen una serie de datos por pantalla
mediante el uso del flujo estándar cout.
#include<iostream.h>
int main()
{
int i=2,j=3;
double dato=5.3;
char a=’a’,b[]=”hola”;
cout << i;
cout << i << j << endl;
cout << “el valor de dato es “ << dato << endl;
cout << “el carácter a=” << a << “y la cadena b” << b << endl;
}
Al igual que en scanf la separación entre cada valor introducido se realiza por medio
de el espacio en blanco según el lenguaje C (espacio, tabulador, cambio de línea, …).
Un aspecto importante que se abordará en el capítulo correspondiente es el de la
gestión de errores y el estado de los flujos, así como la especificación de formatos (por
ejemplo, el número de decimales que se utilizarán en la impresión de un double). De
momento se mencionará que son operaciones fácilmente realizables pero que escapan al
enfoque de este capítulo.
2.13. EJERCICIOS
EJERCICIO 2.1
EJERCICIO 2.2
Diseñar e implementar una función de nombre norma que permita obtener la norma
∑ de un vector de float de dimensión n y que en función del parámetro adicional
bnormaliza de tipo bool permita normalizar (true) o no (false) el vector entrante. Además por
defecto, se considerará que los vectores son de dimensión 3 y que no se desea modificar el
vector introducido.
EJERCICIO 2.3
Una función contiene las siguientes sentencias de C++. Indíquese la impresión por pantalla
que dichas instrucciones provocan.
int a,b,c=1;
a=3,2+3;
b=(3,2+3);
c+=c+=2;
printf("a=%d b=%d c=%d",a,b,c);
EJERCICIO 2.4
Indíquese la impresión por pantalla del siguiente programa:
68 PROGRAMACIÓN C++ Y COMUNICACIONES.
#include <stdio.h>
void main()
{
int i=0;
for(i=0;i<30;i++){
if(!(i%2)||!(i%3))continue;
printf("%d ", i);
}
}
EJERCICIO 2.5
Indíque cual de las siguientes líneas marcadas con un rectánculo es correcta (el compilador la
acepta):
enum notas {suspenso, aprobado, bien, notable, sobresaliente};
int var;
notas nota1, nota2;
□ nota1=bien;
□ nota1=1;
□ nota1=(notas)1;
□ nota1=suspenso+1;
□ nota1=suspenso+aprobado;
□ var=5+bien;
□ var=bien+5;
□ var=notable+bien;
□ var=nota1+bien;
□ nota1++;
□ for(var=suspenso;var<sobresaliente;var++)
printf(“%d”,var);
□ for(nota2=suspenso;nota2<sobresaliente;nota2++)
printf(“%d”,(int)nota2);
EJERCICIO 2.6
Indique la impresión por pantalla del código siguiente, si la secuencia de números introducida
por el usuario es la siguiente: 12, 13, 14, 15, 16, 17, 18, 12, 13 y 14.
#include “stdio.h”
int j=0;
for(int i=0;i<num;i++)if(vector[i]<vector[j])j=I;
return vector[j];
}
void main(){
int lista[3]={0,0,0};
for(int i=0;i<10;i++)scanf(“%d”,&menor(lista,3));
printf(“Valores: %d %d %d”,lista[0],lista[1],lista[2]);
}
3. El Concepto de Clase
laboratorio, la del compañero o la mía) agrupamos todos esos objetos reales en un concepto
más abstracto denominado mesa. De tal forma que podemos decir que un objeto específico
es una realización o instancia de una determinada clase.
Por ejemplo, en los programas de Windows, cada botón clásico de una interfaz es un
objeto. Todos los botones son instancias o realizaciones de la clase CButton de Windows.
Luego lo que realmente existe son los objetos, mientras que las clases se pueden considerar
como patrones para crear objetos. Pero además, estos botones pertenecen a un objeto más
grande que es el tapiz sobre el que están pintados. Este objeto de windows habitualmente no
es más que una instancia de la clase CDialog que permite crear objetos que contienen objetos
que son capaces de interaccionar con el usuario de forma gráfica. Muchas de las interfaces
que estamos acostumbrados a menejar en windows son instancias de esta clase de objetos.
Se observa que mediante este sistema por un lado están los datos y por otro las
funciones, aunque estas funciones estén definidas exclusivamente para trabajar con el tipo de
datos complex.
Las clases en C++ se pueden considerar como la evolución de las estructuras. Las
clases permiten no sólo agrupar los datos –como ocurre en las estructuras‐ sino que además
nos permiten incluir las funciones que operan con estos datos.
Las clases en C++ tienen los siguientes elementos o atributos:
puesto que este valor indica el número de objetos que se han introducido y
no un valor arbitrario. Por ello en C++ se puede establecer que algunos de
los datos y funciones miembro de una clase no puedan ser utilizados por el
programador, sino que están protegidos o son datos privados de la clase ya
que sirven para su funcionamiento interno, mientras que otros son públicos
o totalmente accesibles al programador. Esta cualidad aplicable a los datos
o a las funciones miembro es lo que se denomina como nivel de acceso.
//comienzo de la declaración
class complex
{
private:
double real;
double imag;
public:
void estableceValor(float re,float im) ;
float obtenModulo(void) ;
void imprime() ;
};
//fin de la declaración
Se verá con detalle el significado que tienen todas estas nuevas palabras y porque se
usan de la forma en la que se han utilizado:
Declaración de la clase
La sintaxis más simple de declaración de una clase es igual que la utilizada para la
declaración de las estructuras en C, pero cambiando la palabra clave struct por class y con la
posibilidad de introducir funciones y niveles de acceso entre las llaves:
class <identificador>
{
[<nivel de acceso a>:]
<lista de miembros de la clase>
[<nivel de acceso b>:
<lista de miembros de la clase>]
[<…>]
}[lista de objetos];
La primera línea establece el nombre con el que vamos a identificar la clase por
medio de la palabra introducida en el campo <identificador>. En el ejemplo, el identificador es
la palabra complex.
Una vez establecido el nombre se procede a indicar los datos y los métodos de la
clase. Aunque no existe una especificación en el orden en que los datos y funciones deben ser
declarados, es habitual proceder a introducir en primer lugar los datos miembro con sus
niveles de acceso y después el conjunto de métodos tanto públicos como privados.
En el ejemplo se indica en primer lugar que los elementos que se van a declarar
posteriormente serán de carácter privado. Esto de ha realizado por medio de la palabra clave
private. De esta forma, los datos real e imag de tipo double sólo podrán ser modificados y
consultados por medio de métodos de la clase. Veremos con más detalle la utilidad de todo
esto al hablar del concepto de encapsulamiento en el siguiente apartado.
Después se indica que a partir del punto en el que parece la palabra public se
declaran las funciones y datos miembro de la clase que son accesibles desde fuera de la clase,
y que por tanto constituyen la interfaz. En el ejemplo, tres funciones tienen este nivel de
acceso: estableceValor, obtenModulo e imprime. Con esto se finaliza la declaración de la
clase por lo que se procede a definirla.
76 PROGRAMACIÓN C++ Y COMUNICACIONES.
<tipo> <iden_clase>::<iden_metodo>(<argumentos>)
{
[código del método]
}
Al igual que ocurre en C, para acceder a los miembros de una estructura a partir de
un puntero a una variable de este tipo, se utiliza el operador flecha.
Y para ejecutar los distintos métodos que se han definido, se utiliza el operador “.”
que indica que se tratará de un miembro del objeto que lo antecede. A través del operador
punto, sólo se podrá acceder a los atributos y métodos públicos, quedando los elementos
privados protegidos por el compilador. De esta forma, la siguiente expresión daría un error de
compilación:
micomplejo.real=2.0;
Por lo que para establecer el valor del número complejo sólo se puede hacer uso del
método previsto estableceValor que obliga a introducir tanto la parte real como la imaginaria.
Todo esto tiene mucho que ver con el concepto de encapsulamiento que se verá a
continuación.
3.3. Encapsulamiento
Ya se ha comentado que para una clase, los únicos elementos accesibles desde el
exterior son aquellos que han sido declarados como públicos. Por tanto, los miembros
privados de una clase no son accesibles para funciones y clases exteriores a dicha clase.
En la programación orientada a Objetos, los niveles de acceso son el medio que se
utiliza para lograr el encapsulamiento que no es más que cada objeto se comporte de forma
78 PROGRAMACIÓN C++ Y COMUNICACIONES.
autónoma y lo que pase en su interior sea invisible para el resto de objetos. Cada objeto sólo
responde a ciertos mensajes y proporciona determinadas salidas.
El siguiente ejemplo ilustra como este concepto es muy importante de cara a
obtener un código reutilizable y seguro que es la principal finalidad de la POO. La siguiente
clase lista de números, nos permite almacenar un máximo determinado de números en
memoria estática. Dicha clase, evita que el usuario cometa errores de ejecución tales como
acceder a un número que no existe o introducir un conjunto de números que supere la
capacidad de almacenamiento de esta lista estática.
El precio que hay que pagar por esta protección es el tener que utilizar para todas las
operaciones métodos y por tanto funciones, aunque estas operaciones sean muy sencillas. Sin
embargo, el resultado es robusto, y fácilmente trasladable a varios programas.
class listaNumeros
{
private:
int num;
int lista[100];
public:
int agregarNumero(int val);
int extraerNumero(int ind);
int numeroNumeros(void);
listaNumeros(void);
};
//constructor de la clase… en breve se verá su utilidad
listaNumeros::listaNumeros(void){
num=0;
}
int listaNumeros::agregarNumero(int val){
if(num<100)lista[num++]=val;
else return -1;
return num;
}
int listaNumeros::extraerNumero(int ind){
if((ind<num)&&(ind>=0))return lista[ind];
return 0;
}
int listaNumeros::numeroNumeros(){
return num;
}
#include <iostream.h>
void main()
{
listaNumeros milista;
int i,val=1;
while(val!=0)
{
cout<<"introduzca un numero (finalizar con 0):";
cin>>val;
if(val!=0)val=milista.agregarNumero(val);
}
cout<<"\nLa lista de números es la siguiente:\n";
for(i=0;i<milista.numeroNumeros();i++)
cout<<milista.extraerNumero(i)<<" ";
cout<<"\n*********FIN DEL PROGRAMA**********\n";
}
Por ello, aunque cualquier función puede ser definida como inline, sólo se utiliza
para aquellas funciones pequeñas en las que el proceso de ejecución de un método es mayor
que la ejecución del método en sí. Esto es lo que ocurrirá con todas las funciones de consulta
o modificación de atributos no públicos de una clase.
El código anterior podría ser reescrito entonces de la siguiente forma:
class listaNumeros
{
private:
int num;
int lista[100];
public:
int agregarNumero(int val);
int extraerNumero(int ind)
80 PROGRAMACIÓN C++ Y COMUNICACIONES.
{
if((ind<100)&&(ind>=0))return lista[ind];
return 0;
}
int numeroNumeros(void){return num;};
listaNumeros(void){num=0;};
};
Cuando el método es tan breve que puede ser escrito en la misma línea de la propia
declaración del método, se opta por la brevedad para evitar declaraciones de clase
excesivamente extensas.
(que deberían ser excepcionales) C++ proporciona un mecanismo para sortear el sistema de
protección. Más adelante se verá la utilidad de esta técnica, ahora se limitará a explicar en
que consiste. El mecanismo del que se dispone en C++ es el denominado de amistad (friend).
clases sin embargo deberá incluir dentro de su declaración la copia del prototipo de la función
precedida de la palabra friend.
#include <iostream>
class A
{
public:
A(int i) {a=i;};
void Ver() { cout << a << endl; }
private:
int a;
friend void Visualiza(A); //Visualiza es amiga de la clase A
};
void main()
{
A Na(10);
Visualiza(Na); // imprime el valor de Na.a
Na.Ver(); // Equivalente a la anterior
}
class B
{
public:
B(int i){b=i;};
void ver() { cout << b << endl; };
bool esMayor(A Xa); // Compara b con a
private:
int b;
};
class A
{
public:
A(int i=0) : a(i) {}
void ver() { cout << a << endl; }
private:
// Función amiga tiene acceso
// a miembros privados de la clase A
friend bool B::esMayor(A Xa);
int a;
};
bool B::esMayor(A Xa)
{
return (b > Xa.a);
}
void main(void)
{
A Na(10);
B Nb(12);
Na.ver();
Nb.ver();
if(Nb.esMayor(Na)) cout << "Nb es mayor que Na" << endl;
else cout << "Nb no es mayor que Na" << endl;
}
Los constructores tienen el mismo nombre que la clase, no retornan ningún valor y
no pueden ser heredados. Además suelen ser públicos, dado que habitualmente se usan
desde el exterior de la clase. Algunos patrones más avanzados de lo visto en este curso, como
el “singleton” hacen uso a menudo de constructores privados o protegidos, pero en nuestro
caso en general los declararemos como públicos.
Si una clase posee constructor, será llamado siempre que se declare un objeto de esa
clase, y si requiere argumentos, es obligatorio suministrarlos. Por ejemplo, las siguientes
declaraciones son ilegales:
pareja par1;
pareja par1();
Cuando no se especifica un constructor para una clase, el compilador crea uno por
defecto sin argumentos al que se denomina como constructor de oficio. Por eso los ejemplos
anteriores funcionaban correctamente. Cuando se crean objetos locales, los datos miembros
no se inicializan si el programador no se preocupa de hacerlo. Contendrían la "basura" que
hubiese en la memoria asignada al objeto. Curiosamente, si se trata de objetos globales, los
datos miembros se inicializan a cero. Esto se realiza de esta forma porque el proceso de
inicialización lleva un tiempo. Los objetos locales son por propia definición temporales y
pueden ser creados muchas veces durante la ejecución de un programa, por lo que al final, el
tiempo de inicialización puede ser significativo. Por el contrario, los objetos globales son
inicializados al comenzar la ejecución del programa, por lo que esto se hace una sóla vez y
durante la preparación de la ejecución del código.
Para declarar objetos usando el constructor por defecto o un constructor que se haya
declarado sin parámetros no se debe usar el paréntesis:
pareja par2();
pareja par2;
Inicialización de objetos
Existe un modo simplificado de inicializar los datos miembros de los objetos en los
constructores. Se basa en la idea de que en C++ todo son objetos, incluso las variables de
tipos básicos como int, char o float.
Según esto, cualquier variable (u objeto) tiene un constructor por defecto, incluso
aquellos que son de un tipo básico. Sólo los constructores admiten inicializadores. Cada
inicializador consiste en el nombre de la variable miembro a inicializar, seguida de la
expresión que se usará para inicializarla entre paréntesis. Los inicializadores se añadirán a
continuación del paréntesis cerrado que encierra a los parámetros del constructor, antes del
cuerpo del constructor y separado del paréntesis por dos puntos ":".
Por ejemplo, en el caso anterior de la clase pareja:
pareja::pareja(int a2, int b2)
{
a = a2;
b = b2;
}
Se puede sustituir el constructor por:
pareja::pareja(int a2, int b2) : a(a2), b(b2) {}
Al igual que ocurre con las funciones globales, y como ya veremos con los métodos y
operadores, es posible sobrecargar el constructor. Recuérdese que la sobrecarga significa que
bajo un mismo identificador se ejecutan códigos distintos en función de los argumentos que
se pasen para la ejecución. Por eso, la única restricción en la sobrecarga es que no pueden
definirse dos constuctores que no se diferencien en el número y tipo de los argumentos que
utilizan.
class pareja
88 PROGRAMACIÓN C++ Y COMUNICACIONES.
{
public:
// Constructor anterior
pareja(int a2, int b2) : a(a2), b(b2) {}
//contructor por defecto
pareja() : a(0), b(0) {}
//otro constructor
pareja(double num);
// Funciones miembro de la clase "pareja"
void Lee(int &a2, int &b2);
void Guarda(int a2, int b2);
private:
// Datos miembro de la clase "pareja"
int a, b;
};
pareja::pareja(double num)
{
a=(int)num;
b=((int)(num*10))%10;
}
void main()
{
pareja p1,p2(12,3),p3(2.8);
…
}
La pareja p1 será entonces inicializada con los valores (a=0,b=0), p2 con (a=12,b=3) y
p3 con (a=2, b=8). De este modo se pueden definir muchas maneras de inicializar los objetos
de una clase.
Al igual que cualquier función, es posible definir valores por defecto para los
parámetros. El inconveniente es que si existen dos funciones que tienen definidos valores por
defecto de todos sus argumentos, se producirá una ambigüedad a la hora de definir cual de
las dos se tiene que ejecutar. El compilador informará de esta situación, y en caso de que sea
necesario utilizar el constructor por defecto en alguna parte del código –es posible que
aunque se haya definido no se haga uso de él‐ entonces generará un error de compilación.
Por ejemplo, el compilador no permitiría la ejecución del siguiente programa:
#include <iostream.h>
#include <math.h>
class pareja
{
public:
// Constructor anterior con valores por defecto
pareja(int a2=0, int b2=0) : a(a2), b(b2) {}
void main()
{
pareja p1;
pareja p2(12,3),p3(2.8);
int a,b;
p1.Lee(a,b);
cout<<a<<','<<b;
}
El constructor de copia
Existe otro tipo de constructor especial que en caso de que l programador no defina
el compilador asigna uno de oficio. Este es el llamado constructor de copia, y que como su
propio nombre indica sirve para inicializar un objeto como copia de otro.
{
…
pareja(const pareja &p):a(p.a),b(p.b){}
…
}
void main(void)
{
pareja p1(12,32);
pareja p2(p1); //Uso del constructor de copia
…
}
El destructor
El complemento a los constructores de una clase es el destructor. Así como el
constructor se llama al declarar o crear un objeto, el destructor es llamado cuando el objeto
va a dejar de existir por haber llegado al final de su vida. En el caso de que un objeto local
haya sido definido dentro de un bloque {…}, el destructor es llamado cuando el programa
llega al final de ese bloque.
Si el objeto es global o static su duración es la misma que la del programa, y por
tanto el destructor es llamado al terminar la ejecución del programa. Los objetos creados con
reserva dinámica de memoria (en general, los creados con el operador new) no están
sometidos a las reglas de duración habituales, y existen hasta que el programa termina o
hasta que son explícitamente destruidos con el operador delete. En este caso la
responsabilidad es del programador, y no del compilador o del sistema operativo. El operador
delete llama al destructor del objeto eliminado antes de proceder a liberar la memoria
ocupada por el mismo.
A diferencia del constructor, el destructor es siempre único (no puede estar
sobrecargado) y no tiene argumentos en ningún caso. Tampoco tiene valor de retorno. Su
nombre es el mismo que el de la clase precedido por el carácter tilde (~), carácter que se
consigue con Alt+126 en el teclado del PC o mediante el uso de los denominados trigrafos (??‐
). Los trigrafos son conjuntos de caracteres precedidos por ?? que el preprocesador convierte
a un carácter a veces no presente en algunos teclados o idiomas. Por tanto, para el ordenador
es lo mismo llegar a escribir el carácter ~ que el conjunto de caracteres ??‐.
class persona
{
public:
persona(const char *nom, long dni);
persona():nombre(NULL),DNI(0){};
??-persona(); //podría haberse escrito ~persona();
void mostrar();
private:
char *nombre;
long DNI;
};
persona::persona(const char *nom,long dni)
{
nombre=new char[strlen(nom)+1];
strcpy(nombre,nom);
DNI=dni;
cout<<"Construyendo "<<nombre<<endl;
}
Lo que permite visualizar como han sido llamados los constructores y destructores de
los objetos que se han creado.
Métodos sobrecargados
En el capítulo de modificaciones menores a C, uno se los puntos que se vio era la
posibilidad que ofrece C++ para escribir varias funciones que teniendo el mismo identificador
eran diferenciadas por el tipo de parámetros utilizado. De esta forma se facilitaba la
comprensión del código y la facilidad de programación al evitar el tener que utilizar nombres
distintos para tipos de argumentos distintos cuando la funcionalidad es parecida.
Bueno, pues este mismo concepto es aplicable a los métodos de una clase. Baste
para ilustrarlo el siguiente ejemplo:
#include <iostream>
struct punto3D
{
float x, y, z;
};
class punto
{
public:
void Asignar(float xi, float yi, float zi)
{
x = xi;
y = yi;
z = zi;
}
void Asignar(punto3D p)
{
Asignar(p.x, p.y, p.z);
}
void Ver()
{
cout << "(" << x << "," << y
<< "," << z << ")" << endl;
}
private:
float x, y, z;
};
void main(void)
{
punto3D p3d = {32,45,74};
P.Asignar(p3d);
P.Ver();
P.Asignar(12,35,12);
P.Ver();
}
Operadores sobrecargados
En C++ los operadores pasan a ser unas funciones como otra cualquiera, pero que
permiten para su ejecución una notación especial. Todos ellos tienen asignada una función
por defecto que es la que hasta ahora habíamos utilizado en C. Por ejemplo, el operador ‘+’
realizará la suma aritmética o la suma de punteros de los operandos que pongamos a ambos
lados.
A partir de ahora, podremos hacer que el operador ‘+’ realice acciones distintas en
función de la naturaleza de los operandos sobre los que trabaja. De hecho, en realidad, la
mayoría de los operandos en C++ están sobrecargados. El ordenador realiza operaciones
distintas cuando divide enteros que cuando divide números en coma flotante. Un ejemplo
más claro sería el del operador *, que en unos casos trabaja como multiplicador y en otros
realiza lo operación de indirección.
En C++ el programador puede sobrecargar casi todos los operadores para adaptarlos
a sus propios usos. Para ello disponemos de una sintaxis específica que nos permite definirlos
o declararlos al igual que ocurre con las funciones:
Se pueden sobrecargar todos los operadores excepto “.” , “.*” , “::” ,“?:”.
Al menos uno de los argumentos para los operadores externos deben ser
tipos enumerados o estructurados: struct, enum, union o class.
void main()
{
complex a={5.0F,3.0F},b={3.0F,1.2F},c;
c=a+b;
cout<<c.real<<"+"<<c.imag<<"i"<<endl;
}
Por tanto, en la declaración de la clase se incluirá una línea con la siguiente sintaxis:
Sin embargo no tiene por que ser de esta manera. Por ejemplo en el caso de operar
con matrices, es perfectamente válido definir una operación producto entre una matriz y un
vector, objetos de dos clases distintas, y cuyo resultado es un vector. Si la operación producto
96 PROGRAMACIÓN C++ Y COMUNICACIONES.
está definida en el interior de la clase matriz, el argumento será de tipo vector y el valor de
retorno también.
Se mostrará un ejemplo sencillo, que permite operar con valores de tiempo de forma
sencilla. En esta clase se definirá el operador suma de tal forma que se puedan sumar
periodos de duración medidos en horas y minutos.
#include <iostream.h>
class Tiempo
{
public:
Tiempo(int h=0, int m=0) : hora(h), minuto(m) {}
void Mostrar(){cout << hora << ":" << minuto << endl;};
Tiempo operator+(Tiempo h);
private:
int hora;
int minuto;
};
Tiempo Tiempo::operator+(Tiempo h)
{
Tiempo temp;
temp.minuto = minuto + h.minuto;
temp.hora = hora + h.hora;
if(temp.minuto >= 60){
temp.minuto -= 60;
temp.hora++;
}
return temp;
}
void main(void)
{
Tiempo Ahora(12,24), T1(4,45);
T1 = Ahora + T1;
T1.Mostrar();
(Ahora + Tiempo(4,45)).Mostrar(); //(1)
}
En este ejemplo se ha introducido una de las posibilidades que ofrece C++ hasta
ahora no comentada ni utilizada. En el punto del código marcado con (1), se utiliza una
instancia de la clase sin haberle dado un nombre. Es decir, Tiempo(4,5) crea un objeto que no
podremos referenciar posteriormente una vez ejecutada la sentencia puesto que no le hemos
asignado ningún identificador. Sin embargo el objeto existirá y podrá ser utilizado, de forma
que en (1) se está diciendo que se sume al tiempo guardado en Ahora un total de 4 horas y 45
minutos.
Igual es mucho de golpe, pero es momento de comenzar a ir viendo código más real.
A continuación se realizará una sobrecarga del operador suma de tal forma que a la
hora le vamos a sumar sólo minutos. En ese caso, el operador suma recibirá como segundo
operando un valor entero, y dará como resultado un objeto Tiempo.
#include <iostream.h>
class Tiempo
{
public:
Tiempo(int h=0, int m=0) : hora(h), minuto(m) {};
void Mostrar(){cout << hora << ":" << minuto << endl;};
Tiempo operator+(Tiempo h);
Tiempo operator+(int mins);
private:
int hora;
int minuto;
};
Tiempo Tiempo::operator+(Tiempo h)
{
Tiempo temp;
temp.minuto = minuto + h.minuto;
temp.hora = hora + h.hora;
if(temp.minuto >= 60)
{
temp.minuto -= 60;
temp.hora++;
}
return temp;
}
Tiempo Tiempo::operator +(int mins)
{
Tiempo temp;
temp.minuto=minuto+mins;
temp.hora=hora+temp.minuto/60;
temp.minuto=temp.minuto%60;
return temp;
}
void main(void)
{
Tiempo Ahora(12,24), T1(4,45);
T1 = Ahora + T1;
T1.Mostrar();
(Ahora+45).Mostrar();
}
98 PROGRAMACIÓN C++ Y COMUNICACIONES.
COPIA
1000 1000 1000
C1 C1 C1
Cadena = 1050 Cadena = 1150 Cadena = 1150
Es muy habitual que el operador ‘=’ retorne una referencia al propio objeto para
permitir realizar asignaciones concatenadas sin necesidad de crear un objeto temporal.
Gracias a esta referencia será posible escribir sentencias del estilo: C1=C2=C3… tal y como
sucede con las asignaciones en los tipos básicos.
Además del operador + pueden sobrecargarse prácticamente todos los operadores:
+, -, *, /, %, ^, &, |, (,), <, >, <=, >=, <<, >>, ==,
!=, &&, ||, =, +=. -=, *=, /=, %=, ^=, &=, |=, <<=,>>=, [ ],
(),->, new y delete.
Otro ejemplo para la clase Tiempo sería el del operador +=, que al igual que con los
tipos básicos realizará la suma sobre el primer operando del segundo.
class Tiempo
{
...
void operator+=(Tiempo h);
...
102 PROGRAMACIÓN C++ Y COMUNICACIONES.
};
void Tiempo::operator+=(Tiempo h)
{
minuto += h.minuto;
hora += h.hora;
while(minuto >= 60)
{
minuto -= 60;
hora++;
}
}
void main(void)
{
...
Ahora += Tiempo(1,32);
Ahora.Mostrar();
...
}
No es imprescindible mantener el significado de los operadores. Por ejemplo, para la
clase Tiempo no tiene sentido sobrecargar el operadores >>, <<, * ó /, pero se puede de todos
modos, y olvidar el significado que tengan habitualmente.
Sin embargo, como norma general, es conveniente de cara a obtener un código más
legible que la funcionalidad del operador sea análoga a su significado.
puesto que pertenece a la STL. Por ello, la sobrecarga del operador de inserción se realiza
como una función externa, y además, como habitualmente será necesario acceder a la parte
provada del objeto para poder imprimirlo, se declara como función amiga de la clase.
El aspecto que tiene la sobrecarga tanto del operador >> como <<, siguiendo la
forma más común de realizarse es el siguiente:
ostream & operator<< (ostream &os, const MI_CLASE &obj)
istream & operator>> (istream &is, MI_CLASE &obj)
Los flujos de salida, gracias a la herencia, podremos agruparlos de forma genérica
dentro del concepto ostream. Por tanto cout, es un objeto de tipo ostream. Los flujos de
entrada serán del tipo istream. Para poder concatenar inserciones y extracciones, el resultado
de la operación es el mismo flujo sobre el que se ha extraido o insertado. Pongamos como
ejemplo estos operadores para la clase Tiempo:
class Tiempo
{
...
friend ostream & operator<< (ostream &os, const Tiempo &t)
friend istream & operator>> (istream &is, Tirmpo &t)
...
};
ostream & operator<< (ostream &os, const Tiempo &t)
{
os<<t.hora<<’:’<<t.minuto;
return os;
}
istream & operator>> (istream &is, Tiempo &t)
{
string a;
is>>a;
int index=a.find_first_of(“:”);
if(index!=string::nppos){
t.hora=stoi(a.substring(0,index);
t.minuto=stoi(a.substring(index+1));
}
return is;
}
void main()
{
Tiempo t1;
cin>>t1;
cout<<”Tiempo leído = “<<t1<<endl;
}
104 PROGRAMACIÓN C++ Y COMUNICACIONES.
Sin embargo, existen dos versiones para el operador incremento, el pre incremento y
el post incremento. El ejemplo mostrado es la sobrecarga del operador pre incremento para
la clase Tiempo, ¿cómo se sobrecarga el operador de post incremento?
En realidad no hay forma de decirle al compilador cuál de las dos modalidades del
operador se está sobrecargando, así que los compiladores usan una regla: si se declara un
parámetro para un operador ++ ó ‐‐ se sobrecargará la forma sufija del operador.
El parámetro se ignorará, así que bastará con indicar el tipo.
hora++;
}
return temp;
}
void main(void)
{
…
T1.Mostrar();
(T1++).Mostrar();
T1.Mostrar();
(++T1).Mostrar();
T1.Mostrar();
…
}
El resultado de ejecutar esta parte del programa será
float y después se realiza la operación entre dos números del mismo tipo, siendo entonces el
resultado de la misma un float.
Esto es lo que ocurrirá con la expresión del ejemplo. El valor de minutos se intentará
convertir a un objeto Tiempo, puesto que el operador += definido espera un objeto de este
tipo como operando. Para ello se utilizará el constructor diseñado. Como sólo hay un
parámetro, y ambos tienen definidos valores por defecto, entonces el parámetro h toma el
valor de minutos, y el valor de m toma el de por defecto (o sea 0). El tipo de h es de tipo
entero, por lo que se convierte el valor unsigned int a int y se ejecuta el constructor.
El resultado es que se suman 432 horas, y cuando lo que se deseaba era sumar 432
minutos. Esto se soluciona creando un nuevo constructor que tome como parámetro un
unsigned int, de forma que el compilador diferencia la operación que tiene que realizar
gracias al tipo del argumento:
Tiempo(unsigned int m) : hora(0), minuto(m)
{
while(minuto >= 60)
{
minuto -= 60;
hora++;
}
}
La palabra clave explicit sirve para evitar que el compilador realice conversiones
implícitas mediante un constructor específico.
Sin embargo, es interesante ver como se trabaja en el caso inverso. Es decir, ahora lo
que se desea es dotar a C++ de mecanismos para convertir una instancia de la clase a otro
tipo. Por ejemplo, se desea ahora asignar a un entero el valor contenido en Tiempo. La idea es
que se desea transformar las horas y minutos en el total de minutos que representan:
108 PROGRAMACIÓN C++ Y COMUNICACIONES.
…
Tiempo T1(12,23);
int minutos;
minutos = T1;
…
Los operadores [ ] y ( )
El operador de indexación
El operador [ ] se usa para acceder a valores de objetos de una determinada clase
como si se tratasen de arrays. Los índices no tienen por qué ser de un tipo entero o
enumerado, ahora no existe esa limitación.
Donde más útil resulta este operador es cuando se usa con estructuras dinámicas de
datos: listas, árboles, vectores dinámicos, etc…. Pero también puede servir para crear arrays
asociativos, donde los índices sean por ejemplo, palabras.
#include <iostream.h>
#include <math.h>
class vector
{
public:
vector(int n=0,double *v=NULL);
double operator[](double ind);
~vector(){delete [] valores;}
private:
int num;
double *valores;
};
vector::vector(int n,double *v)
{
valores = new double[n];
num=n;
for(int i=0;i<n;i++)valores[i]=v[i];
}
double vector::operator [](double ind)
{
double temp;
int sup,inf;
inf=(int)floor(ind); //entero redondeado abajo
sup=(int)ceil(ind); //entero redondeado arriba
if(inf>num-1)inf=num-1;
if(sup>num-1)sup=num-1;
if(inf<0)inf=0;
if(sup<0)sup=0;
if(inf<sup)
temp=(sup-ind)*valores[inf]+(ind-inf)*valores[sup];
else temp=valores[inf];
return temp;
}
void main(void)
{
double val[4]={1.0,4.0,-1.0,2.0};
110 PROGRAMACIÓN C++ Y COMUNICACIONES.
vector mivector(4,val);
for(int i=0;i<31;i++)
cout<<mivector[i*0.1]<<endl;
El operador llamada
El operador llamada ( ) funciona exactamente igual que el operador [], aunque
admite más parámetros. Este operador permite usar un objeto de la clase para el que está
definido como si fuera una función.
cout<<mivector(i*0.1,false)<<endl;
}
}
Atributos static
En ocasiones se hace necesario de disponer de una variable común a todos los
objetos de una clase. Por ejemplo, si se quisiera llevar cuenta de cuantos objetos se han
creado de una clase determinada, se debería tener un contador que fuera común a todos los
objetos. Esta funcionalidad es proporcionada en C++ por medio de los miembros estáticos de
una clase.
Para ilustrar esto se va a definir una clase libro sencilla, que permitirá asignar un
identificador único a cada libro creado.
#include <iostream.h>
#include <string.h>
class libro
{
public:
libro (const char *tit, const char *aut);
~libro();
void mostrar();
static int contador; //(1)
private:
char *titulo;
char *autor;
int ID;
};
libro::~libro()
{
delete [] titulo;
delete [] autor;
}
void libro::mostrar()
{
cout<<"Libro "<<ID<<": \t"<<titulo<<endl;
cout<<"\t\t"<<autor<<endl<<endl;
}
void main(void)
{
libro l1("Cucho","Olaizola");
libro l2("Tuareg","Vazquez Figueroa");
libro l3("El Quijote","Cervantes");
l1.mostrar();
l2.mostrar();
l3.mostrar();
}
Sin embargo es necesario inicializar (y por tanto crear) la variable estática en algún
momento y además sólo una vez. Es decir, tiene que ser inicializado a nivel global y hay que
asegurarse de que esta inicialización no se realiza más veces. Es por ello que es necesario
escribir la línea (2), en la que se dice que el atributo de la clase libro llamado contador de tipo
entero vale cuando es creado 0.
void main(void)
{
cout<<"Numero inicial de libros:";
cout<<libro::contador<<endl;
imprimirLibros();
cout<<"Libros creados:";
cout<<libro::contador<<endl;
}
acceder al atributo, pero como esta es global y existe para todos los objetos, es posible
obtener y darle un valor desde cualquier sitio.
Evidentemente, ha sido posible acceder al valor del atributo contador por ser este un
atributo público. Sin embargo parece lógico que dicho atributo fuera un atributo privado. Si
reescribiéramos la clase libro con el atributo contador como miembro privado estático,
entonces el compilador no permitiría la consulta del mismo fuera de un objeto de la clase.
Siempre nos queda la opción de crear un método de interfaz que a través de un
objeto de la clase nos remitiera el valor del contador. Sin embargo C++ nos da la posibilidad
de consultar este tipo de atributos y de modificar su valor aun siendo privados por medio de
los métodos estáticos como se verá a continuación.
Metodos static
Un método declarado como static carece del puntero this por lo que no puede ser
invocado para un objeto de su clase, sino que se invoca en general allí donde se necesite
utilizar para la operación para la que ha sido escrito. Desde este punto de vista es imposible
que un método static pueda acceder a un miembro no static de su clase; por la misma razon,
si puede acceder a un miembro static. Luego si un objeto llama a un método static de su
clase, hay que tener en cuenta que no podrá acceder a ninguno de sus atributos particulares,
a pesar de que su modo de uso no difiere de cualquier otro método de la clase.
Si reescribimos la clase libro con las modificaciones indicadas al final del anterior
epígrafe, sería necesario un método static para poder consultar el valor del atributo contador
que ahora es privado. Al igual que ocurría con los atributos, un método se declara estático en
la declaración de la clase, pero no es necesario (de hecho no hay que hacerlo) en el momento
en el que se define el método.
El siguiente ejemplo ilustra estos aspectos así como el modo de llamar a un método
static sin necesidad de crear un objeto de la clase. Antes de transcribir el mismo hay que
destacar que la utilidad de los miembros static de una clase, no es fácil descubrirla en la
primera impresión, pero a medida que se va aprendiendo a programar en C++ se comprueba
la gran utilidad y la facilidad con que se resuelven muchos problemas gracias a esta
herramienta.
#include <iostream.h>
#include <string.h>
class libro
{
public:
libro (const char *tit, const char *aut);
~libro();
void mostrar();
static int getContador(){return contador;};
private:
char *titulo;
char *autor;
int ID;
static int contador;
};
int libro::contador=0;
libro::~libro()
{
delete [] titulo;
delete [] autor;
}
void libro::mostrar()
{
imprimirLibros();
cout<<"Libros creados:";
cout<<libro::getContador()<<endl;
}
3.7. Ejercicios
El siguiente código extraído de la red realiza una clase para operar cómodamente con
números complejos. Algunas de las soluciones adoptadas tienen finalidad puramente docente
para mostrar casi todos los aspectos de este capítulo:
/******************** fichero Complejo.h**************************/
// fichero Complejo.h
// declaración de la clase Complejo
#ifndef __COMPLEJO_H__
#define __COMPLEJO_H__
#include <iostream.h>
class Complejo
{
private:
double real;
double imag;
public:
// Constructores
Complejo(void);
explicit Complejo(double, double im=0.0);
Complejo(const Complejo&);
// Set Cosas
void SetData(void);
void SetReal(double);
void SetImag(double);
// Get Cosas
double GetReal(void){return real;}
double GetImag(void){return imag;}
// Sobrecarga de operadores aritméticos
Complejo operator+ (const Complejo&);
Complejo operator- (const Complejo&);
Complejo operator* (const Complejo&);
Complejo operator/ (const Complejo&);
// Sobrecarga del operador de asignación
Complejo& operator= (const Complejo&);
// Sobrecarga de operadores de comparación
friend int operator== (const Complejo&, const Complejo&);
friend int operator!= (const Complejo&, const Complejo&);
// Sobrecarga del operador de inserción en el flujo de salida
friend ostream& operator<< (ostream&, const Complejo&);
};
#endif
/******************** fichero
Complejo.cpp**************************/
#include "Complejo.h"
// constructor por defecto
Complejo::Complejo(void)
{
real = 0.0;
imag = 0.0;
}
// constructor general
Complejo::Complejo(double re, double im)
{
real = re;
imag = im;
}
// constructor de copia
Complejo::Complejo(const Complejo& c)
{
real = c.real;
imag = c.imag;
}
// función miembro SetData()
void Complejo::SetData(void)
{
cout << "Introduzca el valor real del Complejo: ";
cin >> real;
cout << "Introduzca el valor imaginario del Complejo: ";
cin >> imag;
}
void Complejo::SetReal(double re)
{
real = re;
}
void Complejo::SetImag(double im)
{
imag = im;
}
// operador miembro + sobrecargado
Complejo Complejo::operator+ (const Complejo &a)
{
Complejo suma;
suma.real = real + a.real;
118 PROGRAMACIÓN C++ Y COMUNICACIONES.
}
// operador friend << sobrecargado
ostream& operator << (ostream& co, const Complejo &a)
{
co << a.real;
long fl = co.setf(ios::showpos);
co << a.imag << "i";
co.flags(fl);
return co;
}
/**************fichero main.cpp**************************/
#include "Complejo.h"
void main(void)
{
// se crean dos Complejos con el constructor general
Complejo c1(1.0, 1.0);
Complejo c2(2.0, 2.0);
// se crea un Complejo con el constructor por defecto
Complejo c3;
// se da valor a la parte real e imaginaria de c3
c3.SetReal(5.0);
c3.SetImag(2.0);
// se crea un Complejo con el valor por defecto (0.0) del 2º
argumento
Complejo c4(4.0);
// se crea un Complejo a partir del resultado de una expresión
// se utiliza el constructor de copia
Complejo suma = c1 + c2;
// se crean tres Complejos con el constructor por defecto
Complejo resta, producto, cociente;
// se asignan valores con los operadores sobrecargados
resta = c1 - c2;
producto = c1 * c2;
cociente = c1 / c2;
// se imprimen los números Complejos con el operador << sobrecargado
cout << c1 << ", " << c2 << ", " << c3 << ", " << c4 << endl;
cout << "Primer Complejo: " << c1 << endl;
cout << "Segundo Complejo: " << c2 << endl;
cout << "Suma: " << suma << endl;
cout << "Resta: " << resta << endl;
cout << "Producto: " << producto << endl;
cout << "Cociente: " << cociente << endl;
if (c1==c2)
cout << "Los Complejos son iguales." << endl;
else
cout << "Los Complejos no son iguales." << endl;
if (c1!=c2)
cout << "Los Complejos son diferentes." << endl;
else
cout << "Los Complejos no son diferentes." << endl;
cout << "Ya he terminado." << endl;
}
Se observa en el ejemplo cómo podríamos considerar a persona clase base para las
otras dos profesor y alumno. Mediante esta estructuración, en determinadas funciones de
nuestro programa, podremos entender alumno o profesor como tales, o considerarlos sólo
como personas (lo que significaría, acceder a los primeros campos de la estructura, los cuales
son comunes a ambos tipos).
En algunos casos una clase no tiene otra utilidad que la de ser clase base para otras
clases que se deriven de ella. A este tipo de clases base, de las que no se declara ningún
objeto, se les denomina clases base abstractas (ABC, Abstract Base Class) y su función es la de
agrupar miembros comunes de otras clases que se derivarán de ellas. Por ejemplo, se puede
definir la clase vehiculo para después derivar de ella coche, bicicleta, patinete, etc., pero
todos los objetos que se declaren pertenecerán a alguna de estas últimas clases; no habrá
vehículos que sean sólo vehículos.
Las características comunes de estas clases (como una variable que indique si está
arado o en marcha, otra que indique su velocidad, la función de arrancar y la de frenar, etc.),
pertenecerán a la clase base y las que sean particulares de alguna de ellas pertenecerán sólo a
la clase derivada (por ejemplo el número de platos y piñones, que sólo tiene sentido para una
124 PROGRAMACIÓN C++ Y COMUNICACIONES.
bicicleta, o la función embragar que sólo se aplicará a los vehículos de motor con varias
marchas).
Este mecanismo de herencia presenta múltiples ventajas evidentes a primera vista,
como la posibilidad de reutilizar código sin tener que escribirlo de nuevo. Esto es posible
porque todas las clases derivadas pueden utilizar el código de la clase base sin tener que
volver a definirlo en cada una de ellas.
Como ejemplo, se puede pensar en dos tipos de cuentas bancarias que comparten
algunas características y que también tienen algunas diferencias. Ambas cuentas tienen un
saldo, un interés y el nombre del titular de la cuenta. La cuenta joven es un tipo de cuenta
que requiere la edad del propietario, mientras que la cuenta empresarial necesita el nombre
de la empresa. El problema podría resolverse estableciendo una clase base llamada C_Cuenta
y creando dos tipos de cuenta derivados de dicha clase base.
Para indicar que una clase deriva de otra es necesario indicarlo en la declaración de
la clase derivada, especificando el modo ‐public o private‐ en que deriva de su clase
base:
Nótese que algunas de las líneas han tenido que partirse –en concreto los
constructores de las clases‐ debido a su longitud. Esto es válido y conveniente en C++,
intentando siempre mantener al máximo la claridad de las definiciones.
#include <iostream.h>
class C_Cuenta
{
// Variables miembro
private:
char *Nombre; // Nombre de la persona
double Saldo; // Saldo Actual de la cuenta
double Interes; // Interés aplicado
public:
// Constructor
C_Cuenta(char *nombre, double saldo=0.0, double interes=0.0)
{
Nombre = new char[strlen(nombre)+1];
strcpy(Nombre, nombre);
SetSaldo(saldo);
SetInteres(interes);
126 PROGRAMACIÓN C++ Y COMUNICACIONES.
}
// Destructor
~Cuenta(){delete [] Nombre;}
// Métodos
char *GetNombre(){ return Nombre; }
double GetSaldo(){ return Saldo; }
double GetInteres(){ return Interes; }
void SetSaldo(double saldo){ Saldo = saldo; }
void SetInteres(double interes){ Interes = interes; }
void Ingreso(double cantidad){SetSaldo(GetSaldo()+cantidad);}
friend ostream& operator<<(ostream& os, C_Cuenta& cuenta){
os << "Nombre=" << cuenta.GetNombre() << endl;
os << "Saldo=" << cuenta.GetSaldo() << endl;
return os;
}
};
//CuentaJoven deriva públicamente de la clase Cuenta
class C_CuentaJoven : public C_Cuenta
{
private:
int Edad;
public:
C_CuentaJoven(char *nombre,int edad, double saldo=0.0,
double interes=0.0):C_Cuenta(nombre, saldo, interes) {
Edad = laEdad; //especifico de Cuenta Joven
}
};
class C_CuentaEmpresarial : public C_Cuenta
{
private:
char *NomEmpresa;
public:
C_CuentaEmpresarial(char *nombre, char *empresa,
double saldo=0.0, double interes=0.0)
:C_Cuenta(nombre, saldo, interes){
//específico de Cuenta empresarial
NomEmpresa = new char[strlen(empresa)+1];
strcpy(NomEmpresa, empresa);
}
~C_CuentaEmpresarial(){delete [] NomEmpresa;}
};
void main()
{
C_CuentaJoven c1("Luis", 18, 10000.0, 1.0);
C_CuentaEmpresarial c2("Sara", "ELAI Ltd." ,100000.0);
void main(void)
{
decimal num(1,2);
128 PROGRAMACIÓN C++ Y COMUNICACIONES.
num.imprimir();
cout<<endl;
num.mostrar();
cout<<endl;
num.numero::imprimir();
}
Constructores.
Destructores.
Funciones friend.
Con lo que se indica que la parte del objeto correspondiente a número, debe ser
inicializado con el primer valor pasado como argumento en el constructor de la clase decimal.
Las listas de argumentos asociadas a las clases base pueden estar formadas por
constantes, variables globales o por parámetros que se pasan a la función constructor de la
clase derivada.
El inicializador base puede ser omitido en el caso de que la clase base tenga un
constructor por defecto. En el caso de que el constructor de la clase base exista, al declarar un
objeto de la clase derivada se ejecuta primero el constructor de la clase base.
class Base
{
public:
Base() {cout << “\nBase creada\n”;}
~Base() {cout << “Base destruida\n\n”;}
}
void main()
{
D_clase1 d1;
D_clase2 d2;
Base creada
D_clase1 creada
Base creada
D_clase1 creada
D_clase2 creada
D_clase2 destruida
D_clase1 destruida
Base destruida
D_clase1 destruida
Base destruida
Herencia Simple: Todas las clases Herencia Múltiple: Las clases derivadas
derivadas tienen una única clase base. tienen varias clases base.
132 PROGRAMACIÓN C++ Y COMUNICACIONES.
Como ejemplo se puede presentar el caso de que se tenga una clase para el manejo
de los datos de la empresa. Se podría definir la clase C_CuentaEmpresarial como la herencia
múltiple de dos clases base: la ya bien conocida clase C_Cuenta y nueva clase llamada
C_Empresa, que se muestra a continuación:
class C_Empresa
{
private:
char *NomEmpresa;
public:
C_Empresa(const char*laEmpresa)
{
NomEmpresa = new char[strlen(laEmpresa)+1];
strcpy(NomEmpresa, laEmpresa);
}
~C_Empresa(){ delete [] NomEmpresa; }
// Otros métodos ...
};
La sintaxis general para heredar una clase de varias clases base es:
Los inicializadotes base siguen esta misma sintaxis. Cuando es necesario utilizarlos,
se colocan tras la clase base separados por comas. Tal es el caso del ejemplo de la cuenta
empresarial expuesto anteriormente.
Hay que destacar que en posteriores lenguajes desarrollados siguiendo la filosofía de
POO, la herencia múltiple se ha desechado por ser una posible fuente de errores. La razón
principal reside en cómo finalmente van a quedar ordenadas las estructuras de los objetos en
memoria. Sin embargo, aunque no existe esta herencia múltiple, podemos generar una serie
de clases abstractas intermedias que obtengan el mismo resultado, puesto que el efecto final
es el mismo… no es más que una agregación de datos y métodos.
La herencia múltiple puede además producir algunos problemas. En ocasiones puede
suceder que en las dos clases base exista una función con el mismo nombre. Esto crea una
ambigüedad cuando se invoca a una de esas funciones.
Veamos un ejemplo:
#include <iostream.h>
class ClaseA {
public:
ClaseA() : valorA(10) {}
int LeerValor(){ return valorA; }
protected:
int valorA;
};
class ClaseB {
public:
ClaseB() : valorB(20) {}
int LeerValor(){ return valorB; }
protected:
int valorB;
};
class ClaseC : public ClaseA, public ClaseB {};
void main() {
ClaseC CC;
cout << CC.LeerValor() << endl;// error de compilación
cout << CC.ClaseA::LeerValor() << endl;
cin.get();
}
#include <iostream.h>
class ClaseA
{
public:
ClaseA() : valorA(10) {}
int LeerValor(){ return valorA; }
protected:
int valorA;
};
class ClaseB
{
public:
ClaseB() : valorB(20) {}
int LeerValor(){ return valorB; }
protected:
int valorB;
};
class ClaseC : public ClaseA, public ClaseB
{
public:
int LeerValor(){return ClaseA::LeerValor();}
};
void main()
{
ClaseC CC;
cout << CC.LeerValor() << endl;
cin.get();
}
Clase A Clase A
Clase B Clase C
Clase D
La ClaseD heredará dos veces los datos y funciones de la ClaseA, con la consiguiente
ambigüedad a la hora de acceder a datos o funciones heredadas de ClaseA. Para solucionar
esto se usan las clases virtuales. Cuando derivemos una clase partiendo de una o varias clases
base, podemos hacer que las clases base sean virtuales. Esto no afectará a la clase derivada.
Forma de declaración de una clase base virtual:
...
}
Por ejemplo:
class ClaseB : virtual public ClaseA {};
Ahora, la ClaseD sólo heredará una vez la ClaseA. La estructura quedará así:
136 PROGRAMACIÓN C++ Y COMUNICACIONES.
Clase A
Clase B Clase C
Clase D
cb.mostrar();
ClaseA *c3=c1,*c4=c2;
c3->mostrar();
c4->mostrar();
ClaseB *c5;
c5 = static_cast<ClaseB *>(c3);
c5->mostrar(); //error en ejecución (2)
c5 = static_cast<ClaseB *>(c4);
c5->mostrar();
delete c1;
delete c2;
}
En (1) se produce un error de compilación porque no se permite la asignación desde
un objeto base a un objeto derivado por ser el segundo más extenso que el primero, por lo
que parte de los atributos no se sabría que valor deben tomar.
2,3
En este caso, sólo se realizará la segunda impresión, puesto que el sistema detecta
que el objeto apuntado por c3 es de tipo ClaseA, y que por tanto no es una asignación válida
la pretendida. Por el contrario, aunque c4 es un puntero de tipo ClaseA, la dirección que
contiene es de un objeto de tipo ClaseB, por lo que el operador detecta durante la ejecución
que la asignación es válida.
Una vez aclarado este aspectod ela sintaxis, vamos a ver de que modo se pueden
implementar estos dos métodos no heredados en una clase derivada:
#include <string.h>
#include <iostream.h>
class Nombre
{
char *nombre;
public:
Nombre(char *nom)
{
nombre=new char[strlen(nom)+1];
strcpy(nombre,nom);
}
Nombre(const Nombre &nom)
{
nombre=new char[strlen(nom.nombre)+1];
strcpy(nombre,nom.nombre);
}
Nombre &operator=(const Nombre &nom)
{
delete [] nombre;
nombre=new char[strlen(nom.nombre)+1];
strcpy(nombre,nom.nombre);
return *this;
}
~Nombre(){delete [] nombre;}
void mostrar(){cout<<nombre;}
};
class Alumno:public Nombre
{
int numero;
public:
Alumno(char *nom, int num):Nombre(nom),numero(num){}
Alumno(const Alumno &al):Nombre(al),numero(al.numero){};
Alumno &operator=(const Alumno &al)
{
Nombre::operator =(al);
numero=al.numero;
return *this;
}
};
void main()
{
Alumno uno("Juan Ramírez",43271);
Alumno dos=uno; //(1)
Alumno tres("Pepito",41278);
tres=dos; //(2)
Los métodos que se ejecutan en las instrucciones marcadas son los siguientes: en (1)
se ejecuta el constructor de copia de Alumno, pero antes de ejecutarse el código contenido
entre las llaves se inicializa por medio del constructor de copia de Nombre, la parte
correspondiente a esta clase base. Puesto que uno es de clase Alumno, y esta es derivada de
nomre, la conversión es váñida e inmediata, tal y como se menciono en el anterior apartado.
En (2) se utiliza el operador de asignación de la clase Alumno. Fíjese que en el código
de este operador, se hace un uso explícito del operador de asignación de Nombre.
En este caso es poco eficiente al no tener un constructor por defecto definido para la
clase Nombre, por lo que se hará una reserva de memoria que inmediatamente será de nuevo
eliminada por la operación de asignación. Sin embargo es interesante que no exista
redundancia en el código de forma que sólo sea necesario modificar o actualizar un sitio para
realizar una operación equivalente.
4.7. Ejemplo
El siguiente ejemplo es el código utilizado en las prácticas para la realización de una
pequeña jerarquía de clases que permite la realización de una pequeña biblioteca:
#include <iostream.h>
typedef std::string cadena;
class CFicha
{
protected:
cadena referencia;
cadena titulo;
public:
// Constructor
CFicha(cadena ref= "", cadena tit= "");
// Destructor
virtual ~CFicha() {};
// Otras funciones
void AsignarReferencia(cadena ref);
cadena ObtenerReferencia() ;
void AsignarTitulo(cadena tit);
cadena ObtenerTitulo() ;
void Imprimir();
void PedirFicha();
friend void TomarLinea(cadena&);
};
class CFichaLibro : public CFicha
{
private:
cadena autor;
cadena editorial;
public:
// Constructor
CFichaLibro(cadena ref= "", cadena tit= "", cadena aut=
"", cadena ed= "");
// Otras funciones
void AsignarAutor(cadena aut);
cadena ObtenerAutor();
void AsignarEditorial(cadena edit);
cadena ObtenerEditorial();
void PedirLibro();
void Imprimir();
};
class CFichaRevista : public CFicha
{
private:
int NroDeRevista;
int Anyo;
public:
// Constructor
CFichaRevista(cadena ref= "", cadena tit= "", int an= 0, int
nro= 0);
// Otras funciones
void AsignarNroDeRevista(int nro);
int ObtenerNroDeRevista() ;
void AsignarAnyo(int any);
int ObtenerAnyo() ;
void PedirRevista();
void Imprimir();
};
class CFichaVolumen : public CFichaLibro
{
private:
int NroDeVolumen;
public:
// Constructor
CFichaVolumen(cadena ref = "", cadena tit= "", cadena aut=
"", cadena edit= "", int Nro= 0);
// Otras funciones
void AsignarNroDeVolumen(int);
int ObtenerNroDeVolumen();
void PedirVolumen();
void Imprimir();
};
class CBiblioteca
{
private:
144 PROGRAMACIÓN C++ Y COMUNICACIONES.
/**************************************************************
Fin de las declaraciones… comienzan las definiciones
**************************************************************/
cadena CFicha::ObtenerReferencia()
{
return referencia;
}
cadena CFicha::ObtenerTitulo()
{
return titulo;
}
void CFicha::PedirFicha()
{
cout << "Referencia.............: ";
TomarLinea(referencia);
cin.ignore(100, '\n');
cout << "Título.................: ";
TomarLinea(titulo);
}
cadena CFichaLibro::ObtenerAutor()
{
return autor;
}
cadena CFichaLibro::ObtenerEditorial()
{
return editorial;
}
146 PROGRAMACIÓN C++ Y COMUNICACIONES.
void CFichaLibro::PedirLibro()
{
PedirFicha();
cout << "Autor..................: ";
TomarLinea(autor);
cout << "Editorial..............: ";
TomarLinea(editorial);
}
void CFichaLibro::Imprimir()
{
cout << "Autor: " << autor.data() <<endl;
cout << "Editorial: " << editorial.data() <<endl;
}
CFichaRevista::CFichaRevista(cadena ref, cadena tit, int
nroderev, int anyo) : CFicha(ref, tit), NroDeRevista(nroderev),
Anyo(anyo){}
int CFichaRevista::ObtenerNroDeRevista()
{
return NroDeRevista;
}
int CFichaRevista::ObtenerAnyo()
{
return Anyo;
}
void CFichaRevista::PedirRevista()
{
PedirFicha();
cout << "Nro. de la revista.....: ";
cin >> NroDeRevista;
cout << "Año de publicación.....: ";
cin >> Anyo;
}
void CFichaRevista::Imprimir()
{
cout << "Nro Revista: " << NroDeRevista <<endl;
int CFichaVolumen::ObtenerNroDeVolumen()
{
return NroDeVolumen;
}
void CFichaVolumen::PedirVolumen()
{
PedirLibro();
cout << "Nro. del volumen.......: ";
cin>>NroDeVolumen;
}
void CFichaVolumen::Imprimir()
{
cout << "Tomo: " << NroDeVolumen <<endl;
}
CBiblioteca::CBiblioteca(int n)
{
if (n < 0) n = 1;
ficha.reserve(n);
}
CBiblioteca::~CBiblioteca()
{
for (int i = 0; i < ficha.size(); i++)delete ficha[i];
}
// Indexación
CFicha *CBiblioteca::operator[](int i)
{
if (i >= 0 && i < ficha.size()) return ficha[i];
else
{
cout << "error: índice fuera de límites\n";
return 0;
}
}
148 PROGRAMACIÓN C++ Y COMUNICACIONES.
int CBiblioteca::longitud()
{
//nos dice cuantos elementos se han introducido en el vector
return ficha.size();
}
cout<<"----------------------------------"<<endl;
}
/**********************************************************
************** MAIN ***********************************
**********************************************************/
void main()
{
150 PROGRAMACIÓN C++ Y COMUNICACIONES.
do
{
opcion = menu();
switch (opcion)
{
case 1: // añadir
cout << "Tipo de ficha < 1-(rev), 2-(lib), 3-(vol) >: ";
do
opcion = (int)leerDato();
while (opcion < 1 || opcion > 3);
unaFicha = leerDatos(opcion);
bibli.AnyadirFicha(unaFicha);
break;
case 2: // buscar
cout << "Título total o parcial, o referencia: ";
TomarLinea(cadenabuscar);
pos = bibli.buscar(cadenabuscar, 0);
if (pos == -1)
{
if (bibli.longitud() != 0)
cout << "búsqueda fallida\n";
else
cout << "no hay fichas\n";
}
else
bibli.VisualizarFicha(pos);
break;
case 3: // buscar siguiente
pos = bibli.buscar(cadenabuscar, pos + 1);
if (pos == -1)
if (bibli.longitud() != 0)
cout << "búsqueda fallida\n";
else
cout << "no hay fichas\n";
else
bibli.VisualizarFicha(pos);
break;
case 4: // eliminar ficha
/*********************************************************
FUNCION: int menu()
ARGUMENTOS: ninguno
RETORNO: int. Devolverá la opción escogida (1-6)
DESCRIPCION: Imprime por pantalla el menú principal y
solicita al usuario que seleccione. No retornará un
valor hasta que la selección sea válida.
*********************************************************/
int menu()
{
cout << "\n\n";
cout << "1. Añadir ficha\n";
cout << "2. Buscar ficha\n";
cout << "3. Buscar siguiente\n";
cout << "4. Eliminar ficha\n";
cout << "5. Listado de la biblioteca\n";
cout << "6. Salir\n";
cout << endl;
cout << " Opción: ";
int op;
do
op = (int)leerDato();
while (op < 1 || op > 6);
return op;
}
152 PROGRAMACIÓN C++ Y COMUNICACIONES.
/*********************************************************
FUNCION: CFicha *leerDatos(int op)
ARGUMENTOS:
int op: Indica el tipo de Ficha que se tiene que crear.
1= Revista, 2= Libro, 3=Volumen
RETORNO: CFicha *. Devolverá un puntero a la ficha creada
dinámicamente en función de la seleccion y los valores
introducidos por el usuario.
DESCRIPCION: Funcion principal que es llamada cada vez que
quiera crear una ficha de cualquire tipo.
*********************************************************/
CFicha *leerDatos(int op)
{
CFicha *obj=NULL;
switch(op)
{
case 1:
{
CFichaRevista *aux = new CFichaRevista();
aux->PedirRevista();
obj=aux;
}
break;
case 2:
{
CFichaLibro *aux = new CFichaLibro();
aux->PedirLibro();
obj=aux;
}
break;
case 3:
{
CFichaVolumen *aux = new CFichaVolumen();
aux->PedirVolumen();
obj=aux;
}
}
return obj;
}
/*********************************************************
FUNCION: int leerDatos()
ARGUMENTOS: ninguno
RETORNO: int. Retorna el número leido
DESCRIPCION: Funcion especial para hacer más cómoda y
segura la lectura de números del teclado.
*********************************************************/
int leerDato()
{
int dato = 0;
cin >> dato;
while (cin.fail()) // si el dato es incorrecto, limpiar el
{ // búfer y volverlo a leer
cout << '\a';
cin.clear();
cin.ignore(100, '\n');
cin >> dato;
}
// Eliminar posibles caracteres sobrantes
cin.clear();
cin.ignore(100, '\n');
return dato;
}
5. El Polimorfismo
Con este capítulo se tendrán las herramientas básicas para el desarrollo de un buen
programa en C++. Aún quedaría el uso de plantillas, pero estas pueden ser sustituidas en
muchos casos por el polimorfismo, y desde luego son más difíciles de utilizar por un
programador novel que los mecanismos hasta ahora explicados.
Antes de adentrarse en el concepto de polimorfismo, su utilidad y su casuística, es
necesario aclarar algún concepto en lo que se refiere a la superposición y la sobrecarga:
Tal y como se vió en el capítulo anterior en una clase derivada se puede definir una
función que ya existía en la clase base. Esto se conoce como "overriding", o superposición de
una función. La definición de la función en la clase derivada oculta la definición previa en la
clase base. En caso necesario, es posible acceder a la función oculta de la clase base mediante
su nombre completo:
<objeto>.<clase_base>::<método>;
Cuando se superpone una función, se ocultan todas las funciones con el mismo
nombre en la clase base. Supongamos que hemos sobrecargado la función de la clase base
que después volveremos a definir en la clase derivada:
#include <iostream.h>
class ClaseA
{
public:
void Incrementar() { cout << "Suma 1" << endl; }
void Incrementar(int n) { cout << "Suma " << n << endl; }
};
class ClaseB : public ClaseA
{
public:
void Incrementar() { cout << "Suma 2" << endl; }
};
int main()
{
ClaseB objeto;
objeto.Incrementar();
objeto.Incrementar(10); //¿Existe este método?
objeto.ClaseA::Incrementar();
objeto.ClaseA::Incrementar(10);
cin.get();
return 0;
}
Suma 1
Suma 10
5.2. Polimorfismo
Ha llegado el momento de introducir uno de los conceptos más importantes de la
programación orientada a objetos: el polimorfismo.
En lo que concierne a clases, el polimorfismo en C++, llega a su máxima expresión
cuando las usamos junto con punteros o con referencias. Como se ha visto, C++ nos permite
acceder a objetos de una clase derivada usando un puntero a la clase base. En eso consiste o
se basa el polimorfismo. Hata ahora sólo podemos acceder a datos y funciones que existan en
la clase base, los datos y funciones propias de los objetos de clases derivadas serán
inaccesibles. Esto es debido a que el compilador decide en tiempo de compilación que
métodos y atributos están disponibles en función del contenedor.
Para ilustrarlo vamos a ver un ejemplo sobre una estructura de clases basado en la
clase "Persona" y dos clases derivadas "Empleado" y "Estudiante":
#include <iostream.h>
#include <string.h>
class Persona
{
public:
Persona(char *n) { strcpy(nombre, n); }
void VerNombre() { cout << nombre << endl; }
protected:
char nombre[30];
};
class Empleado : public Persona
{
public:
Empleado(char *n) : Persona(n) {}
void VerNombre()
{
cout << "Emp: " << nombre << endl;
}
};
void VerNombre()
{
cout << "Est: " << nombre << endl;
}
};
void main() {
Persona *Pepito = new Estudiante("Jose");
Persona *Carlos = new Empleado("Carlos");
Carlos->VerNombre();
Pepito->VerNombre();
delete Pepito;
delete Carlos;
}
Carlos
Jose
Podemos comprobar que se ejecuta la versión de la función "VerNombre" que
hemos definido para la clase base, y no la de las clases derivadas. Esto es debido a que la
función que se ejecuta se resuelve en tiempo de EJECUCION atendiendo no al tipo de objeto
apuntado, sino al tipo del apuntador.
Por eso, si escribimos:
Estudiante *Pepito=new Estudiante(“Jose”);
Empleado *Carlos=new Empleado(“Carlos”);
Métodos virtuales
Un método virtual es un método de una clase base que puede ser redefinido en cada
una de las clases derivadas de esta, y que una vez redefinido puede ser accedido por medio
de un puntero o una referencia a la clase base, resolviéndose entonces la llamada en función
del objeto referido en vez de en función de con qué se hace la referencia.
Que viene a significar que si en una clase base definimos un método como virtual, si
este método es superpuesto por una clase derivada, al invocarlo utilizando un puntero o una
referencia de la clase base, ¡se ejecutará el método de la clase derivada!.
Cuando una clase tiene algún método virtual –bien directamente, bien por herencia‐
se dice que dicha clase es polimórfica.
Est: Jose
Por tanto, la idea central del polimorfismo es la de poder llamar a funciones distintas
aunque tengan el mismo nombre, según la clase a la que pertenece el objeto al que se
aplican. Esto es imposible utilizando nombres de objetos: siempre se aplica la función
miembro de la clase correspondiente al nombre del objeto, y esto se decide en tiempo de
compilación.
Una vez que una función es declarada como virtual, lo seguirá siendo en las
clases derivadas, es decir, la propiedad virtual se hereda.
siendo pública en la clase base, pudiendo por tanto ejecutarse ese método
privado desde fuera por medio de un puntero a la clase base.
Una llamada a un método virtual se resuelve siempre en función del tipo del
objeto referenciado.
Por su modo de funcionamiento interno (es decir, por el modo en que realmente
trabaja el ordenador) las funciones virtuales son un poco menos eficientes que
las funciones normales.
Vamos a ver algún ejemplo adicional que ayude a entender el polimorfismo y las
clases polimórficas.
#include <iostream.h>
class Base
{
public:
virtual void ident(){cout<<"Base"<<endl;}
};
class Derivada1:public Base
{
public:
void ident(){cout<<"Primera derivada"<<endl;}
};
class Derivada2:public Base
{
public:
void ident(){cout<<"Segunda derivada"<<endl;}
};
class Derivada3:public Base
{
public:
void ident(){cout<<"Tercera derivada"<<endl;}
};
void main()
{
Base base,*pbase;
Derivada1 primera;
Derivada2 segunda;
Derivada3 tercera;
pbase=&base;
pbase->ident();
pbase=&primera;
pbase->ident();
pbase=&segunda;
pbase->ident();
pbase=&tercera;
pbase->ident();
}
nuevo cuatro veces Base. Esto es debido a que desde el punto de vista del puntero que
contenedor (de tipo Base) la función ya no es virtual y por tanto se decide en tiempo de
compilación como cualquier método normal. Para poder acceder al método definido por las
clases derivadas al menos será necesario que utilicemos un puntero de tipo Derivada2 para
aquellas clases que son polimórficas (en este caso sólo lo sería Derivada3).
#include <iostream.h>
class Vehiculo
{
public:
virtual void muestra(ostream &co){}
virtual void rellena(){}
friend ostream& operator <<(ostream &co,Vehiculo &ve)
{
ve.muestra(co);
return co;
}
};
class Coche:public Vehiculo
{
protected:
char marca[20];
char modelo[20];
public:
void muestra(ostream &co)
{
co<<"Coche marca "<<marca<<" modelo "<<modelo<<endl;
}
void rellena()
{
cout<<"¿Marca?:";
cin>>marca;
cout<<"¿Modelo?:";
cin>>modelo;
cin.clear();
}
};
class Camion:public Coche
{
private:
int carga;
public:
void muestra(ostream &co)
{
co<<"Camion marca "<<marca<<" modelo "<<modelo<<endl;
co<<"\tCapacidad de carga: "<<carga<< "Kg."<<endl;
}
void rellena()
{
Coche::rellena();
cout<<"¿Carga máxima?:";
cin>>carga;
cin.clear();
}
};
void main()
{
Vehiculo *flota[4];
int seleccion=0;
for(int i=0;i<3;i++)
{
cout<<"Seleccione tipo del vehiculo "<<i<<":\n";
cout<<"(1-Camión, 2-Coche): ";
while((seleccion<1)||(seleccion>2))
cin>>seleccion;
switch(seleccion)
{
case 1: flota[i]=new Camion;break;
case 2: flota[i]=new Coche;break;
}
seleccion=0;
flota[i]->rellena();
}
cout<<"\n Estos son los vehiculos introducidos:\n";
for(i=0;i<3;i++)cout<<i<<":"<<*flota[i];
164 PROGRAMACIÓN C++ Y COMUNICACIONES.
Se observa entonces como es posible almacenar y tratar los objetos como iguales
atendiendo a que heredan de una misma clase base, y sin embargo, estos mismos objetos son
capaces de realizar acciones distintas o especializadas cuando se ejecutan sus métodos
virtuales. Esto es la potencia del polimorfismo, y tal vez gracias al mismo alguno comienze a
vislumbrar el porque se parecen tanto y a la vez son distintos los programas que se manejan
sobre Windows.
decir que buscará primero en la propia clase, luego en la clase anterior en el orden jerárquico
y se irá subiendo en ese orden hasta dar con una clase que tenga definida la función buscada.
Cada objeto creado de una clase que tenga una función virtual contiene un puntero
oculto a la v‐table de su clase. Mediante ese puntero accede a su v‐table correspondiente y a
través de esta tabla accede a la definición adecuada de la función virtual. Es este trabajo extra
el que hace que las funciones virtuales sean menos eficientes que las funciones normales.
#include <iostream>
#include <cstring>
166 PROGRAMACIÓN C++ Y COMUNICACIONES.
Pepito->VerNombre();
Gente[0] = Carlos->Clonar();
Gente[0]->VerNombre();
Gente[1] = Pepito->Clonar();
Gente[1]->VerNombre();
delete Pepito;
delete Carlos;
delete Gente[0];
delete Gente[1];
cin.get();
return 0;
}
Hemos definido el constructor copia para que se pueda ver cuando es invocado. La
salida es ésta:
Emp: Carlos
Est: Jose
Per: constructor copia.
Emp: constructor copia.
Emp: Carlos
Per: constructor copia.
Est: constructor copia.
Est: Jose
Este método asegura que siempre se llama al constructor copia adecuado, ya que se
hace desde una función virtual.
Si un constructor llama a una función virtual, ésta será siempre la de la clase base.
Esto es debido a que el objeto de la clase derivada aún no ha sido creada.
Al definir una función como virtual pura hay que tener en cuenta que:
Se denomina clase abstracta a aquella que contiene una o más funciones virtuales
puras. El nombre proviene de que no puede existir ningún objeto de esa clase. Si una clase
derivada no redefine una función virtual pura, la clase derivada la hereda como función
virtual pura y se convierte también en clase abstracta. Por el contrario, aquellas clases
derivadas que redefinen todas las funciones virtuales puras de sus clases base reciben el
nombre de clases derivadas concretas, nomenclatura únicamente utilizada para diferenciarlas
de las antes mencionadas.
Aparentemente puede parecer que carece de sentido definir una clase de la que no
va a existir ningún objeto, pero se puede afirmar, sin miedo a equivocarse, que la abstracción
es una herramienta imprescindible para un correcto diseño de la Programación Orientada a
Objetos.
Habitualmente las clases superiores de muchas jerarquías de clases son clases
abstractas y las clases que heredan de ellas definen sus propias funciones virtuales,
convirtiéndose así en funciones concretas.
No es posible crear objetos de una clase abstracta, estas clases sólo se usan como
clases base para la declaración de clases derivadas.
Las funciones virtuales puras serán aquellas que siempre se definirán en las clases
derivadas, de modo que no será necesario definirlas en la clase base.
Siempre hay que definir todas las funciones virtuales de una clase abstracta en
sus clases derivadas, no hacerlo así implica que la nueva clase derivada será
también abstracta.
Para crear un ejemplo de clases abstractas, recurriremos de nuevo a nuestra clase
"Persona". Haremos que ésta clase sea abstracta. De hecho, en nuestros programas de
ejemplo nunca hemos declarado un objeto "Persona".
Veamos un ejemplo:
#include <iostream>
#include <cstring>
using namespace std;
class Persona {
public:
Persona(char *n) { strcpy(nombre, n); }
virtual void Mostrar() = 0;
protected:
char nombre[30];
};
class Empleado : public Persona {
public:
Empleado(char *n, int s) : Persona(n), salario(s) {}
void Mostrar() const;
int LeeSalario() const { return salario; }
void ModificaSalario(int s) { salario = s; }
protected:
int salario;
};
void Empleado::Mostrar() const {
cout << "Empleado: " << nombre
<< ", Salario: " << salario
<< endl;
}
class Estudiante : public Persona {
public:
Estudiante(char *n, float no) : Persona(n), nota(no) {}
void Mostrar() const;
float LeeNota() const { return nota; }
170 PROGRAMACIÓN C++ Y COMUNICACIONES.
5.5. Ejemplos
class C_Cuenta {
// Variables miembro
private:
double Saldo; // Saldo Actual de la cuenta
double Interes; // Interés calculado hasta el momento, anual,
// en tanto por ciento %
public:
//Constructor
C_Cuenta(double unSaldo=0.0, double unInteres=4.0)
{
SetSaldo(unSaldo);
SetInteres(unInteres);
}
// Acciones Básicas
inline double GetSaldo()
{ return Saldo; }
inline double GetInteres()
{ return Interes; }
inline void SetSaldo(double unSaldo)
{ Saldo = unSaldo; }
inline void SetInteres(double unInteres)
{ Interes = unInteres; }
void Ingreso(double unaCantidad)
{ SetSaldo( GetSaldo() + unaCantidad ); }
virtual void AbonaInteresMensual()
{
SetSaldo( GetSaldo() * ( 1.0 + GetInteres() / 12.0 / 100.0) );
}
// etc...
};
6. Plantillas
La generalidad es una propiedad que permite definir una clase o una función sin
tener que especificar el tipo de todos o alguno de sus miembros. Esta propiedad no es
imprescindible en un lenguaje de programación orientado a objetos y ni siquiera es una de
sus características. Esta característica del C++ apareció mucho más tarde que el resto del
lenguaje, al final de la década de los ochenta. Esta generalidad se alcanza con las plantillas
(templates).
La utilidad principal de este tipo de clases o funciones es la de agrupar variables cuyo
tipo no esté predeterminado. Así el funcionamiento de una pila, una cola, una lista, un
conjunto, un diccionario o un array es el mismo independientemente del tipo de datos que
almacene (int, long, double, char, u objetos de una clase definida por el usuario). En definitiva
estas clases se definen independientemente del tipo de variables que vayan a contener y es el
usuario de la clase el que debe indicar ese tipo en el momento de crear un objeto de esa
clase.
6.1. Introducción
Hemos indicado que en la programación clásica existía una clara diferenciación entre
los datos y su manipulación, es decir, entre los datos y el conjunto de algoritmos para
manejarlos. Los datos eran tipos muy simples, y generalmente los algoritmos estaban
agrupados en funciones orientadas de forma muy específica a los datos que debían manejar.
Posteriormente la POO introdujo nuevas facilidades: La posibilidad de extender el
concepto de dato, permitiendo que existiesen tipos más Complejos a los que se podía asociar
la operatoria necesaria. Esta nueva habilidad fue perfilada con un par de mejoras adicionales:
La posibilidad de ocultación de determinados detalles internos, irrelevantes para el usuario, y
la capacidad de herencia simple o múltiple .
Observe que las mejoras introducidas por la POO se pueden sintetizar en tres
palabras: Composición, ocultación y herencia. De otro lado, la posibilidad de incluir juntos
los datos y su operatoria no era exactamente novedosa. Esta circunstancia ya existía de
forma subyacente en todos los lenguajes. Recuerde que el concepto de entero (int en C) ya
incluye implícitamente todo un álgebra y reglas de uso para dicho tipo. Observe también que
la POO mantiene un paradigma de programación orientado al dato (o estructuras de datos).
De hecho los "Objetos" se definen como instancias concretas de las clases, y estas
representan nuevos tipos‐de‐datos, de modo que POO es sinónimo de Programación
Orientada a Tipos‐de‐datos
Definida una plantilla, al proceso por el cual se obtienen clases especializadas (es
decir para alguno de los tipos de datos específicos) se denomina instanciación o de la plantilla
o especialización de una clase o método genérico. A las funciones y clases generadas para
determinados tipos de datos se las denomina entonces como clases o funciones concretas o
especializadas.
El tiempo parece demostrar que sus autores realizaron un magnífico trabajo que va
más allá de la potencia, capacidad y versatilidad de la Librería Estándar C++ y de que otros
lenguajes hayan seguido la senda marcada por C++ en este sentido. Tal es el caso de Java,
con su JGL ("Java Generic Library"). Lo que comenzó como una herramienta para la
generación parametrizada de nuevos tipos de datos (clases), se ha convertido por propio
derecho en un nuevo paradigma, la metaprogramación (programas que escriben programas).
De lo dicho hasta ahora puede deducirse, que las funciones y clases obtenidas a
partir de versiones genéricas (plantillas), pueden obtenerse también mediante codificación
manual (en realidad no se diferencian en nada de estas últimas). Aunque en lo tocante a
eficacia y tamaño del código, las primeras puedan competir en igualdad de condiciones con
las obtenidas manualmente. Esto se consigue porque el uso de plantillas no implica ningún
mecanismo de tiempo de ejecución. Las plantillas dependen exclusivamente de las
propiedades de los tipos que utiliza como parámetros y todo se resuelve en tiempo de
compilación. Podríamos pensar que su resolución es similar a las macros de C en el que se
hacía una sustitución textual de la expresión indicada en el define por la expresión puesta a
continuación, pero en función de unos parámetros. De igual forma, cuando se especializa una
plantilla, se escribe el código con los tipos sustitudos por lo indicado en la plantilla, y después
se procede a compilar tanto el código escrito manualmente como el escrito automáticamente
por este mecanismo.
Las plantillas representan un método muy eficaz de generar código (definiciones de
funciones y clases) a partir de definiciones relativamente pequeñas. Además su utilización
permite técnicas de programación avanzadas, en las que implementaciones muy sofisticadas
se muestran mediante interfaces que ocultan al usuario la complejidad, mostrándola solo en
la medida en que necesite hacer uso de ella. De hecho, cada una de las potentes
abstracciones que se utilizan en la Librería Estándar está representada como una plantilla. A
excepción de algunas pocas funciones, prácticamente el 100% de la Librería Estándar está
relacionada con las plantillas, de ahí que hasta ahora no se halla hecho mucha referencia a
esta librería perteneciente al estándar de C++.
C++ utiliza una palabra clave específica template para declarar y definir funciones y
clases genéricas. En estos casos actúa como un especificador de tipo y va unido al par de
ángulos < > que delimitan los argumentos de la plantilla:
template <T> void fun(T& ref); // función genérica
template <T> class C {/*...*/}; // clase genérica
En algunas otras (raras) ocasiones la palabra template se utiliza como calificador para
indicar que determinada entidad es una plantilla (y en consecuencia puede aceptar
argumentos) cuando el compilador no puede deducirlo por sí mismo.
Bien, pues una vez expuestas las ideas principales referentes al concepto de plantilla,
vamos a ver en primer lugar como se realzan funciones genéricas, para después explicar el
concepto y el modo de funcionamiento de las clases genéricas.
El problema que presenta C++ para esta propuesta es que al ser un lenguaje
fuertemente tipado, la declaración c max(a, b) requiere especificar el tipo de argumentos y
valor devuelto. En realidad se requiere algo así:
y definidas después:
template <class T> T max(T a, T b)
{
return (a > b) ? a : b;
}
En (1) los argumentos de la función son dos objetos tipo int; mientras en (2) son dos
objetos tipo UnaClase. El compilador es capaz de construir dos funciones aplicando los
parámetros adecuados a la plantilla. En el primer caso, el parámetro es un int; en el segundo
un tipo UnaClase. Como veremos más adelante, es de la máxima importancia que el
compilador sea capaz de deducir los parámetros de la plantilla a partir de los argumentos
actuales (los utilizados en cada invocación de la función), así como las medidas sintácticas
adoptadas cuando esto no es posible por producirse ambigüedad.
Una función genérica puede tener más argumentos que la plantilla. Por ejemplo:
template <class T> void func(T, inf, char, long, ...);
Las funciones genéricas son entes de nivel de abstracción superior a las funciones
concretas (en este contexto preferimos llamarlas funciones explícitas), pero las funciones
genéricas solo tienen existencia en el código fuente y en la mente del programador. Hemos
dicho que el mecanismo de plantillas C++ se resuelve en tiempo de compilación, de modo que
en el ejecutable, y durante la ejecución, no existe nada parecido a una función genérica, solo
existen especializaciones (instancias de la función genérica).
Ocurre que si esta instancia aparece más de una vez en un módulo, o es generada
en más de un módulo, el enlazador las refunde automáticamente en una sola definición, de
forma que solo exista una copia de cada instancia. Dicho en otras palabras: en la aplicación
resultante solo existirá una definición de cada función. Por contra, si no existe ninguna
invocación no se genera ningún código.
Aunque la utilización de funciones genéricas conduce a un código elegante y
reducido, que no se corresponde con el resultado final en el ejecutable. Si la aplicación utiliza
muchas plantillas con muchos tipos diferentes, el resultado es la generación de gran cantidad
de código con el consiguiente consumo de espacio. Esta crecimiento del código es conocida
como "Code bloat", y puede llegar a ser un problema. En especial cuando se utilizan las
plantillas de la Librería Estándar, aunque existen ciertas técnicas para evitarlo. Como regla
182 PROGRAMACIÓN C++ Y COMUNICACIONES.
general, las aplicaciones que hace uso extensivo de plantillas resultan grandes consumidoras
de memoria (es el costo de la comodidad).
Puesto que cada instancia de una función genérica es una verdadera función, cada
especialización dispone de su propia copia de las variables estáticas locales que hubiese. Se
les pueden declarar punteros y en general gozan de todas las propiedades de las funciones
normales, incluyendo la capacidad de sobrecarga
Veamos un caso concreto con una función genérica que utiliza tanto una clase Vector
como un entero:
#include <iostream.h>
class Vector
{
public:
float x, y;
bool operator>(Vector v)
{
return ((x*x + y*y) > (v.x*v.x + v.y*v.y))? true: false;
}
};
template<class T> T max(T a, T b){ return (a > b) ? a : b;}
void main()
{
Vector v1 = {2, 3}, v2 = {1, 5};
int x = 2, y = 3;
cout <<"Mayor: "<<max(x, y)<< endl;
cout <<"Mayor:"<<max(v1, v2).x<<", "<<max(v1,v2).y << endl;
}
Mayor: 3
Mayor: 1, 5
void main(void)
{
float mivector[10]={2,4,6,8,1,3,5,7,9,0};
char cadena[10]="efghBACDI";
ordenar(mivector,8);
ordenar(cadena,10);
for(i=0;i<10;i++)cout<<mivector[i];
cout<<endl<<cadena;
}
a = b;
b = temp;
}
template <class T> void ordenar (T *vector, int num)
{
int i,j;
for(i=0;i<num-1;i++)
for(j=i+1;j<num;j++)
if(vector[i]<vector[j])permutar(vector[i],vector[j]);
}
Metodos genéricos
Las funciones genéricas pueden ser miembros (métodos) de clases:
class A
{
template<class T> void func(T& t) // def de método genérico
{
...
}
...
}
Aunque aún no se han explicado las clases genéricas, es conveniente indicar ya que
los miembros genéricos pueden ser a su vez miembros de clases genéricas, en cuyo caso
pueden hacer uso de los parámetros de la plantilla de clase genérica. Un ejemplo de un
método genérico es el siguiente:
#include <iostream.h>
class A
{
public:
int x;
template<class T> void fun(T t1, T t2);
A (int a = 0) { x = a; }
};
void main(void)
{
A a(7), b(14);
a.fun(2, 3);
b.fun('x', 'y');
}
Salida:
Parámetros de la plantilla
La definición de la función genérica puede incluir más de un argumento. Es decir, el
especificador template <...> puede contener una lista con varios tipos. Estos parámetros
pueden ser tipos Complejos o fundamentales, por ejemplo un int; incluso especializaciones de
clases genéricas y constantes de tiempo de compilación.
Un ejemplo que ilustra este último caso, es el siguiente en el que se utilizan dos
plantillas muy parecidas desde el punto de vista de uso, pero diferentes en cuanto al código
compilado es el siguiente:
#include <iostream.h>
Los argumentos a la plantilla de las funciones genéricas no pueden tener valores por
defecto. Lo mismo que en las funciones explícitas, en las funciones genéricas debe existir
concordancia entre el número y tipo de los argumentos formales y actuales.
imprime2(vector);
if(aux==NULL)
{
cout<<”Error de construcción”;
abort();
}
return aux;
}
....
func(b, c, i); // Error!!
func <A, B, C>(b, c, i); // Ok. B y C redundantes
func<A>(b, c, i); // Ok.
func<A>(b, c); // Error!! falta argumento i
#include <iostream.h>
class Vector
{
public:
float x, y;
Vector(float a,float b):x(a),y(b){}
bool operator==(const Vector& v)
{
return ( x == v.x && y == v.y)? true : false;
}
};
template<class T> bool igual(T a, T b) función genérica
{
return (a == b) ? true : false;
}
void main()
{
192 PROGRAMACIÓN C++ Y COMUNICACIONES.
doubles distintos
enteros distintos
Versión explícita
Consideremos ahora que es necesario rebajar la exigencia para que dos variables
sean consideradas iguales en el caso de que sean doubles. Para ello introducimos una
instancia de igual codificada manualmente en el que reflejamos la nueva condición de
igualdad:
#include <iostream.h>
class Vector
{
public:
float x, y;
Vector(float a,float b):x(a),y(b){}
bool operator==(const Vector& v)
{
return ( x == v.x && y == v.y)? true : false;
}
};
template<class T> bool igual(T a, T b)
{
return (a == b) ? true : false;
};
bool igual(double a, double b) // versión explícita
{
return (labs(a-b) < 1.0) ? true : false;
};
void main()
{
Vector v1(2, 3), v2 (1, 5);
int x = 2, y = 3;
double d1 = 2.0, d2 = 2.2;
vectores distintos
doubles iguales
enteros distintos
La versión explícita para tipos double utiliza la función de librería labs para conseguir
que dos doubles sean considerados iguales si la diferencia es solo en los decimales. La
inclusión de esta definición supone que el compilador no necesita generar una versión de
igual() cuando los parámetros son tipo double. En este caso, el compilador utiliza la versión
suministrada "manualmente" por el programador.
Además de permitir introducir modificaciones puntuales en el comportamiento
general, las versiones explícitas pueden utilizarse también para eliminar algunas de las
limitaciones de las funciones genéricas.
Por ejemplo, si sustituimos la sentencia:
if ( igual(d1, d2) ) cout << "doubles iguales" << endl;
por:
if ( igual(d1, y) ) cout << "doubles iguales" << endl;
194 PROGRAMACIÓN C++ Y COMUNICACIONES.
Hemos indicado al comienzo del capítulo que las clases‐plantilla, clases genéricas o
generadores de clases, son un artificio C++ que permite definir una clase mediante uno o
varios parámetros. Este mecanismo es capaz de generar la definición de clases (instancias o
especializaciones de la plantilla) distintas, pero compartiendo un diseño común. Podemos
imaginar que una clase genérica es un constructor de clases, que como tal acepta
determinados argumentos (no confundir con el constructor de‐una‐clase, que genera
objetos).
Para ilustrarlo veremos la clase mVector. Los objetos mVector son matrices cuyos
elementos son objetos de la clase Vector; que a su vez representan vectores de un espacio de
dos dimensiones.
El diseño básico de la clase es como sigue:
196 PROGRAMACIÓN C++ Y COMUNICACIONES.
class mVector
{
int dimension;
public:
Vector* mVptr;
mVector(int n = 1)
{
dimension = n;
mVptr = new Vector[dimension];
}
~mVector() { delete [] mVptr; }
Vector& operator[](int i) { return mVptr[i]; }
void mostrar(int);
};
void mVector::mostrar (int i)
{
if((i >= 0) && (i <= dimension)) mVptr[i].mostrar();
}
El sistema de plantillas permite definir una clase genérica que instancie versiones de
mVector para matrices de cualquier tipo especificado por un parámetro. La ventaja de este
diseño parametrizado, es que cualquiera que sea el tipo de objetos utilizados por las
especializaciones de la plantilla, las operaciones básicas son siempre las mismas (inserción,
borrado, selección de un elemento, etc).
Una clase genérica puede tener una declaración adelantada (forward) para ser
declarada después:
Observe que la definición de una plantilla comienza siempre con template<...>, y que
los parámetros de la lista <...> no son valores, sino tipos de datos .
La definición de la clase genérica correspondiente al caso anterior es la siguiente:
template<class T> class mVector
{
int dimension;
public:
T* mVptr;
mVector(int n = 1)
{
dimension = n;
mVptr = new T[dimension];
}
~mVector() { delete [] mVptr; }
T& operator[](int i) { return mVptr[i]; }
void mostrar (int);
};
Observe que aparte del cambio de la declaración, se han sustituido las ocurrencias de
Vector (un tipo concreto) por el parámetro T. Observe también la definición de mostrar() que
se realiza off‐line con la sintaxis de una función genérica.
Recordemos que en estas expresiones, el especificador class puede ser sustituido por
typename , de forma que la primera línea puede ser sustituida por:
{
...
};
void main()
{
mVector<Vector> mV1(5);
mV1[0].x = 0; mV1[0].y = 1;
mV1[1].x = 2; mV1[1].y = 3;
mV1[2].x = 4; mV1[2].y = 5;
mV1[3].x = 6; mV1[3].y = 7;
mV1[4].x = 8; mV1[4].y = 9;
mV1.mostrar();
mVector<Vector> mV2 = mV1;
mV2.mostrar();
mV1[0].x = 9; mV1[0].y = 0;
mV2.mostrar(0);
mV1.mostrar(0);
mVector<Vector> mV3(0);
mV3.mostrar();
mV3 = mV1;
mV3.mostrar();
}
Salida:
Matriz de: 5 elementos.
0 - X = 0; Y = 1
1 - X = 2; Y = 3
2- X = 4; Y = 5
3- X = 6; Y = 7
200 PROGRAMACIÓN C++ Y COMUNICACIONES.
4- X = 8; Y = 9
Matriz de: 5 elementos.
0- X = 0; Y = 1
1- X = 2; Y = 3
2- X = 4; Y = 5
3- X = 6; Y = 7
4- X = 8; Y = 9
X = 0; Y = 1
X = 9; Y = 0
Matriz de: 0 elementos.
Matriz de: 5 elementos.
0- X = 9; Y = 0
1- X = 2; Y = 3
2- X = 4; Y = 5
3- X = 6; Y = 7
4- X = 8; Y = 9
Otro ejemplo que describe una clase genérica para la realización de una pila de datos
sin que se utilicen listas enlazadas y reserva dinámica de memoria adicional durant
funcionmiento de un objeto de una clase instanciada es el siguiente:
// fichero Pila.h
template <class T>
// declaración de la clase
class Pila
{
public:
Pila(int nelem=10); // constructor
void Poner(T);
void Imprimir();
private:
int nelementos;
T* cadena;
int limite;
};
// definición del constructor
template <class T> Pila<T>::Pila(int nelem)
{
nelementos = nelem;
cadena = new T(nelementos);
limite = 0;
};
// definición de las funciones miembro
template <class T> void Pila<T>::Poner(T elem)
{
if (limite < nelementos)
cadena[limite++] = elem;
};
template <class T> void Pila<T>::Imprimir()
{
int i;
for (i=0; i<limite; i++)
cout << cadena[i] << endl;
};
};
Sin embargo, no es exactamente así por diversas razones: La primera es que, por
ejemplo, se estaría definiendo la plantilla mostrar sin utilizar el parámetro T en su lista de
argumentos (lo que en un principio no está permitido ). Otra es que no está permitido
declarar los destructores como funciones genéricas. Además los especificadores <T>
referidos a mVector dentro de la propia definición son redundantes.
Estas consideraciones hacen que los prototipos puedan ser dejados como sigue (los
datos faltantes pueden ser deducidos por el contexto):
template<class T> class mVector
{
int dimension;
public:
T* mVptr;
mVector& operator= (const mVector&);
mVector(int); // constructor por defecto
~mVector(); // destructor
mVector(const mVector& mv); // constructor-copia
T& operator[](int i) { return mVptr[i]; }
void mostrar (int); // función auxiliar
void mostrar (); // función auxiliar
};
Sin embargo, las definiciones de métodos realizadas off‐line (fuera del cuerpo de una
plantilla) deben ser declaradas explícitamente como funciones genéricas.
Por ejemplo:
template <class T> void mVector <T>::mostrar (int i)
{
...
}
Miembros estáticos
Las clases genéricas pueden tener miembros estáticos (propiedades y métodos).
Posteriormente cada especialización dispondrá de su propio conjunto de estos miembros.
Estos miembros estáticos deben ser definidos fuera del cuerpo de la plantilla, exactamente
igual que si fuesen miembros estáticos de clases concretas:
template<class T> class mVector
{
...
static T* mVptr;
static void mostrarNumero (int);
...
};
Métodos genéricos
Hemos señalado que, por su propia naturaleza, los métodos de clases genéricas son a
su vez (implícitamente) funciones genéricas con los mismos parámetros que la clase, pero
pueden ser además funciones genéricas explícitas (que dependan de parámetros distintos de
la plantilla a que pertenecen):
Según es usual, la definición del miembro genérico puede efectuarse de dos formas,
inline u off‐line:
#include <iostream.h>
{
x = i;
xptr = b;
}
};
int main(void)
{
char c = 'c'; char* cptr = &c;
int x = 13; int* iptr = &x;
A<int> a(iptr, 2);
A<char> b(cptr, 3);
a.fun(2, 3);
a.fun('x', 'y');
b.fun(2, 3);
b.fun('x', 'y');
return 0;
}
Salida:
Por ejemplo:
mVector<char> mv1;
mVector mv2 = mv1; // Error !!
mVector<char> mv2 = mv1; // Ok.
Sin embargo, como veremos a continuación, las clases genéricas pueden tener
argumentos por defecto, por lo que en estos casos la declaración puede no ser explícita sino
implícita (referida a los valores por defecto de los argumentos). La consecuencia es que en
estos casos el compilador tampoco realiza ninguna suposición sobre los argumentos a utilizar.
Las clases genéricas pueden ser utilizadas en los mecanismos de herencia. En ese
caso, la clase derivada estará parametrizada por los mimmos argumentos que la plantilla de la
clse base.
Por ejemplo:
template <class T> class Base { ... };
template <class T> class Deriv : public Base<T> {...};
Los typedef son muy adecuados para acortar la notación de objetos de clases
genéricas cuando se trata de declaraciones muy largas o no interesan los detalles.
Por ejemplo :
typedef basic_string <char> string;
…
string st1;
Las clases genéricas pueden tener argumentos por defecto, en cuyo caso, el tipo T
puede omitirse, pero no los ángulos <>. Por ejemplo:
template<class T = int> class mVector {/* ... */};
...
mVector<char> mv1; // Ok. argumento char explícito
mVector<> mv2; // Ok. argumento int implícito
Cada instancia de una clase genérica es realmente una clase, y sigue las reglas
generales de las clases. Dispondrá por tanto de su propia versión de todos los miembros
estáticos si los hubiere. Estas clases son denominadas implícitas, para distinguirlas de las
definidas "manualmente", que se denominan explícitas.
La primera vez que el compilador encuentra una sentencia del tipo
mVector<Vector> crea la función‐clase para dicho tipo; es el punto de instanciación. Con
206 PROGRAMACIÓN C++ Y COMUNICACIONES.
objeto de que solo exista una definición de la clase, si existen más ocurrencias de este mismo
tipo, las funciones‐clase redundantes son eliminadas por el enlazador. Por la razón inversa, si
el compilador no encuentra ninguna razón para instanciar una clase (generar la función‐
clase), esta generación no se producirá y no existirá en el código ninguna instancia de la
plantilla.
Al igual que ocurre con las funciones genéricas, en las clases genéricas también
puede evitarse la generación de versiones implícitas para tipos concretos proporcionando una
especialización explícita.
Por ejemplo:
class mVector<T> { ... }; // definición genérica
...
class mVector<char> { ... }; // definición específica
más tarde, las declaraciones del tipo
mVector<char> mv1, mv2;
generará objetos utilizando la definición específica. En este caso mv1 y mv2 serán
matrices alfanuméricas (cadenas de caracteres).
Argumentos de la plantilla
La declaración de clases genéricas puede incluir una lista con varios parámetros.
Estos pueden ser casi de cualquier tipo: Complejos, fundamentales, por ejemplo un int, o
incluso otra clase genérica (plantilla). Además en todos los casos pueden presentar valores
por defecto.:
template<class T, int dimension = 128> class mVector { ... };
int i = 256;
mVector<int, 2*K> mV1; // OK
mVector<Vector, i> mV2; // Error: i no es constante
Los argumentos también pueden ser otras plantillas, pero solo de clases genéricas.
Introducir como argumento una plantilla de funcione genérica no está permitido:
template <class T, template<class X> class C> class MatrizC
{ ...
};
template <class T, template<class X> void Func(X a)> class MatrizF
{ // Error!!
...
};
Al principio del capitulo se ha señalado que las plantillas fueron introducidas en C++
para dar soporte a determinadas técnicas utilizadas en la Librería Estándar; de hecho, la STL
está constituida casi exclusivamente por plantillas. Hablar y utilizar en profundidad las
plantillas de la librería estándar puede permitirnos el reducir drásticamente el esfuerzo
Una idea básica dentro de las STL es el contenedor, que es precisamente lo que
parece: un sitio en el que almacenar cosas. Ya se ha visto que una de las operaciones más
habituales es esta, así como una de las justificaciones para el uso de plantillas de clases
(recuérdese el ejemplo de Lista<Esferas> o de la clase genérica mVector. Habitualmente
necesitamos este tipo de sitios de almacenamiento cuando de antemano desconocemos
cuantos objetos o datos vamos a tener. Los contenedores de la STL hacen esto; son capaces
de mantener colecciones de objetos y además se pueden redimensionar. El modo como
almacenan estas colecciones de objetos, y como consecuencia las operaciones que se pueden
realizar sobre las colecciones, hacen que aparezcan distintos tipos de contenedores. Los
contenedores más secillos son:
vector. Un vector es la versión STL de una matriz dinámica de una dimensión. Las
instancias de esta clase genérica conforman una secuencia (una clase de contenedor ). La
clase dispone de acceso aleatorio a sus elementos y de un mecanismo muy eficiente de
inserción y borrado de elementos al final. Aunque también pueden insertarse y borrarse
elementos en medio, por el modo en que se organizan los elementos en memoria, esta
operación es muy poco eficiente. Está definida en el fichero <vector> y responde a la
siguiente declaración:
template <class T, class Allocator = allocator<T> > class vector;
deque. Es muy parecido a vector, una secuencia lineal de elementos, pero admite con
eficiencia la inserción de objetos tanto al principio como al final, mientras que sigue
comportándose mal para inserciones intermedias. Admite el acceso aleatorio casi con la
misma eficiencia que vector, pero es especialmente más rápido cuando requiere solicitar
más espacio para almacenar más datos (redimensionamiento).
list. Las instancias de esta plantilla conforman también una secuencia que dispone de
mecanismos muy eficientes para insertar y eliminar elementos en cualquier punto. Está
internamente implementada como una lista doblemente enlazada. Como consecuencia
210 PROGRAMACIÓN C++ Y COMUNICACIONES.
de su estructura, no está diseñada para el acceso aleatorio siendo este bastante más
ineficiente que en las dos clases anteriores. Está definida en el fichero <list> y responde a
la siguiente declaración:
template <class T, class Allocator = allocator<T> > class list;
class Shape {
public:
virtual void draw() = 0;
virtual ~Shape() {};
};
int main() {
Container shapes;
shapes.push_back(new Circle);
shapes.push_back(new Triangle);
for(Iter i = shapes.begin();
i != shapes.end(); i++)
(*i)->draw(); //se puede usar i->draw() dado que el iterador tiene
//sobrecargado el operador ->
// ... al final si queremos limpiar la memoria:
for(Iter j = shapes.begin();
j != shapes.end(); j++)
delete *j;
}
Es interesante ver que es posible cambiar sin problemas el tipo básico del contenedor a
una lista o un deque, y el código no se ve alterado más que en la definición del
contenedor.
basic_string: una plantilla para utilizar entidades como secuencias de caracteres. Está
definida en el fichero de cabecera <string>, y responde a la siguiente declaración:
template <class charT, class traits = char_traits<charT>,
class Allocator = allocator<charT> > class basic_string;
Como puede verse, acepta tres argumentos, de los que dos tienen valores por defecto.
Existen dos especializaciones que tienen nombres específicos. En concreto si charT es
char la especialización se denomina string, y wstring si charT es wchar_t. Esto se hace
mediante sendos typedef:
typedef basic_string <char> string;
212 PROGRAMACIÓN C++ Y COMUNICACIONES.
Las cadenas en C++ son en el fondo contenedores de caracteres y como tales pueden
ser tratados. Sin embargo, dado su uso tan común, se ha dotado a basic_string de un
conjunto de funciones y operadores que simplifican (¡por fin!) el uso de este tipo de datos.
Por ejemplo, la concatenación, creación, reserva, comparación, paso a mayusculas, etc... son
operaciones incluidas directamente:
#include <string>
#include <iostream>
using namespace std;
int main() {
string s1("This ");
string s2="That ";
string s3("The other ");
// operator+ concatena strings
s1 = s1 + s2;
cout << s1 << endl;
// otra forma de concatenar strings
s1 += s3;
cout << s1 << endl;
// o acceder a la vez que se concatena a un caracter
s1 += s3 + s3[4] + "oh lala";
cout << s1 << endl;
if(s1 != s2) {
cout << "los strings son distitos:" << " ";
if(s1 > s2)
cout << "s1 es > s2" << endl;
else
cout << "s2 es > s1" << endl;
}
}
map. Esta plantilla está definida en el fichero <map>, y tiene la siguiente declaración:
template <class Key, class T, class Compare = less<Key>
class Allocator = allocator<pair<const Key, T>> > class map;
6.6. Ejemplo
El siguiente estracto de código muestra una clase genérica que sirve para alamecenar
punteros de objetos o elementos creados externamente a ella. Internamente se comporta
como un vector dinámico que admite por tanto un número indefinido de elementos. Incluye
además una serie de operaciones básicas para facilitar el manejo y mantenimiento del
conjunto de direcciones almacenadas:
Fichero contenedor.h
template <class T> class contenedor
{
private:
T **array;
int nElem;
int elemMax;
public:
contenedor(void);
contenedor(contenedor &td); /*copia*/
~contenedor(void);
inline T* operator [] (int a);
void operator += (T *ele);
void destruirObjetos(void);
void eliminar (int a);
214 PROGRAMACIÓN C++ Y COMUNICACIONES.
T* quitar(int a);
T* quitar(T* ele);
int buscar(T* ele);
void swap(int a, int b);
void insertar(T *ele, int a);
inline int numElem (void);
};
Fichero contenedor.cpp
#include “contenedor.h”
template <class T> contenedor<T>::contenedor (void)
{
array = new T* [5];
nElem = 0;
elemMax = 5;
}
template <class T> void contenedor<T>::destruirObjetos (void)
{
for (int i=0; i<numElem; i++)
delete array[i];
numElem=0;
}
template <class T> contenedor<T>::~contenedor (void)
{
delete [] array;
}
if (nElem == elemMax)
{
elemMax+=5;
Uno de los aspectos que con más dificultad se abordan en los distintos lenguajes de
programación es el correcto tratamiento de los errores. Es decir, el funcionamiento normal de
un algoritmo o programa contiene la dificultad inherente del problema que se quiere
resolver, y como consecuencia generará un flujograma más o menos complicado. Sin
embargo, todo ello se incrementa en complejidad de forma notable en el momento en el que
se pretende verificar y comprobar los posibles errores o situaciones "no nominales" durante
el proceso de ejecución.
Este problema no es solo específico de la ingeniería del software, sino que a menudo
es el mayor quebradero de cabeza en la mayoría de los proyectos de diseño y en particular
aquellos en los que la seguridad es importante. De hecho, se puede decir sin problema, que
es más Complejo el sistema de redundancia y seguridad que se incluye en el software de
control de un avión que el propio sistema de control.
1. Hay una parte de código en el que se considera que se pueden producir situaciones
anómalas, y que consecuencia se pueden generar una serie de excepciones. Por eso,
se considera que es posible que la acción que esa parte de código quiere realizar no
se consiga, y por tanto es una zona de "intento de ejecución" (try).
2. Cuando se detecta una circunstancia extraña o anómala (una excepción), se informa
al proceso, función, o programa que ha ocurrido algo imprevisto, y se interrumpe el
curso normal de ejecución. Para realizarlo se "lanza" una excepción (throw). Esa
excepción es un objeto que contiene información sobre el error para que el que
escuche sea capaz de reaccionar ante esta situación.
3. Para que el programa pueda decidir que hacer en estos casos, tras intentar ejecutar
algo susceptible de fallar, indicará una zona del código que se ejecutará para
reaccionar ante esas sistuaciones. Para ello "captura" (catch) las excepciones
lanzadas en la zona de "intento" y se decide que hacer al respecto con la
información que estas contienen.
6
Exception Handler
222 PROGRAMACIÓN C++ Y COMUNICACIONES.
Generalmente las implementaciones C++ solo consideran las excepciones síncronas, de forma
que no se pueden capturar con ellas excepciones tales como la pulsación de una tecla. Dicho
con otras palabras: solo pueden manejar las excepciones lanzadas con la sentencia
throw. Siguen un modelo denominado de excepciones síncronas con terminación, lo que
significa que una vez que se ha lanzado una excepción, el control no puede volver al punto
que la lanzó. El "handler" no puede devolver el control al punto de origen del error mediante
una sentencia return. En este contexto, un return en el bloque catch supone salir de la
función que contiene dicho bloque.
Antes de ver un ejemplo completo se verá en primer lugar como se ha previsto en C++ la
transcripción de cada uno de estos compoenentes del mecanismo de tratamiento de
excepciones.
El bloque "try"
En síntesis podemos decir que el programa se prepara para cierta acción, decimos
que "lo intenta". Para ello se especifica un bloque de código cuya ejecución se va a intentar
("try‐block") utilizando la palabra clave try:
try
{
La idea del bloque try en este caso podría ejemplarizarse de la siguiente forma:
INTENTA {
1.‐ Abrir el fichero
2.‐ Guardar la información
3.‐ Cerrar el fichero
}
SI HAY UN ERROR CAPTURALO Y EJECUTA
{
4.‐ Informa al usuario
}
Así pues, try es una sentencia que en cierta forma es capaz de especificar el flujo de ejecución
del programa. Un bloque‐intento debe ser seguido inmediatamente por el bloque manejador
de la excepción, el cual se especifica por medio de la palabra clave catch.
El bloque "catch"
Mediante la palabra clave catch especificamos el código encargado de reaccionar ante una
determinada excepción o conjunto de excepciones. Como ya se ha comentado, se dice que un
manejador "handler" captura una excepción.
En C++ es obligatorio que tras un bloque try se incluya al menos un bloque catch, por lo que la
sintaxis anterior quedaría completa de la siguiente forma:
}
... // continua la ejecución normal
Las excepciónes no sólo las lanzan las funciones incluidas dentro de las librerías externas, sino
que nosotros podemos querer informar que algo no ha ido bien dentro de la lógica de nuestro
programa.
Al igual que ocurre con el código de las librerías estandar, esto se realiza por medio de la
instrucción throw.
El lenguaje C++ especifica que todas las excepciones deben ser lanzadas desde el interior de
un bloque‐intento y permite que sean de cualquier tipo. Como se ha apuntado antes,
generalmente son un objeto (instancia de una clase) que contiene información. Este objeto
es creado y lanzado en el punto de la sentencia throw y capturado donde está la sentencia
catch. El tipo de información contenido en el objeto es justamente el que nos gustaría tener
para saber que tipo de error se ha producido. En este sentido puede pensarse en las
excepciones como en una especie de correos que transportan información desde el punto del
error hasta el sitio donde esta información puede ser analizada. Todo esto lo veremos con
más detalle a continuación.
Como se ha comentado C++ permite lanzar excepciones de cualquier tipo. Sin embargo lo
normal es que sean instancias de una clase tipo X, que contiene la información necesaria para
conocer la naturaleza de la circunstancia excepcional (probablemente un error). El tipo X
debe corresponder con el tipo de argumento usado en el bloque catch, de tal forma que por
medio del tipo de las excepciones será posible ejecutar manejadores distintos aún siendo
estas lanzadas desde elmismo bloque try.
Nota: Se recomienda que las clases diseñadas para instanciar este tipo de
objetos (denominadas Excepciones) sean específicas y diseñadas para este fin
exclusivo, sin que tengan otro uso que la identificación y manejo de las
excepciones.
La expresión throw (X arg) inicializará un objeto temporal arg de tipo X, aunque puede que se
generen otras copias por necesidad del compilador. En consecuencia puede ser útil definir un
constructor‐copia cuando el objeto a lanzar pertenece a una clase que contiene subobjetos,
como en el caso de cualquier otra clase cuyos objetos quieran ser pasados por valor
(recuérdese el apartado dedicado al contructor de copia y el operador de asignación en el
capítulo 3).
#include <stdio.h>
bool pass;
class Out{}; // L.3: Para instanciar el objeto a lanzar
Como puede verse, la filosofía C++ respecto al manejo de excepciones no consiste en corregir
el error y volver al punto de partida. Por el contrario, cuando se genera una excepción el
control sale del bloque try que lanzó la excepción (incluso de la función), y pasa al bloque
catch cuyo manejador corresponde con la excepción lanzada (si es que existe).
Si el bloque catch termina normalmente sin lanzar una nueva excepción, el control se salta
todos los bloques catch que hubiese asociados al bloque try correspondiente y sigue la
ejecución del programa a continuación.
Puede ocurrir que el bloque catch lance a su vez una excepción. Lo que nos conduce a
excepciones anidadas. Esto puede ocurrir, por el hecho de que se ejecutan intrucciones y por
tanto es posible que alguna de ellas vuelva a generar a su vez una excepción (supongase que
es una excepción lanzada por intentar cerrar un fichero que no existe o que no se puede
cerrar)
Como se puede ver, un programador avanzado puede utilizar las excepciones C++ como un
mecanismo de return o break multinivel, controlado no por una circunstancia excepcional,
sino como un acto deliberado del programador para controlar el flujo de ejecución. se lanzan
deliberadamente excepciones con la idea de moverse entre bloques a distintos niveles. Dado
el nivel de este curso no se recomienda esta práctica hasta que no se esté totalmente
familiarizado con la programación estructurada.
El siguiente ejemplo sería incorrecto, dado que se intenta relanzar una excepción desde fuera
de un bloque try:
try {
...
if (x) throw A(); // lanzar excepción
}
catch (A a) { // capturar excepción
... // hacer algo respecto al error
throw; // Error!! no está en un bloque try
}
El modo más habitual de realizar un relanzamiento es debido a que se trata de un bloque try
anidado en otro bloque try (en la misma función o en funciones jerarquicamente por encima
en la pila de llamadas). El siguiente ejemplo refleja este modo de proceder:
void foo();
void main () {
try {
...
foo();
}
catch (A a) {
...
}
return 0;
}
void foo() {
...
if (x) try {
throw A();
}
catch (A) {
...
throw;
}
}
Departamento de Electrónica Automática e Informática Industrial
230 PROGRAMACIÓN C++ Y COMUNICACIONES.
El término UML proviene de las siglas inglesas de Lenguaje Unificado de Modelado (Unified
Modeling Language) y es el lenguaje de modelado de sistemas software más conocido y
utilizado en la actualidad, y se encuentra respaldado por el OMG (Object Management
Group).
Collage obtenido de Wikipedia en el que se reflejan algunos de los múltiples diagramas que esfecifica UML.
UML cuenta con varios tipos de diagramas, los cuales muestran diferentes aspectos de las
entidades representadas .
Finalmente no hay que confundir UML con un lenguaje de programación. De hecho, a pesar
de constituir un estándar prácticamente aceptado por el conjunto de programadores, no deja
de recibir ciertas críticas debido a la ambigüedad en la interpretación que se puede realizar en
los distintos diagramas. Además, al constituir una serie de herramientas de modelado, en
general, una misma realidad se representa o dibuja desde distitnas perspectivas que se
complementan para finalmente dotar al conjunto de un significado. Luego por eso es muy
importante que los diagramas se centren en aquel aspecto que quieren mostrar y no intentar
reflejar toda la realidad en un solo gráfico.
232 PROGRAMACIÓN C++ Y COMUNICACIONES.
Elementos estructurales
CLASE: Refleja un conjunto de objetos que comparten los mismos atributos, operaciones,
relaciones y semántica. Lógicamente constituirá una representación unívoca del
elemento homónimo de C++. Se refleja como un rectángulo dividido en tres secciones:
nombre, atributos y métodos.
Componente: Es una parte modular del sistema de diseño, agrupando por tanto sus
elementos lógicos (clases, interfaces), y mostrando fundamentalmente un conjunto de
funcionalidades utilizables por el exterior. Por ejemplo, un componente podría ser el
reporductor de Video de un sistema de ventanas. Internamente contendrá una
complejidad elevada, pero finalizado el componente, lo que se necesita para su uso es las
interfaces publicas del componente (abrir un fichero, reproducirlo, subir el volumen…
etc). Un aspecto importante es que un componente en principio puede ser intercambiado
por otro siempre que se mantenga la interfaz.
Nodo: Elemento físico que existe en tiempo de ejecución y que representa un recurso
computacional que, por lo general, dispone de algo de memoria y, con frecuencia, de
capacidad de procesamiento. Un conjunto de componentes puede residir en un nodo.
Elementos de comportamiento
Estado: Se utiliza para reflejar en una secuencia los estados por los que pasa un objeto o una
interacción en respuesta a eventoz o sucesos en general.
Elementos de agrupación
Elementos de anotación
Nota: el tipo principal de anotación. Son comentarios que se pueden aplicar para describir,
clarificar y hacer observaciones cobre cualquier elemento de un modelo
Elementos de relación
Una relación es una conexión entre elementos. Para diferenciar las distintas relaciones se
utilizan diferentes tipos de líneas. Hay cuatro tipos de relaciones: dependencia, asociación,
generalización y realización
236 PROGRAMACIÓN C++ Y COMUNICACIONES.
Asociación: Relación estructural que describe un conjunto de enlaces, los cuals son relaciones
entre objetos. La agregación es un tipo especila de asociación y representa una relación
estructural entre un todo y sus partes.
El modelado es la parte de UML que se ocupa de identificar todas las partes importantes de
un sistema así como sus interacciones. De igual forma se entiende como modelado
estructural, al modelo de los aspectos estáticos de un sistema, para lo cual se utilizan
fundamentalmente como sustantivos o elementos estructrales básicos las clases y las
interfaces así como sus relaciones entre ellas. Dado que en estos apuntes sólo se pretende
dar unas nociones que nos permitan representar la estructura de clases y algo de la evolución
de nuestros programas, se procede ahroa a ver los elementos más comunes en este tipo de
diagramas denominados DCD (Diagramas de Clases de Diseño).
La forma en que representamos las interacciones entre clases queda reflejado por sus
relaciones. En los diagramas de modelado sin embargo no hay porque reflejar TODAS las
relaciones existentes, porque en ese caso el diagrama puede convertirse en algo ilegible. Las
relaciones más habituales (y es aquí en donde nos centraremos en este curso, dejando otros
diagramas y relaciones para más adelante) son:
Relación de dependencia:
Relación de Generalización:
Relación de Asociación:
Mediante esta relación representamos como los objetos de un elemento están conectados
con los objetos de otros. Pueden establecerse incluso relaciones recursivas, es decir que un
objeto esté asociado a un objeto del mismo tipo.
En las relaciones de asociación se suelen incluir adornos para facilitar su comprensión, y para
diferenciar los distintos modos que la asociación puede realizarse:
Agregación: Representa una relación estructural entre iguales (sirve para relacionar
todo /parte, pero en el que tanto el todo como la parte tienen una “vida” propia. En
C++ refleja agregaciones de objetos por referencia.
Composición: Como una agregación simple pero en este caso el todo da la “vida” a la
parte. En C++, refleja el hecho de que un objeto o conjunto de objetos son atributos
del objeto que los contiene.
240 PROGRAMACIÓN C++ Y COMUNICACIONES.
8.3. Diagramas
Diagramas de Clases:
Dado que solo se trata de una breve introducción, incluimos en este capítulo uno de los
diagramas más recurrentemente utilizados en DOO, el diagrama de clases de diseño. En este
se muestra un conjunto de clases, con interfaces, colaboraciones y sus relaciones.
Se usa fundamentalmente para modelar el vocabulario del sistema (abstracciones que son
parte del sistema y las que no lo son) así como modelar colaboraciones simples, y el esquema
lógico de una base de datos. Por tanto se utilizan para visualizar los aspectos estáticos de los
bloques de construcción del sistema.
Para trazar un diagrama de clases en el que se reflejen las colaboraciones simples, en primer
lugar habrá que identificar las funciones o comportamientos del sistema que se está
modelando, y que quedarán reflejados por el diagrama.
Para cada elemento entonces, habrá que identificar las clases, iterfaces y relaciones con otros
elementos. Habrá que ir dontando a estos elementos de contenido, intentando que haya un
reparto de responsabilidades entre clases y terminando por convertir las responsabilidaders
en atributos.
Diagramas de Sequecia:
Otro de los diagramas más recurrentes cuando se desea reflejar el comportamiento dinámico
de las interacciones entre distintos elementos es el diagrama de secuencia.
Los objetos que participan en una interacción son o bien elementos concretos (objetos) o
bien elementos prototípicos (clases, nodos, actores y casos de uso).
El foco de control: el cual representa el periodo de tiempo durante el cual un objeto ejecuta
una acción. Se representan con rectángulos vacíos sobre la línea de vida.
La importancia de los sistemas operativos nace históricamente desde los 50's, cuando se hizo
evidente que el operar una computadora por medio de tableros enchufables en la primera
generación y luego por medio del trabajo en lote en la segunda generación se podía mejorar
notoriamente, pues el operador realizaba siempre una secuencia de pasos repetitivos, lo cual
es una de las características contempladas en la definición de lo que es un programa. Es decir,
se comenzó a ver que las tareas mismas del operador podían plasmarse en un programa, el
cual a través del tiempo y por su enorme complejidad se le llamó "Sistema Operativo". Así,
tenemos entre los primeros sistemas operativos al Fortran Monitor System ( FMS ) e IBSYS.
El resultado fue un sistema del cual uno de sus mismos diseñadores patentizó su opinión en la
portada de un libro: una horda de bestias prehistóricas atascadas en un foso de brea. Surge
también en la tercera generación de computadoras el concepto de la multiproceso, porque
debido al alto costo de las computadoras era necesario idear un esquema de trabajo que
mantuviese a launidad central de procesamiento más tiempo ocupada, así como el encolado
(spooling ) de trabajos para su lectura hacia los lugares libres de memoria o la escritura de
resultados. Sin embargo, se puede afirmar que los sistemas durante la tercera generación
siguieron siendo básicamente sistemas de lote.
Para finales de los 80's, comienza el auge de las redes de computadores y la necesidad de
sistemas operativos en red y sistemas operativos distribuidos. La red mundial Internet se va
haciendo accesible a toda clase de instituciones y se comienzan a dar muchas soluciones ( y
problemas ) al querer hacer convivir recursos residentes en computadoras con sistemas
operativos diferentes. Para los 90's el paradigma de la programación orientada a objetos
cobra auge, así como el manejo de objetos desde los sistemas operativos. Las aplicaciones
intentan crearse para ser ejecutadas en una plataforma específica y poder ver sus resultados
en la pantalla o monitor de otra diferente (por ejemplo, ejecutar una simulación en una
máquinacon UNIX y ver los resultados en otra con DOS ). Los niveles de interacción se van
haciendo cada vez más profundos.
Actualmente podemos resumir que de todo este proceso histórico se extrae que un Sistema
Operativo es un conjunto de programas que controla los dispositivos que forman el
ordenador (memoria y periféricos), administra los recursos y gestiona la ejecución del resto
del software. De alguna forma actúa como enlace entre el usuario y los programas y el
hardware del ordenador. Aunque ahora los veremos en más detalle, los objetivos básicos de
un sistema operativo podrían resumirse en dos:
• Órdenes de administración
• Actividades de temporización.
• Establecimiento de prioridades.
De forma resumida podemos establecer las funciones del sistema operativo en base a los
gestores básicos de los que habitualmente consta:
Gestión de procesos
Estrategias para la gestión de procesos
Existen dos formas básicas de trabajar con un computador: por lotes y de forma interactiva.
En esta última, la CPU está constantemente atendiendo al usuario, y en cualquier caso,
aunque la CPU tenga varios procesos en memoria, se tiene la impresión de que el usuario está
trabajando directamente con el computador. Los primeros sistemas operativos funcionaban
como monotarea o serie; es decir, hasta que no finaliza la ejecución de un programa no
empieza a ejecutarse otro.
Según el sistema operativo en particular se definen diferentes estados para un proceso. Los
estados básicos son activo, bloqueado y preparado. Se dice que un proceso está en estado
activo, o de ejecución, cuando la CPU está ejecutando sus instrucciones. Se dice que un
proceso entra en estado de bloqueo cuando la CPU no puede continuar trabajando con él, a
causa de tener que esperar a la realización de una operación de entrada/salida, o a algún otro
evento de naturaleza similar. Se dice que un proceso está en estado preparado, o ejecutable,
cuando la CPU puede iniciar o continuar su ejecución.
En los sistemas multitarea, cuando un proceso entra en estado de bloqueado, un módulo del
sistema operativo denominado distribuidor (“dispacher”) pasa el turno de ejecución a uno de
los procesos de la memoria principal que esté en estado preparado. Para ello, se produce una
interrupción que provoca una conmutación de contexto entre procesos. El cambio de
contexto implica almacenar en una zona de la memoria principal toda la información
referente al proceso interrumpido. Esta información, denominada contexto de un proceso,
está referida a los contenidos de los registros de la CPU, indicadores de los biestables,
punteros a archivos de discos, contenido de la pila, etc.; así como, información de las tablas
referentes al estado de los procesos en memoria. El cambio de contexto supone un consumo
de tiempo de CPU por parte del distribuidor. Cuando el distribuidor vuelva a dar el turno al
proceso interrumpido, por haber finalizado la operación de E/S; es decir, haber salido del
estado de bloqueado y entrado en el de preparado, debe producirse la recuperación de
contexto en el sentido contrario al anterior, ya que, tiene que restituirse desde la zona
auxiliar de memoria los contenidos de los registros salvados, quedando la CPU tal y como
estaba en el instante que interrumpió este proceso, y preparada, por tanto, para proseguir su
ejecución. Estas operaciones ocurren sucesivamente para cada uno de los procesos existentes
en memoria principal.
Uno de los algoritmos de planificación de mayor interés, por su antigüedad, sencillez y amplio
uso, es el de petición circular (“round robin”). Este algoritmo, también llamado cooperativo,
ha sido usado por Windows y Macintosh. Con el algoritmo de petición circular, a cada uno de
los procesos en memoria, se le asigna un intervalo de tiempo fijo, o periodo, llamado
quantum. El objetivo de este algoritmo es cambiar de contexto de un proceso a otro, de
forma rotatoria, conforme se van consumiendo su quantum.
b) El proceso Pi se bloquee.
Grafico que muestra el comportamiento de un sistema de planificación "round ‐ robin" para dar atención a
procesos (A‐E) que se van ejecutando de forma dinámica. En negro se pinta el estado de espera.
Otros sistemas operativos de gran interés son los sistemas operativos de tiempo real. El
concepto de tiempo real hace referencia a que el computador debe garantizar la ejecución de
un proceso, o obtención de un resultado dentro de un límite de tiempo preestablecido. Este
tiempo puede ser pequeño o grande, dependiendo de la aplicación. Los sistemas de tiempo
real se usan ampliamente en control industrial, equipos de conmutación telefónicas, control
de vuelo, aplicaciones militares, simuladores, etc. Los sistemas operativos de tiempo real
deben ser capaces de responder a los eventos, o interrupciones, que se puedan producir de
forma asíncrona, a veces miles por segundo, en unos plazos de tiempo previamente
especificados. Frecuentemente son sistemas multitarea en los que los algoritmos de
planificación son del tipo de derecho preferencial, con los que siempre se ejecuta la tarea de
mayor prioridad y no se interrumpe hasta que finalice, a no ser que se genere otra de
prioridad mayor. El estándar POSIX tiene definidas unas extensiones de tiempo real,
estableciéndose diferentes niveles de requisitos.
Estados de un proceso
Del estado de bloqueado se puede pasar al estado preparado cuando acaba la operación de
E/S pendiente, o se le asigna el recurso esperado. También, es necesario tener en cuenta que
algunos sistemas operativos realizan intercambio entre la memoria principal y el disco, lo cual
les permite la ejecución concurrente de más procesos de los que admite la memoria principal,
ya que un proceso puede ser trasvasado a disco, definiéndose por tanto el estado de
bloqueado intercambiado, o bloqueado en disco. Un ejemplo de proceso bloqueado es aquel
que solicita al operador del computador montar una determinada cinta y, si no hay memoria
suficiente para los otros trabajos en progreso, lo trasvasa a disco. Si la carga de procesos es
grande, incluso los procesos preparados pueden trasvasarse a disco, pasando así al estado de
preparado intercambiado o preparado en disco.
Gestión de la memoria
En los sistemas operativos de monoprogramación la memoria principal se puede organizar de
diversas maneras. El sistema operativo puede ocupar las primeras posiciones de memoria, o
las últimas, o incluso parte del sistema operativo puede estar en la zona RAM de direcciones
bajas, y otra parte de él, como son los gestores de periféricos, o el cargador inicial, pudieran
encontrarse en ROM en las direcciones altas. Esta última situación se corresponde con la de
los antoguos PCs compatibles, en los que la ROM contiene el sistema básico de entradas y
salidas o BIOS (“Basic Input Output System”, Sistema Básico de Entradas y Salidas).
En cualquier caso, cuando el usuario da una orden, si el proceso que la implementa no está en
memoria, el intérprete de comandos del sistema operativo se encarga de cargarlo en
memoria desde disco, y el distribuidor da paso a su ejecución. Cuando finaliza el proceso, el
sistema operativo visualiza el indicador de petición de orden y espera a que se le dé una
nueva orden, en cuyo caso libera la zona de memoria ocupada, sobreescribiendo el nuevo
programa sobre el anterior proceso.
Particiones estáticas.
Particiones dinámicas.
Paginación.
Segmentación.
Memoria virtual.
Particiones estáticas
El sistema operativo mantiene una tabla en la que cada fila corresponde a una
partición, conteniendo la posición base de la partición; su tamaño, no todas las particiones
tienen por qué ser iguales; y el estado de la partición, ocupada o no ocupada. El planificador
de trabajos, una vez que una partición está libre, hace que se introduzca el programa de
máxima prioridad que haya en la cola de espera y que quepa en dicha partición. Si el espacio
de una partición es m palabras y el programa ocupa n posiciones, se verificará siempre que: n
m.
Particiones dinámicas
La utilización de particiones dinámicas se basa en que los programas son introducidos por el
sistema operativo inicialmente en posiciones consecutivas de memoria, no existiendo, por
tanto, particiones predefinidas. El sistema operativo puede gestionar el espacio de memoria
usando una tabla de procesos, en la que cada línea contiene el número de proceso o
identificativo del mismo, el espacio que ocupa y la dirección base. Existe una tabla
complementaria a la anterior con los fragmentos o huecos libres. El planificador de trabajos
periódicamente consulta la tabla, introduciendo en memoria los programas que quepan en
los fragmentos.
Al iniciarse una sesión de trabajo se carga el primer programa, dejando un fragmento libre
donde se pueden incluir otros programas. Al finalizarse los procesos, el número de
fragmentos crecerá y el espacio disminuirá, llegando un momento en que el porcentaje de
memoria aprovechado es muy reducido. El problema puede resolverse haciendo una
compactación. Esta consiste en agrupar todos los fragmentos cuando acaba la ejecución de
un proceso, quedando así sólo uno grande. La compactación se efectúa reubicando los
procesos en ejecución.
Paginación
1. Tabla del mapa de memoria. Contiene tantas filas como marcos de página. Se indica
el identificador del proceso que está en cada bloque y, en su caso, si está libre.
3. Tabla del mapa de páginas. Hay una por proceso, y contiene en número de marco
donde se encuentra cada una de sus páginas. La longitud de cada tabla es variable,
dependiendo del número de páginas; es decir, depende del tamaño de cada
proceso.
Las tablas de paginación o tablas de páginas son una parte integral del Sistema de Memoria
Virtual en sistemas operativos, cuando se utiliza paginación. Son usadas para realizar las
traducciones de direcciones de memoria virtual (o lógica) a memoria real (o física) y en
general el sistema operativo mantiene una por cada proceso corriendo en el sistema.
En cada entrada de la tabla de paginación (en inglés PTE, Page Table Entry) existe un bit de
presencia, que está activado cuando la página se encuentra en memoria principal. Otro bit
que puede encontrarse es el de modificado, que advierte que la página ha sido modificada
desde que fue traída del disco, y por lo tanto deberá guardarse si es elegida para abandonar
la memoria principal; y el bit de accedido, usado en el algoritmo de reemplazo de páginas
llamado Menos Usado Recientemente (LRU, least recently used). También podrían haber
otros bits indicando los permisos que tiene el proceso sobre la página (leer, escribir, ejecutar).
Dado que las tablas de paginación pueden ocupar un espacio considerable de la memoria
principal, estas también podrían estar sujetas a paginación, lo que da lugar a una organización
paginada de múltiples niveles (o tabla de páginas multinivel). En los sistemas con un tamaño
de direcciones muy grande ( 64 bits ), podría usarse una tabla de páginas invertida, la cual
utiliza menos espacio, aunque puede aumentar el tiempo de búsqueda de la página.
Las tablas son mantenidas por el sistema operativo y utilizadas por la Unidad de Gestión de
Memoria (MMU) para realizar las traducciones. Para evitar un acceso a las tablas de
paginación, hay un dispositivo llamado Buffer de Traducción Adelantada (TLB, Translation
Lookaside Buffer), acelerando el proceso de traducción
Segmentación
La gestión de los segmentos la realiza el sistema operativo, como con las particiones
dinámicas, sólo que cada partición no corresponde a un proceso sino a un segmento. El
sistema operativo mantiene una tabla con los segmentos de un proceso, de forma similar a la
de páginas. Como el tamaño de los segmentos es variable, antes de realizar un acceso a
memoria se comprueba que el desplazamiento no supere al tamaño del segmento, para
proteger las zonas de memoria ocupadas por otro segmento.
La segmentación permite que ciertos procesos puedan compartir código, rutinas, o datos, sin
necesidad de estar duplicados en memoria principal. Así pues, si seis usuarios están utilizando
interactivamente un procesador de textos determinado, no es necesario que estuviesen
cargadas en memoria seis copias idénticas del código del programa procesador de textos. El
código del programa estaría una sola vez, y el sistema operativo se limitaría a anotar en las
tablas‐mapas de segmentos de cada uno de los procesos la dirección donde se encuentra el
código. Por tanto, en un momento dado, en memoria existirían: segmentos asignados a
trabajos concretos, segmentos compartidos, y bloques libres de memoria.
Memoria virtual
La memoria virtual permite a los usuarios hacer programas de una capacidad muy superior a
la que físicamente tiene la memoria principal del computador. En realidad, la memoria virtual
hace posible que la capacidad máxima de los programas esté limitada por el espacio que se le
258 PROGRAMACIÓN C++ Y COMUNICACIONES.
reserve en disco, y no por el tamaño de la memoria principal. En definitiva, los sistemas con
memoria virtual presentan al usuario una memoria principal aparentemente mayor que la
memoria física real.
Por otra parte, la memoria virtual permite aumentar el número de procesos en la memoria
principal en ejecución concurrente, ya que con ella sólo es necesario que esté en memoria
principal un trozo mínimo de cada proceso, y no el proceso completo. Para la gestión de la
memoria virtual suele utilizarse alguna de las anteriores técnicas de gestión de la memoria:
En un sistema de memoria virtual se mantiene en disco un archivo con la imagen del proceso
completo, que está troceado en páginas o segmentos, dependiendo del método. En cambio,
en la memoria principal únicamente debe estar la página, o segmento, que en ese momento
deba estar en ejecución, intercambiándose páginas entre disco y memoria principal cuando
sea necesario, lo cual se conoce por “swapping”.
La gestión de memoria virtual segmentada es más compleja que la del tipo de paginación, ya
que, los segmentos son de tamaño variable y son más difíciles de gestionar. Las páginas, por
el contrario, son de capacidad constante y preestablecida. Por ello, lo habitual es utilizar
gestión de memoria por paginación o por segmentos paginados.
La memoria virtual con paginación combina las técnicas de paginación e intercambio. Por lo
general, se utiliza un método de “intercambio perezoso” (“lazzy swapper”), únicamente se
lleva a memoria una página cuando es necesaria para algún proceso, de esta forma, el
número de procesos en ejecución concurrente puede aumentar.
Uno de los temas de mayor interés para el diseño del sistema de gestión de memoria virtual
es la búsqueda de algoritmos eficientes para decidir qué página debe sustituirse en caso de
fallo de página, ya que cada fallo de página supone una pérdida de tiempo de la CPU, que
puede hacer disminuir considerablemente la productividad del computador incluso aunque
toda la gestión se realice con hardware específico. El algoritmo descrito en el párrafo anterior,
que utilizan campo de números de referencias, es del tipo LRU. Los algoritmos más conocidos
son los siguientes:
LRU (“Least Recently Used”, Último Recientemente Usado). En este caso se sustituye
la página menos recientemente usada. Se basa en el principio de localidad temporal,
ya que, supone que la página utilizada hace mayor tiempo es menos probable que se
use próximamente.
NRU (“No Recently Used”, No Recientemente Usado). Una de las múltiples formas de
implementar este tipo de algoritmo consiste en utilizar un bit de referencia que se
pone a 1 cada vez que se usa la página. Un puntero recorre circularmente la tabla de
marcos de página. Inicialmente, el puntero está detenido en un marco de página,
cuando se tiene que desalojar una página analiza el bit de referencia del marco al
que apunta, si es 1, lo pone a 0 y avanza apuntando a los siguientes procesos,
cambiando los bits de referencia de 1 a 0, hasta que encuentre una página con el bit
de referencia a 0, en cuyo caso la elimina de la memoria y se detiene en la posición
siguiente de la tabla, hasta que se necesite desalojar una nueva página.
Se comprende mejor como actúa el software de E/S utilizando un modelo conceptual por
capas. El nivel inferior es el hardware, que es el que realmente ejecuta la operación de E/S, y
el nivel superior los procesos de los usuarios. Resumidamente, las funciones de cada nivel
son las que se indican en la figura 9.2.
Gestión de archivos.
Un archivo puede estructurarse en distintos soportes físicos, dependiendo del uso o
capacidad que vaya a tener. Se distingue entre un nivel físico, más o menos Complejo, y nivel
lógico, que proporciona una utilización adecuada para el usuario. El sistema operativo hace
posible utilizar esta última, haciendo de interfaz entre los dos. En efecto, desde el punto de
vista hardware, para almacenar datos o programas sólo existen direcciones físicas. En un
262 PROGRAMACIÓN C++ Y COMUNICACIONES.
Gestión de archivos
Dependiendo del sistema operativo se pueden hacer unas u otras operaciones con los
archivos. Cada archivo usualmente contiene nombre, atributos, y datos. Los atributos pueden
incluir cuestiones tales como fecha y hora de creación, fecha y hora de la última actualización,
bits de protección, contraseña de acceso, número de bytes por registro, capacidad máxima
del archivo y capacidad actualmente ocupada.
Lista de enlaces: Cada disco dispone de una tabla con tantos elementos como
bloques físicos, la posición de cada elemento se corresponde biunívocamente con
cada bloque, y contiene el puntero del lugar donde se encuentra el siguiente bloque
del archivo. Cuando se abre un archivo el sistema de archivos se carga en la memoria
principal la lista de enlaces, pudiéndose obtener rápidamente las direcciones,
usualmente 3 bytes, de los bloques consecutivos del archivo. Presenta el
Esto da lugar a una serie de estándares de gestión de archivos en los que cabe destacar FAT,
NTFS y EXT:
Sistema FAT32
Tabla de asignación de archivos, comúnmente conocido como FAT (del inglés file allocation
table), es un sistema de archivos desarrollado para MS‐DOS, así como el sistema de archivos
principal de las ediciones no empresariales de Microsoft Windows hasta Windows Me.
FAT es relativamente sencillo. A causa de ello, es un formato popular para disquetes admitido
prácticamente por todos los sistemas operativos existentes para computadora personal. Se
utiliza como mecanismo de intercambio de datos entre sistemas operativos distintos que
coexisten en la misma computadora, lo que se conoce como entorno multiarranque. También
se utiliza en tarjetas de memoria y dispositivos similares.
Las implementaciones más extendidas de FAT tienen algunas desventajas. Cuando se borran y
se escriben nuevos archivos tiende a dejar fragmentos dispersos de éstos por todo el soporte.
Con el tiempo, esto hace que el proceso de lectura o escritura sea cada vez más lento. La
denominada desfragmentación es la solución a esto, pero es un proceso largo que debe
repetirse regularmente para mantener el sistema de archivos en perfectas condiciones. FAT
tampoco fue diseñado para ser redundante ante fallos. Inicialmente solamente soportaba
264 PROGRAMACIÓN C++ Y COMUNICACIONES.
nombres cortos de archivo: ocho caracteres para el nombre más tres para la extensión.
También carece de permisos de seguridad: cualquier usuario puede acceder a cualquier
archivo.
FAT32 fue la respuesta para superar el límite de tamaño de FAT16 al mismo tiempo que se
mantenía la compatibilidad con MS‐DOS en modo real. Microsoft decidió implementar una
nueva generación de FAT utilizando direcciones de cluster de 32 bits (aunque sólo 28 de esos
bits se utilizaban realmente).
FAT32 apareció por primera vez en Windows 95 OSR2. Era necesario reformatear para usar
las ventajas de FAT32. Curiosamente, DriveSpace 3 (incluido con Windows 95 y 98) no lo
soportaba. Windows 98 incorporó una herramienta para convertir de FAT16 a FAT32 sin
pérdida de los datos. Este soporte no estuvo disponible en la línea empresarial hasta
Windows 2000.
El tamaño máximo de un archivo en FAT32 es 4 GiB (232−1 bytes), lo que resulta engorroso
para aplicaciones de captura y edición de video, ya que los archivos generados por éstas
superan fácilmente ese límite.
NTFS (del inglés New Technology File System) es un sistema de archivos de Windows NT
incluido en las versiones de Windows 2000, Windows XP, Windows Server 2003, Windows
Server 2008, Windows Vista, Windows 7 y Windows 8. Está basado en el sistema de archivos
HPFS de IBM/Microsoft usado en el sistema operativo OS/2, y también tiene ciertas
influencias del formato de archivos HFS diseñado por Apple.
NTFS permite definir el tamaño del clúster a partir de 512 bytes (tamaño mínimo de un
sector) de forma independiente al tamaño de la partición.
Su principal inconveniente es que necesita para sí mismo una buena cantidad de espacio en
disco duro, por lo que no es recomendable su uso en discos con menos de 400 MiB libres.
Actualmente se utiliza la cuarta versión de este sistema de ficheros utilizado por Linux. ext4
(fourth extended filesystem o «cuarto sistema de archivos extendido») es un sistema de
archivos transaccional (en inglés journaling), anunciado el 10 de octubre de 2006 por Andrew
Morton, como una mejora compatible de ext3. El 25 de diciembre de 2008 se publicó el
kernel Linux 2.6.28, que elimina ya la etiqueta de "experimental" de código de ext4. Utiliza un
árbol binario balanceado (árbol AVL) e incorpora el asignador de bloques de disco Orlov.
HFS Plus o HFS+ es un sistema de archivos desarrollado por Apple Inc. para reemplazar al HFS
(Sistema jerárquico de archivos). También es el formato usado por el iPod al ser formateado
desde un Mac.
Los volúmenes de HFS+ están divididos en sectores (bloques lógicos en HFS), de 512 Bytes.
Estos sectores están agrupados juntos en un bloque de asignación que contiene uno o más
sectores; el número de bloques de asignación depende del tamaño total del volumen. HFS+
usa un valor de dirección para los bloques de asignación mayor que HFS, 32 bit frente a 16 bit
de HFS; lo que significa que puede acceder a 232 bloques de asignación. Típicamente un
266 PROGRAMACIÓN C++ Y COMUNICACIONES.
volumen HFS+ esta embebido en un Envoltorio HFS (HFS Wrapper), aunque esto es menos
relevante. El envoltorio fue diseñado para dos propósitos; permitir a los ordenadores
Macintosh HFS+ sin soporte para HFS+, arrancar los volúmenes HFS+ y ayudar a los usuarios a
realizar la transición a HFS+. HFS+ arrancaba con un volumen de ficheros de solo lectura
llamado Where_have_all_my_files_gone?, que explicaba a los usuarios con versiones del Mac
OS sin HFS+, que el volumen requiere un sistema con soporte para HFS+. El volumen origina
HFS contiene una firma y un desplazamiento en los volúmenes HFS + embebidos en su
cabecera del volumen. Todos los bloques de asignación en el volumen HFS que contienen el
volumen embebido son mapeados fuera del archivo de asignación HFS como bloques dañados
Gestión de directorios
Un directorio se gestiona con una tabla de índices, que contiene un elemento por
cada archivo o directorio dependiente de él. Cada elemento está formado por el nombre del
archivo dado por el usuario, y por información adicional utilizada por el sistema operativo. La
información adicional sobre el archivo puede estar constituida por los atributos y el bloque
donde comienza el archivo, caso de MS‐DOS. También, la información adicional puede ser un
puntero a otra estructura con información sobre el archivo. Este es el caso de UNIX, en el que
el puntero sencillamente es la dirección del i‐nodo del archivo, que contiene tanto los
atributos del archivo como la tabla de posiciones.
El BIOS (“Basic Input Output System”, Sistema Básico de Entradas y Salidas) del DOS se
encuentra en un archivo, que en el PC‐DOS de IBM se llama IBMIO.SYS; y que en MS‐DOS se
llama IO.SYS. Este archivo se encuentra en todos los PC como primer archivo en el directorio
raíz del disco de arranque, y está equipado con los atributos de archivo HIDDEN y SYSTEM,
para que no aparezca en la pantalla con la llamada del comando DIR.
Cuando DOS quiere comunicarse con alguno de estos dispositivos utiliza los controladores de
dispositivos contenidos en ese módulo, que a su vez emplean las rutinas del ROM‐BIOS. Sólo
el BIOS del DOS y los controladores de dispositivos entran en contacto con el hardware, esto
hace que estos sean los elementos más dependientes del hardware. En las primeras versiones
esta parte del DOS debía ser adaptada por los diferentes fabricantes de hardware, debido a
que las diferencias entre los PC de diferentes fabricantes eran significativas.
El núcleo del DOS, también conocido por su denominación inglesa Kernel, se encuentra en el
archivo IBMDOS.SYS en el PC‐DOS, o en el archivo MSDOS.SYS para MS‐DOS. Se encuentra
inmediatamente después del archivo IMBIO.SYS, o IO.SYS, en el directorio raíz de la unidad de
arranque. Al igual que el anterior archivo no es visible por el usuario, ya que lleva los atributos
de archivo SYSTEM y HIDDEN; además, al igual que su predecesor, no se puede borrar porque
además se marcó con el atributo READ‐ONLY.
En este archivo se encuentra las innumerables funciones del DOS‐API, que se pueden alcanzar
a través de la interrupción 21h. Todas las rutinas están diseñadas de forma independiente del
hardware, y se utilizan para el acceso de otros dispositivos del BIOS del DOS. Por esta razón,
este módulo no se ha de adaptar al hardware de los diferentes ordenadores.
El procesador de comandos
Al contrario que en el caso de los dos módulos presentados hasta ahora, el procesador de
comandos del DOS, denominado SHELL, se encuentra en un archivo visible con el nombre
COMMAND.COM. Este es el programa que se llama automáticamente durante el arranque del
ordenador. Muestra el “prompt” (símbolo de la línea de comandos) del DOS en la pantalla (A>
o C>). Su función principal es procesar las sucesivas entradas del usuario.
270 PROGRAMACIÓN C++ Y COMUNICACIONES.
El procesador de comandos es el único componente visible del DOS, de modo que en algunos
casos es erróneamente considerado como el sistema operativo en sí. En realidad, sólo un
programa normal, que funciona bajo el control del DOS. El procesador de comandos no es sin
embargo un bloque monolítico, sino que se encuentra dividido en tres módulos, una parte
residente, una parte no residente, y la rutina de inicialización.
La parte no residente contiene el código del programa para la salida del indicador del sistema,
para leer las entradas de usuario del teclado y para su ejecución. El nombre de este módulo
se debe a que puede ser sobrescrito por los programas de usuario, ya que, se encuentra en la
parte superior de la memoria, por encima de estos programas. Pero esto no importa, ya que
tras la ejecución de un programa se vuelve a pasar a la parte residente. Este comprueba si la
parte no‐residente fue sobrescrita, y la vuelve a cargar de la unidad de arranque, si fuera
necesario.
La parte de inicialización se carga durante el arranque del computador, y acoge las tareas de
inicio el sistema DOS. Cuando finaliza su trabajo, ya no es necesaria, y se puede sobreescribir
por otro programa.
Cuando aparecieron las primeras versiones de Windows hacia el año 1986, tanto desde el
punto de vista del usuario, como del programador aparecieron unas prestaciones muy
limitadas. En esencia, se trataba de una capa que se establecía encima del DOS y cuyas únicas
ventajas eran su sistema gráfico; la independencia de los dispositivos, es decir, del hardware;
y una multitarea cooperativa entre las escasas aplicaciones que existían. En lo referente a la
presentación, se puede decir que aquella primera versión era muy simple.
En pocos años Microsoft lanzó una nueva saga de revisiones bajo la versión 2.0, con un lavado
de cara y una mayor eficacia en el código, pero es en la versión 3.0 donde el impacto de
Windows rompe todas las previsiones. Por primera vez, Windows aprovechaba por completo
de la arquitectura 80386 para construir un sistema de memoria virtual de forma que ésta
podía extenderse hasta cuatro veces, siempre y cuando existiese suficiente espacio en el disco
duro. Las ventas iniciales de Windows 3.0 fueron espectaculares.
Una ventana completa está constituida por un trozo de pantalla, habitualmente con un título,
en el que aparece en la parte superior izquierda un menú general con forma de botón, que se
despliega al pulsarlo. En la derecha existen otros botones que permiten actuar sobre el
tamaño de la ventana. Estos últimos tienen la posibilidad de maximizar, la ventana ocupará
toda la pantalla; o minimizar la ventana, pasando a ocupar la mínima extensión,
convirtiéndose en un icono. Adicionalmente, la ventana puede ser redimensionada pinchando
el borde de la misma con el ratón, de forma que al arrastrarlo se modifica su tamaño. Para
moverla, se pincha sobre la barra del título y se arrastra la ventana a la posición requerida.
Según lo expuesto, el tamaño de las ventanas es muy variable y la información que contienen
puede no ser presentable toda a un tiempo. Aquí entra en juego la figura de las barras de
desplazamiento, encargadas de mover el contenido de la ventana sobre un espacio de trabajo
que se considera más amplio.
En sus primeras versiones Windows admiíae tres modos de ejecución: real, sólo versión 3.0;
estándar; y mejorada. En los dos últimos se rompe la barrera de los 640k que tenía
antiguamente el DOS, aprovechando toda la memoria que consiga almacenar nuestro
ordenador. La diferencia entre el modo estándar y el mejorado se traduce en un diferente
aprovechamiento de la memoria. El modo mejorado requiere disponer de al menos un
microprocesador 80386 y 2Mbytes de memoria RAM. Las ventajas de este modo se apoyan
más que nada en la existencia de un modo virtual de memoria que permite cuadruplicar la
cantidad de memoria visible para las aplicaciones, gracias a la utilización del espacio existente
en el disco duro. Más adelante, cuando se hable sobre el Panel de Control se verá el lugar
preciso donde configurar esta posibilidad.
Con la versión 3.0 de Windows aparecieron una serie de aplicaciones que cambiaron
significativamente la actuación sobre un computador, estos elementos que en la actualidad
han sido mejorados permitieron una interfaz más amigable con el ordenador. Los elementos
que se describen a continuación permiten cierto control sobre la configuración del sistema
operativo, y en su momento representaron un gran avance para la gestión del los recursos del
computador. Los principales componentes eran:
El panel de control es un programa que incluye una serie de componentes que sirven
para ajustar la configuración del sistema. El panel de control es una aplicación
Windows que permite modificar de forma visual las características del sistema
durante la utilización de Windows. Cada una de las opciones que pueden modificarse
aparece representada con el icono correspondiente en la ventada del Panel de
control. Las opciones existentes son las siguientes: los colores de la pantalla, otras
opciones del Escritorio que determinan el aspecto de la pantalla, las fuentes que
reconocerán las aplicaciones de Windows, las impresoras utilizadas, los parámetros
del teclado y del dispositivo apuntador (ratón), las opciones internacionales, puertos
y redes, la fecha y hora del sistema, los sonidos que utilizará el sistema, los
parámetros MIDI que utilizará el sintetizador conectado a la computadora, y
finalmente las opciones de multitarea para la ejecución de Windows en el modo
extendido del 80386.
El protocolo DDE está orientado al intercambio dinámico de datos que permite que
las aplicaciones de Windows puedan intercambiar información y actuar en base a ella de una
forma totalmente cooperativa. Bajo DOS, cada programa se comporta como una isla en
medio de un océano, obligando al programador a hacer ingeniosos esfuerzos cuando dos
aplicaciones necesitan compartir datos. Aunque su diseño esté orientado a la ejecución de un
único programa, es posible obtener multitarea dejando programas residentes en memoria. El
problema de la comunicación no parece tal cuando una aplicación genera un archivo de salida
que otra puede leer, aunque no se trate de un verdadero intercambio de información, dado
274 PROGRAMACIÓN C++ Y COMUNICACIONES.
que las aplicaciones difícilmente pueden mantener el control sobre en qué momento se debe
enviar o recibir la información. Para que dos aplicaciones puedan acceder a áreas de memoria
comunes normalmente se utilizan interrupciones que son instaladas a modo de interfaces. Las
aplicaciones que intervienen tienen que estar de acuerdo en la forma de acceder a las
interrupciones y en el formato de los datos sin que el DOS provea de ningún mecanismo
práctico para llevar a cabo la tarea.
El protocolo OLE está basado en mensajes que establece una conversación entre cliente y
servidor, informando acerca de los objetos: aplicación origen, formatos, cambios en los
mismos, solicitud de actualizaciones, etc. OLE implica ciertos cambios en la mayoría de los
programas, con el fin de que lo puedan gestionar y dejen de considerar como exclusiva la
información que manejas. A nivel de usuario OLE utiliza comandos a los que ya se está
acostumbrado, tales como copiar, pegar, cortar, etc., que aparecen normalmente en el menú
de edición.
En primer lugar, hay que señalar que Windows 95 es por sí mismo un sistema
operativo, a diferencia de Windows 3.1 que necesita trabajar sobre el MS‐DOS. Esto quiere
decir que el usuario cuando arranca el ordenador no tiene primero que cargar el DOS y, luego,
ejecutar la entonces famosa orden WIN para cargar Windows. Ahora cuando se enciende el
ordenador se arranca directamente en Windows 95. Por otra parte, el usuario ya no tiene que
instalar primero MS‐DOS y luego Windows, sino sólo instalar Windows 95 directamente.
Una de las características más atractivas de Windows 95 es que se trata del primer
sistema operativo Plug and Play. El estándar o conjunto de normas Plug and Play pretende
crear una estructura que permita gestionar de forma inteligente la instalación y configuración
de nuevos dispositivos, sin requerir la intervención del usuario. Con un sistema Plug and Play
el usuario puede añadir y quitar dispositivos o conectarse y desconectarse a una red o
estación maestra sin necesidad de reinicializar el sistema y definir varios parámetros. El
sistema Plug and Play detecta automáticamente la existencia de un nuevo dispositivo,
determina la configuración óptima y permite que las aplicaciones se autoajusten
automáticamente teniendo en cuenta los cambios ocurridos. Por ejemplo, una aplicación que
muestra difuminadas, o inactivas, las opciones para CD‐ROM y que reconoce inmediatamente
la presencia de una unidad CD‐ROM, activando la posibilidad de seleccionar las opciones para
CD‐ROM. De esta forma, los usuarios ya no necesitan conmutar puentes, activar
interruptores, seleccionar IRQs libres; ni siquiera tiene que cambiar los ficheros de
configuración del sistema operativo par añadir los nuevos controladores.
En primer lugar, los dispositivos Plug and Play tienen que ser capaces de identificarse
a sí mismos, es decir, informar al ordenador de qué tipo de dispositivo se trata y de los
requisitos que necesita. Por ejemplo, una tarjeta de sonido Plug and Play debe indicar al
ordenador que se trata de una tarjeta de sonido y que necesita seleccionar una IRQ, un canal
DMA y una dirección de memoria base. Además, debe permitir que el ordenador configure de
forma automática dichos requisitos, lo cual implica que los dispositivos Plug and Play tienen
que poder modificarse a sí mismos. Por ejemplo, en el caso de una impresora que exige la
presencia de un puerto paralelo bidireccional que permita enviar datos desde la impresora al
ordenador, y no sólo desde el ordenador a al impresora.
En segundo lugar, tiene que haber una BIOS Plug and Play que esté preparada para
pedir y aceptar las características y los requisitos de los nuevos dispositivos. Esta BIOS
detectará la presencia de un nuevo dispositivo y, en vez de generar varios pitidos seguidos de
un escueto mensaje, pedirá los valores por defecto para los requisitos del dispositivo y
seleccionará automáticamente los adecuados.
Por último, es necesario un sistema operativo Plug and Play, tal como Windows 95,
que gestione todos los componentes cargando los controladores de dispositivo adecuados y
reconfigurando el sistema automáticamente sin intervención del usuario.
El estándar Plug and Play no sólo pretende alcanzar una configuración automática de
los dispositivos hardware, sino ser capaz de reconocer cambios dinámicos de configuración.
Esta característica es fundamental en la informática móvil, puesto que los usuarios de
portátiles necesitan poder conectarse a redes locales sin tener que apagar el ordenador o
reconfigurarlo. Un sistema operativo Plug and Play, reconoce inmediatamente los nuevos
dispositivos instalados y los recursos que necesitan, cargando de forma automática los
controladores de dispositivos adecuados. Las aplicaciones son informadas de los cambios
dinámicos para que puedan aprovechar las nuevas características, o bien impidan el acceso a
dispositivos no existentes. Supuestamente ya no es preciso apagar y reinicializar el ordenador
cuando realicen cambios en la configuración hardware, y ni siquiera han de intervenir en el
proceso de configuración.
Sin duda alguna, la característica más importante del Menú de Contexto es una
opción denominada Propiedades que permite configurar adecuadamente el objeto.
Finalmente, otro aspecto importante referente a los objetos es la posibilidad de arrastrar un
icono sobre otro, acción que se interpreta según el tipo de objeto que se está arrastrando y el
objeto de destino.
En la parte inferior de la pantalla existe una Barra de Tareas que cumple un papel
fundamental en la nueva interfaz además de sustituir a la Lista de Tareas de Windows 3.1.
Cada vez que se ejecuta una aplicación, o se minimiza, aparece un icono en la Barra de Tareas
que representa dicha aplicación. Puesto que la Barra de Tareas siempre está activa, pulsando
el icono se accede inmediatamente a la aplicación. Esto resuelve uno de los principales
problemas que tienen los usuarios de Windows 3.1: saber exactamente el número de
ventanas que tiene abiertas con aplicaciones ejecutándose y saber cómo acceder a una
ventana que no aparece en pantalla.
La nueva interfaz
El escritorio de Windows, puede utilizar como fondo una página Web, permite instalar los
componentes activos, pequeños trozos de código HTML que muestran información
cambiante, como la cotización de bolsa o las últimas noticias de un determinado tema, y
soporta protectores de pantalla de canales. Todas estas características son resultado de la
vista Web, pues el escritorio no es más que una carpeta del disco duro y, por tanto, posee la
capacidad de entender y mostrar código HTML. Las barras de herramientas incluyen ahora
nuevas opciones. Ahora puede incluir en la barra de tareas iconos que ejecutan programas
automáticamente de forma rápida y cómoda. Además hay varios iconos predefinidos entre
los que destaca uno que minimiza todas las ventanas abiertas, dejando el escritorio limpio.
Pero sin duda la novedad más interesante es la capacidad de trabajar con dos monitores. El
funcionamiento es muy sencillo: hay que instalar en el ordenador dos tarjetas de vídeo, que
pueden ser 2 PCI O 1 PCI y AGP, ya que no se pueden usar tarjetas de vídeo ISA, y conectar
los monitores correspondientes. A partir de ese momento Windows funciona con una
pantalla virtual que es la suma de los dos monitores. Por ejemplo, si tiene en un monitor 800
x 600 y en otro 640 x 480, se crea una pantalla virtual de 800 x 1080 donde se puede colocar
libremente los objetos. Es decir, puede arrastrar un icono desde una pantalla a otra, pues
280 PROGRAMACIÓN C++ Y COMUNICACIONES.
para Windows se trata de una sola pantalla virtual. También puede hacer que un programa se
ejecute en uno de los monitores, usando el otro para otras tareas.
Ventajas de la FAT32
Además del convertidor de FAT32, Windows 98 incluye varias herramientas de sistema como
el Liberador de espacio en disco duro, un programa de copias de seguridad, un programa de
información del sistema.
Windows 98 está preparado para las últimas tecnologías hardware y especificaciones que
estarán presentes de forma masiva en los próximos años. Se soportan las nuevas tarjetas de
vídeo AGP (“Accelerated Graphics Port”, Puerto para Aceleración de Gráficos), que usan un
chip de vídeo que aumenta drásticamente la velocidad de procesamiento gráfico al evitar el
bus PCI, y utilizar una línea directa entre el subsistema gráfico y la memoria del ordenador.
También se reconocen los nuevos módulos de cámaras digitales y los lectores DVD. Windows
98 está preparado para los ordenadores con conectores de tipo USB (“Universal Serial Bus”,
Bus Serie Universal), que permite conectar hasta 127 dispositivos serie a un único conector
del PC, como escáner, cámaras digitales, ratones, módems, impresoras, etc. Esta es una forma
cómoda de eliminar la necesidad de cables y puertos serie adicionales. En este sentido,
Windows 98 soporta el estándar IEEE 1394, cuya función es similar al de USB. En el campo de
la administración de energía, Windows 98 sigue el estándar ACPI (“Advanced Configuration
and Power Interface”, Configuración Avanzada e Interfaz de Alimentación), que permite al
ordenador activar o desactivar automáticamente periféricos como discos duros, impresoras o
tarjetas de red; por ejemplo, desactiva el consumo, y con ello el ruido del disco duro, cuando
se producen tiempos de inactividad, activándose automáticamente al usar el teclado o el
ratón. Señalar también que Windows 98 soporta IrDA 3.0 (“Infrared Data Association”,
Asociación de Datos por Infrarrojos), la última versión del estándar para comunicación
inalámbrica a través de infrarrojos.
Finalmente Windows Scripting Host permite ejecutar archivos VisualBasic Script o Java Script,
tanto desde el entorno de Windows como desde la línea de comandos del MS‐DOS. De esta
forma, se pueden crear en estos lenguajes verdaderos programas Windows que se ejecutan
de forma sencilla y cómoda. A grandes rasgos, es una forma de retomar los habituales
archivos por lotes BAT, pero ahora con funciones de Windows y con múltiples posibilidades. El
motor de Windows Scripting Host soporta los lenguajes VBScript y Jscript, pero compañías
independientes pueden crear controles ActiveX para otros lenguajes como Perl, TCL o REXX.
Comunicaciones
El apartado de las comunicaciones es uno de los más importantes en todo sistema operativo
actual. La inclusión de Explorer 4 en Windows 98 supone dotarle de un valor añadido
importante, pues añade el navegador Explorer, un programa de correo, y grupos de noticias
Outlook Express, un editor de páginas Web llamado Front‐Page Express, y una herramienta de
telefonía y vídeo conferencia denomianda NetMeeting.
Más novedades en comunicaciones son la nueva versión de Acceso telefónico a redes 1.2, que
incluye soporte para multienlace; incluyéndose el protocolo PPTP para crear las denominadas
VPN (“Virtual Private Networks”, Redes Privadas Virtuales), que son conexiones seguras a una
red desde Internet, actuando de la misma forma que si estuviera conectado a la red. Una de
las novedades más importantes de Windows 98, conocida como Web TV, se trata de la
posibilidad de recoger páginas Web a través de las señales de TV, algo similar al teletexto.
282 PROGRAMACIÓN C++ Y COMUNICACIONES.
Este nuevo sistema operativo incorpora avances en los dispositivos multimedia, acordes con
las prestaciones de los computadores en que se instalan; es decir, Pentium II o III, con
elevadas capacidades de almacenamiento en disco y en memoria RAM. La finalidad es ofrecer
un entorno amigable al usuario que le permita realizar fácilmente tareas de edición de vídeo,
reproducción y/o creación de sonido, acceder a los diferentes servicios de Internet, etc.
Ejemplo de estas nuevas prestaciones son las aplicaciones que Microsoft, y otros fabricantes,
suministran para Windows Millenium; como por ejemplo: Movie Maker, Windows Media
Player,..., para las aplicaciones de multimedia; y Microsoft Explorer 5.5, MSN Messenger y
NetMeeting para acceso a los diversos servicios que ofrece Internet.
Las herramientas de administración y gestión del sistema operativo, a las que tiene acceso el
usuario, son similares a las de Windows 98, aunque en algunos casos cambian de aspecto.
Pocas utilidades han sido añadidas a este sistema operativo, entre ellas destaca la de
restauración del sistema, que pretende recuperar al ordenador en caso de quedar bloqueado;
el desarrollo de varios asistentes para facilitar la gestión y administración de aplicaciones; y el
control de instalación de aplicaciones, que tendría la finalidad de evitar sobreescrituras no
deseadas en el proceso de instalación de nuevos programas.
Todo esto permitió que las aplicaciones fueran más complejas e interrelacionadas, de forma
que un usuario puede estar utilizando varias aplicaciones simultáneamente. Microsoft
desarrolló Windows NT con una apariencia cara la usuario similar a Windows 3.1, pero basado
en un concepto radicalmente distinto. Windows NT explota la potencia de sus
microprocesadores contemporáneos de 32 bits y suministra una capacidad completa de
multitarea en un entorno monousuario. Además, ofrece una gran potencia al poder ejecutar
aplicaciones escritas para otros sistemas operativos y cambiar de plataforma hardware sin
modificar el sistema operativo ni las aplicaciones.
Windows NT tiene una estructura modular, lo que le da una gran flexibilidad. Además, se
puede ejecutar en una gran variedad de plataformas y soporta aplicaciones escritas para
otros sistemas operativos. Al igual que en otros sistemas operativos, en Windows NT se
distingue entre el software orientado a las aplicaciones, que se ejecuta en modo usuario; y el
software del sistema operativo, que se ejecuta en modo privilegiado como administrador.
Este ultimo tiene acceso a los datos del sistema y al hardware, mientras que el resto de
usuarios tiene accesos limitados.
En la figura 9.4 se muestra la estructura de Windows NT. Para conseguir que NT tenga la
misma visión del hardware, independientemente del sistema en el que se esté ejecutando, el
sistema operativo se divide en cuatro capas:
4. Servicios del sistema. Esta es la última capa y proporciona una interfaz para el
software que se ejecuta en modo usuario.
Por encima de estas capas están los subsistemas protegidos, que se encargan de la
comunicación con el usuario final. Un
subsistema protegido proporciona una
interfaz de usuario gráfica, la línea de
órdenes del usuario. Otra función que
tienen asignados es suministrar la
interfaz de programación de
aplicación (API, “Application
Programming Interface”). Esto
significa que aplicaciones creadas para
otros sistemas operativos pueden
ejecutarse sobre NT sin ninguna
modificación, como es el caso de
OS/2, MS‐DOS, o Windows.
mediante otro mensaje. Así pues, el servidor es un programa que se limita a realizar aquellas
funciones que le son solicitadas desde el exterior.
En NT no todas las entidades son objetos. De hecho, los objetos se emplean en los casos
donde los datos están abiertos para acceso en modo usuario o cuando el acceso a los datos es
compartido o restringido. Entre las entidades representadas por objetos están los procesos,
las hebras, los archivos, los semáforos, los relojes y las ventanas. NT maneja todos los tipos de
objetos de una forma similar, mediante el gestor de objetos, cuya responsabilidad es crear y
eliminar los objetos según la petición de las aplicaciones y otorgar acceso a servicios de
objetos y datos. Un objeto, ya sea un proceso o una hebra, puede referenciar a otro objeto
abriendo un gestor del mismo.
286 PROGRAMACIÓN C++ Y COMUNICACIONES.
En NT los objetos pueden estar etiquetados, o no. Cuando un proceso crea un objeto no
etiquetado, el gestor de objetos devuelve un gestor al objeto, y la única forma de
referenciarlo es mediante su gestor. Los objetos etiquetados tienen un nombre que puede
usarse por otro proceso para obtener el gestor del objeto. Los objetos etiquetados tienen
asociada la información de seguridad en la forma de un testigo de acceso. Esta información se
puede utilizar para limitar el acceso al objeto. Por ejemplo, un proceso puede crear un objeto
semáforo etiquetado de forma que sólo los procesos que los conozcan lo puedan utilizar. El
testigo de acceso asociado con el semáforo tendrá una lista de todos los procesos que pueden
acceder a él.
Gestión de procesos
Tanto los objetos de los procesos como los da las hebras llevan incorporadas las
capacidades de sincronización.
la familia de objetos de sincronización, que son entre otros: procesos, hebras, archivos,
sucesos semáforos y relojes. Los tres primeros objetos tienen otros usos, pero también se
pueden utilizar para sincronización, el resto de los tipos de objetos se diseñan
específicamente para soportar la sincronización.
Gestión de la memoria
Existen dos formas de protección asociadas con cada segmento: niveles de privilegio, y de
atributos de acceso. Son cuatro los niveles de privilegio, del 0 al 3, cuando se trata de un
segmento de datos corresponde a la clasificación del mismo y si es un segmento de programa
es su acreditación. Un programa sólo puede acceder a segmentos de datos para los que el
nivel de acreditación es menor o igual que el de clasificación. El uso de estos niveles de
privilegio depende del sistema operativo. Generalmente los niveles 0 y 1 corresponden al
sistema operativo, el nivel 2 a algunos subsistemas y el nivel 3 a las aplicaciones. Los atributos
288 PROGRAMACIÓN C++ Y COMUNICACIONES.
La dirección virtual se tiene que transformar en una dirección física mediante un mecanismo
análogo al explicado anteriormente. La segmentación es una característica opcional que se
puede desactivar. Cuando no se usa segmentación los programas emplean direcciones
lineales, que corresponden a las direcciones en el espacio de memoria del proceso que
utilizan 46 bits, la cual hay que convertir en una dirección real de 32 bits. El mecanismo de
paginación utilizado para hacer esto consiste en una operación de búsqueda en una tabla de
dos niveles. El primer nivel es un directorio de páginas de 1024 entradas, con lo que se divide
el espacio de memoria de 4 Gb en grupos de 1024 páginas, de 4Mb de longitud cada una con
su propia tabla de páginas. Cada tabla de páginas contiene 1024 entradas y cada entrada
corresponde a una página de 4Kb. El gestor de memoria puede usar un directorio de páginas
para todos los procesos, un directorio de páginas para cada proceso o una combinación de
ellos. El directorio de páginas de la tarea actual está en memoria principal, pero las tablas de
páginas pueden estar en memoria virtual.
Windows 2000 es un sistema basado en la tecnología NT, es decir, que siguiendo con
una numeración coherente, Windows 2000 en realidad debería haber sido Windows NT 5.0.
Desde el punto de vista de la gestión de los equipos, las características de Windows 2000
hacen que una red basada en éste sistema sea más fácil de administrar, facilitando la
instalación de equipos clientes y mejorando el soporte de perfiles flotantes y acceso remoto.
Respecto a Windows 98 ha heredado la facilidad de uso y el soporte de dispositivos plug and
play; pero intentando mantener la robustez y fiabilidad de Windows NT.
Otro de los grandes avances que se han producido en Windows 2000 se encuentra
en la seguridad a todos los niveles. No sólo se ha mejorado la seguridad de cara al acceso al
sistema o protección de archivos, sino que también los controladores son más seguros que en
Windows 9x/NT. Esto se debe a que Microsoft certifica los controladores de los fabricantes
que lo soliciten. Aunque, se pueden instalar controladores sin certificar, si se opta por los
certificados, se tiene una mayor certeza de que todo va a funcionar correctamente, evitando
en gran manera el bloqueo de ordenador debido a los controladores.
Esta fusión de las dos familias de sistemas operativos de Microsoft es una vieja aspiración de
este fabricante, que pretende fusionarlos en un solo. Esta no es una tarea fácil y de hecho uno
de los principales requisitos de diseño es hacia qué tipo de usuario está destinado el sistema
operativo. En el caso de Windows XP, el usuario denominado doméstico será el destinatario.
Este tipo de perfil limita significativamente el sistema operativo, ya que se supone que la
persona que va a trabajar y administrar la máquina, no debe tener problemas al instalar
aplicaciones y que todo debería instalarse casi automáticamente, lo cual impide plantearse
cuestiones tan elementales como implantar una buena gestión de usuarios, el control de
procesos, etc.
Algunas de las principales novedades aparecidas con Windows XP han sido la activación de la
licencia del sistema operativo, y la certificación del hardware. La activación de licencia es un
intento de lucha contra el pirateo del software, consiste en registrar cada una de las
instalaciones que realiza el usuario, de forma que combinando los números series del
hardware del computador y de la licencia del sistema operativo, entonces Microsoft devuelve
una clave que permite la utilización correcta del ordenador, y en caso contrario se limitaría
drásticamente su uso. Evidentemente, surgen numerosas cuestiones que resolver, como son
la actualización del hardware del equipo, el uso de equipos diferentes, y la aparición de cracks
que se invalidan los mecanismos de activación. El segundo punto citado de certificación del
hardware es un intento de estandarización de todos los dispositivos que puedan ser
El Windows Vista es el primer sistema operativo de Microsoft concebido para garantizar una
compatibilidad total con EFI (Extensible Firmware Interface), la tecnología llamada a
reemplazar a las arcaicas BIOS que desde hace más de dos décadas han formado parte
indisoluble de los ordenadores personales, por lo tanto no empleará MBR (Master Boot
Record), sino GPT (GUID Partition Table).
Introduce las ventanas dibujadas con gráficos vectoriales usando XAML y DirectX. Para ello,
se utilizaría una nueva API llamada Windows Presentation Foundation, cuyo nombre en
código es Avalon, que requiere una tarjeta gráfica con aceleración 3D compatible con DirectX.
Agrega una interfaz de línea de comando denominada Windows PowerShell, que finalmente
se ofreció como una descarga independiente para Windows Vista y Windows XP SP2.
Se anunció una nueva extensión de base de datos al sistema de archivos llamada WinFS. El
desarrollo de dicho sistema de ficheros ha sido abandonado por Microsoft, por lo tanto no
será incluido en Windows Vista, por el momento, siendo compensado por un sistema de
búsqueda basado en la indexación.
Integra directamente en el sistema un lector de noticias RSS (Really Simple Syndication, por
sus siglas en inglés).
Añade al firewall de sistema la capacidad de bloquear conexiones que salen del sistema sin
previa autorización.
Se incluye Windows ReadyBoost, la cual es una tecnología de caché de disco incluida por
primera vez en el sistema operativo Windows Vista. Su objetivo es hacer más veloces a
aquellos computadores que se ejecutan con el mencionado sistema operativo mediante
pendrives, tarjetas SD, compactFlash o similares.
Incluye un Sync Center para sincronización de Windows Vista con Pocket PC sin necesidad de
instalar el Active Sync.
Carga aplicaciones un 15% más rápido que Windows XP gracias a la característica SuperFetch.
Se reduce en un 50% la cantidad de veces que es necesario reiniciar el sistema después de las
actualizaciones.
La barra de tareas fue rediseñada, es más ancha, y los botones de las ventanas ya no traen
texto, sino únicamente el icono de la aplicación. Estos cambios se hacen para mejorar el
desempeño en sistemas de pantalla táctil. Estos iconos se han integrado con la barra «Inicio
rápido» usada en versiones anteriores de Windows, y las ventanas abiertas se muestran
agrupadas en un único icono de aplicación con un borde, que indica que están abiertas. Los
accesos directos sin abrir no tienen un borde. También se colocó un botón para mostrar el
escritorio en el extremo derecho de la barra de tareas, que permite ver el escritorio al posar
el puntero del ratón por encima.
Se añadieron las «Bibliotecas», que son carpetas virtuales que agregan el contenido de varias
carpetas y las muestran en una sola vista. Por ejemplo, las carpetas agregadas en la biblioteca
«Vídeos» son: «Mis vídeos» y «Vídeos públicos», aunque se pueden agregar más,
manualmente. Sirven para clasificar los diferentes tipos de archivos (documentos, música,
vídeos, imágenes).
Una característica llamada «Jump lists» guarda una lista de los archivos abiertos
recientemente. Haciendo clic derecho a cualquier aplicación de la barra de tareas aparece una
jump list, donde se pueden hacer tareas sencillas según la aplicación. Por ejemplo, abrir
documentos recientes de Office, abrir pestañas recientes de Internet Explorer, escoger listas
de reproducción en el reproductor, cambiar el estado en Windows Live Messenger,anclar
sitos o documentos, etcétera
Existen seis ediciones de Windows 7, construidas una sobre otra de manera incremental,
aunque solamente se centrarán en comercializar dos de ellas para el común de los usuarios:
las ediciones Home Premium y Professional. A estas dos, se suman las versiones Starter,
Home Basic, y Ultimate, además de la versión Enterprise, que está destinada a grupos
empresariales que cuenten con licenciamiento Open o Select de Microsoft.
Windows 8 ha recibido duras críticas desde su lanzamiento, lo que ha motivado ventas por
debajo de las expectativas para la empresa desarrolladora. Incluso el propio Paul Allen, co
fundador de Microsoft, ha dicho que este sistema operativo es «extraño y confuso» en un
primer contacto, pero se ha mostrado confiado en que los usuarios aprenderán a «querer» la
nueva versión.
The Verge pensó que el énfasis de Windows 8 en la tecnología fue un aspecto importante de
la plataforma, y que los dispositivos de Windows 8 (especialmente aquellos que combinan los
rasgos de las computadoras portátiles y las tabletas) «convertiría inmediatamente al iPad en
algo pasado de moda» debido a las capacidades del modelo híbrido del sistema operativo y el
creciente interés en el servicio de la nube. Algunas de las aplicaciones incluidas en Windows 8
fueron consideradas básicas y con carencia de una funcionalidad precisa, pero las aplicaciones
de Xbox fueron elogiadas por su promoción de una experiencia de entretenimiento en multi‐
plataforma. Otras mejoras y características (como el historial de archivos, los espacios de
almacenamiento y las actualizaciones para el administrador de tareas) fueron considerados
como cambios positivos.36 Peter Bright de Ars Technica sintió que mientras sus cambios de
interfaz de usuario quizás los eclipse, la mejoría, el administrador de archivos actualizados, la
funcionalidad de un nuevo almacenamiento, las características expandidas de seguridad y la
La interfaz de Windows 8 ha sido objeto de reacciones mixtas. Bright indicó que el sistema de
Edge UI del puntero y desplazamiento «no fueron muy obvios» debido a la carencia de
instrucciones proporcionadas por el sistema operativo en las funciones accedidas a través del
interfaz del usuario, incluso por el manual de vídeo añadido en el lanzamiento del RTM (que
solamente instruye a los usuarios a apuntar las esquinas de la pantalla y el toque de sus
lados). A pesar de este «obstáculo» autodescrito, Bright aclara que la interfaz de Windows 8
trabajó muy bien en algunos lugares, pero empezó a ser incoherente cuando se cambia entre
los ambientes «Metro» y de escritorio, algunas veces a través de medios inconsistentes.37
Tom Warren de The Verge aclaró que la nueva interfaz fue «asombrosa como sorprendente»,
contribuyendo a una experiencia «increíblemente personal» una vez que es personalizado por
el usuario. Al mismo tiempo, Warren vio que la interfaz tiene una empinada curva de
aprendizaje, y fue difícil de usar con un teclado y un ratón. Sin embargo, se señaló que, si bien
obliga a los usuarios a utilizar la nueva interfaz con una utilidad más táctil, fue un movimiento
arriesgado para Microsoft en su conjunto, que era necesario con el fin de impulsar el
desarrollo de aplicaciones para el almacén de Windows.
Windows Embedded Standard, que es un sistema basado en Windows NT; Windows CE está
desarrollado independientemente.
Originalmente apareció bajo el nombre de Pocket PC, como una ramificación de desarrollo de
Windows CE para equipos móviles con capacidades limitadas. En la actualidad, la mayoría de
los teléfonos con Windows Mobile vienen con un estilete digital, que se utiliza para introducir
comandos pulsando en la pantalla.
Si bien muchos pensamos que Windows Mobile habia sido descontinuado temporalmente en
favor del nuevo sistema operativo Windows Phone, la amplia gama de teléfonos industriales
ha hecho a Microsoft optar por una tercera linea de sistemas operativos para móviles que ha
llamado Windows Embedded Handheld 6.5, que vendría a ser la nueva linea de sistemas
operativos basados en Windows Mobile 6.5
Windows Phone es un sistema operativo móvil desarrollado por Microsoft, como sucesor de
la plataforma Windows Mobile.2 A diferencia de su predecesor, está enfocado en el mercado
de consumo generalista en lugar del mercado empresarial3 por lo que carece de muchas
funcionalidades que proporcionaba la versión anterior. Microsoft ha decidido no hacer
compatible Windows Phone con Windows Mobile por lo que las aplicaciones existentes no
funcionan en Windows Phone haciendo necesario desarrollar nuevas aplicaciones. Con
Windows Phone, Microsoft ofrece una nueva interfaz de usuario que integra varios servicios
en el sistema operativo. Microsoft planeaba un estricto control del hardware que
implementaría el sistema operativo, para evitar la fragmentación con la evolución del
sistema, pero han reducido los requisitos de hardware de tal forma que puede que eso no sea
posible.4
Historia
Unix tiene una larga e interesante historia. El primer desarrollo de Unix se hizo en los
laboratorios Bell en el lenguaje ensamblador de la máquina. Pronto se vio que no era práctico
tener que reescribir el sistema completamente para cada máquina, por lo que decidieron
hacerlo en C, un lenguaje de alto nivel.
Unix 4.3BSD. el resultado fue algo parecido al antepasado común de los dos, la versión 7 de
Unix.
Descripción.
El hardware está rodeado por el núcleo de Unix, que es el auténtico SO, denominado así para
enfatizar su aislamiento de las aplicaciones y de los usuarios. Sigue a continuación la capa de
librerías, que contiene un procedimiento para cada llamada al sistema. En el estándar Posix se
especifica la interfaz de la librería y no el de la llamada al sistema. Por encima de estas capas,
todas las versiones de Unix proporcionan una serie de programas de utilidades, como el
procesador de órdenes (“shell”), los compiladores, los editores, los programas de
procesamiento de texto y las utilidades para el manejo de los archivos. Desde el terminal el
usuario llama directamente a estos programas.
La interfaz de librería.
La interfaz del usuario, que está formada por los programas de utilidades
estándar.
El auténtico sistema operativo es el núcleo que interactúa directamente con el hardware y las
rutinas primitivas, que corresponden al bloque de control del hardware y en la parte superior
está la interfaz de llamadas al sistema, que permite al software de alto nivel tener acceso a
funciones específicas del núcleo. El resto del núcleo se puede dividir en dos partes principales,
que están asociadas al control de procesos y a la gestión de archivos y de la entrada/salida.
En Unix todos los procesos, excepto dos procesos básicos del sistema, se crean mediante
órdenes del programa de usuario. Los dos procesos son los de arranque e inicialización. Los
nueve estados en los que estar un proceso son:
Zombi. El proceso ejecuta la llamada exit() (fin del programa), pero todavía no
puede finalizar por lo que queda en estado zombi. Normalmente, un proceso
pasa a estado de zombi cuando está a la espera de que finalice alguno de sus
hijos.
Tuberías (“pipes”).
Señales.
Mensajes.
Memoria compartida.
Semáforos.
Las tuberías permiten transferir datos entre procesos y sincronizar la ejecución de los mismos.
Usan un modelo de productor‐consumidor, y hay una cola FIFO donde un proceso escribe y el
otro lee.
Las tuberías y las señales constituyen una forma limitada de comunicación. Los otros
mecanismos estándar de comunicación son:
1. Los mensajes, que permiten a los procesos enviar a cualquier proceso cadenas
de datos con un determinado formato.
Los mensajes son cadenas de datos con un formato establecido. El proceso que envía un
mensaje especifica el tipo del mismo con cada uno de los que envía. El receptor puede
usar este dato como un criterio de selección, de forma que puede atender a los mensajes
según el orden de llegada en una cola o por el tipo. Cuando un proceso intenta enviar un
mensaje a una cola que está llena, el proceso queda suspendido, lo mismo ocurre si
intenta leer de una cola vacía.
Pero quizá, la forma más rápida de comunicación entre procesos en Unix es mediante la
memoria compartida, que permite que varios procesos accedan al mismo espacio de
direcciones virtuales de memoria. Los procesos leen y escriben en la memoria compartida
utilizando las mismas instrucciones máquina. Para manipular la memoria compartida se
utilizan llamadas al sistema similares a las llamadas de los mensajes.
Gestión de la memoria
306 PROGRAMACIÓN C++ Y COMUNICACIONES.
Por último, la tabla de intercambio tiene una entrada por cada página en el dispositivo y
existe una por cada dispositivo de intercambio. Esta tabla tiene dos entradas: el controlador
de referencia, que es el número de puntos de entradas de la tabla de páginas a una página en
el dispositivo de intercambio, y el identificador de la página en la unidad de almacenamiento.
Sistema de archivos
Ordinarios. Son los archivos que contiene la información del usuario, los
programas de aplicación o de utilización del sistema.
Directorios. Son archivos que contiene listas de nombres de archivos, más los
punteros asociados a los nodos‐i. Están organizados de una forma jerárquica.
Todos los tipos de archivos de Unix se gestionan por el sistema operativo mediante los
nodos‐i. Estos corresponden a una tabla que contiene los atributos y las direcciones de los
bloques del archivo. Los archivos se ubican dinámicamente, según es necesario, sin usar
una preubicación, de esta forma, los bloques de un archivo en el disco no son
necesariamente contiguos.
Subsistema de entrada/salida
Para el sistema operativo Unix todos los periféricos están asociados a un archivo especial, que
se gestiona por el sistema de archivos, pudiéndose leer y escribir como otro archivo más. Se
consideran dos tipos de periféricos: de bloque y de carácter. Los periféricos de bloque son
periféricos de almacenamiento de acceso arbitrario (por ejemplo los discos). Los periféricos
orientados a caracteres incluyen a los otros tipos, por ejemplo las impresoras o los
terminales.
La E/S se puede realizar utilizando un buffer, como una zona de almacenamiento intermedio
de los datos procedentes o con destino a los periféricos. Hay dos mecanismos de buffer:
sistema de caché y colas de caracteres. El caché de buffer es esencialmente una caché de
disco. La transferencia de datos entre la caché del buffer y el espacio de E/S del proceso del
usuario se realiza mediante DMA, ya que ambos están localizados en la memoria principal. El
otro mecanismo de buffer, las colas de caracteres, resulta apropiado para los periféricos
orientados a caracteres. El dispositivo de E/S escribe una cola de caracteres que son leídos
por el proceso o, inversamente, el proceso los escribe y el periférico los lee. En ambos casos
se utiliza un modelo de productor consumidor.
La E/S sin buffer es simplemente una operación de acceso directo a memoria (“DMA”, “Direct
Memory Access”), entre el periférico y el espacio de memoria del proceso. Este es el método
más rápido para realizar una entrada/salida, sin embargo, un proceso que esté realizando una
transferencia de entrada/salida sin buffer está bloqueado en la memoria principal y no puede
intercambiarse.
308 PROGRAMACIÓN C++ Y COMUNICACIONES.
Propiedad de Linux
Linus Torvalds conserva los derechos de autor del kernel básico de Linux. Red Hat,
Inc, posee los derechos de la distribución Red Hat y Paul Volkerding, que se acogen a la GPL
(“General Public License”, Licencia Pública Gerenal) de GNU. De echo, Linux y muchos de los
que han contribuido al desarrollo de Linux, han protegido su trabajo con la GPL de GNU.
En ocasiones, esta licencia se denomina GNU Copyleft, que no es más que un juego
de palabras con el término inglés Copyright. Esta licencia cubre todos los programas
producidos por GNU y por la FSF (“Free Software Foundation”, Fundación de Software de
Libre distribución). Esta licencia permite a los desarrollares crear programas para el público en
general. La premisa fundamental de GNU es la de permitir a todos los usuarios acceso libre a
los programas con la posibilidad de modificarlos, si así lo desean. La única condición impuesta
es que no puede limitarse el código modificado; es decir, que el resto de usuarios tiene
derecho también a utilizar el nuevo código.
El GNU Copyleft, o GPL, permite a los creadores de programas conservar sus derechos de
autor, pero permitiendo al resto de usuarios la posibilidad de copiarlos, modificarlos y hasta
de venderlos. Sin embargo, al hacerlo, no pueden limitar ningún derecho similar a los que
compren el programa. Si se vende el programa tal y como está, o una modificación del mismo,
el usuario debe facilitar además el código fuente. Por ello, cualquier versión de Linux
incorpora siempre el código fuente completo.
La distribución de Linux corre a cargo de distintas compañías, cada una de ellas con
su propio paquete de programas, aunque todas faciliten un núcleo de archivos que
conforman una versión de Linux. Las más difundidas son: Debian Linux, Red Hat, Slackware,
Mandrake y Ubuntu.
Características de Linux
Alguna de las ventajas que ofrece la multitarea preferente, además de reducir los tiempos
muertos, es decir, aquellos momentos en los que no puede ejecutar ninguna aplicación
porque todavía no se ha completado una tarea anterior, posee una gran flexibilidad, que le
permite abrir nuevas ventanas sin tener que cerrar otras con las que está trabajando.
Shell programables
310 PROGRAMACIÓN C++ Y COMUNICACIONES.
Aunque muchas versiones UNIX y Linux incluyen más de un tipo de shell, todos ellos
funcionan básicamente del mismo modo. Un shell realiza la tarea de mediar entre el usuario y
el núcleo del sistema operativo Linux. La diferencia esencial entre los tres shell disponibles
radica en la sintaxis de la línea de órdenes. Aunque no supone una limitación estrictamente
hablando, el uso de las órdenes del shell C o la sintaxis de los shells Bourne o bash pueden
traerle problemas.
Algunos usuarios van incluso más allá, diseñando programas que enlazan procesos y
aplicaciones para reducir su trabajo a veces hasta una única sesión de entrada de datos,
consiguiendo así que el sistema actualice de una sola vez los numerosos paquetes de
programas.
UNIX soluciona los problemas que supone añadir otros periféricos, contemplando
cada uno de ellos como un archivo aparte. Cuando se necesitan nuevos dispositivos, el
administrador del sistema añade al kernel el enlace necesario. Este enlace, también
denominado controlador de dispositivo, se ocupa de que el kernel y el dispositivo se fusionen
del mismo modo cada vez que se solicita el servicio del dispositivo.
Linux integra además una implementación completa del protocolo de red TCP/IP.
Linux permite conectarse a Internet y acceder a toda la información que incluye. Linux
también dispone de un sistema completo de correo electrónico con el que se puede enviar y
recibir mensajes a través de Internet, y otras redes de ordenadores.
elementos GUI habituales que incluyen otras plataformas GUI comerciales, como Windows y
OS/2.
Varios sistemas operativos distintos pueden coexistir sobre el mismo ordenador, en sólido
aislamiento el uno del otro, por ejemplo para probar un sistema operativo nuevo sin
necesidad de instalarlo directamente.
La máquina virtual puede proporcionar una arquitectura de instrucciones que sea algo
distinta de la de la verdadera máquina. Es decir, podemos simular hardware.
Definición y tipos.
Una red de computadores es una agrupación de dos o más computadores que se comunican
entre sí. Según la dimensión de la red se suelen dividir en dos grupos:
Redes de área local (“LAN”, Local Area Network). Conectan computadores cercanos
unos de las otros. En algunos casos, "local" significa dentro de la misma habitación o
edificio; en otros casos, se refiere a computadores ubicados a varios kilómetros de
distancia.
Redes de área extendida o redes de gran alcance (“WAN”, Wide Area Network).
Constan de computadores que se encuentran en diferentes ciudades o incluso
países. Son redes de larga distancia, debido al gran trayecto que debe recorrer la
información que intercambian.
Compartir recursos. Todos los programas, datos y equipos, están disponibles para
cualquier ordenador de la red que lo solicite, sin importar la localización física del
recurso y del usuario.
Mejora de la fiabilidad. Proporcionan una alta fiabilidad, al contar con fuentes
alternativas de suministro. La presencia de múltiples CPU, significa que si una de ellas
deja de funcionar, las otras son capaces de su trabajo, aunque se tenga un
rendimiento global menor.
Ahorro económico. Los ordenadores pequeños tienen una mejor relación
coste/rendimiento, comparada con la ofrecida por las máquinas grandes. Estas son, a
grandes rasgos, diez veces más rápidas que el más rápido de los microprocesadores,
pero su costo es miles de veces mayor.
Medio de comunicación. Una red de ordenadores puede proporcionar un poderoso
medio de comunicación entre personas que se encuentran muy alejadas entre sí. A la
larga el uso de redes, como un medio para enriquecer la comunicación entre seres
humanos, puede ser más importante que los mismos objetivos técnicos, como por
ejemplo, la mejora de la fiabilidad.
Acceso a programas remotos. Una empresa que ha producido un modelo que simula
la economía mundial puede permitir que sus clientes se conecten usando la red y
ejecuten el programa para ver cómo pueden afectar a sus negocios las diferentes
proyecciones de inflación, de tasas de interés y de fluctuaciones de tipos de cambio.
Acceso a bases de datos remotos. Hoy en día, ya es muy fácil ver, por ejemplo, a
cualquier persona hacer desde su casa reservas de avión, autobús, barco y hoteles,
restaurantes, teatros, etc., para cualquier parte del mundo y obteniendo la
confirmación de forma instantánea. En esta categoría también caen las operaciones
bancarias que se llevan a cabo desde el domicilio particular, así como las noticias del
periódico recibidas de forma automática.
Medios alternativos de comunicación. La utilización del correo electrónico, que
permite mandar y recibir mensajes de cualquier parte del mundo, muestra el gran
potencial de las redes en su empleo como medio de comunicación. El correo
electrónico es capaz de transmitir la voz digitalizada, fotografías e imágenes móviles
de vídeo.
316 PROGRAMACIÓN C++ Y COMUNICACIONES.
Arquitecturas de redes
La mayoría de las redes se organizan en una serie de capas o niveles, con objeto de reducir la
complejidad de su diseño. Cada una de ellas se construye sobre su predecesora. El número de
capas, el nombre, el contenido, y la función de cada una varían de una red a otra. Sin
embargo, en cualquier red, el propósito de cada capa es ofrecer ciertos servicios a las capas
superiores, liberándolas del conocimiento detallado sobre cómo se realizan dichos servicios.
La capa n en una máquina conversa con la capa n de otra máquina. Las reglas y convenciones
utilizadas en esta conversación se conocen como protocolo de la capa n. A las entidades que
forman las capas correspondientes en máquinas diferentes se les denomina proceso pares; es
decir de igual a igual. En otras palabras, son los procesos pares los que se comunican
mediante el uso del protocolo.
En realidad, no existe una transferencia directa de datos desde la capa n de una máquina a la
capa n de otra, sino más bien, cada capa pasa la información de datos y control a la capa
inmediatamente inferior; y así sucesivamente, hasta que se alcanza la capa localizada en la
parte más baja de la estructura. Debajo de la 1 está el medio físico, a través del cual se realiza
la comunicación real.
Entre cada par de capas adyacentes existe una interfaz, la cual define los servicios y
operaciones primitivas que la capa inferior ofrece a la superior. Al conjunto de capas y
protocolos se le denomina arquitectura de la red. Las especificaciones de ésta deberán
contener la información suficiente que le permita al diseñador escribir un programa o
construir el hardware correspondiente a cada capa, y que siga en forma correcta el protocolo
apropiado. Tanto los detalles de realización, como las especificaciones de las interfaces, no
forman parte de la arquitectura, porque se encuentran escondidas en el interior de la
máquina y no son visibles desde el exterior. Más aún, no es necesario que las interfaces de
todas las máquinas en una red sean iguales, supuesto que cada una de las máquinas utilice
correctamente todos los protocolos.
La abstracción del proceso par es importante para el diseño de redes, sin esta técnica de
abstracción sería difícil, dividir el diseño de una red completa; es decir, sería un problema
intratable si no se divide en varios más pequeños y manejables, el diseño de capas
individuales. En la figura 10.2 se muestra un esquema del significado de los términos capa,
protocolo e interfaz.
El modelo que se va a tratar a continuación está basado en una propuesta desarrollada por la
Organización Internacional de Normas (ISO). Este modelo supuso un primer paso hacia la
normalización internacional de varios protocolos, [Stallings 00] y [Tanenbaum 97]. A este
modelo se le conoce como Modelo de Referencia Interconexión de sistemas abiertos, más
conocido como modelo OSI de ISO, (“OSI”, Open System Interconnection). Este modelo de
referencia se refiere a la conexión de sistemas heterogéneos, es decir, a sistemas dispuestos a
establecer comunicación con otros distintos.
El modelo OSI consta de 7 capas. Los principios aplicados para el establecimiento de siete
capas fueron los siguientes:
2. Cada capa deberá efectuar una función bien definida, que le proporcione entidad
propia.
3. La función que realizará cada capa deberá seleccionarse con la intención de definir
protocolos normalizados internacionalmente.
318 PROGRAMACIÓN C++ Y COMUNICACIONES.
Obsérvese que le modelo OSI, por sí mismo, no es una arquitectura de red, dado que no
especifica, en forma exacta, los servicios y protocolos que se utilizarán en cada una de las
capas. Sólo indica lo que cada capa deberá hacer.
Capa física.
La capa física se ocupa de la transmisión de bits a lo largo del canal de comunicación. Los
problemas de diseño a considerar aquí son los aspectos mecánico, eléctrico, de
procedimiento de interfaz y el medio de transmisión física, que se encuentra bajo la capa
física.
Capa de enlace.
La tarea primordial de la capa de enlace consiste en, a partir de un medio de transmisión
común y corriente, transformarlo en una línea sin errores de transmisión para la capa de red.
Esta tarea requiere que el emisor trocee la entrada de datos en tramas de datos, típicamente
construidas por algunos cientos de octetos. Las tramas así formadas son transmitidas de
forma secuencial, y requieren de un asentimiento del receptor que confirme su correcta
recepción.
Capa de red.
La capa de red se ocupa del control de la operación de la subred. Un punto de suma
importancia en su diseño, es la determinación sobre cómo encaminar los paquetes de origen
a destino. Si en un momento dado hay demasiados paquetes presentes en la subred, ellos
mismos se obstruirán mutuamente y darán lugar a un cuello de botella. El control de tal
congestión dependerá de la capa de red. Además, es responsabilidad de la capa de red
resolver problemas de interconexión de redes heterogéneas.
Capa de transporte.
La función principal de la capa de transporte consiste en aceptar los datos de la capa de
sesión, dividirlos, siempre que sea necesario en unidades más pequeñas, pasarlos a la capa de
red, y asegurar que todos ellos lleguen correctamente al otro extremo. Además, todo este
trabajo se debe hacer de manera eficiente, de tal forma que aísle la capa de sesión de los
cambios inevitables a los que está sujeta la tecnología del hardware.
Capa de sesión.
La capa de sesión permite que los usuarios de diferentes máquinas puedan establecer
sesiones entre ellos. A través de una sesión se puede llevar a cabo un transporte de datos
ordinario, tal y como lo hace la capa de transporte, pero mejorando los servicios que ésta
proporciona y que se utilizan en algunas aplicaciones.
Uno de los servicios de la capa de sesión consiste en gestionar el control de diálogo. Las
sesiones permiten que el tráfico vaya en ambas direcciones al mismo tiempo, o bien, en una
sola dirección en un instante dado. Otros servicios relacionados con esta capa son la
administración del testigo y la sincronización.
Capa de presentación.
A diferencia de las capas inferiores, que únicamente están interesadas en el movimiento
fiable de bits de un lugar a otro, la capa de presentación se ocupa de los aspectos de sintaxis y
semántica de la información que se transmite.
número de bits que tienen que transmitirse, y el concepto de criptografía se necesita utilizar a
menudo por razones de privacidad y de autentificación.
Capa de aplicación.
La capa de aplicación contiene una variedad de protocolos que se necesitan frecuentemente.
Por ejemplo, hay centenares de tipos de terminales incompatibles en el mundo. Una forma de
resolver este problema consiste en definir un terminal virtual de red abstracto.
Este proceso se sigue repitiendo hasta que los datos alcanzan la capa física, lugar en donde
efectivamente se transmiten datos a la máquina receptora. La idea fundamental, a lo largo de
este proceso, es que si bien la transmisión efectiva de datos es vertical, como se muestra en
la figura 3.3, cada una de las capas está programada como si fuera una transmisión horizontal.
Las entidades de la capa N desarrollan un servicio que utiliza la capa (N+1), en este caso a la
capa N se le denomina proveedor del servicio y a la capa (N+1) usuario del servicio. La capa N
puede utilizar servicios de la capa (N‐1) con objeto de proporcionar su servicio.
Para que se lleve a cabo el intercambio de información entre dos capas, deberá existir un
acuerdo sobre un conjunto de reglas acerca de la interfaz. En una interfaz típica, la entidad de
la capa (N+1) pasa una Unidad de Datos de la Interfaz (“IDU”, Interface Data Unit) a la entidad
de la capa N, a través del correspondiente SAP.
El IDU consiste en una Unidad de Datos del Servicio (“SDU”, Service Data Unit) y de alguna
información de control. La SDU es la información que se pasa, a través de la red, a la entidad
par y posteriormente, a la capa (N+1). La información de control es necesaria porque ayuda a
que las capas inferiores realicen su trabajo; por ejemplo, el número de bytes en el SDU, que
no forma parte de los datos.
Para hacer la transferencia de una SDU, podrá ser necesario su fragmentación por parte de la
entidad de la capa N en varias partes, de tal forma que a cada una de ellas se le asigne una
cabecera y se envíe como una distinta Unidad de Datos del Protocolo (“PDU”, Protocol Data
Unit). Las entidades pares utilizan las cabeceras de la PDU para llevar a cabo su protocolo de
igual a igual. Por medio de ellos se identifica cuáles son las PDU que contienen datos y cuáles
las que llevan información de control.
Con frecuencia a las PDU de transporte, sesión y aplicación se les conoce como Unidad de
Datos del Protocolo de Transporte (“TPDU”, Transport Protocol Data Unit), Unidad de Datos
del Protocolo de Sesión (“SPDU”, Service Protocol Data Unit) y Unidad de Datos del Protocolo
de Aplicación (“APDU”, Application Protocol Data Unit), respectivamente.
Las capas pueden ofrecer dos tipos diferentes de servicios a las capas que se encuentran
sobre ellas; uno orientado a conexión y otro sin conexión.
sus definiciones de servicio, teniendo libertad para cambiar de protocolo, pero asegurándose
de no modificar el servicio visible a los usuarios.
Normalización de redes
En los primeros tiempos en que apareció el concepto de redes, cada compañía fabricante de
ordenadores tenía sus propios protocolos; por ejemplo, IBM tenía más de una docena. Esto
daba como resultado que aquellos usuarios que adquirían ordenadores de diferentes
compañías, no podían conectarlos y establecer una sola red con ellos. El caos generado por
esta incompatibilidad dio lugar a la exigencia de los usuarios para que se estableciera una
normalización al respecto.
Las normas se dividen en dos categorías que pueden definirse como: de facto y de jure. Las
normas De Facto (derivado del latín, que significa "del hecho"), son aquellas que se han
establecido sin ningún planteamiento formal. Las normas IBM PC y sus sucesoras son normas
de facto para ordenadores pequeños de oficina, porque docenas de fabricantes decidieron
copiar fielmente las máquinas que IBM sacó al mercado. En contraste, las normas De Jure
(derivado del latín, que significa "por ley"), son normas formales, legales, adoptadas por un
organismo que se encarga de su normalización. Las autoridades internacionales encargadas
de la normalización se dividen, por lo general, en dos clases: la establecida por convenio entre
gobiernos nacionales, y la establecida sin un tratado entre organizaciones voluntariamente.
La ISO consta de aproximadamente 200 comités técnicos (TC), cuyo orden de numeración se
basa en el momento de su creación, ocupándose cada uno de ellos de un tema específico.
Cada uno de los TC tiene subcomités (SC), los cuales a su vez se dividen en grupos de trabajo
(WG).
Ejemplos de redes
Actualmente se encuentra funcionando un número muy grande de redes en todo el mundo.
Algunas de ellas son redes públicas operadas por proveedores de servicios portadores
comunes o PTT, otras están dedicadas a la investigación, también hay redes en cooperativa
operadas por los mismos usuarios y redes de tipo comercial o corporativo. Las redes, por lo
general, se caracterizan por los puntos citados a continuación:
Los servicios ofrecidos, que pueden variar desde una comunicación arbitraria de proceso
a proceso, hasta el desarrollo de un sistema de correo electrónico, transferencia de
archivos o acceso remoto.
Por último, las comunidades de usuarios, que pueden variar desde una sola corporación,
hasta aquella que incluye todos los ordenadores científicos que se encuentran en el
mundo industrializado.
Redes públicas: Las empresas privadas, y los gobiernos de varios países han
comenzado a ofrecer servicios de redes a cualquier organización que desee
subscribirse a ellas. La subred es propiedad de la compañía operadora de redes y
proporciona un servicio de comunicación para los clientes y terminales. Aunque las
redes públicas, en diferentes países, son en general, muy diferentes en cuanto a su
estructura interna, todas ellas utilizan el modelo OSI y las normas CCITT o los
protocolos OSI para todas las capas.
CSENET realmente, CSNET no es una red real como ARPANET, sino más bien es una
metared que utiliza los servicios de transmisión brindados por otras redes y añade
una capa protocolo uniformadora en la parte superior, para hacer que todo ello
parezca como una sola red lógica para los usuarios.
USENET esta red fue desarrollada en las Universidades de Duke y North Carolina,
ofrece un servicio de red de noticias para los sistemas UNIX. Las noticias por red se
dividen en cientos de grupos de noticias con una gran diversidad de temas, algunos
de ellos son de carácter técnico y otros no. Los usuarios de USENET se pueden
subscribir a cualquier grupo que les interese.
Las funciones propias de una red de computadoras pueden ser divididas en las siete capas
propuestas por ISO para su modelo de referencia OSI; sin embargo la implantación real de
una arquitectura puede diferir de este modelo. Las arquitecturas basadas en TCP/IP proponen
cuatro capas en las que las funciones de las capas de sesión y presentación son
responsabilidad de la capa de aplicación y las capas de enlace y física son vistas como la capa
de Interfaz a la Red. Por tal motivo, para TCP/IP sólo existen las capas Interfaz de Red, la de
Intercomunicación en Red, la de Transporte y la de Aplicación. Como puede verse TCP/IP
presupone independencia del medio físico de comunicación, sin embargo existen estándares
bien definidos a los niveles de enlace de datos y físico que proveen mecanismos de acceso a
los diferentes medios y que en el modelo TCP/IP deben considerarse la capa de Interfaz de
Red; siendo los más usuales el proyecto IEEE802, Ethernet, Token Ring y FDI.
Capa de Interfaz de Red. Emite al medio físico los flujos de bits y recibe los que
de él provienen. Consiste en los gestores de los dispositivos que se conectan al
medio de transmisión.
Para que en una red dos computadoras puedan comunicarse entre sí ellas deben estar
identificadas con precisión Este identificador puede estar definido en niveles bajos
(identificador físico) o en niveles altos (identificador lógico) de pendiendo del protocolo
utilizado. TCP/IP utiliza un identificador denominado dirección Internet o dirección IP, cuya
longitud es de 32 bites. La dirección IP identifica tanto a la red a la que pertenece una
computadora como a ella misma dentro de dicha red. En la siguiente tabla se establecen los
diferentes tipos de redes que se pueden establecer según la dirección IP.
Teniendo en cuenta una dirección IP podría surgir la duda de cómo identificar qué parte de la
dirección identifica a la red y qué parte al nodo en dicha red. Lo anterior se resuelve mediante
la definición de las "Clases de Direcciones IP". Para clarificar lo anterior puede verse que una
red con dirección clase A queda precisamente definida con el primer octeto de la dirección, la
clase B con los dos primeros y la C con los tres primeros octetos. Los octetos restantes definen
los nodos en la red específica.
Las subredes tienen una gran importancia en la implantación de redes. Estas subredes son
redes físicas distintas que comparten una misma dirección IP. Deben identificarse una de otra
usando una máscara de subred. La máscara de subred es de cuatro bytes y para obtener el
número de subred se realiza un operación AND lógica entre ella y la dirección IP de algún
equipo. La máscara de subred deberá ser la misma para todos los equipos de la red IP.
Las direcciones IP y física de la computadora que consulta es incluida en cada emisión general
ARP, el equipo que contesta toma esta información y actualiza su tabla de conversión. ARP es
un protocolo de bajo nivel que oculta el direccionamiento de la red en las capas inferiores,
permitiendo asignar, a nuestra elección, direcciones IP a los equipos en una red física.
La implementación del protocolo ARP requiere varios pasos que se describen a continuación.
En primer lugar, la interfaz de red recibe un datagrama IP a enviar a un equipo destino, en
este nivel se coteja la tabla temporal de conversión, si existe una la referencia adecuada ésta
se incorpora al paquete y se envía. Si no existe la referencia un paquete ARP de emisión
general, con la dirección IP destino, es generado y enviado. Todos los equipos en la red física
reciben el mensaje general y comparan la dirección IP que contiene con la suya propia,
enviando un paquete de respuesta que contiene su dirección IP. El computador origen
actualiza su tabla temporal y envía el paquete IP original, y los subsecuentes, directamente a
la computadora destino.
El funcionamiento de ARP no es tan simple como parece. Supóngase que en una tabla de
conversión exista un mapeo de una máquina que ha fallado y se le ha reemplazado la interfaz
de red; en este caso los paquetes que se transmitan hacia ella se perderán pues ha cambiado
la dirección física, por tal motivo la tabla debe eliminar entradas periódicamente.
Sólo ser realiza verificación por suma al encabezado del paquete, no a los
datos éste que contiene
La Unidad de Transferencia Máxima determina la longitud máxima, en bytes, que podrá tener
un datagrama para ser transmitida por una red física. Obsérvese que este parámetro está
determinado por la arquitectura de la red: para una red Ethernet el valor de la MTU es de
1500 bytes. Dependiendo de la tecnología de la red los valores de la MTU pueden ir desde
128 hasta unos cuantos miles de bytes. La arquitectura de interconexión de redes propuesta
por TCP/IP indica que éstas deben ser conectadas mediante una compuerta. Sin obligar a que
la tecnología de las redes físicas que se conecten sea homogénea. Por tal motivo si para
interconectar dos redes se utilizan medios con diferente MTU, los datagramas deberán ser
fragmentados para que puedan ser transmitidos. Una vez que los paquetes han alcanzado la
red extrema los datagramas deberán ser reensamblados.
Enrutamiento de paquetes
La arquitectura de enrutamiento por Compuerta Núcleo se basa es definir una compuerta que
centraliza las funciones de enrutamiento entre redes, a esta compuerta se le denomina
núcleo. Cada compuerta en las redes a conectar tiene como compuerta por defecto a la
compuerta núcleo. Varias compuertas núcleo pueden conectarse para formar una gran red;
entre las compuertas núcleo se intercambiará información concerniente a las redes que cada
una de ellas alcanzan. La arquitectura centralizada de enrutamiento fue la primera que
existió. Sus principales problemas radican no tanto en la arquitectura en sí, si no en la forma
en que se propagaban las rutas entre las compuertas núcleo.
Conforme las complejidades de las redes aumentaron se debió buscar un mecanismo que
propagase la información de rutas entre las compuertas. Este mecanismo debía ser
automático esto obligado por el cambio dinámico de las redes. De no ser así las transiciones
entre las compuertas podían ser muy lentas y no reflejar el estado de la red en un momento
dado. Para resolver este problema se define el vector de distancia. Se asume que cada
compuerta comienza su operación con un conjunto de reglas básicas de cómo alcanzar las
redes que conecta. Las rutas son almacenadas en tablas que indican la red y los saltos para
alcanzar esa red. Periódicamente cada compuerta envía una copia de las tablas que alcanza
directamente. Cuando una compuerta recibe el comunicado de la otra actualiza su tabla
Cada vez que un paquete es enviado se inicializa un contador de tiempo, al alcanzar el tiempo
de expiración, sin haber recibido el reconocimiento, el paquete se reenvía. Al llegar el
reconocimiento el tiempo de expiración se cancela. A cada paquete que es enviado se le
asigna un número de identificador, el equipo que lo recibe deberá enviar un reconocimiento
334 PROGRAMACIÓN C++ Y COMUNICACIONES.
de dicho paquete, lo que indicará que fue recibido. Si después de un tiempo dado el
reconocimiento no ha sido recibido el paquete se volverá a enviar. Obsérvese que puede
darse el caso en el que el reconocimiento sea el que se pierda, en este caso se reenviará un
paquete repetido.
pseudo‐cabecera IP
cabecera UDP
Mensaje UDP
Origen y evolución
Para evitar que un ataque nuclear pudiera dejar aisladas a las instituciones militares y
universidades, en 1969 el ARPA (Advanced Research Projects Agency), una agencia subsidiaria
del Departamento de Defensa de Estados Unidos, desarrolló una red denominada ARPAnet
basada en el protocolo de intercambio de paquetes.
Otra ventaja es que este tipo de sistemas permite distribuir más fácilmente los datos, ya que
cada paquete incluye toda la información necesaria para llegar a su destino, por lo que
paquetes con distinto objetivo pueden compartir un mismo canal. Además, es posible
comprimir cada paquete para aumentar la capacidad de transmisión, o encriptar su contenido
para asegurar la confidencialidad de los datos. Estas virtudes han asegurado la supervivencia
de los protocolos desde las primeras pruebas realizadas en 1968 por el Laboratorio Nacional
de Física del Reino Unido hasta nuestros días.
El primer protocolo utilizado por ARPAnet fue el denominado NCP (Network Control Protocol,
Protocolo de Control de Red), que se empleó en la red hasta 1982. En este año se adoptó el
protocolo TCP/IP procedente de los sistemas Unix, que cada vez tenían más importancia
dentro de ARPAnet.
Sin embargo, la red Internet como «red de redes» no comenzó a funcionar hasta después de
la primera conferencia de comunicaciones por ordenador en octubre de 1972. En esta
convención ARPAnet presentaba una red de 40 nodos y se propuso su conexión con otras
redes internacionales. Representantes de varios países formaron así el INWG (Inter Network
Working Group) para establecer un protocolo común con el ARPA, empezando a dar forma lo
que hoy en día conocemos por Internet.
Con el tiempo ARPAnet fue sustituida por NSFnet, la red de la fundación nacional para la
ciencia de EEUU, como organismo coordinador de la red central de Internet. Desde los
primeros pasos de ARPAnet, hasta hoy en día, la red ha sufrido pocos cambios, comparado
con los avances de la informática. Los cambios más drásticos se han producido en la
infraestructura de la red, aumentando la velocidad de transmisión hasta permitir el
funcionamiento de aplicaciones multimedia y la transmisión de vídeo o sonido en tiempo real.
También han sufrido cambios el tipo de servicios ofrecidos por Internet, ya que si bien las
utilidades en modo texto han sobrevivido hasta nuestros días, la verdadera estrella de la Red
es la World Wide Web, un servicio de consulta de documentos hipertextuales que ha logrado
gran popularidad tanto entre expertos como entre profanos.
Uno de los cambios más espectaculares que ha sufrido Internet es el del número de usuarios.
Desde los primeros tiempos de ARPAnet en los que sólo una decena de ordenadores y un
centenar de usuarios tenían acceso a la red, hemos pasado a cifras importantes. Se calcula
que existen unos 4 millones de sistemas conectados actualmente a Internet, facilitando
acceso a unos 50 millones de usuarios en todo el mundo.
Las cifras son aún más sorprendentes si se considera que el crecimiento actual del censo de
usuarios de Internet es aproximadamente de un diez por ciento mensual. Estas cifras indican,
por ejemplo, que en el siglo XXI los usuarios de Internet podrían alcanzar en número a los que
ven televisión actualmente. Esto hace que Internet se está convirtiendo en una realidad de
nuestro tiempo, y puede provocar una pequeña revolución en nuestra forma de vida, del
mismo modo que lo han hecho los ordenadores, teléfonos móviles, discos compactos, etc.
Este fenómeno ha atraído los intereses de muchas empresas de todos los sectores, que ven
en Internet un vehículo ideal para actividades comerciales, técnicas o de marketing, además
de un medio de distribución directa de software, y en general de información de todo tipo.
CORREO ELECTRÓNICO
338 PROGRAMACIÓN C++ Y COMUNICACIONES.
Uno de los primeros servicios que la red ARPAnet incorporó desde sus inicios fue el correo
electrónico. Como el correo normal, su versión electrónica, también denominada e‐mail,
proporciona un medio para que cualquier usuario de una red pueda enviar mensajes a otras
personas.
A pesar de que cada usuario puede estar utilizando un ordenador o una aplicación de correo
electrónico diferente, e incluso pertenecer a redes de ordenadores no conectadas
directamente a Internet, el protocolo utilizado SMTP (Simple Mail Transfer Protocol,
Protocolo de Transmisión de Mensajes Simples), asegura una absoluta compatibilidad en el
intercambio de mensajes.
Existen también otros servicios que pueden ser utilizados a través del correo electrónico:
acceso a librerías de software, consulta de información sobre ciertos temas, participación en
juegos de estrategia, o discusión con otros usuarios en foros temáticos.
Las direcciones de correo electrónico están compuestas por un nombre de usuario, el símbolo
«@» y el nombre completo del dominio del ordenador que estamos utilizando o del
proveedor de acceso que hemos contratado. Si el usuario con el que queremos contactar no
se encuentra en una red unida directamente a Internet, existen unas tablas de conversión que
permitirán a nuestros mensajes llegar a su destino a través de las oportunas «pasarelas», que
no son otra cosa que ordenadores que hacen de puente entre dos redes y que permiten
cierto intercambio de información entre ellas.
Si se dispone de un acceso a Internet se puede contar con una dirección propia de correo
electrónico y un espacio, denominado buzón, en el disco del ordenador que funciona como
servidor de correo. En este espacio se irán almacenando los mensajes de correo electrónico
que vayan llegando con nuestra dirección para que, cuando nos conectemos, podamos
comprobar si tenemos correo nuevo y acceder a él. En muchos casos el tamaño de este buzón
será limitado. Al enviar o recibir un mensaje de correo electrónico debe tenerse en cuenta
que su viaje por Internet va a ser más lento que con otro tipo de servicios, ya que tiene
asignado un ancho de banda menor que el de otros de acceso directo.
Desde su creación el correo electrónico es sin duda el servicio más utilizado dentro de
Internet. Millones de personas de todo el mundo envían sus mensajes, incluyendo en algunos
casos imágenes digitales y música. Por otro lado, este medio está desplazando en importancia
al Fax en muchos ambientes empresariales, ya que ofrece grandes ventajas de costes y
calidad además de una gran flexibilidad, a pesar del inconveniente de su escasa rapidez de
transmisión.
TELNET
El programa Telnet permite acceder mediante un terminal virtual a una máquina conectada a
Internet, siempre que se conozca su dirección IP, o nombre de dominio, y se disponga de un
nombre de usuario y la correspondiente clave de acceso. Según sea el sistema al que se
accede, se tendrá que utilizar un tipo de terminal distinto. Los más habituales son el vt100
para máquinas Unix o VAX y el ANSI para PCs.
Una imagen del programa TELNET sobre Windows se puede observar en la figura 3.5. Para la
conexión con la otra máquina deberemos introducir el Nombre de host o su dirección IP, el
Puerto por el que se realizará la conexión y el Tipo de terminal.
gráficos y con posibilidad de usar el ratón, mientras que el Telnet sólo puede funcionar en
modo texto. Sin embargo, aún son muchos los servicios que se ofrecen mediante Telnet para
los nostálgicos de los terminales de texto, y en todo caso su utilidad como herramienta para
trabajar desde casa sigue siendo importante.
FTP
Este es otro de los servicios más populares dentro de Internet y sigue siendo uno de los más
útiles a la hora de transmitir o recibir ficheros de cualquier tipo. El FTP (File Transfer Protocol,
Protocolo de Transferencia de Ficheros). Es un programa que funciona con el protocolo
TCP/IP y que permite acceder a un servidor de este sistema de intercambio para recibir o
transmitir archivos de todo tipo.
Existen multitud de sistemas a lo largo de la red dedicados a ofrecer servicio de FTP anónimo,
poniendo a disposición ficheros de todo tipo. Muchas empresas como Creative Labs, IBM o
Microsoft utilizan este método para ofrecer a los usuarios las últimas versiones de los drivers
o utilidades disponibles para sus productos.
También existen servidores dedicados exclusivamente a recoger todo tipo de software con
licencia shareware y freeware para cualquier sistema operativo. Además, dentro de Internet
encontramos servidores dedicados a recoger documentos como las famosas «FAQ»
(Frequently Asked Questions, Preguntas y respuestas habituales), que tienen respuesta a las
preguntas más frecuentes realizadas por los usuarios de Internet dentro del servicio de News,
o los documentos «FYI» (For Your Information, Información para usted), con información de
todo tipo sobre la Red.
Aunque ya han pasado muchos años desde la primera implementación del FTP, este servicio
sigue siendo el método de transporte de datos por excelencia en Internet.
La WWW es una red de servidores dentro de Internet que ofrece páginas hipertextuales en un
formato denominado HTML (HyperText Markup Language, Lenguaje de Marcas de
Hipertexto). Es un lenguaje de definición de páginas con extensiones hipertextuales portable
a cualquier tipo de plataforma gráfica. Estas páginas contienen, además de texto en varios
formatos, imágenes, sonidos o vídeo, y permiten lanzar la lectura de páginas de otros
servidores activando ciertas palabras resaltadas dentro del mismo documento.
Tanto las páginas Web como otros servicios de Internet tienen asignada su propia URL
(Uniform Resource Locator, Código Uniforme de Recursos). Se trata de un código que
342 PROGRAMACIÓN C++ Y COMUNICACIONES.
Así, una URL de la forma «http://www.mec.es» indica que queremos consultar una página de
World Wide Web en formato http (“HyperText Transfer Protocol”, Protocolo de Transferencia
de Hipertexto). En el servidor de dirección de dominio completa «www.mec.es». Si
quisiéramos realizar un FTP, la URL sería por ejemplo «ftp://ftp.microsoft.com/pub», donde
«ftp.microsoft.com» es la dirección del servidor y «/pub» es el directorio donde se
encuentran los ficheros que nos interesan.
El lenguaje HTML
Navegar por Internet mediante un programa cliente de World Wide Web se convierte en una
tarea sumamente sencilla y agradable, aprovechando todas las posibilidades de la Red. Esta
sensación ha sido aumentada considerablemente gracias a empresas como Netscape, que
han desarrollado programas cliente realmente amistosos con el usuario y de muy sencillo
manejo, incorporando multitud de utilidades para facilitar la exploración de la Red.
Todas estas virtudes son posibles gracias al lenguaje HTML propuesto por vez primera, en
marzo del año 1989, en los laboratorios de investigación de física de partículas de Suiza, el
CERN. Esta institución encargó a uno de sus departamentos el desarrollo de un sistema de
lectura de documentos que facilitara la divulgación científica, [Alvarez 97], [Musciano 98]
No fue sino hasta el año 1990, cuando el proyecto de esta prestigiosa institución, recibió el
nombre definitivo de World Wide Web. A finales de ese mismo año empezaron a funcionar
las primeras versiones del sistema en plataformas NeXT. A partir de la presentación de este
prototipo en determinadas convenciones y especialmente en un seminario que ofreció el
propio CERN, en junio de 1991, muchas instituciones se interesaron por el proyecto.
que los servidores de este tipo pueden llegar a ser varias decenas de miles, aunque el cálculo
probablemente se quede corto dada la dificultad del recuento por el rápido crecimiento del
número de ellos.
Desde que el CERN sacara a la luz su primera especificación, el lenguaje HTML fue
adquiriendo cada vez más posibilidades, incorporando capacidades multimedia y más
recientemente la posibilidad de crear páginas en tres dimensiones. Casi sin darse cuenta los
responsables del CERN habían creado uno de los fenómenos de mayor repercusión en
Internet a lo largo de su historia.
Gracias a estas facilidades proporcionadas por el lenguaje muchas otras empresas han
empezado a desarrollar multitud de applets con distintas funciones. Un applet es un pequeño
programa ejecutable que envía el servidor al programa cliente para que funcione incorporado
a la página.
344 PROGRAMACIÓN C++ Y COMUNICACIONES.
De esta manera, aunque el lector de páginas no incorpore ciertas funciones, éstas pueden
añadirse desde el servidor para que pueda leer, por ejemplo, diversos formatos multimedia
sin incorporar en el mismo programa el lector para dichos documentos.
Aplicaciones externas
Entre las aplicaciones accesorias que han sido desarrolladas últimamente encontramos
aquellas que permiten hablar y recibir sonido a través de Internet. Estos programas
convierten nuestro ordenador en un verdadero teléfono digital de cobertura mundial con
funciones interesantes y un ahorro considerable en llamadas internacionales.
La ventaja de los applets frente a este tipo de añadidos es que se cargan mientras se está
consultando la página y luego son borrados del disco local. Además, si el propio servidor es el
que proporciona la aplicación para reproducir los ficheros que contiene, la compatibilidad de
medios está asegurada.
Para sacar el máximo partido a la navegación por la World Wide Web es aconsejable
conseguir un navegador de 32 bits que pueda funcionar en cualquiera de los sistemas
operativos de la actualidad que admitan multitarea. De esta manera podemos esperar la
carga de una página de Web, mientras nos ocupamos de otras cosas, e incluso utilizar a la vez
otro servicio Internet para no desperdiciar el ancho de banda de la comunicación.
Algunos de los navegadores, como es el caso de la última versión del Netscape Navigator,
incorporan herramientas para el uso de otros servicios de Internet como correo electrónico o
News, facilitando la interacción con la Red.
Además de las funciones de los propios navegadores, hay que tener en cuenta las múltiples
utilidades que ofrecen los servidores de WWW. Las más útiles y populares son las
herramientas de búsqueda por la Red, que mediante determinados criterios y consultando
una base de datos de direcciones de páginas más o menos extensa nos ofrecen una lista de
direcciones que cumplen con los criterios que hemos especificado.
Navegadores.
Desde que diera sus primeros pasos en el laboratorio suizo de física nuclear del CERN allá por
el año 1989, la World Wide Web ha experimentado un espectacular aumento, tanto en oferta
de servidores, como en número de usuarios, convirtiéndose sin lugar a dudas en el servicio
más popular dentro de Internet, sobre todo por la facilidad de acceso a los recursos de la red.
La World Wide Web, por su propia estructura, tiene la virtud de convertir la exploración de
Internet en una tarea sencilla y agradable, ya que ofrece la posibilidad de acceder a gran
cantidad de información de una manera intuitiva mediante un sistema de consulta basado en
un entorno gráfico. Sin embargo, este conjunto de posibilidades sólo son accesibles a través
del programa adecuado, se precisa de un navegador, o programa cliente que sea capaz de
interpretar el lenguaje de descripción de página que utiliza la WWW.
Casi todas estas aplicaciones son de libre distribución para la mayoría de las plataformas,
utilizando entornos gráficos tan extendidos como X‐Window y Windows. La utilización del
346 PROGRAMACIÓN C++ Y COMUNICACIONES.
De hecho, existe una gran diferencia entre explorar la red con un buen programa cliente de
WWW que abarque todas las posibilidades del lenguaje de descripción HTML y que añada sus
propias utilidades, a realizar esta misma operación con un programa de menor sofisticación
técnica que puede recortar sensiblemente las posibilidades del sistema.
Como se ha visto en el capítulo anterior, para que varios ordenadores se comuniquen entre sí, es
claro que deben estar de acuerdo en cómo transmitirse información, de forma que cualquiera de
ellos pueda entender lo que están transmitiendo los otros, de la misma forma que nosotros nos
ponemos de acuerdo para hablar en inglés cuando uno es italiano, el otro francés, el otro español y
el otro alemán.
Al "idioma" que utilizan los ordenadores para comunicarse cuando están en red se le
denomina protocolo. Como se ha visto, hay muchos protocolos de comunicación, entre los cuales el
más extendido es el TCP/IP porque entre otras cosas es el que se utiliza en Internet.
De hecho, tanto en Linux/Unix como en Windows contamos con serie de librerías que nos permiten
enviar y recibir datos de otros programas, en C/C++ o en otros lenguajes de programación, que
estén corriendo en otros ordenadores de la misma red.
En este capítulo se presenta una introducción a los sistemas distribuidos, los servicios
proporcionados en POSIX para el manejo de Sockets, que son los conectores necesarios (el recurso
software) para la comunicación por la red, y su uso en nuestra aplicación. No pretende ser una guía
exhaustiva de dichos servicios sino una descripción práctica del uso más sencillo de los mismos, y
como integrarlos en nuestra aplicación para conseguir nuestros objetivos. De hecho, en el curso del
capítulo se desarrolla una clase C++ que encapsula los servicios de Sockets, permitiendo al usuario
un uso muy sencillo de los mismos que puede valer para numerosas aplicaciones, aunque
obviamente no para todo.
Desde el punto de vista de programación, un socket no es más que un "fichero" que se abre
de una manera especial. Una vez abierto se pueden escribir y leer datos de él con las
habituales funciones de read() y write() del lenguaje C. Hablaremos de todo esto con detalle
más adelante.
En el primer caso ambos programas deben conectarse entre ellos con un socket y hasta que
no esté establecida correctamente la conexión, ninguno de los dos puede transmitir datos.
Esta es la parte TCP del protocoloTCP/IP, y garantiza que todos los datos van a llegar de un
programa al otro correctamente. Se utiliza cuando la información a transmitir es importante,
no se puede perder ningún dato y no importa que los programas se queden "bloqueados"
esperando o transmitiendo datos. Si uno de los programas está atareado en otra cosa y no
atiende la comunicación, el otro quedará bloqueado hasta que el primero lea o escriba los
datos.
En este ejemplo, el servidor de páginas web se llama servidor porque está (o debería de estar)
siempre encendido y pendiente de que alguien se conecte a él y le pida una página. El
navegador de Internet es el cliente, puesto que se arranca cuando nosotros lo arrancamos y
solicita conexión con el servidor cuando nosotros escribimos en la barra del navegador , por
ejemplo, http://www.elai.upm.es.com
En los juegos de red, por ejemplo el Age of Empires, debe haber un servidor que es el que
tiene el escenario del juego y la situación de todos los jugadores en él. Cuando un nuevo
jugador arranca el juego en su ordenador, se conecta al servidor y le pide el escenario del
juego para presentarlo en la pantalla. Los movimientos que realiza el jugador se transmiten al
servidor y este actualiza escenarios a todos los jugadores.
EL SERVIDOR
A partir de este punto comenzamos con lo que es la programación en C/C++ de los sockets.
Una vez incluida e inicializada la librería de sockets (paso que hay que hacer en Windows, no
así en Linux) en C los pasos que debe seguir un programa servidor son los siguientes:
Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie
nuestro programa a dicho socket. Se consigue mediante la función bind(). El sistema
todavía no atenderá a las conexiones de clientes, simplemente anota que cuando
empiece a hacerlo, tendrá que avisarnos a nosotros. Es en esta llamada cuando se
debe indicar el número de puerto al que se quiere atender.
Avisar al sistema de que comience a atender dicha conexión de red. Se consigue
mediante la función listen(). A partir de este momento el sistema operativo anotará
la conexión de cualquier cliente para pasárnosla cuando se lo pidamos. Si llegan
clientes más rápido de lo que somos capaces de atenderlos, el sistema operativo
hace una "cola" con ellos y nos los irá pasando según vayamos pidiéndolo.
Pedir y aceptar las conexiones de clientes al sistema operativo. Para ello hacemos
una llamada a la función accept(). Esta función le indica al sistema operativo que nos
dé al siguiente cliente de la cola. Si no hay clientes se quedará bloqueada hasta que
algún cliente se conecte.
Escribir y recibir datos del cliente, por medio de las funciones write() y read(), que
son exactamente las mismas que usamos para escribir o leer de un fichero.
Obviamente, tanto cliente como servidor deben saber qué datos esperan recibir, qué
datos deben enviar y en qué formato.
Cierre de la comunicación y del socket, por medio de la función close(), que es la
misma que sirve para cerrar un fichero.
EL CLIENTE
Los pasos que debe seguir un programa cliente son los siguientes:
Programa cliente
El código del programa cliente, más simple que el del servidor, es el siguiente:
#define INVALID_SOCKET -1
int main()
{
//declaracion de variables
int socket_conn;//handle del socket utilizado
struct sockaddr_in server_address;
char address[]="127.0.0.1";
int port=12000;
// Configuracion de la direccion IP de connexion al servidor
352 PROGRAMACIÓN C++ Y COMUNICACIONES.
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);
//creacion del socket
socket_conn=socket(AF_INET, SOCK_STREAM,0);
//conexion
int len= sizeof(server_address);
connect(socket_conn,(struct sockaddr *) &server_address,len);
//comunicacion
char cad[100];
int length=100; //lee un máximo de 100 bytes
int r=recv(socket_conn,cad,length,0);
std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;
//cierre del socket
shutdown(socket_conn, SHUT_RDWR);
close(socket_conn);
socket_conn=INVALID_SOCKET;
return 1;
}
A continuación se describe brevemente el programa:
Las primeras líneas son algunos #includes necesarios para el manejo de servicios de sockets.
En el caso de querer utilizar los sockets en Windows, el fichero de cabecera y la librería con la
que hay que enlazar se podrían establecer con las líneas:
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")
En las primeras líneas del main() se declaran las variables necesarias para el socket.
La segunda declaración declara una estructura de datos que sirve para almacenar la dirección
IP y el número de puerto del servidor y la familia de protocolos que se utilizaran en la
comunicación. Como ya se ha explicado, un ordenador puede comunicarse simultáneamente
con muchos programas y muchos ordenadores. De alguna forma el puerto es como una
extensión a la dirección IP. Haciendo una analogía, la dirección podría entenderse como la
dirección de la escuela, mientras que el pueeto es un despacho o extensión determinado. La
asignación de esta estructura a partir de la IP definida como una cadena de texto y el puerto
definido como un entero se hace como sigue:
char address[]="127.0.0.1";
int port=12000;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);
Nótese que la IP que utilizaremos será la “127.0.0.1”. Esta IP es una IP especial que significa la
máquina actual (dirección local). Realmente ejecutaremos las dos aplicaciones (cliente y
servidor) en la misma máquina, utilizando la dirección local de la misma. No obstante esto se
puede cambiar. Para ejecutar el servidor en una máquina que tiene la IP “192.168.1.13” por
ejemplo, basta poner dicha dirección en ambos programas, ejecutar el servidor en esa
máquina, y el cliente en cualquier otra (que sea capaz de enrutar mensajes hacia esa IP).
También es posible utilizar funciones del sistema operativo para intentar obtener una
dirección IP en base al conocimiento de los nombres de los ordenadores.
Hay dos ficheros en Unix/Linux y en Windows que nos facilitan esta tarea, aunque hay que
tener permisos de root para modificarlos. Estos ficheros serían el equivalente a una agenda
de teléfonos, en uno tenemos apuntados el nombre de la empresa con su número de
teléfono y en el otro fichero el nombre de la persona y su extensión (UPM‐EUITI, tlfn 91 336
6799; Miguel Hernando, extensión 6878; ...)
Note que para iniciar un tipo de comunicación mediante un socket se requiere designar un
puerto de comunicaciones. Esto significa que algunos puertos deben reservarse para éstos
usos “bien conocidos”.
echo 7/tcp
echo 7/udp
discard 9/tcp sink null
discard 9/udp sink null
systat 11/tcp users #Active users
systat 11/udp users #Active users
daytime 13/tcp
daytime 13/udp
qotd 17/tcp quote #Quote of the day
qotd 17/udp quote #Quote of the day
chargen 19/tcp ttytst source #Character generator
chargen 19/udp ttytst source #Character generator
ftp-data 20/tcp #FTP, data
ftp 21/tcp #FTP. control
ssh 22/tcp #SSH Remote Login Protocol
telnet 23/tcp
smtp 25/tcp mail #Simple Mail Transfer Protocol
time 37/tcp timserver
time 37/udp timserver
rlp 39/udp resource #Resource Location Protocol
nameserver 42/tcp name #Host Name Server
nameserver 42/udp name #Host Name Server
nicname 43/tcp whois
domain 53/tcp #Domain Name Server
domain 53/udp #Domain Name Server
bootps 67/udp dhcps #Bootstrap Protocol Server
bootpc 68/udp dhcpc #Bootstrap Protocol Client
tftp 69/udp #Trivial File Transfer
gopher 70/tcp
finger 79/tcp
http 80/tcp www www-http #World Wide Web
hosts2-ns 81/tcp #HOSTS2 Name Server
hosts2-ns 81/udp #HOSTS2 Name Server
kerberos 88/tcp krb5 kerberos-sec #Kerberos
kerberos 88/udp krb5 kerberos-sec #Kerberos
hostname 101/tcp hostnames #NIC Host Name Server
Se puede observar como el protocolo http del navegador aparece reflejado en el conocido
puerto 80, o como el protocolo smtp se realiza en principio sobre el puerto 25.
El primer parámetro es AF_INET o AF_UNIX para indicar si los clientes pueden estar en otros
ordenadores distintos del servidor o van a correr en el mismo ordenador. AF_INET vale para
los dos casos. AF_UNIX sólo para el caso de que el cliente corra en el mismo ordenador que el
servidor, pero lo implementa de forma más eficiente. Si pusieramos AF_UNIX, el resto de las
funciones varían ligeramente.
El segundo parámetro indica si el socket es orientado a conexión (SOCK_STREAM) TCP/IP o no
lo es (SOCK_DGRAM) UDP/IP. De esta forma podremos hacer sockets de red o de Unix tanto
orientados a conexión como no orientados a conexión.
En nuestro caso vamos a utilizar comunicación orientada a la conexión y por tanto fiable.
//creacion del socket
socket_conn=socket(AF_INET, SOCK_STREAM,0);
Esta función generalmente no produce errores, aunque en algún caso podría hacerlo. Como
regla general conviene comprobar su valor, que será igual a ‐1 (INVALID_SOCKET) si la función
ha fallado.
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);
sin_family es el tipo de conexión (por red o interna), igual que el primer parámetro
de socket().
sin_port es el número de puerto correspondiente al servicio que podriamos haber
obtenido con getservbyname(). El valor estaría en en el campo s_port de Puerto de
la estructura devuelta por getserbyname(). En el ejemplo sin embargo se especifica
el número de puerto directamente, pero convirtiéndolo al formato
Finalmente sin_addr.s_addr es la dirección del cliente al que queremos atender
356 PROGRAMACIÓN C++ Y COMUNICACIONES.
Para entender el uso de htons y inet_addr, es necesario indicar como es importante que en la
red se envien los datos con un formato claro. Las máquinas en función de la arquitectura y el
procesador utilizan un modo diferente de representar los números. inet_addr, además de
conseguir que la dirección quede representada con un orden de bytes correcto, lo que logra
es traducir una dirección IP dada como una secuencia de caracteres en un long int de 32
bytes.
Network byte order y Host byte order son dos formas en las que el sistema puede almacenar
los datos en memoria. Está relacionado con el orden en que se almacenan los bytes en la
memoria RAM.
Si al almacenar un short int (2 bytes) o un long int (4 bytes) en RAM, y en la posición más alta
se almacena el byte menos significativo, entonces está en network byte order, caso contrario
es host byte order.
Cuando enviamos los datos por la red deben ir en un orden especificado, sino enviaríamos
todos los datos al revés. Lo mismo sucede cuando recibimos datos de la red, debemos
ordenarlos al orden que utiliza nuestro sistema. Debemos cumplir las siguientes reglas:
Todos los bytes que se transmiten hacia la red, sean números IP o datos, deben estar
en network byte order.
Todos los datos que se reciben de la red, deben convertirse a host byte order.
Para realizar estas conversiones utilizamos las funciones que se describen a continuación.
htons() ‐ host to network short ‐ convierte un short int de host byte order a network
byte order (es el caso del número de servicio o puerto).
htonl() ‐ host to network long ‐ convierte un long int de host byte order a network
byte order.
ntohs() ‐ network to host short ‐ convierte un short int de network byte order a host
byte order.
ntohl() ‐ network to host long ‐ convierte un long int de network byte order a host
byte order.
Puede ser que el sistema donde se esté programando almacene los datos en network byte
order y no haga falta realizar ninguna conversión, pero si tratamos de compilar el mismo
código fuente en otra plataforma host byte order no funcionará. Como conclusión, para que
nuestro código fuente sea portable se debe utilizar siempre las funciones de conversión.
//conexion
int len= sizeof(server_address);
connect(socket_conn,(struct sockaddr *) &server_address,len);
Esta función connect() fallará si no esta el servidor preparado por algún motivo (lo que sucede
muy a menudo). Por lo tanto es más que conveniente comprobar el valor de retorno de
connect() para actuar en consecuencia. Se podría hacer algo como:
if(connect(socket_conn,(struct sockaddr *)
&server_address,len)!=0)
{
std::cout<<"Client could not connect"<<std::endl;
return -1;
}
Si la conexión se realiza correctamente, el socket ya esta preparado para enviar y recibir
información.
En este caso hemos decidido que va a ser el servidor el que envía datos al cliente. Esto es un
convenio entre el cliente y el servidor, que adopta el programador cuando diseña e
implementa el sistema.
Dicha información puede ser menor que el tamaño máximo suministrado. El valor de retorno
de la función recv() es el numero de bytes recibidos.
358 PROGRAMACIÓN C++ Y COMUNICACIONES.
//comunicacion
char cad[100];
int length=100; //read a maximum of 100 bytes
int r=recv(socket_conn,cad,length,0);
std::cout<<"Rec: "<<r<<" contenido: "<<cad<<std::endl;
Por ultimo se cierra la comunicación y se cierra el socket.
shutdown(socket_conn, SHUT_RDWR);
close(socket_conn);
socket_conn=INVALID_SOCKET;
Servidor
El código del programa servidor es algo más Complejo, ya que debe realizar más tareas. La
principal característica es que se utilizan dos sockets diferentes, uno para la conexión y otro
para la comunicación. El servidor comienza enlazando el socket de conexión a una dirección IP
y un puerto (siendo la IP la de la máquina en la que corre el servidor), escuchando en ese
puerto y quedando a la espera “Accept” de una conexión, en estado de bloqueo. Cuando el
cliente se conecta, el “Accept” se desbloquea y devuelve un nuevo socket, que es por el que
realmente se envían y reciben datos.
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <iostream>
#define INVALID_SOCKET -1
int main()
{
int socket_conn=INVALID_SOCKET;//used for communication
int socket_server=INVALID_SOCKET;//used for connection
struct sockaddr_in server_address;
struct sockaddr_in client_address;
// Configuracion de la direccion del servidor
char address[]="127.0.0.1";
int port=12000;
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr(address);
server_address.sin_port = htons(port);
//creacion del socket servidor y escucha
socket_server = socket(AF_INET, SOCK_STREAM, 0);
int len = sizeof(server_address);
int on=1; //configuracion del socket para reusar direcciones
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
//escucha
La segunda diferencia es que en vez de intentar la conexión con connect(), el servidor debe
establecer primero en que dirección va a estar escuchando su socket de conexión, lo que se
establece con las líneas:
Esta función también es susceptible de fallo. El fallo más común es cuando se intenta enlazar
el socket con una dirección y puerto que ya están ocupados por otro socket. En este caso la
función devolverá ‐1, indicando el error. A veces es posible que si no se cierra correctamente
un socket (por ejemplo, si el programa finaliza bruscamente), el SO piense que dicho puerto
esta ocupado, y al volver a ejecutar el programa, el bind() falle, no teniendo sentido continuar
la ejecución. La gestión básica de este error podría ser:
if(0!=bind(socket_server,(struct sockaddr *)
&server_address,len))
{
std::cout<<”Fallo en el Bind()”<<std::endl;
return -1;
}
La función listen() permite definir cuantas peticiones de conexión al servidor serán encoladas
por el sistema. Nótese que esto no significa que realmente se atiendan las peticiones de
conexión. Es el usuario a través de la función accept() el que acepta una conexión. El numero
de conexiones dependerá de cuantas veces ejecute el programa dicho accept().
Lo más importante del accept() es que en su modo normal bloquea el programa hasta que
realmente se realiza la conexión por parte del cliente. A esta función se le suministra el socket
de conexión, y devuelve el socket que realmente se utilizará para la comunicación. Si algo
falla en la conexión, la función devolverá ‐1, lo que corresponde a nuestra definición de
socket invalido INVALID_SOCKET, lo que podemos comprobar:
if(socket_conn==INVALID_SOCKET)
{
std::cout<<”Error en el accept”<<std::endl;
return -1;
}
Una vez que se ha realizado la conexión, la comunicación se hace por el nuevo socket,
utilizando las mismas funciones de envío y recepción que se podrían usar en el cliente. Notese
que un modo de atender a varios clientes, es ir haciendo accepts consecutivos y creando
sockets de comunicación hasta alcanzar un número determinado, momento en el que ya
trabajamos con las comunicaciones a los distintos clientes.
En el ejemplo actual, realmente sólo atendemos a un cliente cada vez y por convenio hemos
establecido que será el servidor el que envía un primer mensaje al cliente. El código siguiente
envía el mensaje “Hola Mundo” por el socket:
La función send() también puede fallar, si el socket no esta correctamente conectado (se ha
desconectado el cliente por ejemplo). La función devuelve el número de bytes enviados
correctamente o ‐1 en caso de error. Típicamente, si la conexión es buena, la función
devolverá como retorno un valor igual a “length”, aunque también es posible que no consiga
enviar todos los datos que se le ha solicitado. Una solución completa debe contemplar esta
posibilidad y reenviar los datos que no han sido enviados. No obstante y por simplicidad,
realizamos ahora una gestión sencilla de este posible error:
El cierre de los sockets se realiza de la misma manera que en el cliente, exceptuando que se
deben cerrar correctamente los dos sockets, el de conexión y el de comunicación. La salida
por pantalla al ejecutar las aplicaciones (primero arrancar el servidor y luego el cliente)
debería ser (en el lado del cliente):
Nótese que los bytes recibidos son 11 porque incluyen el carácter nulo ‘\0’ de final de la
cadena.
Existen diversas formas de atender las conexiones de clientes en un servidor. Sin embargo
dado que la programación concurrente no la trataremos en este curso, simplemente se
exponen a continuación para entender la importancia de estas estructuras. La primera de
ellas responde al modo visto en el ejemplo anterios, la segunda requiere de el uso de threads
(hilos de ejecución de un proceso), y la tercera es la estructura habitual con comunicación no
orientada a la conexión.
En el siguiente gráfico se muestran los procesos que se ejecutan cuando se realiza una
conexión Simple Cliente‐Servidor:
En el servidor:
En el cliente:
Para entender elmodelo concurrente, es preciso al menos tener un primer concepto de lo que
es un hilo de ejecución o thread:
Concepto de thread
Un thread es la unidad básica de ejecución en la mayoría de los S.O. . Cualquier programa que
se ejecute consta de, al menos, un thread.
siguiente. Sin embargo, esta conmutación no se puede hacer de cualquier manera. Cada vez
que el S.O. se adueña de la CPU para cedersela a otro thread, los registros y la pila (o sea,
el contexto del hilo) contienen unos valores determinados. Por eso, el S.O. guarda todos esos
datos en cada cambio, de modo que al volver a conmutar al thread inicial, pueda restaurar el
contexto inicial. La mayoría de los S.O. multitarea son de tipo preemptivo, lo que significa que
la CPU puede ser arrebatada en cualquier momento. Esto significa que un thread no puede
saber cuando se le va a arrebatar la CPU, por lo que no puede guardar los registros ni la pila
de forma 'voluntaria'.
La diferencia más importante entre hilo y proceso, consiste en que dos procesos distintos en
un sistema multitarea, no comparten recursos ni siquiera el mapa de memoria, de tal forma
que la comunicación entre los mismos se realiza por medio de mecanismos de comunicación
dados por el sistema operativo (memorias compartidas, pipes, mensajes…). Son como dos
instancias diferentes del mismo tipo. Sin embargo los threads, salvo la pila y el contexto de la
CPU, comparten recursos, memoria y mapa de direcciónes y por tanto las zonas de datos son
comunes para todos los threads de un mismo proceso. Una de las ventajas que tienen por
tanto es que la conmutación entre hilos de un mismo programa (proceso) es muy rápida,
frente al cambio de contexto entre dos procesos.
Por otro lado, debemos recordar que cada thread se ejecuta de forma absolutamente
independiente. De hecho, cada uno trabaja como si tuviese un microprocesador para el solo.
Esto significa que si tenemos una zona de datos compartida entre varios threads de modo que
puedan intercambiar información entre ellos, es necesario usar algún sistema de
sincronización para evitar que uno de ellos acceda a un grupo de datos que pueden estar a
medio actualizar por otro thread. Estos problemas clásicos en los sistemas concurrentes
quedan fuera del enfoque del curso, pero baste aquí por lo menos mencionarlo.
En el siguiente gráfico se muestran los procesos que se ejecutan cuando se realiza una
conexión Concurrente Cliente‐Servidor:
En el servidor:
En el cliente:
366 PROGRAMACIÓN C++ Y COMUNICACIONES.
En el siguiente gráfico se muestran los procesos que se ejecutan cuando se realiza una
conexión Cliente‐Servidor mediante Datagrams:
En el servidor:
En el cliente:
Capítulo 2
EJERCICIO 2.1
EJERCICIO 2.2
#include <math.h>
EJERCICIO 2.3
a=3 b=5 c=6
EJERCICIO 2.4
1 5 7 11 13 17 19 23 25 29
EJERCICIO 2.5
OK nota1=bien;
MAL nota1=1;
OK nota1=(notas)1;
MAL nota1=suspenso+1;
MAL nota1=suspenso+aprobado;
OK var=5+bien;
OK var=bien+5;
OK var=notable+bien;
OK var=nota1+bien;
MAL nota1++;
OK for(var=suspenso;var<sobresaliente;var++)
printf(“%d”,var);
MAL for(nota2=suspenso;nota2<sobresaliente;nota2++)
printf(“%d”,(int)nota2);
EJERCICIO 2.6
Valores: 18 14 17